C++: 多态

目录

含虚函数的类内存空间:

父类, 子类的虚表内部结构:

将子类赋值给父类的指针/引用:

析构函数的虚化问题:

不能用virtual修饰的函数:

final和override关键字:

final:修饰虚函数,表示该虚函数不能再被继承

 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
    {

    }
};

因为纯虚函数规范了派生类必须重写. 所以纯虚函数更体现出了接口继承。

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。

虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值