C++ 多态

Solitary_walk 2024-09-11 15:35:01 阅读 98


目录:

思维导图

一·多态概念

在我们日常生活中,买票这个行为对于我们来说再熟悉不过了。同样都是买票这一个行为,对于学生,成人,军人,残疾人……产生的结果却是不一样的。

其实这就是多态的一个体现。

多态,广义上讲,是多种形态;狭义上,是不同的主体执行相同的动作,最终产生的结果是不一样

的。

二· 多态的定义和实现

2.1构成条件

第一个条件:必须通过基类(父类)的指针或者引用来调用

第二个条件:虚函数重写:要求被调用的是虚函数,同时在子类里面已经对基类的虚函数进行了重

写。

2.2 虚函数 

 虚函数:对类的成员函数使用关键字 virtual 进行修饰。

2.3 虚函数重写(覆盖)

在子类的成员函数里面,有和父类的虚函数完全一样(符合三同:函数名字,函数的参数类型,函

数返回值类型)的一个虚函数,此时这个虚函数就完成了父类虚函数的重写。

注意:当父类的虚函数参数采用缺省值的形式,在子类里面的虚函数的缺省值与父类不同,此时

也是符合虚函数重写的,但此时的缺省值使用的是父类的

因为函数重写:只是继承父类的接口,重写的是函数体里面的具体实现的内容。

 2.3.1 虚函数重写的2个特例 

第一个:协变

协变:父类域子类的虚函数的返回值类型不同:父类虚函数返回值类型是父类的指针或者父类 的

引用,子类的虚函数返回值类型是子类的指针或者引用

第二个:析构函数

在继承体系里面,父子类的析构函数是构造虚函数重写的。无论子类的析构函数是否加上关键字 virtual 。

虽然此时析构函数的名字不一样(返回值类型,参数类型都保持一样),但在多态里面,编译器会

把析构函数处理成2部分:首先会把析构函数名字处理成 destructer(此时符合函数重写条件),

之后去调用 operator delete()函数,进行资源释放。这样可以保证对指针指向对象进行正确的释

放。

 这也恰好说明析构函数调用为什么需要先子后父

对父类指针进行堆空间申请的时候,new  子类对象,调用operator delete (),进行资源释

放,是根据指针类型进行free 的,此时free 的是一个父类指针,但new 的对象是一个子类的,因此

造成内存泄漏。

2.4 关键字 override ,final

override ,final 是在C++11 ,新出来的关键字。

C++对函数重写要求比较严格,当我们在函数重写不小心把函数名字  的字母顺序写反,就不能得

到预期的结果,但是编译器是不能检测出来的,直到运行结束后,通过对结果的分析,才能逐一确

定问题。

 C++11提供了override和final两个关键字,可以帮助用户检测是否重写

final :修饰虚函数,表示该虚函数不能被重写。 

 override : 检查子类的某个虚函数是否对父类的某个虚函数进行重写,若没有,编译报错。

2.5 重载,重写,重定义的区别

函数重载:必须在同一个作用域里面,要求函数名字一样,参数类型 或者参数的个数 或者参数的

顺序不同

函数重写(覆盖):2个函数分别在父类和子类里面;要求三同;对虚函数完成重写

函数重定义(隐藏):2个函数分别在父类和子类里面;只要求函数名字一样。

三· 抽象类

3.1 概念

虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口

类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,

只有重写纯虚函数,派生类才能实例化出对象。

3.2 函数的接口继承 和函数的实现继承

对于普通函数,子类继承的只是函数的实现;

对于虚函数的继承,子类继承的接口,对于虚函数的实现需要自己进行重写,从而实现多态。

所以说,要没有实现多态,不要把函数定义成虚函数。

四· 多态原理

4.1 虚函数表

对于这个问题,想必各位初始的答案也是 4字节吧!

借助调试,发现此时b 对象里面不仅仅存了-b这个成员,还有一个 _vfptr ,对应的类型是 void** 类

型的指针 。

最后结合对齐规则,Base 类型的大小就是8字节

对齐规则不太了解的,可以康康此篇博客。对齐规则

_vfptr 是一个虚函数表指针,一个有虚函数的类至少有一个虚函数表指针,虚表用来存放虚函数地址的 。

4.2 多态的原理
 4.2.1单继承的虚表

此时通过切片p 变为一个父类 Person 类型对象,在进行BuyTicket()函数调用的时候,编译器是如

何确定调用的是 子类的函数而不是父类的函数

验证分析:

 打开监视窗口,我们可以看到当前虚函数的地址以及对应具体哪一个类域的,同时借助内存窗

口,对当前的对象进行取地址(&s),就可以看到一个虚函数表指针,借助这个虚函数指针找到虚

,进而找到对应的虚函数地址(注意在VS 下,对于一个虚函数指针数组的存放默认以0结束或

者是nullptr)

 1)父类的虚表与子类的虚表是不同的

2)父类显示写了虚函数,子类没有显示的写虚函数,此时2者的虚表也不一样。

 3)对于单继承来说,子类的虚表只有一个。

4)同一个类的虚函数表是一样的,不同类的虚函数表不同

5)多态的条件之一是父类的指针或者引用不能是父类的对象

假设是父类的对象调用,此时在进行传参的时候,也会把子类的虚函数表拷贝过去,这时候不能保

证这个虚函数表一定全部就是父类的虚函数

4.3 动态绑定与静态绑定

1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比

如:函数重载

2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行

为,调用具体的函数,也称为动态多态

五· 虚函数表

5.1 多继承的虚函数表

对于多继承,子类的虚表是有一个还是多个?

子类的虚表:有多个;有几个父类同时每一个父类都有虚函数,那么就有几个虚表

分析:

<code>// 多继承

class Base1 {

public:

virtual void func1() { cout << "Base1::func1" << endl; }

virtual void func2() { cout << "Base1::func2" << endl; }

private:

int b1;

};

class Base2 {

public:

virtual void func1() { cout << "Base2::func1" << endl; }

virtual void func2() { cout << "Base2::func2" << endl; }

private:

int b2;

};

class Derive : public Base1, public Base2 {

public:

virtual void func1()

{

cout << "Derive::func1" << endl;

}

virtual void func3() { cout << "Derive::func3" << endl; }

private:

int d1;

};

typedef void (*VFUNC)(); // 对虚函数指针进行重命名为 VFUNC

//打印一下虚函数表的地址

void Print(VFUNC* p)

{

for (int i = 0; p[i] != 0; i++)

{

printf("[%d]%p->", i, p[i]);//虚函数地址

VFUNC f = p[i];

f();//进一步验证是否为当前类型的虚函数

}

cout << endl;

}

 借助监视窗口,发现确实是有2张虚表。

但是此时子类的虚函数为什么没有,是不是子类的虚函数没有进行存储?

其实不是这样的:编译器为了方便用户的观察,进行了优化。这时需要借助内存窗口来进一步探究。

 我们方向此时b 对象确实是有3个虚函数地址,这也就说明了,子类的虚函数确实是存在的

 5.1.1 为何相同函数的地址不一样

 分析:

从画图角度分析:

 从汇编角度分析:

eax 是一个寄存器:在当前情况下,存放的是一个地址 


 

其实在底层调用的是同一个对象的函数(Derive::func1()) 

5.2 菱形继承和虚拟继承

对于菱形继承以及菱形虚拟继承在底层实现较为复杂,而且访问父类的成员在性能上代价较大,在

实际应用上用途不大,这里就不做详细讲解,感兴趣的友友们,可以康康以下大佬的博客

C++虚函数表详解

六· 多态想关的面试题

1. inline 函数可以是虚函数吗?

可以;普通函数调用具有inline 属性;多态调用,不具有inline 属性。

2. static 函数 可以是虚函数吗?

不可以;static 的函数没有this 指针,通过类域进行调用,无法实现多态。

3. 构造函数可以是虚函数吗?

不可以;编译报错;对象的虚表指针是在调用构造函数的时候进行初始化的,构造函数如果支持多

态调用,就需要对虚表指针进行初始化。

4.析构函数一定是虚函数吗?

不一定,但是最好析构函数是虚函数;当一个父类指针 = new 子类对象的时候,此时析构函数只

有支持虚函数 的重写,才能正确进行资源释放。

5. 虚函数表存在哪里,又是在什么阶段完成的?

存放在代码段,在编译阶段完成的。

注意虚函数表指针在调用构造函数阶段完成的;虚函数表指针存放在代码段。

6. 普通函数调用快还是多态调用快?

都是普通函数调用的时候,效率一样;当多态调用的时候,多态要满一些,需要到虚表里面找到对

应的虚函数地址。



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。