关于虚函数
虚函数好像是一个比较喜欢考的东西
本文大部分转载于 这里 还有一部分是在网上拼凑出来的
虚函数的作用
虚函数带来的好处就是: 可以定义一个基类的指针, 其指向一个继承类, 当通过基类的指针去调用函数时, 可以在运行时决定该调用基类的函数还是继承类的函数. 虚函数是实现多态(动态绑定)/接口函数的基础
虚函数的实现
编译器处理虚函数时,给每个对象添加一个隐藏的成员。隐藏的成员是一个指针类型的数据,指向的是函数地址数组,这个数组被称为虚函数表
虚函数表中存储的是类中的虚函数的地址
如果不使用指针调用, 虚函数调用不会发生动态绑定
1 | |
调用a.func()不会发生动态绑定
调用p->func() 会,因为p不知道所指向的对象到底是A还是继承于A的对象
虚函数的额外开销
使用虚函数时,对于内存和执行速度方面会有一定的成本:
每个对象都会变大,变大的量为存储虚函数表指针
对于每个类,编译器都会创建一个虚函数表
对于每次调用虚函数,都需要额外执行一个操作,就是到表中查找虚函数地址
虚函数和纯虚函数
| 虚函数 | 纯虚函数 |
|---|---|
| 可以直接使用 | 必须在派生类中实现后才能使用 |
| virtual | 除了加上virtual 关键字还需要加上 =0 |
| 必须实现 | 不用实现 |
析构函数最好定义为虚函数,特别是对于含有继承关系的类;析构函数可以定义为纯虚函数,此时,其所在的类为抽象基类,不能创建实例化对象
纯虚函数的作用
很多情况下,在基类中不能对虚函数给出具体的有意义的实现,就可以把它声明为纯虚函数,它的实现留给该基类的派生类去做
虚函数表
存在虚函数的类都有一个一维的虚函数表叫做虚表,每一个类的对象都有一个指向虚表开始的虚指针。虚表是和类对应的,虚表指针是和对象对应的
虚函数表存放的内容:类的虚函数的地址。
虚函数表建立的时间:编译阶段,即程序的编译过程中会将虚函数的地址放在虚函数表中。
虚表指针保存的位置:虚表指针存放在对象的内存空间中最前面的位置,这是为了保证正确取到虚函数的偏移量。
1 | |
虚表与继承
单继承无覆盖
1 | |
由于Base1只知道自己的两个虚函数索引[0]和[1], 所以就算在后面加上了[2], Base1根本不知情, 不会对它造成任何影响.
单继承有覆盖
1 | |
多继承无覆盖
1 | |
多继承有覆盖
1 | |
基类没有虚函数
1 | |
总结:
子类虚表跟在第一个父类后面,有覆盖则覆盖
有虚表的放前面
关于构造函数和析构函数与虚函数的问题
为什么构造函数不能定义为虚函数
如果父类的构造函数设置成虚函数,那么子类的构造函数会直接覆盖掉父类的构造函数。而父类的构造函数就失去了一些初始化的功能。这与子类的构造需要先完成父类的构造的流程相违背了。
虚函数的调用是需要通过“虚函数表”来进行的,而虚函数表也需要在对象实例化之后才能够进行调用。在构造对象的过程中,还没有为“虚函数表”分配内存。所以,这个调用也是违背先实例化后调用的准则。
虚函数是运行中的多态,而对象的构建是由编译器完成的
虚函数的调用是由父类指针进行完成的,而对象的构造则是由编译器完成的,由于在创建一个对象的过程中,涉及到资源的创建,类型的确定,而这些是无法在运行过程中确定的,需要在编译的过程中就确定下来。而多态是在运行过程中体现出来的,所以是不能够通过虚函数来创建构造函数的,与实例化的次序不同也有关系。
为什么基类的析构函数常定义为虚函数
在实现多态时,当用基类操作派生类,在析构时防止只析构基类而不析构派生类的状况发生,要将基类的析构函数声明为虚函数
由于类的多态性,基类指针可以指向派生类的对象,如果删除该基类的指针,就会调用该指针指向的派生类析构函数,而派生类的析构函数又自动调用基类的析构函数,这样整个派生类的对象完全被释放。
如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全,造成内存泄漏。
虚函数的内联
ps:虚函数存在的意义是运行时的多态,所以虚函数的内联基本没有意义
虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联
内联是在编译期建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时不可以内联
inline virtual 唯一可以内联的时候
编译器知道所调用的对象是哪个类,这只有在编译器具有实际对象而不是对象的指针或引用时才会发生。