🐱作者:一只大喵咪1201
🐱专栏:《C++学习》
🔥格言:你只管努力,剩下的交给时间!
🍕多态
- 多态概念:去完成某个行为,当不同的对象去完成时会产生出不同的状态。
拿生活中买火车票的例子来说,买票的人分别是普通人,学生,军人。
- 普通人买的是全价票
- 学生买的是半价票
- 军人是优先买票
同样是买票这个行为,不同人群得到的结果,行为都不同,这就是多态。体现在代码中就是:
class Person
{
public:
virtual void BuyTicket()
{
cout << "Person : 全价票" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "Student : 半价票" << endl;
}
};
class Solider : public Person
{
public:
virtual void BuyTicket()
{
cout << "Solider : 优先买票" << endl;
}
};
void Func(Person& p)
{
p.BuyTicket();
}
可以看到,不同的人做买票这件事时,状态不一样。这里的状态包括结果行为等等。
有人会觉得,这不是不同的类对象调用各自的成员函数吗?这有什么特殊的。
- 给Func函数传参的时候确实传的是不同的类对象。
- 但是函数的形参只是Person&,它始终没有改变。
也就是说,无论外面给还是传什么类型,是子类都会发生切片,但是函数的形参都不会变,一直都是父类的引用,也就意味着在Func内调用的成员函数始终都是父类Person的成员函数。这是我们之前学习到的内容。
但是此时产生的结果却是三种,而不是都是调用父类Person成员函数的结果,这和我们之前所认识的矛盾。
- 站在Func函数的角度去看,它只知道自己是父类的引用,调用的函数是BuyTicket()。
但是父类的引用对象调用的并不是同一个成员函数,这就是多态。
🍟构成多态的条件
虚函数:
- 被virtual修饰的函数就叫做虚函数。
上面类中的成员函数就是虚函数。
- 它和虚拟继承不一样,虽然都是被virtual修饰,就像继承关系和访问限定符一样,虽然关键字都一样,但是意义不一样,各论各的。
重写/覆盖:
- 虚函数的重写(覆盖):子类中有一个跟父类完全相同的虚函数(函数名相同,返回类型相同,形参相同)。
- 重写的前提:父类和子类中必须都是虚函数。
上图中,父类中的BuyTicket就和子类中的BuyTicket构成了重新关系。
- 子类和父类的虚函数完全相同,但是实现却不同。
- 这是因为在子类继承了父类的虚函数后,重写了父类虚函数的实现。
它和继承中的隐藏/重定义有点相似:
它们的关系就像上图所示,重写是在隐藏的基础上多加两个条件,条件更加苛刻。
- 但是符合重写就是重写,而不是隐藏。
虚函数重写的两个例外:
- 子类的虚函数可以不加virtual。
父类虚函数有virtual,子类没有,此时仍然构成重写。
- 因为子类会继承父类的虚函数,所以认为在继承之后,子类的虚函数同样有了virtual,这是从父类继承下来的。
但是一般建议父子类的虚函数都加上virtual。
- 协变:构成重写的虚函数返回值类型可以不同,但是必须是父子类关系的引用或者指针。
如上图,此时构成重写的虚函数返回类型就不相同,但是属于父子关系的指针,当然也可以是引用。
- 返回类型可以是其他类的父子关系的指针或引用类型。
这种重写的两个虚函数的返回类型不同的例外情况叫做协变。
多态的条件:
- 必须是父子类关系中,构成重写的两个虚函数。
- 父类的指针或者引用去调用虚函数。
必须符合这两个条件才能构成多态。
再看最开始的代码。
- 在Func函数中,调用的成员函数BuyTicket在父子类中构成重写。
- 重写函数是由父类的引用调用。
完全符合多态的两个条件,所以结果也是多态的,调用了三个不同的成员函数。
多态调用和普通调用有什么区别呢?
- 普通调用:跟调用对象有关。
- 多态调用:跟引用/指针指向的对象有关。
普通调用时调用的时对象所对应的成员函数,多态调用时调用的是指针或者引用指向的那个对象的成员函数。
父类的析构函数最好用virtual修饰:
父类和子类都会在堆区开辟动态空间。
创建两个父类的指针ptr1指向堆区的父类对象,ptr2指向堆区的子类对象。然后使用delete释放堆上的两个对象。
- new一个对象时,会调用它的构造函数。
- delete一个对象时,会调用它的析构函数。
- 对于父类Person对象,只有它自己在堆区上new出来的数组。
- 对于子类Student对象,有继承自父类和自己的两个在堆区上new出来的数组。
- 两个指针都是父类的指针Person*, 在delete的时候,调用析构函数时,传递的this指针都是Person*的,所以调用的都是父类的析构函数。
- 此时子类自己在堆区上的数组并没有释放,所以会造成内存泄漏。
此时如果是多态调用析构函数就好了,delete指针ptr2的时候就可以调用子类的析构函数,就会将子类中属于自己的和父类的堆区数据全部释放掉。
再看代码,可以注意到,在delete时,
- ptr1和ptr2都是父类的指针
- 符合了多态的一个条件,还缺一个构成重写的条件。
所以在父类和子类的析构函数前都加上virtual,此时就都是虚函数了。但是父类和子类的析构函数名不同。
- 为了让父类和子类的析构函数可以构成重写关系,编译器会将父类和子类的析构函数名都处理成destructor。
最终形成如上图所示的样子,此时父类和子类的析构函数就可以构成重写关系了。(语法上不能这样写)
这也解释了在学习继承的时候,父子类的析构函数为什么会构成隐藏关系。
回想构成重写虚构函数的第一个例外,子类的虚函数可以不加virtual。
- 此时父子类的两个虚函数就构成了重写关系。
- 此时构成多态的两个条件,构成重写关系,又父类的指针调用虚函数都具备了。
由于是多态调用,此时ptr2调用的就是子类的析构函数,而不是父类的析构函数,所以会先调用子类的析构函数清理子类的资源,再调用父类的析构函数清理从父类继承下来的资源(继承中析构函数的另一怪)。
- 父类的构造函数虽然加了virtual,但是如果不符合多态调用条件的话,还是和没加一样,父类对象正常调用。
为了防止出现子类资源析构不彻底的情况,最好在父类的析构函数前加上virtual。
🍟C++11 final override
final:
这里对继承部分的知识进行补充。
如何设计出不能被继承的类呢?
- 父类构造函数私有
创建子类对象的时候,子类的构造函数会先调用父类的构造函数将从父类继承下来的成员进行初始化,再初始化子类自己的成员。
- 将父类构造函数私有化以后,子类的构造函数就无法调用父类的构造函数。
- 所以子类的构造函数就无法完成整个子类对象的构造。
- 在父类后加final
这种类也被叫做最终类,顾名思义就是最后的类,意味着无法继承。
在父类被final修饰以后,创建子类对象时会直接报错,子类无法继承。
如果需要设计无法继承的父类时,最好使用第二种方式,也就是用final修饰父类。
回归正题:
- final还可以修饰父类中的虚函数,表示该虚函数不能被重写。
父类和子类中虽然都是虚函数,并且符合三同的条件,但是由于父类中的虚函数被final修饰了,所以在创建子类对象的时候报错父类的虚函数无法重写。
override:
- override用来检查子类虚函数是否重写了父类某个虚函数,如果没有重写编译报错。
- 左边代码中,子类中的虚函数成功重写了父类中的虚函数,所以没有报错。
- 右边代码中,由于父类中并不是虚函数,所以子类中的虚函数无法进行重写,又因为加了override,所以报错了。
🍟重载、覆盖(重写)、隐藏(重定义)的对比
概念 | 条件1 | 条件2 | 条件3 |
---|---|---|---|
重载 | 两个函数在同一个作用域 | 函数名相同 | 参数不同 |
重定义(隐藏) | 两个函数分别在父类和子类的作用域 | 函数名相同 | |
重写(覆盖) | 两个函数分别在父类和子类的作用域 | 函数名,返回类型,形参相同(协变除外) | 两个函数必须是虚函数 |
两个分别在父类和子类中的同名函数,不是构成隐藏就构成重写。
🍕抽象类
- 概念:在虚函数后面加上=0,这个虚函数就称为纯虚函数,含有纯虚函数的类就叫做抽象类(也叫做接口类),抽象类不能实例化出对象。
如上图,在父类的虚函数后加上了=0,此时就成立纯虚函数,所以class Car就是一个抽象类。在创建父类对象的时候,报错误无法实例化对象。
- 父类的纯虚函数被子类继承了下来。
- 由于构成重写关系,所以在子类中对继承下来的纯虚函数进行了重写。
- 子类可以实例化并且能够调用重写后的虚函数。
当父类是抽象类的时候,子类只会继承它的接口,具体实现会重写,换言之,抽象类会强制子类重写纯虚函数。
接口继承和实现继承:
- 普通函数的继承是一种实现继承,子类继承了父类的函数,可以使用函数,继承的是函数的实现。
- 虚函数的继承是一种接口继承,子类继承的是父类的虚函数接口,目的是为了重写,达到多态的目的,继承的是接口。
不实现多态,不要把函数定义成虚函数。
抽象类就是典型的接口继承,当父类在现实中没有具体的事物对照,但是它的方法框架已经有了,只是没有实现,此时就可以使用抽象类。
当子类继承了父类以后,就会在现实中有事物的对照,此时再将继承下来的纯虚函数接口进行重写,这样一个完整的类就有了。
🍕多态的原理
class Base
{
public:
virtual void func1()
{
cout << "Base::func1()" << endl;
}
protected:
int _b = 1;
};
这样一个类有多大,占据多少个字节的内存?
按照之前的认识:
- 成员变量只有一个int类型,占4个字节。
- 成员函数在代码段,所以计算大小时不考虑。
- 所以这个类的大小是4字节。
结果却是8,不是4,这是为什么呢?
🍟虚函数表
通过监视窗口可以看到,在Base对象创建后,不仅包含成员变量,还有一个vdptr的指针变量,这个指针叫做虚函数表指针。
- 此时就可以理解为什么Base的大小是8,而不是4了。
- 因为32位机器上指针的大小是4个字节,再加上包含的成员变量int也是4个字节,所以总共大小是8个字节。
包含虚函数的类对象中,存在一个虚函数表指针。
那么,这个虚函数表指针指向的虚函数表中放的是什么呢?顾名思义是虚函数,下面本喵来给大家验证一下。
- 在父类Base中,func1和func2是虚函数,func3是普通函数。
- 在子类Drive中,func1是虚函数,并且和父类的func1构成重写关系。
父类Base对象b:
- 虚函数func1和func2都入虚函数表,如上图中红色箭头所示。
- 普通函数func3不在虚函数表中。
子类Drive对象d:
- 父类的虚函数表指针被继承了下来,所以父类对象中也指向一张虚函数表,如上图蓝色框。
- 子类中的func1进行了重写,如上图橘色框中所示,由父类虚函数表中的Base::func1()变成了Drive::func1()。
- 从父类继承下来的普通函数同样不会进虚函数表。
再来画一下它们的对象结构图:
- 首先,这些函数,无论是虚函数也好,还是普通还函数也好,它们都是存放在代码段的,并且每个函数都有一个地址。
- 父类Base中的虚函数表指针指向的虚函数表中,放着的是父类中虚函数的地址。
- 子类Drive中的虚函数表指针指向的虚函数表中,放的是重写后的虚函数以及从父类中继承下来的虚函数。
虚函数表是一个指针数组,里面放的都是函数指针。
那么,虚函数表是存放在哪里的呢?栈区吗?其实根据数组名就是首元素地址,也就是第一个函数指针的地址,可以推断出虚函数表是存放在代码段的。
下面本来进行一个不太严谨的验证:
将我们知道存储位置的变量的地址打印出来,分别是栈区,堆区,数据段,代码段。
- 将虚表指针的地址打印出来后,发现和代码段的地址挨的很近,所以可以推断出虚表存在于代码段中。
复习一下指针:
- 取类对象的地址,此时该指针指向的是整个对象。
- 由于虚表指针在类对象的第一个位置,并且大小是4个字节的,所以将类指针强转成int*的,此时指针指向的就是vfptr。
- 然后进行解引用,得到vfptr中的内容,也就是虚表名,相当于虚表首元素的地址。
- 由于解引用得到的是int类型的数据,所以需要再强转位void*后,才会按照指针类型将地址打印出来,而不是整数。
思考:在32位机器上,需要将类指针强转为int*,那么64位机器呢?如何让它自适应不同的机器呢?
- 如果是64位的机器,需要强转位long long*,因为此时指针的大小是8个字节。
- 强转成void**就可以自适应不同的机器。
*(void**)(&be)
有人可能觉得为什么要这么麻烦,直接访问vfptr不就行了吗?
根本就无法直接访问vfptr虚表指针。
🍟原理分析
父类中有两个虚函数func1和func2,子类中有两个虚函数func1和func3,其中func1对子类虚函数进行了重写,还有一个普通函数func4。
由于监视窗口VS2019会做一些不合理的优化,所以用内存窗口来看。
Base b:
- 在对象b的内存模型中,第一个存放的是vfptr指针,值是0x00c69b34,第二个是它的成员变量,值是1。
- vfptr指向的虚表中,存放虚函数的地址。父类中有两个虚函数,所以有两个地址,在虚表的末尾以nullptr结束(vs2019采用这种结尾,g++就不是)。
Drive d:
- 在对象d的内存模型中,第一个存放的是vfptr指针,值是00c69b64,第二个是从父类继承下来的成员变量,值还是1,第三个是它自己的成员变量,值是2。
- vfptr指向的虚表中,存放自己的虚函数,从父类继承下来的虚函数以及重写后的虚函数的地址。所以子类的虚表中共有三个虚函数地址,同样虚表的结尾以nullptr结束。
从这里可以看出,子类继承父类的虚表指针以及虚表是在设计层面的。
接下来本喵给大家看一下虚表中的内容,也就是看一下虚表中函数的地址:
typedef void(*VFPtr)();
void PrintVFTable(VFPtr vft[])
{
for (int i = 0; vft[i]; ++i)
{
printf("[%d]:0x%p-->", i, vft[i]);
vft[i]();//调用对应虚函数
}
cout << endl;
}
封装一个打印虚函数表的函数,指针的应用,本喵就不解释了。
此时各自打印出了自己的虚表内容并且还调用了相应的函数。
说了这么多,原理到底是什么呢?肯定是和虚表指针以及虚表息息相关。
- 通过不同对象的虚表指针去调用虚表中对应的函数。
在上面的多态调用中:
- ptr指向的是父类时,就从父类的虚表指针找到虚表中对应的func1()。
- ptr指向的是子类时,就从子类的虚表指针找到虚表中重写后的func1()。
当ptr指向的是父类时,通过父类的虚表指针和虚表调用的就是父类的虚函数,着很容易理解。
当ptr指向的是子类时:
- 子类取地址赋值给父类指针时会发生切片。
- 切片后ptr指针中的虚表指针vfptr是从子类切片过来的,不再是父类的了。
- 此时vfptr指向的是子类虚表,调用的也就是重写后的虚函数了。
🍟静态绑定和动态绑定
- 静态绑定:又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载。
- 动态绑定:又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
我们来看上面程序的汇编代码来感受一下:
静态绑定:
- 普通调用的函数,在编译期间就确定了调用函数所在的地址。
- 在执行到调用普通函数时,call调用相应的函数。
静态绑定时,要调用的函数地址已经确定好了,直接调用就行。
动态绑定:
- 首先在汇编代码的篇幅上,多态调用就比普通调用多很多,因为这个过程是在寻找要调用函数的地址。
- 指针指向的对象不同,在运行过程中通过虚表取到的虚函数地址也不同。
- 最终将虚函数的地址放在寄存器eax中,可以看到,两次调用eax中的地址是不同的。
动态绑定时,要调用函数的地址并不是确定好的,而是需要在执行过程中计算得到,并且放在eax寄存器中。
反思:多态的两个条件中,为什么必须是父类的指针或者引用取调用虚函数?父类的对象不可以吗?
先说答案,不可以,可以看到,上图代码中并没有实现多态调用,两次调用都是掉的父类中的虚函数。
- 相同类型的对象,共用一个虚表。
- 如上图中的Base b和Base func,它们的虚表都是同一个,地址是00409b34。
- 将子类对象赋值给基类对象时,只会对成员进行切割,但是虚表并不会也赋值过去。
- 由于父类对象共用一个虚表,所以即使切割以后,vfptr都不会边,所以调用的函数也都是父类虚表中的虚函数,无法实现多态。
- 但是指针和引用不一样,它并不是切割赋值,而是父类指向了子类对象,使用的就是子类的vfptr和虚表,所以能实现多态。
由于普通子类对象在赋值给普通父类对象时,子类对象的虚表并不会给过去,而所有父类对象都共用一个虚表,所以普通父类对象无法实现多态,必须是父类的指针或者引用。
🍕多继承中的虚函数表
- Base1和Base2中都有两个虚函数,分别是func1和func2.
- Drive中也有两个虚函数,一个是func1,和Base1和Base2中的func1都构成重写关系。另一个func3是自己特有的。
现在我们要看的是子类对象的虚函数表是什么样的。
- 子类继承自父类Base1和Base2,所以子类中有两个虚函数表指针。指向两个虚表。
typedef void(*VFPtr)();
void PrintVFTable(VFPtr vft[])
{
cout << "虚表地址:" << vft << endl;
for (int i = 0; vft[i]; ++i)
{
printf("[%d]:0x%p-->", i, vft[i]);
vft[i]();//调用对应虚函数
}
cout << endl;
}
int main()
{
Drive d;
//从Base1继承的虚表指针
VFPtr* VFTBase1 = (VFPtr*)(*(int*)(&d));
PrintVFTable(VFTBase1);
//从Base2继承的虚表指针
VFPtr* VFTBase2 = (VFPtr*)(*(int*)((char*)(&d) + sizeof(Base1)));
PrintVFTable(VFTBase2);
return 0;
}
使用上面代码来查看虚表中的内容。
从Base1继承下来的虚表:
- 父类Base1中有两个虚函数,其中func1和子类Drive中的func1构成重写,所以原本放func1的位置被覆盖成了重写后的func1,放在下标为0处。
- Base1中的另一个虚函数func2没有被重写,所以不受影响,正常继承了下来,放在下标为1处。
- 子类自己有一个虚函数func3,它不与任何一个父类中的虚函数构成从写,此时放在从Base1中继承下来的虚表中,放在下标为2的位置。
从Base2继承下来的虚表:
- 父类Base2中有两个虚函数,其中func1和子类Drive中的func1构成重写,所以原本放func1的位置被覆盖成了重写后的func1,放在下标为0处。
- Base2中的另一个虚函数func2没有被重写,所以不受影响,正常继承了下来,放在下标为1处。
多继承中,有多少个父类,子类中就会继承多少个虚表,每个虚表中防着的是各自父类的虚函数,若父类中的虚函数和子类构造重写,则将对应虚表中的位置进行覆盖。子类特有的虚函数会放在第一张虚表中。
多继承对象的内存模型:
🍟菱形继承
class A
{
public:
virtual void func1()
{
cout << "A::func1()" << endl;
}
int _a;
};
class B : public A
{
public:
virtual void func1()
{
cout << "B::func1()" << endl;
}
virtual void func2()
{
cout << "B::func2()" << endl;
}
int _b;
};
class C : public A
{
public:
virtual void func1()
{
cout << "C::func1()" << endl;
}
virtual void func3()
{
cout << "C::func3()" << endl;
}
int _c;
};
class D : public B, public C
{
public:
virtual void func1()
{
cout << "D::func1()" << endl;
}
int _d;
};
- B和C都继承A,并且都重写了A中的func1,并且都又有一个自己的虚函数。
- D继承了B和C,并且再次重写了func1。
菱形继承中的虚表和多继承一样,子类会继承父类的虚表指针和虚表,如果发生重写就去对应的虚表中覆盖,如果子类自己有虚函数则放入第一个虚表中。
其他和菱形继承一样,会导致数据冗余和二义性。
将两个虚表打印出来发现并不是同一个表。虽然两个虚表最终都是来自于A,但是在B和C继承后并不是同一个,得得到了重新维护。
🍟菱形虚拟继承
代码就不放了,只需要在菱形的腰部使用虚拟继承就行。
冗余的变量和菱形虚拟继承一样,都是放在d对象的最下面。
- 从B继承下来的部分,首先存放的仍然是虚表指针,虚表中存放从B继承下来的虚函数,但是没有从A继承下来的func1。
- 接着存放的是虚基表指针,继承部分讲过,第二个数据存放的是虚基类偏移量,现在的第一个存放的是虚表的偏移量,不是全0了。
- 接着存放的是B的成员变量。
可以看到,在d对象的下边不仅有冗余的成员变量,还有一个指针一样的东西。
- 从A继承下来的func1同样是冗余的,所以和冗余的变量一样,也会放在d对象的下边。
- 此时B和C也是共享这个虚函数func1的。
注意:如果B和C都对A中的虚函数进行了重写,那么D必须得重新,因为它们是共用一个的,否则不知道该用B重写后的还是C重写后的。
对象模型:
虚拟菱形继承中,来自B和C的虚表指针和虚表正常存在,只是虚表中没有了从A继承下来的虚函数。从A继承下来的虚函数和冗余的变量一样,放在了d对象的后边,进行共用。
再次表达继承中谈到的观点,强烈不建议设计菱形继承,不仅数据冗余和二义性复杂,虚表的处理也复杂。
🍕常见问题
-
inline函数可以是虚函数吗?
答:不可以,因为内联函数没有地址,但是虚函数地址要被填入到虚表中。不过是可以编译通过的,因为inline只是个建议,到底有没有展开要视情况而定:若调用时不构成多态,保持inline属性;若构成多态,则没有inline属性。 -
静态成员可以是虚函数吗?
答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。 -
构造函数可以是虚函数吗?
答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。 -
析构函数可以是虚函数吗? 什么场景下析构函数是虚函数?
答:可以,并且最好把基类的析构函数定义成虚函数。具体例子文章中有提到。 -
对象访问普通函数快还是虚函数更快?
答:虚函数不构成多态就一样快,虚函数构成多态的调用,普通函数快,因为多态调用时进行动态绑定。 -
虚函数表是在什么阶段生成的,存在哪的?
答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。 -
C++菱形继承的问题?虚拟菱形继承的原理?
答:注意虚表和虚基表是两个东西,不要混为一谈。
巨坑一:
class A
{
public:
virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
virtual void test() { func(); }
};
class B : public A
{
public:
void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};
int main(int argc, char* argv[])
{
B* p = new B;
p->test();
return 0;
}
上面代码输出结果是什么?
A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确
- func被重写了,重写是实现重写,但是接口是继承下来的,所以B中func的缺省值是继承自A的,val = 0 被覆盖成了val = 1。
- test()没有被重写,它的作用域是A,所以在子类B的指针p调用test的时候,必须将切片成A*然后给this指针。
- 此时this是父类的指针,在调用test的时候,会调用func,而func又进行了重写,所以构成了多态的条件。
- this虽然是A*类型的,但是由p切割来的,所以指向的是子类,所以调用子类的func。
所以结果应该是B。该题考查了接口继承和多态调用,非常坑。
巨坑二:
class A {
public:
A(char* s) { cout << s << endl; }
~A() {}
};
class B :virtual public A
{
public:
B(char* s1, char* s2) :A(s1) { cout << s2 << endl; }
};
class C :virtual public A
{
public:
C(char* s1, char* s2) :A(s1) { cout << s2 << endl; }
};
class D :public B, public C
{
public:
D(char* s1, char* s2, char* s3, char* s4) :B(s1, s2), C(s1, s3), A(s1)
{
cout << s4 << endl;
}
};
int main() {
D* p = new D("class A", "class B", "class C", "class D");
delete p;
return 0;
}
A:class A class B class C class D B:class D class B class C class A
C:class D class C class B class A D:class A class C class B class D
答案选哪个?
- 因为是虚拟继承,所以A只有一份,也就是只会初始化一次。
- 在继承关系顺序中,B比C先声明,所以先初始化B再初始化C。
- 在初始化B和C的时候,它们的构造函数会先调用A的初始化函数初始化A。
所以,它们的初始化顺序是A,B,C,D。
继承关系中,谁先声明就先初始化谁。文章来源:https://www.toymoban.com/news/detail-408149.html
🍕总结
多态离不开继承,所以多态和继承必须都理解透彻,同时切记不要设计菱形继承以及虚拟继承。文章来源地址https://www.toymoban.com/news/detail-408149.html
到了这里,关于【C++学习】多态的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!