深入理解cpp-虚函数
摘要
本文将详细介绍C++中的继承和虚函数。本文不介绍基础的概念,只详细分析一些需要注意或难以理解的点。
Overview
本质上,为了实现代码复用,提出了继承和多态的概念。而为了实现继承,便有了虚函数的概念。为了实现多态,便有了运行时绑定(动态绑定)的概念。
虚函数的原理
C++编译器会在类中添加一个私有指针*__vptr,用来指向类自身的虚函数表。
通常会将虚函数指针存放在类的起始地址。多继承的环境下,就会存放多个虚函数表指针,指向不同基类的虚函数表。
虚函数表中存放虚函数的地址,其实就是一个数组中,存放函数指针。
在继承时,派生类继承了父类的虚函数表。表现为:
- 先复制基类的虚函数列表。
- 如果出现了重写,则替换虚函数表中的函数指针。
- 如果添加了新的虚函数,则追加在虚函数表的末尾
此时便可以理解多态了,若指针指向的是基类对象,其实是在基类的虚函数表中调用函数,如果指向的是派生类对象,则在派生类的虚函数表中调用函数,以此来实现多态。
动态绑定
普通成员函数的调用是在编译时发生绑定。而虚函数的调用是在运行时才进行绑定。
1 | class Base; |
基类指针指向派生类对象时,通过该指针调用虚函数,会根据指针所指对象的实际类型来调用相应的函数版本。
有一种情况下,我们可能需要回避动态绑定机制。例如,我们在派生类的虚函数中需要用基类中的函数版本,此时不能出现动态绑定,否则在调用该虚函数时,会被调用派生类的版本,从而导致无限递归。可以通过下面的方式避免:
1 | double res = a->A::func(); |
override 和 final
如果我们不想让一个类被继承,那么可以用final关键字,final关键字在类名后面。例如:
1 | class A final{}; |
override用来表示派生类中的某个函数需要被重写。override仅仅是用来提醒程序员需要进行重写。
1 | class Base{ |
静态成员与继承
值得一提的是,静态成员在整个继承体系中都只存在唯一定义。也就是说,不论存在多少派生类,都只有唯一一个静态成员实例。静态成员同意遵循普通的访问控制方式,既可以通过基类调用,也可以通过派生类调用。
静态成员函数不可以是虚函数。因为静态成员不属于任何一个对象,不存在this指针。而虚函数需要通过this指针访问虚函数表。
纯虚函数
利用=0可以将一个成员函数标识为纯虚函数。纯虚函数不需要定义,纯虚函数所在的类是抽象类,抽象类不能创建对象。而抽象类的派生类必须对纯虚函数进行定义,否则仍然是抽象类。实际上,抽象类虽然不能创建对象,但是仍然可以定义构造函数,这用来实现抽象类内部自己的成员初始化过程。
例如:
1 | class abstract_base{ |
访问控制
在类中,不仅仅成员有public、protect、private的访问控制权限,类在继承的时候也有相应的访问控制权限。
首先,C++类中的变量默认是private的。
对于成员变量的访问权限:
public: 所有人都可以访问
protect: 不能通过类的对象访问,只有派生类类的成员和友元可以访问。
private: 只有类内可以访问
值得注意的是,尽管protect成员可以在派生类或者友元中进行访问,但是也是只能访问派生类的成员,而不是基类的成员,这个需要注意。
1 | class Base{ |
接下来来看继承的访问控制:
其实对于继承的访问控制,不会改变类内的访问控制权限,只是改变类的对象的访问控制权限。
对于public继承,那么所以的访问控制权限不变。
但是对于protected继承,那么所有访问控制权限变成protect和private。
对于private继承,那么所有的访问控制权限变成private。
其实就是将访问控制权限进行降低,使得权限不超过继承标明的权限。
例如:
1 | class Base{ |
拷贝移动和继承
首先,如果我们在类中定义了拷贝操作或者析构函数,那么编译器就不会为这个类生成合成的移动操作,即使是通过=default的形式使用了合成的析构函数。这是因为如果类中定义了拷贝操作或者析构函数,那么编译器就认为程序员要自己来控制对象的复制和释放,就不会再自动生成了,防止合成出一个不符合程序员意图的移动操作。
通常在继承关系中,都会定义虚析构函数,基类缺少移动操作会阻止派生类有合成的移动操作。所以如果确实需要使用移动操作,应该在基类中首先定义移动操作。
如果基类中的默认构造函数、拷贝操作或者析构函数是删除的或者不可访问的,那么派生类对应的成员就是被删除的。 因为派生类在赋值或者析构时无法为基类进行赋值或者析构。
通常情况下,派生类都会继承基类的构造函数,但是除了两种情况:
- 如果派生类定义的构造函数和基类的构造函数具有相同的参数列表,则该构造函数不会被继承。
- 默认、拷贝和移动构造函数不会被继承。