多态即多种形态。在Linux基础IO一文中@一切皆文件,咱们说过语言上的多态是漫长软件开发过程中探索出的实现“一切皆...”的高级版本。那现在就来了解多态的语法细节。
不要害怕!不要害怕!不要害怕!怕了咱们就先玩儿完了!!
正文开始@一个人的乐队:guitar:
前置文章:继承;类和对象;指针进阶
反爬链接:
1. 多态
多态分为两类 ——
静态的多态:函数重载。传入不同参数,看起来调用一个函数,但是有不同的行为,最典型的比如流插入流提取的“自动识别类型”。
int i = 10; double d = 1.1; cout << i; //cout.operator<<(int) cout << d; //cout.operator<<(double)
- 动态的多态:一个父类的引用或指针调用同一个函数,传递不同的对象,会调用不同的函数。
所谓静态还是动态在于 ——
- 静态:在编译时决议,(编译时决定调用谁)
- 动态:在运行时决议,(运行时决定调用谁)
本文重点讨论的是动态多态。
现在人类有一个买票行为,我们想让不同身份的人,买票的价格不同,就可以借助多态实现。
上层看来我们都是人类,只不过传入对象的身份不同,因买票行为也不同。
这是怎么实现的?这是传子类对象时发生切片,与类型转换无关,否则会产生临时变量,临时变量具有常属性,需要加const
class Person {
public:
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
class Student : public Person {
public:
virtual void BuyTicket()
{
cout << "买票-半价" << endl;
}
};
void Func(Person& p) //父类的指针/引用
{
p.BuyTicket(); /*多态*/
}
int main()
{
Person p;
Student s;
Func(p); //传父类对象 —— 调父类的
Func(s); //传子类对象 —— 调子类的
return 0;
}
子类中的函数满足三同(返回值类型、函数名、参数列表完全相同)的虚函数这两个条件,叫做重写(覆盖)。
注:此时函数名相同也不再是所谓的构成隐藏,实际上,父子类的同名函数非重写即隐藏。
这样就做到了,同一函数不同类型的人来做,有不同行为 ——
" title="">
2. 多态的定义和实现
2.1 多态的条件
:purple_heart: 多态有两个条件,缺一不可 ——
- 必须通过基类的指针或者引用调用虚函数,对象没有多态。
被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写(底层原理后面看)
- 虚函数:被
virtual
修饰的类成员函数 ( 准确的说,只有类的非静态成员函数才能是虚函数,其他函数不能成为虚函数,原理虚函数表详谈) - 重写要求:虚函数 + 三同,总之,这条可以算作四个条件
- 虚函数:被
下面进行一系列验证,first one,若用对象来调用,没有多态 ——
" title="">
- 构成多态,传的哪个类型的对象,调用的就是哪个类型的虚函数 - 跟对象有关
- 不构成多态,调用的就是p类型函数 - 跟类型有关
思考为什么一定要是父类的指针或引用呢?因为这样才能既接收基类对象,又接收子类对象。这样才能在上层看来,"一切皆...",而在其下各自行为不同。
那父类对象呢?父类对象不也是既能接收基类对象,又接收子类对象吗?yes...但对于引用,会从一而终,没法一会儿父类对象一会儿子类对象;对于指针,
next,若不是虚函数, 没有多态,所以调用什么跟p的类型有关 (这跟与隐藏无关,隐藏是针对子类对象调用的而言的) ——
" title="">
then,破坏参数,也没有多态,所以调用什么跟p的类型有关——
" title="">
last,返回值不同,破坏多态,直接报错咧?! ——
" title="">
它们背后的原理在第5小节详谈。
2.2 虚函数重写的两个例外
虚函数重写条件例外。
2.2.1 协变
协变,返回值是父子关系的指针或引用 (不能是对象),依然可以构成多态。
" title="">
用处很少。
2.2.2 析构函数的重写
如果析构函数是虚函数,是否构成重写?yes,是因为在继承我们就说过,析构函数名儿被特殊处理了,都处理成了destructor
,至于为什么要特殊处理,就是源于多态。
" title="">
如上我们发现,对于普通对象,不构成多态,都能正确调用:父类的调用父类的;子类的调用子类的,完了自动调用父类的,这我们在继承时候就谈过。就算我不把析构函数写成虚函数,我也不重写,都没关系。你可以自己验证。
but~ 下面这时候就糟了,这也是面试时的高频问题:
:purple_heart: 那什么场景下,析构函数要是虚函数呢?
如果是动态申请的子类对象,给了父类的指针,若想正确调用,那么析构函数需要是虚函数(右图)
因为如果不是虚函数,不构成多态,那与类型有关,都会去调用父类的析构函数(左图),但是这样会导致子类对象可能有资源未被清理;我们希望指向父类调用父类的,指向子类调用子类的(完了再调用父类的),那就需要满足多态的条件,完成重写
当然了,析构函数的重写特简单,它本身函数名“相同”,没参数,自己再加一个virtual
就行 ——
" title="">
其他场景,析构函数是不是虚函数都可以,都可以正确调用析构函数。
当然了,我们推荐在继承体系中,把析构函数写成虚函数。
2.3 只有父类带 virtual 的情况
虚函数,允许父子类两个都是虚函数 或 只有父类是虚函数也行。这其实是C++不是很规范的地方,建议两个都写上virtual
.
这是因为虽然子类没带virtual
,但是它继承了父类的虚函数属性。
" title="">
大佬这样设计的初衷是,考虑到“析构函数”。。因为在一个巨大的项目中父&子类可能不是一个人儿写的,如果只是父类加了virtual而子类没加,因此不构成多态,没有调用子类析构函数,就可能有内存泄漏问题。
那经过大佬的一番思索,在一个项目中,最好在父类析构函数加上virtual,那么这个漏洞确实就被完完全全的补上了。
但是大佬没有考虑到,这又构成了其他的歧义。令人震惊的事情发生了,如下图,就算Buyticket()
是private的,还是继承了父类的属性,能够调的到,震惊!!
不过没关系,我们学完虚表就知道原因了。
" title="">
建议我们自己写的时候,都加上virtual,肯定不会出错。
2.4 C++11 final & override
2.4.1 final
可以修饰类和重写函数。
:purple_heart:1. 设计一个不能被继承的类?
在C++11没有引入final
时,C++98中通过间接限制,是把父类构造函数设为私有,因为子类一定要调用父类的构造函数要初始化父类的部分,但是private对子类不可见(左图),因此这样无法实例化子类对象。同时这也带来了问题,父类A
也构造不了喂!
" title="">
这就是要蛋没鸡,要鸡没蛋的问题。。。现在父类构造函数被private
修饰,也就是我在类外调不到它(嘘~我在类内还可以调到),可是成员函数的调用本来又依赖对象,可是我连对象都没有呜呜(我就是想造对象)
我们可以造一个静态成员函数来造对象(右图),在Java中就是经典的静态工厂方法(算了我也不会jvav,那为什么用静态函数呢?因为静态函数的调用不依赖于对象而依赖于类,可以通过类域直接访问。
但是在C++11就优化掉了这个复杂的方法,加final
直接限制 ——
" title="">
:purple_heart:2. C++11中final
还可以限制重写
修饰虚函数,限制它不能被子类中的虚函数重写。
" title="">
2.4.2 override
override
放在子类重写的虚函数后面,帮助检查是否完成重写,没有重写会报错
在Java中有@override来帮助检查 (算了我也不会jvav
" title="">
3. 重载 vs 重写 vs 隐藏
" title="">
4. 抽象类
:yellow_heart: 包含纯虚函数的类叫做抽象类(接口类)。在虚函数的后面写上=0
,则这个函数为纯虚函数。
纯虚函数一般只声明,不实现,抽象类不能实例化出对象。
(它是可以实现的,只不过实现的没有价值。为什么呢?因为,抽象类不能实例化出对象)
哦,那好吧,我总可以定义一个Car的指针/引用来调用函数吧(如下图)。极端一点给一个nullptr
,发现可以编译通过,你可能有些震惊,p->Drive();
这不是空指针解引用了吗?嘿嘿,这是我们在类和对象 - 隐藏的this
指针一文中就讨论过的问题,这只是把nullptr传给了隐藏的this指针而已,并没有发生解引用,所以编译是通过了(我还用p->func();
调用普通函数再次验证了这件事儿)
当然了,再看这调用虚函数的p->Drive();
看上去是编译通过了,但是运行起来什么也没有打印,这是不是崩了?一调试,诶确实崩了。这就关系到后面的虚函数表了,在这里浅说一下,我们调用的这个虚函数地址存放在虚函数表中,虚表指针在对象中,需要通过this
指针解引用去找到,这理所当然的崩了;那对于p->func();
呢,确实也没崩,因为普通成员函数存放在公共的代码段,不在对象中,不需要this
指针去找。
" title="">
那好吧,既然我也造不出Car父类对象给Car*,那我造一个子类对象,darn it,其派生类继承后也不能实例化出对象(左图),因为继承了抽象类后,这个派生类就继承了纯虚函数,那它同样也是一个抽象类!
只有重写纯虚函数,派生类才能实例化出对象(右图)。所以呀,抽象类本质上强制继承它的子类完成虚函数重写。
现在就能调用到子类的虚函数了(如右图)
" title="">
综上,你真没必要去实现纯虚函数,因为实现了,也没人调用你这个父类实现,声明一下即可。
上述提到的这种种现象,你都可以自己实操一下,代码贴给宝子们了 ——
class Car
{
public:
// virtual void Drive() = 0;
virtual void Drive() = 0
{
cout << "virtual void Drive() = 0" << endl;
}
void func()
{
cout << "void func()" << endl;
}
};
class Benz :public Car
{
public:
public:
virtual void Drive()
{
cout << "Benz-舒适" << endl;
}
};
int main()
{
Car* p = new Benz;
p->Drive();
return 0;
}
什么样的类要设计为抽象类呢?一个类型如果在现实世界中,没有具体的对应实物,就定义为抽象类,这个类没必要实例化出来。这话也够抽象的了(
在Java中,这应该是类似一个叫做“接口”的语法,确实很常用,算了我不在这儿胡说了( 我也不会jvav
override只是在语法上检查是否完成重写。
继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。
5. 多态的原理
5.1 虚函数表
:yellow_heart: 引入
// sizeof(Base)是多少?
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
virtual void Func2()
{
cout << "Func2()" << endl;
}
private:
int _b = 1;
char _ch = 'A';
};
由结构体对齐规则,我就知道肯定不是8
哈哈( ——
那是多了什么呢?通过监视窗口,发现这个对象多了一个成员,虚函数表指针_vfptr
(简称虚表指针) ,所谓的虚函数表就是一个指针数组,里面存放的是函数指针(虚函数地址),一般这个数组的最后面放了一个nullptr
——
这就实在的解释了我们在抽象类中讨论好久的问题@4
5.2 多态的原理
虚函数表是理解多态原理的关键,下面将集中从原理层解释2.*小节中的现象,以如下代码为例 ——
class Person {
public:
virtual void BuyTicket()
{ cout << "买票-全价" << endl; }
protected:
int _a = 0;
};
class Student : public Person
{
public:
virtual void BuyTicket()
{ cout << "买票-半价" << endl; }
protected:
int _b = 0;
};
void Func(Person& p) {
p.BuyTicket();
}
int main()
{
Person Peter;
Func(Peter);
Student Mable;
Func(Mable);
return 0;
}
虚函数的“重写”也叫“覆盖”,重写是语法上的概念,覆盖是原理层的概念。子类继承父类的虚函数,可以认为深拷贝了一份虚函数表,没重写时,子类与父类虚表完全相同;若重写了,便会用新地址覆盖。这些及建议你都可以,或者你也应该自己打开监视窗口动手验证 (效果已经展示在下下图中了),代码贴给宝子们了。
转到反汇编可以发现,对于普通成员函数的调用,是在编译后就已经确定了调用地址(橙色的);却发现,给父类/子类对象,调用虚函数p.BuyTichet();
的汇编代码一模一样儿,那一样儿是怎样实现多态的呢?发现此时调用函数时,不再是直接确定地址,而是借助了eax
这个寄存器,这是多态原理的关键。
" title="">
(这段汇编指令不强求看懂,但你应该大胆猜测,这是在去虚表中拿待调用的虚函数地址,放入eax
中)
:purple_heart: 多态的原理:基类的指针/引用指向谁,就去谁的虚函数表中找到对应位置的虚函数进行调用。这是在运行时确定的,所以这叫“动态的多态”。
" title="">
:yellow_heart: 那现在我们再来反思,为什么一定要是基类的指针或引用类型,而对象不行,对象不是也可以传父类/子类切片吗?@2.1多态的条件
- 引用切片就是作为给过去的子类的父类部分的别名,它的
_vfptr
就是理所当然的和子类指向同一空间,哦那就可以实现多态了; - 而对象切片,我们打开监视窗口, 观察发现相当于拷贝构造。对象的确可以接收父类或子类,但并没有把
_vfptr
拷贝过来(拷贝过来可就乱了,后面说),也就是此时这个父类对象的_vfptr
仍指向父类虚表,那当然就仍然调用的是父类的虚函数,就算你传参时确实好像发生了子类切片,就算你重写了虚函数,但都没用呀,因此没法实现多态——
" title="">
同类型的对象虚表指针_vfptr
是否一样?是的,同类型的对象,虚表指针指向同一张虚表。
也就是说多个对象共享一个虚函数,虚表中的内容是不允许修改的。
" title="">
我们总说多态是“运行时多态”,不构成多态时,编译时就会确定调用函数的地址;构成多态,编译时,不能确定调用哪个函数(eax
),它还不知道传的是啥对象,运行时,才确定传入的是父类还是子类对象,去p
指向对象的虚表中找到虚函数地址。此时p
作为父类对象/子类对象那部分的引用 (指针视角同理,在原理层也是“一切皆...”的视角)。 (不要搞混,编译时是会确定虚函数地址的,不过是运行时再确定填入哪个对象的虚表)。
究竟是怎样?判断的唯一标准就是“是否构成多态”,又回归到构成多态的两个条件(1+4)。
什么?!你说我编译器处理的时候可以把子类的_vfptr
强制拷贝过来,这样就能实现多态了?是,但那可就乱了,因为在此之前你可以做任意行为,你都不知道这个对象里存的是父类的虚表还是子类的虚表,会造成混乱的结果:比如一父类对象调用的是子类的析构函数。。。
:yellow_heart: 为什子类么重写虚函数时,设置为private
权限,依然能调用到,呈现多态?
因为。。因为编译器是不会检查出来的,它看到的是一个父类调用虚函数p.BuyTicket();
,看到的就是父类的publlic接口。子类对象该去虚表中找,能找到就能调用,虚表中也不分公私有。重写是一种“接口继承”。
" title="">
这么说,C++的访问修饰符不一定安全。。那我能不能通过一些bug的操作,访问到私有的虚函数呢?是可以的。。这个我们在6.1小节会给出。
直接顺着虚表拿到函数地址,这种非常规的操作不受公私有限制。
5.3 虚函数表在哪
普通函数和虚函数存储的位置是否一样?一样的,都在公共代码段,只不过虚函数要把地址存一份到虚表,以实现多态。
那虚表在哪里呢?从前从前,我们就铺垫过虚函数表不能修改,所以我大胆的猜测是在常量区嘿嘿。
我们写一段代码来验证一下 ——
" title="">
所以,虚函数表是存在“常量区”的。
(在操作系统角度,是不区分常量区和代码段的,都叫代码段。这儿是语言角度)
代码贴给宝子们了 ——
int main()
{
int* ptr = (int*)malloc(4);
printf("heap: %p\n", ptr);
int a = 0;
printf("stack: %p\n", &a);
static int s = 0;
printf("数据段:%p\n", &s);
const char* p = "always";
printf("常量区:%p\n", p);
printf("代码段:%p\n", &Base::func1);
Base b;
// 取对象头4/8个字节 —— 强转(Base* -> int*) —— 再解引用拿到_vfptr
printf("虚函数表: %p\n", *((int*)&b));
return 0;
}
//ps: 有一小点代码在上面
6. 单继承和多继承关系中的虚函数表
首先我们要再来观察如下代码在监视窗中的状况,这儿vs起到了很好的误导作用,我们要解释一下,以便后续内容的正常进行,当然了其实并不复杂 ——
class Base
{
private:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
void func3() { cout << "Base::func3" << endl; }
private:
int _a = 0;
};
class Derive : public Base
{
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void fun4() { cout << "Derive::func4()" << endl; }
private:
int _b = 1;
};
int main()
{
Base b;
Derive d;
return 0;
}
打开监视 窗口观察,发现父类对象虚表中如期有两个虚函数地址func1
和func2
;子类继承父类,可以认为深拷贝了虚函数表,重写func1
,覆盖了原来的地址,其中的func2
安然不动。
对于子类新增加的虚函数func4
,却没看到,是被vs给隐藏了,我们暂且通过内存 窗口观察(如下图) ——
" title="">
令人疑惑的地方又来了,这内存 中的Derive::func4
怎么跟监视 窗的&Derive::func4
地址不一样呀。
事实上,虚函数表并不是真实地址,而是这句jmp
跳转指令的地址(橙色的)!&Derive::func4
这样取到的,即jmp后面跟的地址(绿色的),才是虚函数的真实地址.
" title="">
6.0 打印虚函数表
虚表指针是在什么时候初始化的?是在构造函数的初始化列表初始化的。
前文我们就说过,有时虚函数地址被隐藏掉了,之前我们委屈的在内存窗口中观察,现在我们来学习打印虚函数表。
从前从前,我们仔细研究过“函数指针”如何定义变量,它与一般int a=0;
这样类型后跟名字不同,是混杂在其中的,我们在typedef
时,依然保留了这个原则。
我们已经熟知,虚表是一个函数指针数组,打印它并不难,这个经验来自于我们学习Linux环境变量时打印过环境表、命令行参数数组 ——
typedef void(*VF_PTR)(); //类型重定义:(虚)函数指针
void VFTable(VF_PTR table[])
{
for (int i = 0; table[i] != nullptr; i++)
{
printf("vft[%d]: %p\n", i, table[i]);
}
}
那么在调用这个函数的时候,就需要传入虚函数表的地址,即指针数组的(首元素)地址,即对象中的虚表指针_vfptr
。
问题就转化成了如何取到对象头4/8个字节呢?
嗯...没法直接转成int,那取&
个地址,指针的类型决定看待内存的视角,如果强转为(int*)
,再解引用拿的就是头四个字节。可是传入的参数类型还不匹配,那就再(VF_PTR*)
强转一下 ——
Base b;
PrintVFTable((VF_PTR*)(*(int*)&b));
嗯如果你直接看这一坨当然会有些眼晕,但其实只要你能稍稍的独立思考就很很很简单!
(注:vs有一些bug,你一打印可能打印出了很多无关地址,可以清理一下解决方案;或者通过函数地址来手动调用函数验证,方法如下)
typedef void(*VF_PTR)(); //(虚)函数指针
void PrintVFTable(VF_PTR table[])
{
for (int i = 0; table[i] != nullptr; i++)
{
printf("vft[%d]: %p -> ", i, table[i]);
VF_PTR f = table[i];
f();
}
}
是的,在尝试写它的时候,我就感受到这有多bug。。按理来说,这些函数都要通过对象调用,传入this指针,受到访问修饰限定;但是在这儿,我都没搞,就直接拿着函数地址调用。。也就是说此时this指针是一个随机值,如果访问成员就可能出现一些越界访问,打印出随机值甚至崩溃。。
我们也完全可以通过这种bug的方式,访问到私有虚函数,所以虚表是有安全隐患的。。
Base b;
PrintVFTable((VF_PTR*)(*(int*)&b)); // 32位
PrintVFTable((VF_PTR*)(*(long long*)&b)); // 64位
- 32位平台,用
(int*)
强转。 - 64位平台,用
(long long*)
强转。用double好像行但其实不太行,因为会有精度丢失 ( double在内存中确实占了64个字节,但是double类型的有效位M
只有52位,虽然我们把取出的数字按照地址看待,但是和double转int同理)
震惊的是在32位平台下,用(long long*)
强转的居然还能正常跑。。
是因为之后再用(VF_PTR*)
这个函数指针强转,8字节恰好截断到头上4字节。
:yellow_heart: 我们想要探索出,32/64位平台下能自适应的方式 ——
Base b;
PrintVFTable((VF_PTR*)(*(int*)&b)); // 32位
PrintVFTable((VF_PTR*)(*(long long*)&b)); // 64位
PrintVFTable((VF_PTR*)(*(void**)&b)); // 32/64位
怎么忽然就是是void**
呢?(int*)
解引用看一个int的大小,(long long*)
解引用看的是long long的大小,void 不能解引用,这(void**)
解引用看的是void 的大小,void* 的大小就和平台相关。
当然了,这样说char /int 什么的都可以,只要是二级指针都可以。
:yellow_heart: 也可以条件编译 ——
Base b;
#ifdef _WIN64
PrintVFTable((VF_PTR*)(*(long long*)&b));
#else
PrintVFTable((VF_PTR*)(*(int*)&b));
#endif
注:_WIN32:Defined for applications for Win32 and Win64. Always defined. 不能用于判断平台环境。
_WIN64:Defined for applications for Win64.
那好嘞,我们现在就能方便的看一看子类虚表了。
6.1 单继承的虚函数表
代码贴给宝子们了,你最好,哦不,你也应该自己验证一下 ——
class Base
{
private:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
private:
int _a = 0;
};
class Derive :public Base
{
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
void fun4() { cout << "func4()" << endl; }
private:
int _b = 1;
};
int main()
{
Base b;
PrintVFTable((VF_PTR*)(*(void**)&b));
Derive d;
PrintVFTable((VF_PTR*)(*(void**)&d));
return 0;
}
重写的fuc1,拷贝继承下来的func2,自己的func3 ——
" title="">
6.2 多继承的虚函数表
代码贴给宝子们了,你最好,哦或者说,你也应该自己验证一下 ——
class Base1 {
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
private:
int _b1 = 0;
};
class Base2 {
public:
virtual void func1() { cout << "Base2::func1" << endl; }
virtual void func2() { cout << "Base2::func2" << endl; }
private:
int _b2 = 0;
};
class Derive : public Base1, public Base2 {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
private:
int _d1 = 0;
};
int main()
{
Derive d;
Base1* p1 = &d;
p1->func1();
Base2* p2 = &d;
p2->func1();
return 0;
}
打开监视 窗口观察,此时Derive
对象中理所当然的有两个虚表,并且即使子类重写了func1
后,你发现这对象虚表中,Base1和Base2的虚函数func1
的地址不一样,你早就不应该感到惊奇,因为这时jmp
跳转指令的地址,最终会一跳到同一位置执行函数Derive::func1
的 ——
" title="">
发现p2->func1()
调用函数时,还跳了好多层。这是为了做准备工作ecx-8
,修正this指针(eax
),为什么呢?调用虚函数时,要传递this指针,-8由指向Base1
到指向Base2
,从而看到对应类型视角下的那部分。当然,这你了解即可。
:purple_heart: 我们需要打印虚函数表,来观察多继承下的对象模型:
由于子类中有两份虚表,我们需要再认真思考如何传入第二个虚表指针_vfptr
——
(建议,哦不,你也应该独立思考,因为你直接看下面这一坨肯定会眼晕,当然了,我就是知道你会眼晕,所以我好好给你解释)
int main()
{
Base1 b1;
PrintVFTable((VF_PTR*)(*(void**)&b1));
Base2 b2;
PrintVFTable((VF_PTR*)(*(void**)&b2));
cout << "_____________________________________" << endl;
Derive d;
PrintVFTable((VF_PTR*)(*(void**)&d));
/*打印第二个虚函数表*/
PrintVFTable((VF_PTR*)(*(void**)((char*)&d+sizeof(Base1))));
return 0;
}
" title="">
切片时会引起指针的自动偏移,可以直接打印:) ——
Base1* p1 = &d;
Base2* p2 = &d; // 切片 - 也干了类似 (char*)&d+sizeof(Base1) 这样的操作
PrintVFTable((VF_PTR*)*((void**)p2));
当然了,在汇编角度它一定也做了我们手动移动类似的事情。
:yellow_heart: 发现多继承时,子类自己的虚函数Derive::func3
只放在第一个父亲的虚表中 ——
" title="">
你可以把Base2放到前面试试。
6.3 菱形继承的虚函数表
实际中我们不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的模型,访问基类成员有一定得性能损耗。所以菱形继承、菱形虚拟继承我们的虚表我们就不看了,一般我们也不需要研究清楚,因为实际中很少用。可以去看下面的两篇链接:
当然了,还是简单跟宝子们说说,来一段老朋友代码,打开监视 窗口 ——
class A
{
public:
virtual void f()
{}
public:
int _a;
};
class B : virtual public A
{
public:
int _b;
};
class C : virtual public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
//d._a = 0; //不存在二义性,可以直接找
return 0;
}
" title="">
另外,在钻石型继承中,如果B和C都重写了A的虚函数func1
,那么D必须重写func1
,否则会报错“D”:“void A::f1(void)”的不明确继承,因为这儿是虚继承,共用一个虚表,不知道用哪个重写,看如下代码:
public:
virtual void f1() {}
public:
int _a;
};
class B : virtual public A
{
public:
virtual void f1() {}
virtual void f2() {}
public:
int _b;
};
class C : virtual public A
{
public:
virtual void f1() {}
virtual void f2() {}
public:
int _c;
};
class D : public B, public C
{
public:
virtual void f1() {}
public:
int _d;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
在继承中@7.3 菱形继承的原理,我们说过虚基表中,曾经内容是00000000是为其他东西预留的,那它究竟是什么呢?这是找虚表的偏移量。
" title="">
还是挺麻烦的,所以你没事儿别定义菱形继承(
7. 总结
来几道经典的问答题小伙子小姑娘?!不完全给答案是为了让你别背~
- 什么是多态?
- 什么是重载、重写(覆盖)、重定义(隐藏)?
- 多态的实现原理?
- inline函数可以是虚函数吗?
准确的说不可以,因为内联函数没有地址,但是虚函数地址要被填入到虚表中。不过是可以编译通过的,因为inline只是个建议,到底有没有展开要视情况而定:若调用时不构成多态,保持inline属性;若构成多态,则没有inline属性。
- 静态成员函数可以是虚函数吗? 不可以,会直接报错“virtual”不能和"static"一起使用。
因为静态成员函数没有this指针,只能使用类型::
成员函数的调用方式,这样无法构成多态,而虚函数的价值就在于重写后构成多态。 - 构造函数可以是虚函数吗? nope
同样的,构造函数设为虚函数没有价值,虚函数的意义就在于构成多态调用。多态调用就要去虚函数表中查找虚函数,这又涉及先有鸡还是先有蛋的问题,因为对象中的虚表中的虚函数指针,就是在构造函数初始化列表阶段才初始化的。 - 析构函数可以是虚函数吗? 什么场景下析构函数必须是虚函数?
yes,并且继承体系中推荐写成虚函数。
- 对象访问普通函数更快还是虚函数更快?要看是否构成多态。
如果不构成多态,那都是编译时确定调用函数的地址,一样快;如果构成多态,那么虚函数调用是运行时虚函数表中确定函数地址,普通函数编译时直接确定地址,则普通函数更快。 - 虚函数表是在什么阶段生成的?存在于哪?
注:别把虚基表和虚函数表搞混了。编译阶段;常量区。
- C++菱形继承的问题?虚继承原理?
- 什么是抽象类?抽象类的作用?
强制子类重写虚函数,另外体现了接口继承关系。
持续更新@一个人的乐队:guitar:
所以你看,其实也没多复杂。还是那句话,不要害怕,不要害怕,怕了咱们就先玩儿完了!!!