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. 普通函数调用快还是多态调用快?
都是普通函数调用的时候,效率一样;当多态调用的时候,多态要满一些,需要到虚表里面找到对
应的虚函数地址。
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。