目录
override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
多态分为编译时多态和运行时多态
编译时(静态绑定): 模板,重载
运行时(动态绑定): 重写.
重写的实现: 1. 父类要有虚函数(虚函数就是被virtual修饰的函数) 2. 子类对父类虚函数进行重写, 重写: 函数返回值, 函数名, 函数参数都相同.(子类重写时可加virtual也可不加,规范性问题),重写也可以叫覆盖.
如果即使子类中达成了三同条件, 但基类函数不是虚函数, 就不构成多态, 只构成同名隐藏,把父类的函数在子类隐藏掉(同名隐藏只需要函数名相同.)
除三同条件外达成重写的情况:
1.协变: 派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
2.析构函数的重写: 如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
实现多态后就可以使用不同继承关系的对象调用同意函数, 产生各自的行为.
含虚函数的类内存空间:
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
通过测试, b对象是8个字节大小,除了_b成员,还多一个__vfptr成员放在对象空间的前面(注意个别平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代virtual,f代表function)。__vfptr和其他成员变量一样一起遵循内存对齐规则构成类的大小. (vs下c/c++指针大小都为4个字节)
虚函数表中存放的是虚函数的地址, 而虚函数指针则指向了这个表.
一个含有虚函数的类中都至少都有一个虚函数表指针, 多继承则会有多个 .
虚函数和普通函数一样存放在代码段, 而虚函数表存在数据段, 同一个类的所有对象共同一个虚函数表. 相当于static了
但同一类不同对象的虚函数表指针是不同的,指针的值相同,即指向同一虚函数表)
父类, 子类的虚表内部结构:
情况1:
情况2:
注意: 相同类的不同对象的虚表指针的值相同, 即指向同一张虚表.(是两个虚表指针但是两个指针值相同.)
如果子类没有重写, 我们可以发现虽然子类也有虚表指针且虚表指针的值不同, 指向的虚表不同, 但两个虚表内函数的地址相同, 所以没有重写是无法实现多态的 :
将子类赋值给父类的指针/引用:
a a1;
base* base1=&a1;
base& base2=a1;
这两种情况的base1和base2,再调用被重写的父类虚函数, 都将调用子类重写的虚函数, 因为__vfptr已被替换, 自然指向的虚表也变成了子类的, 虚表里的函数指针也成了子类的, 所以调用的子类的函数:
析构函数的虚化问题:
new一个子类赋给一个父类指针:
析构时只析构了父类自己, 但构造时构造了子类和父类(new一个子类, 构造子类本来就得先构造父类). 所以会造成内存泄露.
而将析构函数虚化后就没有这个问题:
不能用virtual修饰的函数:
- 普通函数(即非类成员函数)不能是virtual的,否则不能通过编译,virtual只能出现在类声明中。
- 构造函数(拷贝构造函数/赋值构造函数)不能是virtual的。编译器会为每一个含有virtual函数生成一个函数表(位于rodata段),每个类实例的最前端会包含一个指向该表的指针。如果构造函数也可以virtual,那么需要一个虚函数指针指向对应的虚函数表,但此时对象并未构造,虚函数指针是不存在的。这就出现了矛盾。如果在基类和子类构造函数中都调用了虚函数的话,将发如下事件:调用子类构造函数,之前先调用基类的构造函数,此时只会调用基类的该函数而非子类的重载函数,因为此时子类对象并未构造完全,虚函数指针不起作用。
- staic静态成员函数不能是virtual的,静态成员函数没有this指针。
- inline成员函数可以声明为virtual,但是在编译时不会实际将代码直接在调用处展开。
-
友元函数也不能声明为virtual,因为友元关系是不能被继承的,编译会出错。
final和override关键字:
final:修饰虚函数,表示该虚函数不能再被继承
override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
静态成员函数不能定义为虚函数重写.
重载, 重写, 隐藏:
虚函数机制在构造函数内部不起作用:
通过虚表地址来访问虚函数:
因为一个有虚表指针的对象第一个成员是虚表指针(指针, 前四个字节大小), 所以对象取地址之后强转为int*, (int*指向的是四个字节的空间) 所以刚好能成为__vfptr虚函数表指针. 再解引用就得到虚函数表地址. 虚函数表地址再强转int* 就取到了虚函数表前四个字节, 前四个字节为虚函数函数指针刚好也是四个字节, 再解引用就得到第一个虚函数的地址. 虚函数地址表强转int* 再+1跳过4个字节大小就指向了第二个虚函数首地址,因为是int*能取到4个字节数据,所以再解引用得到第二个数函数地址. 以此类推.
(此时即使得到私有的虚函数地址,此时我们通过的是地址调用,越过了保护规则,是可以类外调用的)
多继承的虚表问题:
对于如上这种情况, 子类自己的虚函数只会往在第一个父类的虚表后添加:
如果base1,base2,base3三个类都有一个virtual void fun()函数 (三同),且子类重写了这个函数, 那个就是同时对三个都重写了,子类的三个虚函数表中的fun()地址都是同一个, 即子类fun()函数的地址
抽象类:
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。
但是抽象类可以定义指针.
class Car
{
public:
virtual void Drive() = 0
{
}
};
因为纯虚函数规范了派生类必须重写. 所以纯虚函数更体现出了接口继承。
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。