C++: 虚函数,一些可能被忽视的细节
C++: 虚函数,一些可能被忽视的细节
引言:关于C++虚函数,对某些细节的理解不深入,可能导致我们的程序无法按预期结果运行,或是表明我们对其基本原理理解不够透彻。本文详细解答以下几个问题:实现多态,忘记写virtual会怎么样?虚函数的默认参数可以重载吗?纯虚函数真的不能有实现吗?析构函数可以是纯虚函数吗?
1.1 虚函数是什么?
虚函数是在基类中使用关键字
virtual声明的函数,它在派生类中可以被重写,且在运行时根据对象的类型来调用相应的函数。虚函数的作用是实现多态。多态是指同一操作作用于不同的对象上面,可以产生不同的解释和不同的执行结果。多态分为编译时多态和运行时多态,编译时多态是指函数重载,运行时多态是指虚函数。
虚函数动态绑定的实现原理:每个含有虚函数的类都有一个虚函数表,虚函数表中存储着虚函数的地址,当基类指针绑定了类对象后,通过类对象虚表指针指向的虚函数表找到虚函数的地址,然后调用对应的虚函数。
1.2 实现多态,忘记写virtual会怎么样?
如果忘记在派生类中写virtual关键字,那么就不会实现多态,而是静态绑定。因此以下例子中,调用的是基类的函数,而不是派生类的函数。
class Base {
public:
void func() {
std::cout << "Base func" << std::endl;
}
};
class Derived : public Base {
public:
void func() {
std::cout << "Derived func" << std::endl;
}
};
int main() {
Base *b = new Derived();
b->func();
delete b;
return 0;
}
输出:
Base func
为了避免这种情况发生,C++11中引入了override关键字对需要重写的函数进行声明,这样如果派生类中没有重写基类的函数,编译器就会报错。
1.3 虚函数的默认参数可以重载吗?
虚函数的默认参数不可以重载,因为虚函数的调用是在运行时确定的,而默认参数是在编译时确定的。从设计角度来说,这样做是合理的,如果虚函数的默认参数可以重载,那么在运行时,编译器就需要在运行时选择合适的默认参数,这样就会增加编译器的复杂度。因此,虚函数的默认参数不可以重载。
class Base {
public:
virtual void func(int i = 0) = 0;
};
void Base::func(int i) {
std::cout << "Base func: " << i << endl;
}
class Derived : public Base {
public:
void func(int i = 2) {
std::cout<< "Derived func: " << i << endl;
}
};
int main() {
Base *b = new Derived();
b->func();
delete b;
return 0;
}
输出:
Base func: 0
1.4 纯虚函数是什么?
纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法,否则编译失败。
纯虚函数的声明格式为:
virtual 函数类型 函数名(参数表) = 0;,其中“= 0”是纯虚函数的标志,它告诉编译系统,该虚函数没有实现。含有纯虚函数的类是抽象类,抽象类是不能实例化的。
纯虚函数真的不能有实现吗?其实不然,纯虚函数是可以有自己的实现的,但是这个实现是在类外部实现的,而不是在类内部实现的。详见1.5中的代码实例。
1.5 析构函数可以是纯虚函数吗?
我们知道,析构函数是在对象销毁时调用的,而纯虚函数是没有实现的虚函数,含有纯虚函数的类是抽象类,那么,析构函数可以是纯虚函数吗?
程序验证如下:
class Base {
public:
virtual ~Base() = 0;
};
Base::~Base() {
std::cout << "Base destructor" << std::endl;
}
class Derived : public Base {
public:
~Derived() {
std::cout << "Derived destructor" << std::endl;
}
};
int main() {
Base *b = new Derived();
delete b;
return 0;
}
输出:
Derived destructor
Base destructor
结论:析构函数可以是纯虚函数,含有纯虚析构函数的类无法实例化。因为析构函数是在派生类析构函数调用之后才调用基类析构函数,而派生类析构函数在派生类对象销毁时才会调用,而派生类对象的销毁必须要调用基类的析构函数,因此基类析构函数必须要在类外提供定义。
如果不定义纯虚析构函数的实现,则会链接失败,报以下错误。
:(.text$_ZN7DerivedD1Ev[__ZN7DerivedD1Ev]+0x3e): undefined reference to `Base::~Base()'
collect2.exe: error: ld returned 1 exit status
为什么在类外部实现就可以呢?因为含有纯虚函数的类是抽象类,抽象类是不能实例化的,但是抽象类可以有指针和引用,因此,我们可以通过抽象类的指针或引用调用纯虚函数,但是如果纯虚函数没有实现,那么就会出现问题,因此,我们需要在类外部实现纯虚函数。
1.6 纯虚函数可以被显示调用吗?
派生类的成员函数可以通过限定函数id自由调用基类在类外定义的纯虚函数。
class Base {
public:
virtual void func() = 0;
};
void Base::func() {
std::cout << "Base func" << std::endl;
}
class Derived : public Base {
public:
void func() {
Base::func();
std::cout << "Derived func" << std::endl;
}
};
int main() {
Base *b = new Derived();
b->func();
delete b;
return 0;
}
输出:
Base func
Derived func
参考
- https://en.cppreference.com/w/cpp/language/abstract_class
- Effective C++: 55 Specific Ways to Improve Your Programs and Designs.
C++: 虚函数,一些可能被忽视的细节的更多相关文章
- 构造函数为什么不能是虚函数 ( 转载自C/C++程序员之家)
从存储空间角度,虚函数对应一个指向vtable虚函数表的指针,这大家都知道,可是这个指向vtable的指针其实是存储在对象的内存空间的.问题出来了,如果构造函数是虚的,就需要通过 vtable来调用, ...
- 构造函数为什么不能为虚函数 & 基类的析构函数为什么要为虚函数
一.构造函数为什么不能为虚函数 1. 从存储空间角度,虚函数相应一个指向vtable虚函数表的指针,这大家都知道,但是这个指向vtable的指针事实上是存储在对象的内存空间的.问题出来了,假设构造函数 ...
- C++中纯虚函数
1.纯虚函数 virtual ReturnType Function()= 0; 纯虚函数可以让类先具有一个操作名称,而没有操作内容,让派生类在继承时再去具体地给出定义.凡是含有纯虚函数的类叫做抽象类 ...
- 2013级C++第14周(春)项目——多态性、虚函数和抽象类
课程首页在:http://blog.csdn.net/sxhelijian/article/details/11890759,内有完整教学方案及资源链接 第一部分 阅读程序1.阅读.改动和执行关于交通 ...
- C++虚函数和函数指针一起使用
C++虚函数和函数指针一起使用,写起来有点麻烦. 下面贴出一份示例代码,可作参考.(需要支持C++11编译) #include <stdio.h> #include <list> ...
- 匹夫细说C#:从园友留言到动手实现C#虚函数机制
前言 上一篇文章匹夫通过CIL代码简析了一下C#函数调用的话题.虽然点击进来的童鞋并不如匹夫预料的那么多,但也还是有一些挺有质量的来自园友的回复.这不,就有一个园友提出了这样一个代码,这段代码如果被编 ...
- 【C++】多态性(函数重载与虚函数)
多态性就是同一符号或名字在不同情况下具有不同解释的现象.多态性有两种表现形式: 编译时多态性:同一对象收到相同的消息却产生不同的函数调用,一般通过函数重载来实现,在编译时就实现了绑定,属于静态绑定. ...
- 虚函数的使用 以及虚函数与重载的关系, 空虚函数的作用,纯虚函数->抽象类,基类虚析构函数使释放对象更彻底
为了访问公有派生类的特定成员,可以通过讲基类指针显示转换为派生类指针. 也可以将基类的非静态成员函数定义为虚函数(在函数前加上virtual) #include<iostream> usi ...
- C++ 系列:虚函数
Copyright © 1900-2016, NORYES, All Rights Reserved. http://www.cnblogs.com/noryes/ 欢迎转载,请保留此版权声明. -- ...
- EC笔记,第二部分:9.不在构造、析构函数中调用虚函数
9.不在构造.析构函数中调用虚函数 1.在构造函数和析构函数中调用虚函数会产生什么结果呢? #; } 上述程序会产生什么样的输出呢? 你一定会以为会输出: cls2 make cls2 delete ...
随机推荐
- 洛谷P1439
这道题也给了我很多的思考,因为很久没有做过LIS和KLCS的题了 为什么能采用二分 因为f数组保存的是LCS长度为i时的最小末尾的值,可以证明f数组一定是单调的,并且是严格单调的 为什么要保存末尾最小 ...
- useCookie函数:管理SSR环境下的Cookie
title: useCookie函数:管理SSR环境下的Cookie date: 2024/7/13 updated: 2024/7/13 author: cmdragon excerpt: 摘要:本 ...
- 3.1 Y86-64指令集体系结构
程序员可见的状态 这里的程序员即可以是用汇编代码写程序的人,也可以是产生机器级代码的编译器.程序员可见的状态如下,有15个程序寄存器(%rax,%rbx等),三个一位的条件(ZF,OF,SF) ,程序 ...
- vue中 lang="ts"与js的区别
`lang="ts"` 与 `js` 的区别在于指定了脚本语言的类型,其中: - `lang="ts"` 指定了 TypeScript,一种由微软开发的 Jav ...
- oeasy教您玩转vim - 58 - # 块可视化
块可视化编辑 回忆上节课内容 上次我们了解到行可视模式 行可视模式 V 也可配合各种motion o切换首尾 选区的开头和结尾是mark标记 开头是 '< 结尾是 '> 可以在选区内进 ...
- pytest-req插件:更简单的做接口测试
pytest-req插件:更简单的做接口测试 背景 我们经常会用到 pytest 和 requests 进行接口自动化测试. pytest 提供了非常方便的插件开发能力,在pytest中使用reque ...
- Hyperledger Fabric 2.x 环境搭建
一.说明 区块链网络的核心是分布式账本,在这个账本中记录了网络中发生的所有交易信息. Hyperledger Fabric是一个是开源的,企业级的,带权限的分布式账本解决方案的平台.Hyperledg ...
- 【Docker】07 部署挂载本地目录的Tomcat
1.拉取Tomcat镜像: docker pull tomcat:9.0.37 2.创建并运行Tomcat容器: 挂载容器的webapps目录到本机(宿主机)自己设置的目录 docker run -d ...
- 自动驾驶开源数据库 —— nuscenes
地址: https://www.nuscenes.org/
- HPA* (Near Optimal hierarchical Path-finding) —— 外网的讲解blog
原地址: https://alexene.dev/2019/06/02/Hierarchical-pathfinding.html 讲解视频: https://www.youtube.com/watc ...