【C++】多态(万字详细总结)
「前言」
🌈个人主页: 代码探秘者
🌈C语言专栏:C语言
🌈C++专栏: C++
🌈喜欢的诗句:天行健,君子以自强不息.
一、多态的概念
通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态
比如,在现实生活中买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票。
这里先看看多态分类,先了解,具体了解可以点多态原理-动态绑定和静态绑定那里。
二、多态的定义及实现
2.1 多态的构成条件
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为,在继承中要构成多态还有两个条件:
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
重载:函数名相同,参数不同。
重写(覆盖):返回值类型,函数名,参数列表都相同
2.2 虚函数
虚函数:即被virtual修饰的类成员函数称为虚函数
- 重写时,子类可以不要求加virtual关键字
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
注:
虚函数的 virtual 和虚继承中的 virtual 是同一个关键字,但是它们之间没有任何关系
- 虚函数的 virtual 是为了实现多态
- 而虚继承的 virtual 是为了解决菱形继承的数据冗余和二义性
2.3 虚函数的重写
重载:函数名相同,参数不同。
重写(覆盖):返回值类型,函数名,参数列表都相同
测试代码,比如买票
//基类 -- 普通人
class Person {
public:
//基类的虚函数
virtual void BuyTicket() { cout << "Person-买票-全价" << endl; }
};
//派生类 -- 学生
class Student : public Person {
public:
//派生类的虚函数重写了父类的虚函数
virtual void BuyTicket() { cout << "Student-买票-半价" << endl; }
};
//派生类 -- 军人
class Soldier : public Person
{
public:
//派生类的虚函数重写了父类的虚函数
virtual void BuyTicket(){ cout << "Soldier-优先-买票" << endl;}
};
上面的派生类已经对基类的虚函数进行重写,下面再通过基类的引用调用虚函数即可构成多态
- 如果不加virtual,就是地址早绑定,编译阶段确定函数地址,自动调用基类的BuyTicket()。
- 加virtual ,就是地址晚绑定,运行阶段确定函数地址,根据需要调用谁的BuyTicket()。
void Func(Person& p)//基类的引用(这里继承的赋值那里有说明)
{
p.BuyTicket();//基类的引用调用虚函数
}
int main()
{
Person p;
Student s;
Soldier sd;
Func(p); //普通人
Func(s); //学生
Func(sd); //军人
return 0;
}
运行结果,多态已经构成,BuyTicket 函数进行多态调用
下面测试使用基类的指针, 基类的指针调用虚函数也可以形成多态
void Func(Person* p)//基类的指针
{
p->BuyTicket();//基类的指针调用虚函数
}
int main()
{
Person p;
Student s;
Soldier sd;
Func(&p);
Func(&s);
Func(&sd);
return 0;
}
运行结果,多态可以构成
注意:
在重写基类虚函数时,派生类的虚函数在不加 virtual 关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用,派生类的虚函数也建议加上 virtual 关键字
不满足多态的任何一个条件,构造不了多态
(1)基类是引用或指针调用函数,但是基类调用的不是虚函数,无法形成多态(基类一定要有 virtual)
(2)基类不是引用或指针调用虚函数,也无法形成多态
重写(覆盖)和隐藏(重定义)的区别:
- (1)隐藏的定义是:基类和派生类的成员函数名字相同,就构成隐藏
- (2)虚函数重写的条件是:虚函数(virtual) + 三同
三个相同:
- 基类和派生类的成员函数名字相同
- 参数列(参数类型、数量)表完全相同
- 函数返回值相同
可以看出,虚函数的重写比隐藏的条件更苛刻
2.4 虚函数重写的两个例外
2.4.1 协变
- 派生类重写基类虚函数时,与基类虚函数返回值类型不同
- 即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,仍可构造虚函数的重写,这种行为称为协变(协变简单了解即可,平时基本不使用)
测试代码
class Person {
public:
virtual Person* fun() {
cout << "Person" << endl;
return new Person;
}
};
class Student : public Person {
public:
virtual Student* fun() {
cout << "Student" << endl;
return new Student;
}
};
int main()
{
Person p;
Student s;
//基类的引用调用虚函数
Person& rp = p;
rp.fun();
Person& rs = s;
rs.fun();
cout << endl;
//基类的指针调用虚函数
Person* ptr1 = &p;
ptr1->fun();
Person* ptr2 = &s;
ptr2->fun();
return 0;
}
运行结果
注:
基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用,返回基类和返回派生类的对象也可以是其它的基类或派生类,也可构成虚函数的重写
测试代码
//基类
class A
{};
//派生类
class B : public A
{};
//基类
class Person
{
public:
//返回基类A的指针
virtual A* fun()
{
cout << "A* Person::f()" << endl;
return new A;
}
};
//派生类
class Student : public Person
{
public:
//返回派生类B的指针
virtual B* fun()
{
cout << "B* Student::f()" << endl;
return new B;
}
};
2.4.2 析构函数的重写
析构函数的重写(基类与派生类析构函数的名字不同)
- 如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。
- 虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor
例如,下面代码中基类 Person 和派生类 Student 的析构函数构成重写
//基类
class Person
{
public:
//基类和派生类析构函数构成重写
virtual ~Person() { cout << "~Person()" << endl; }
};
//派生类
class Student : public Person
{
public:
virtual ~Student() { cout << "~Student()" << endl; }
};
为什么要把析构函数进行重写??
因为在某种场景下析构函数的重写是必要的
比如,如果析构函数不构成重写,在这种场景下会造成内存泄漏
//基类
class Person
{
public:
//没有加virtual,基类和派生类析构函数不构成重写
~Person() { cout << "~Person()" << endl; }
};
//派生类
class Student : public Person
{
public:
~Student() { cout << "~Student()" << endl; }
};
int main()
{
//分别new一个父类对象和子类对象,并均用父类指针指向它们
Person* p1 = new Person;
Person* p2 = new Student;
//使用delete调用析构函数并释放对象空间
delete p1;
delete p2;
return 0;
}
运行结果,很明显派生类的部分没有进行析构,造成了内存泄漏,因为此时delete p1和delete p2都是调用的父类的析构函数
而我们所期望的是p1调用父类的析构函数,p2调用子类的析构函数,即我们期望析构函数的行为是的是一种多态行为
只有派生类Student的析构函数重写了 Person 的析构函数,delete 对象调用析构函数,才能构成多态,才能保证 p1和 p2指向的对象正确的调用析构函数
此时只有基类和派生类的析构函数构成了重写,才能实现多态,才能使得 delete 按照我们的预期进行析构函数的调用。因此,为了避免出现这种情况,非常建议将基类类的析构函数定义为虚函数,在什么情况下都没有错
注:在继承的时候基类和派生类的析构函数构成隐藏原因也在这里,编译器编译后把析构函数的名称统一处理成destructor(),目的就是为了形成多态
2.5 C++11 final和override
从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失。
因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写
2.5.1 final
final:修饰虚函数,表示该虚函数不能再被重写
测试代码
//基类
class Person {
public:
//基类的虚函数
//virtual void BuyTicket()
virtual void BuyTicket() final /表示不能重写
{
cout << "Person-买票-全价" << endl;
}
};
//派生类 -- 学生
class Student : public Person {
public:
virtual void BuyTicket() override
{
cout << "Student-买票-半价" << endl;
}
编译直接报错
final 关键字也可用于实现一个不能被继承的类
如何实现一个不能被继承的类??
- 把构造函数进行私有,这是 C++98 的做法
- 类定义时 加 final 关键字,这是 C++11的做法
测试代码
//基类使用 final 修饰
class A final
{};
//派生类无法进行继承基类
class B : public A
{};
编译也是直接报错
2.5.2 override
override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
测试代码
class Person {
public:
//基类的虚函数
virtual void BuyTicket()
{
cout << "Person-买票-全价" << endl;
}
};
//派生类 -- 学生
class Student : public Person {
public:
//overrigde:检查派生类是不是重写了基类的虚函数
//重写成功,编译通过
//virtual void BuyTicket() override
//{
// cout << "Student-买票-半价" << endl;
//}
//重写失败,报错
virtual void BuyTicket(int n)override
{
cout << "Student-买票-半价" << endl;
}
};
编译结果:
2.6 重载、覆盖(重写)、隐藏(重定义)的对比
三、纯虚函数和抽象类
3.1 纯虚函数
- 在虚函数(virtual)的后面写上 =0,则这个函数为纯虚函数。
//纯虚函数
virtual void BuyTicket() = 0;
3.2 抽象类
- 包含纯虚函数的类叫做抽象类(也叫接口类)
- 抽象类不能实例化出对象
//基类 -- 普通人(含纯虚函数的类-抽象类)
class Person {
public:
//纯虚函数
virtual void BuyTicket() = 0;
};
int main()
{
//抽象类不能实例化对象
//Person p;
//new Person;
return 0;
}
- 派生类必须重写基类的纯虚函数
- 不然派生类就变成抽象类,无法实例化对象了哦
- 纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承
#include<iostream>
using namespace std;
//基类 -- 普通人(含纯虚函数的类-抽象类)
class Person {
public:
//纯虚函数
virtual void BuyTicket() = 0;
};
//派生类 -- 学生
class Student : public Person
{
public:
//派生类必须重写基类的纯虚函数
virtual void BuyTicket()
{
cout << "Student-买票-半价" << endl;
}
};
int main()
{
//抽象类不能实例化对象
//Person p;
//new Person;
//派生类必须重写基类的纯虚函数
//不然派生类就变成抽象类,无法实例化对象了哦
Student s;
Person* pp = &s;
pp->BuyTicket();
return 0;
}
编译通过,可以实例化出对象
3.3 虚析构和纯虚析构
3.3.1 为什么需要?
#include<iostream>
using namespace std;
//基类 -- 普通人
class Person {
public:
Person()
{
cout << "Person的构造函数调用" << endl;
}
~Person()
{
cout << "Person的析构调用" << endl;
}
};
//派生类 -- 学生
class Student : public Person
{
public:
Student()
{
cout << "Student的构造函数调用" << endl;
}
~Student()
{
cout << "Student的析构调用" << endl;
}
};
int main()
{
Person* p=new Student;
delete p;
return 0;
}
运行结果:
发现:
- 在某种场景下析构函数的重写是必要的,比如,如果析构函数不构成重写,在这种场景下会造成内存泄漏。
- 如果在堆上开辟空间,释放父类时,子类没有释放干净,就是说没有调用子类析构函数
*解决方法: - 1.虚析构
- 2.纯虚析构
3.1.2 虚析构
- 加virtual后,变成虚析构
运行结果:问题得到解决
3.1.3 纯虚析构
- 要求
- 类内声明,同时()后加=0;
- 类外定义
如果单纯像纯虚函数一样()后面加0
会报错!
应该这么来:
- 运行结果:问题得到解决
3.4 接口继承和实现继承
- 普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现
- 虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数
四、多态的原理
4.1 虚函数表
下面 Base 类实例化出对象的大小是多少?
// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
int main()
{
Base b;
cout << sizeof(b) << endl;
return 0;
}
运行结果是 8
结果令人诧异,居然是8,为什么是 8 呢??
调试模式下监视窗口查看 b
通过监视窗口我们发现除了 _b 成员外还有了一个 _vfptr 在 b 对象中 (注意有些平台可能会放到对象的最后面,这个跟平台有关)
- _vfptr 是一个指针,对象中的这个指针我们叫做虚函数表(虚函数)指针(v代表 virtual,f代表 function)。
- 一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表(vftable)
- 如果多少个有虚函数的父类,则就会有几张虚表,自身子类不会产生多余的虚表
32 位平台下,指针为 4字节,所以上面实例化出的对象 b 的大小是8,虚表指针(4字节)+ 成员 _b(4字节)
这个表放了些什么呢? 我们继续往下分析
测试代码
基类有三个成员函数,Func1 和 Func2 是虚函数,Func3 是普通成员函数,派生类仅对基类的 Func1 函数进行了重写
//基类
class Base
{
public:
//虚函数
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
//虚函数
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
//普通成员函数
void Func3()
{
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
};
//派生类
class Derive : public Base
{
public:
//重写虚函数Func1
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}
调试模式下在监视窗口查看 b 和 d 对象
通过观察发现:派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员
对象模型如下
- 基类b对象和派生类d对象虚表是不一样的,这里我们发现 Func1 完成了重写,所以d的虚表中存的是重写的 Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖
- 重写是语法的叫法,覆盖是原理层的叫法
另外 Func2 派生类继承下来后是虚函数,所以放进了虚表,Func3 也继承下来了,但是不是虚函数,所以不会放进虚表,所以 Func3 不会在虚表里面
虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr
内存窗口查看
总结一下派生类的虚表生成:
- 先将基类中的虚表内容拷贝一份到派生类虚表中
- 如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
- 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后
虚表是什么阶段初始化的?虚函数存在哪里?
- 虚表实际上是在构造函数初始化列表阶段进行初始化的
- 注意虚表当中存的是虚函数的地址不是虚函数,虚函数和普通函数一样,都是存在代码段的,只是他的地址又存到了虚表当中。另外,对象中存的是指向虚表的指针,不是虚表
虚表存在哪里?
可以通过以下段代码进行判断
//基类
class Base
{
public:
//虚函数
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
//虚函数
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
//普通成员函数
void Func3()
{
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
};
//派生类
class Derive : public Base
{
public:
//重写虚函数Func1
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
int main()
{
int a = 0;
cout << "栈:" << &a << endl;
int* p1 = new int;
cout << "堆:" << p1 << endl;
const char* str = "hello world";
cout << "代码段/常量区:" << (void*)str << endl;
static int b = 0;
cout << "静态区/数据段:" << &b << endl;
Base b1;
cout << "虚表:" << (void*)*((int*)&b1) << endl;
Derive de;
cout << "虚表:" << (void*)*((int*)&de) << endl;
return 0;
}
运行结果,可以发现代码当中打印了对象b1 和 对象de 的虚表指针,也就是虚表的地址,可以发现虚表地址与代码段的地址非常接近,由此我们可以得出虚表实际上是存在代码段的
4.2 多态原理
谈论那么久的虚表,那多态的原理到底是什么??
下面进行测试,测试代码,依旧是买票的例子
//基类
class Person
{
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
private:
int _a = 1;
};
//派生类
class Student : public Person
{
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
private:
int _b = 2;
};
void Func(Person& p)//基类引用
{
p.BuyTicket();//基类引用调用虚函数
}
int main()
{
Person A;
Func(A);
Student B;
Func(B);
return 0;
}
也是与上面一样,进行调试查看
- 对象A中包含一个虚表指针和 _a,对象 B 中也包含一个虚表指针 和 两个成员变量 _a 和 _b
- 这两个对象当中的虚表指针分别指向自己的虚表
- 一开始继承时,子类和父类的虚表都保存&Person::BuyTicket()
- 当子类重写父类的虚函数时,子类的虚表内部会替换成子类的虚函数地址
为了方便观察,找了一个图,动物叫和猫叫。
子类重写父类的虚函数
我们继续调试前面代码:
模型如下
进行分析:
- 基类引用 p 指向 A 对象时,p.BuyTicket() 在 A 的虚表中找到虚函数是 Person::BuyTicket
- 基类引用 p 指向 B 对象时,p.BuyTicket() 在 B 的虚表中找到虚函数是 Student::BuyTicket
传递的是 A 对象,就直接去基类虚表中寻找该虚函数的地址
传递的是 B 对象,先会完成切片操作,再去派生类的虚表中寻找 重写的虚函数,进而完成调用,实现多态
基类的指针或者引用,调用谁,就去谁的虚表中找到对应的位置的虚函数,就实现了对应的多态的功能
这样就实现出了不同对象去完成同一行为时,展现出不同的形态
注意:同类型的对象共享一张虚表,他们的虚表指针指向的虚表是一样的
总结:
- 构成多态,指向谁就调用谁的虚函数,跟对象有关
- 不构成多态,对象类型是什么就调用谁的虚函数,跟类型有关
4.3 动态绑定与静态绑定
静态绑定:静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
动态绑定:动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态
测试代码
//基类
class Person
{
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
//派生类
class Student : public Person
{
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
int main()
{
Student A;
Person B = A;//不够成多态
B.BuyTicket();
Person* C = &A;//够成多态
C->BuyTicket();
Person& D = A;//够成多态
D.BuyTicket();
return 0;
}
在调试模式下查看反汇编
不构成多态的对象 A 函数调用的反汇编如下:
下面看构成多态的对象 C 和 D 函数调用的反汇编如下:
我们进行对比一下函数调用的 call
484: Person B = A;//不够成多态
004027A6 call Person::BuyTicket (04011BDh)
488: C->BuyTicket();
004027BD call eax
490: D.BuyTicket();
004027D8 call eax
- 对于对象B,首先 BuyTicket 虽然是虚函数,但是 B 对象不满足多态的条件,所以这里是普通函数的调用转换成地址时,是在编译时已经从符号表确认了函数的地址,直接 call 地址
- 对于对象 C、D,call eax中存虚函数的指针。这里可以看出满足多态的调用,不是在编译时确定的,是运行起来以后到对象的中取找的
这样就很好的体现了静态绑定是在编译时确定的,而动态绑定是在运行时确定的
五、单继承和多继承关系的虚函数表
5.1 单继承中的虚函数表
直接上代码:
class Base {
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
private:
int a;
};
class Derive :public Base {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
virtual void func4() { cout << "Derive::func4" << endl; }
private:
int b;
};
int main()
{
Base b;
Derive d;
return 0;
}
在调试下的监视窗口查看
很奇怪,在监视窗口居然没有看见 func3 和 func4 , 原因是可能是VS编译器的监视窗口故意隐藏了这两个函数,也可以认为这是一个小bug,此时如果我们想要看到派生类对象完整的虚表有两个方法
直接去内存窗口查看虚表,看看有没有 func3 和 func4,内存窗口确实有 func3 和 func4
但是这种方式我们很难看出,另一个方法就是把虚表打印到输出屏上
我们可以使用以下代码,打印上述基类和派生类对象的虚表内容,在打印过程中可以顺便用虚函数地址调用对应的虚函数,从而打印出虚函数的函数名,这样可以进一步确定虚表当中存储的是哪一个函数的地址
typedef void(*VFPTR) (); //虚函数指针类型重命名
//打印虚表地址及其内容
void PrintVTable(VFPTR vTable[])
{
// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf(" 第%d个虚函数地址:0x%x-->", i, vTable[i]);
vTable[i](); // 使用虚函数地址调用虚函数
}
cout << endl;
}
// 思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr
// 1.先取b的地址,强转成一个int*的指针
// 2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
// 3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
// 4.虚表指针传递给PrintVTable进行打印虚表
// 5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的 - 生成 - 清理解决方案,再编译就好了。
int main()
{
Base b;
Derive d;
VFPTR* vTableb = (VFPTR*)(*(int*)&b);
PrintVTable(vTableb);
VFPTR* vTabled = (VFPTR*)(*(int*)&d);
PrintVTable(vTabled);
return 0;
}
运行结果,可以打印虚表,即可以查看完整的虚表了
5.2 多继承中的虚函数表
测试代码
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(*VFPTR) (); //虚函数指针类型重命名
//打印虚表地址及其内容
void PrintVTable(VFPTR vTable[])
{
// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf(" 第%d个虚函数地址:0x%x-->", i, vTable[i]);
vTable[i](); // 使用虚函数地址调用虚函数
}
cout << endl;
}
int main()
{
Base1 b1;
Base2 b2;
PrintVTable((VFPTR*)(*(int*)&b1)); //打印基类对象b1的虚表地址及其内容
PrintVTable((VFPTR*)(*(int*)&b2)); //打印基类对象b2的虚表地址及其内容
Derive d;
VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
PrintVTable(vTableb1);//打印派生类对象d的第一个虚表地址及其内容
VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
PrintVTable(vTableb2);//打印派生类对象d的第2个虚表地址及其内容
return 0;
}
两个基类 Base1 和 Base2 的虚表模型如下:
派生类的虚表模更复杂了,派生类的虚表模型如下:
内存查看
模型如下
在多继承关系当中,派生类的虚表生成过程如下:
- 分别继承各个基类的虚表内容到派生类的各个虚表当中
- 对派生类重写了的虚函数地址进行覆盖(派生类中的各个虚表中存有该被重写虚函数地址的都需要进行覆盖),比如func1
- 在派生类第一个继承基类部分的虚表当中新增派生类当中新的虚函数地址,比如 func3
运行结果如下
5.3. 菱形继承、菱形虚拟继承
实际中我们不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的模型,访问基类成员有一定得性能损耗。所以菱形继承、菱形虚拟继承我们的虚表我们就不看了,一般我们也不需要研究清楚,因为实际中很少用。
--------------------- END ----------------------
「 作者 」 代码探秘者
「 更新 」 2024 11.01