【C++】三大特性之多态
1 定义及实现
1.1 概念
多态是C++三大特性之一。通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如学生买票半价这个行为,Student继承了Person。Person对象买票全价,Student对象买票半价。对于买票这个行为,产生了不同的结果。
virtual关键字只在声明时加上,在类外实现时不能加
静态成员函数与具体对象无关,属于整个类,核心关键是没有隐藏的this指针,此时没有this无法拿到虚表,就无法实现多态,因此不能设置为虚函数
1.2 多态的构成条件
由上我们可以知道,多态是基于继承关系的。而想要在继承中要构成多态还有两个条件:
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
父子类中两个虚函数,三同(函数名、参数、返回值)
重写和隐藏比较类似,隐藏是函数名相同就构成隐藏,而重写的条件更加苛刻,可以看做是隐藏的子集再子集
- 必须通过父类的指针或引用去调用虚函数
注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用
普通调用 看指针、引用、对象的类型
多态调用 看指针、引用指向的对象
什么是虚函数呢?其实,被virtual修饰的类成员函数称为虚函数
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl;}
};
1.3 虚函数重写的两个例外
- 协变(基类与派生类虚函数返回值类型不同)
虚函数返回值可以不同,但是必须是父子类关系的指针或引用,即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用
编译器进行了强制检查,否则应该构成隐藏
- 析构函数的重写,名字特殊处理(基类与派生类析构函数的名字不同)
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
1.4 重载、覆盖(重写)、隐藏(重定义)的对比
☆☆☆重写是实现重写
对虚函数进行重写时,用的是父类中的声明,子类中只是对函数体内容的实现进行重写,最终的函数其实是父类声明和子类实现的组合体。
2 C++11 override 和 final
- final:修饰虚函数,表示该虚函数不能被重写 #final
// 以下编译不通过
class Car
{
public:
virtual void Drive() final {}
};
class Benz :public Car
{
public:
virtual void Drive() {cout << "Benz-舒适" << endl;}
};
- override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
3 抽象类
3.1 概念
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
3.2 接口继承与实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
4 多态的原理
4.1 虚函数表
实现多态的原理是虚函数表。
一个类存在虚函数,那么编译器就会为这个类生成一个虚表,在虚表里存放的是这个类所有虚函数的地址。当生成类对象的时候,编译器会自动的将类对象的前四个字节设置为虚表的地址,而这四个字节就可以看作是一个指向虚表的指针。虚表里依次存放的是虚函数的地址,每个虚函数的地址占4个字节。
虚函数表(本质是函数指针数组)
_vfptr 虚函数表指针,简称虚表指针
虚函数的重写也叫覆盖
重写是语法层的概念
覆盖是原理层的概念
解释:子类先把父类的虚表拷贝一份,如果发现有重写,就在子类虚表中将被重写的函数指针覆盖掉
实际就是编译器也不知道指向的是谁,只需要到指向的对象中去找虚函数表调用,本质从汇编的角度看到的就是父类,只是看该父类是原生父类还是子类切片出的父类
多态调用的函数和普通调用的函数其实是一个函数,只是调用的方式不一样
子类新增虚函数,监视窗口看不到,但是内存窗口可以看到
![[Pasted image 20240312205400.png]]
虚函数表存在哪里呢?
虚表存放在常量区
一个类存在虚函数,那么编译器就会为这个类生成一个虚表,在虚表里存放的是这个类所有虚函数的地址。当生成类对象的时候,编译器会自动的将类对象的前四个字节设置为虚表的地址,而这四个字节就可以看作是一个指向虚表的指针。虚表里依次存放的是虚函数的地址,每个虚函数的地址占4个字节。