前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >C++:深入理解多态

C++:深入理解多态

作者头像
小陈在拼命
发布2024-05-26 10:09:39
620
发布2024-05-26 10:09:39
举报

一、多态的概念

多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。

那究竟多态的实际价值体现在哪里呢??

1、举个例子比如说购买高铁票这个行为,如果是普通人就是原价购买,如果是学生的话就是半价购买,如果是军人的话,可以优先走绿色通道购买……

2、再举个例子比如说大数据杀熟(个人看法,不一定正确,只是为了方便解释多态)

(1)在线支付市场,如果你平时经常用微信而很少用支付宝,那么在支付宝举办一些类似领取红包的活动的时候,他可能会有相关的算法去分析你的账户信息,对于很少用支付宝的用户,可能相对来说得到的红包金额就会更大,这是为了鼓励你去使用支付宝,可能你某一天在商场购物的时候,想起来自己有个红包没用,就会放弃使用微信而转而使用支付宝。同样是扫码动作,不同的用户扫得到的不一样的红包,这也是一种多态行为。

(2)游戏抽卡相信大家也体验过,充钱充的越少的可能反而抽卡的运气会更好,这样会使得你不至于跟氪佬的差距特别大,鼓励你继续玩游戏。而充值充得多可能运气就会越不好,因为你不缺钱,同样是抽卡,不同的玩家抽卡概率不同,这也是一种多态行为。

总而言之就是,我们生活中一件事情不同的群体去做需要有不同的反馈,那这就是多态!!

二、多态的定义和实现

2.1 构成多态的条件

首先多态现象的产生是在继承的基础上产生的。 多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。Person对象买票全价,Student对象买票半价。

构成多态需要以下两个条件(重点): (1)对父类虚函数的重写->三同(函数名、参数、返回值) (2)必须是父类的指针或者引用去调用

是否构成多态的不同表现: 1、不满足多态 -- 看调用者的类型,调用这个类型的成员函数 2、满足多态 -- 看指向的对象的类型,调用这个类型的成员函数

满足多态:

不满足多态:

思考:你可能会有这样的疑惑->我直接用在函数体里面用if……else不也可以达到这样的效果吗??为什么非得用多态来完成呢??? ——>答:有些场景下必须得用多态才能解决,比如父类的指针或者引用调用析构函数

但是由于父类的指针或引用是可以指向子类的对象的,甚至在某些场景下子类的指针或引用也可以指向父类的对象(前提是父类的对象被子类对象给赋值过) ,如果没有发生多态的话,那么就会去看调用者的类型而不是去看指向对象的类型,从而导致指向对象没有被析构,造成内存泄露。

加了virtual之后,就可以解决这个问题了。

综上我们可以发现,if……else并不能替代多态!!!

2.2 虚函数的重写

虚函数:即被virtual修饰的类成员函数称为虚函数 虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。

虚函数重写有两个例外:

(1)协变(返回值可以不同->但是必须是父子关系的指针或者引用(其实不一定是自己的父子类,其他的父子类也行))

代码语言:javascript
复制
class Person {
public:
	virtual Person* f() { return this; }
};
class Student : public Person {
public:
	virtual Student* f() { return  this; }
};

int main()
{
	return 0;
}

返回值也可以是其他父子类的指针或者引用,也可以是协变。

代码语言:javascript
复制
class A {};
class B : public A {};
class Person {
public:
	virtual A* f() { return nullptr; }
};
class Student : public Person {
public:
	virtual B* f() { return nullptr; }
};

int main()
{
	return 0;
}

(2)子类的virtual可以省略

因为虚函数的重写本身就是接口继承 我把除函数体以外的全部部分都可以继承下来,然后再去重写继承父类的这个函数的实现 这样只要父类写了virtual就可以了。

我们来看一道经典的题目

代码语言:javascript
复制
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;
}

该题的变形:

代码语言:javascript
复制
class A
{
public:
	virtual void func(int val = 1){ std::cout << "A->" << val << std::endl; }
};

class B : public A
{
public:
	void func(int val = 0){ std::cout << "B->" << val << std::endl; }

	virtual void test(){ func(); }
};

int main(int argc, char* argv[])
{
	B*p = new B;
	p->test();
	return 0;
}

(3)析构函数的重写(基类与派生类析构函数的名字不同)

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。

代码语言:javascript
复制
class Person {
public:
	virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
	virtual ~Student() { cout << "~Student()" << endl; }
};
// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函
//数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{
	Person* p1 = new Person;
	Person* p2 = new Student;
	delete p1;
	delete p2;
	return 0;
}

2.3 C++11 override 和 final

C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。

1. final:修饰虚函数,表示该虚函数不能再被重写(实际上这样的应用场景很少,因为我们建立虚函数的目的基本上都是为了重写)

2. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

下面这种场景下,我们父类漏写了virtual,但是这样并不会报错

但是我们加了override 他就会提示你

总的来说,override就相当于是对前面语法的一个填坑,因为按道理来说虚函数的意义就是要为了重写而生的,而没有重写就失去了意义,最好的方法其实是让编译器对没重写的虚函数进行报错,但是之前在这方面没有去严格地限制说不重写就会报错,所以这边做了一个妥协就是你可以通过增加override来帮助你检查,防止你写漏,

2.4 重载、覆盖(重写)、隐藏(重定义)的对比

三、抽象类

3.1 什么是抽象类

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。 在生活中一个类型在现实中没有对应的实体,我们就可以一个类定义为抽象类!

1、抽象类不能实例化出对象

2、子类必须重新父类的虚函数(不重写的话自己也无法实例化)

总的来说,纯虚函数强制子类必须重写虚函数!相当于是一种强制性的要求! 如果不重写的话,代价就是自己也和抽象类父类一样无法实例化出对象!!

3.2 理解接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数(充分说明了多态的意义就是为了重写虚函数而生的!!!)

四、多态的底层原理

4.1 虚函数表

接下来我们要从原理层去剖析多态具体是如何构成的。

在这之前,我们来看一道题目:sizeof(Base)是多少?

代码语言:javascript
复制
class Base
	{
	public:
		virtual void Func1()
		{
			cout << "Func1()" << endl;
		}
	private:
		int _b = 1;
		char _ch;
	};
	int main()
	{
		cout << sizeof(Base) << endl;
		Base bb;
		return 0;
	}

根据以前对于内存对齐的学习,你可以很快就猜到是8,但是答案是12(x86环境)

原因就是由于有了虚函数,所以就出现了一个虚函数表指针!!

除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。那么派生类中这个表放了些什么呢?我们接着往下分析:

代码语言:javascript
复制
//	}
class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
	virtual void Func() { cout << "买票-全价" << endl; }
	int _a = 0;

};
class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }

	int _b = 1;
};
void Func(Person*p)
{
	p->BuyTicket();
}

int main()
{
	Person* p1 = new Person;
	Person* p2 = new Student;
	return 0;
}

从上的分析我们可以总结出: 1、虚函数表(简称虚表)本质上是一个虚函数指针数组 2、子类在创建对象的时候会拷贝父类的虚函数表,然后如果没有发生重写(比如Func()) 那虚函数表存的就是父类的虚函数,如果发生了重写(比如BuyTicket()),那么子类首先会继承父类的接口,然后重写父类的实现,然后用这个虚函数的地址覆盖掉原先拷贝父类虚函数的位置。 3、通过对2的分析,我们可以知道(1)重写是语法层的概念,如果发生了多态,那么子类会继承父类的接口,然后重写父类的实现。(2)覆盖是原理层的概念,当子类重写了父类的实现后,会将虚表中对应的该函数的地址更新成新的虚函数地址。

4.2 多态的原理

那么虚函数表究竟是如何帮助我们实现多态的呢???在研究这个问题之前,我们来看一下这个汇编

代码语言:javascript
复制
class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
	virtual void Func() { cout << "买票-全价" << endl; }
	int _a = 0;

};
class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }

	int _b = 1;
};
void Func(Person&p)
{
	p.BuyTicket();
}
int main()
{
	Person mike;
	Func(mike);
	Student johnson;
	Func(johnson);
	return 0;
}

通过上图的分析我们可以知道: (1)在没有发生多态的时候,相当于一个普通函数的调用,是在编译时就确定了的。 (2)如果发生了多态,那么编译器只能通过找到对应的虚函数表的位置,然后虚函数表里面存的函数是什么,就调用什么。 这充分说明了一个道理就是,多态并不是在编译时就确定的,编译器只是知道自己需要调用的函数在什么地方,但是具体这个函数是什么,他并不知道,只有等到汇编代码转成二进制代码后执行了那么才会知道调用的是什么!!!

我们来看看下面代码的运行结果:

代码语言:javascript
复制
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:
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}

	virtual void Func4()
	{
		cout << "Derive::Func4()" << endl;
	}
private:
	int _d = 2;
};
int main()
{
	Base b;
	Derive d;
}

b按道理来说应该有三个虚函数,但是在监视窗口只能看到两个,原因是监视窗口其实是被处理过的,所以我们看到的并不是真的。既然监视窗口看不了,我们就去看看内存窗口

所以我们可以得到一个结论,监视窗口其实是被加以修饰的,不一定能够准确地看到底层信息,相比之下内存更为纯粹 ,可以看到更加真实的情况。

但是内存函数也是也不太方便我们去观察,所以最好的方法是想办法把虚函数表打印出来!!

4.3 实现虚函数表的打印

1、首先我们要先封装一个打印虚表的函数PrintVFTable

虚函数表本质上是一个虚函数指针数组,存的都是void(*)()类型的函数指针,为了方便我们书写函数指针以及可读性,我们用typedef去帮助我们重命名这个void(*)()变成VF_PTR。虚函数表里面存的是一个个VF_PTR类型,那么我们虚表指针就是VF_PTR*类型。这就是我们要传递的参数。

接下来就是打印这个虚函数的指针数组,但是我们并不知道这个指针数组有多大,但是在VS下,虚函数表的指针末尾默认都有一个nullptr,所以我们可以用这个来充当一个结束条件

那么根据上面的分析,我们就可以封装出一个通过传虚函数指针来打印虚表的一个函数如下:

代码语言:javascript
复制
typedef void(*VF_PTR)(); //将函数指针重命名为VF_PTR
void PrintVFTable(VF_PTR* table)
{
	for (int i = 0; table[i] != nullptr; ++i)
		printf("[%d]:%p->", i, table[i]);
	cout << endl;
}

2、那么接下来的问题就是我们如何在这个对象去找到这个虚函数指针??

在学习大小端的时候,我们解决判断机器大小端的问题有2个方法,一个是用联合体,另一个是暴力取到第一个字节,这个方法本质上就是将一个int*类型强转成char*来得到的。因为指针的类型决定解引用看多大字节。

类比这题,假设是32位的环境,那么我们通过调试可以知道虚函数指针在对象的头四个字节,所以首先我们要取得这个地址,然后再将他强转成int类型,然后再解引用,就可以拿到该位置的函数指针了!!即 *(int*)&d 但是还有一个问题就是我们的接口是VF_PTR*类型,所以我们还得将其强转成VF_PTR*才能传过去 所以调用的方法为PrintVFTable((VF_PTR*)(*(int*)&d))

但是这个只有在32位的环境下才有效,如果换成64位,代码就不成立了。那么还有一种调用方法可以解决这个问题。

通过上图分析可以得到的调用方式: PrintVFTable(*(VF_PTR**)&d)

3、如何查看对应地址的函数是什么???

我们可以在打印虚表的时候顺便通过这个函数指针去调用一下这个函数,只需要对PrintVFTable函数修改一下。

代码语言:javascript
复制
typedef void(*VF_PTR)(); //将函数指针重命名为VF_PTR
void PrintVFTable(VF_PTR* table)
{
	for (int i = 0; table[i] != nullptr; ++i)
	{
		printf("[%d]:%p->", i, table[i]);
		VF_PTR f = table[i];
		f();
	}
	cout << endl;
}

通过打印虚表,我们可以更好地观察结果。

总结一下派生类的虚表生成过程: a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后

在C++中,常常通过声明顺序来代表实际顺序,比如说 1、初始化列表的初始化顺序取决于成员变量定义的先后顺序 2、一个子类继承多个父类的时候,内存中先继承的父类的成员变量在前面,后继承的父类的成员变量再后面,然后才是子类自己新的成员变量。 3、一个类有多个虚函数的时候,虚函数表的存储也是先声明的虚函数在前面,然后子类继承后创建的虚表也是先是父类的虚函数,然后才是自己新增的虚函数

4.4 多态的思考

1、虚表是什么阶段生成的??——>编译阶段

一个类是否存在虚表关键就在于是否有虚函数,而是否有虚函数取决于是否有virtual关键字,所以是在编译阶段生成。

2、对象中的虚表指针是什么时候初始化的??——>初始化列表阶段

通过代码观察:

b还没初始化的时候就已经存在虚表了,说明虚表是早于构造函数的。那么如果我们将_b的初始化放在初始化列表阶段。

上图可以证明虚表指针是在初始化列表阶段初始化的。

3、虚表是存在哪里的??——>代码段(常量区)

易错点:有虚函数的对象里面存的其实是指向虚表的指针,并不是虚表!!!

通过监视窗口其实可以大致推断出来

因为我们知道虚函数是存在 代码段(常量区)而虚表的地址和虚函数的地址其实挺接近的。

但是这样其实不够严谨,所以我们自己来写一个代码判断虚表具体是存在哪个区域。

代码语言:javascript
复制
int main()
{
	Base b;
	Derive d;
	int x = 0;
	static int y = 0;
	int* z = new int;
	const char* p = "xxxxxxxxxxxxxxxxxx";
	printf("栈对象:%p\n", &x);
	printf("堆对象:%p\n", z);
	printf("静态区对象:%p\n", &y);
	printf("常量区对象:%p\n", p);
	printf("b对象虚表:%p\n", *((VF_PTR**)&b));
	printf("d对象虚表:%p\n", *((VF_PTR**)&d));
	return 0;
}

因此可以得出虚表是存在 代码段(常量区)。

4、反向分析规则:子类也可以赋值给父类,但为什么这样不能构成多态??——>因为父类对象不敢随意拷贝子类虚表

无论是父类的指针或者引用,其本质上指代子类中所继承的父类的那一块区域,子类有子类的虚表,父类有父类的虚表,互不影响。但是如果是赋值,虽然也可以把父类那部分切片切过去,但是编译器不敢把子类的虚表也拷贝过去,因为父类自己也有一个虚表,如果随意拷贝的话我们就分不清这究竟是一个person类还是student类!!

4.5 动态绑定与静态绑定

1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态(编译时),比如:函数重载 2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态(运行时)。

五、多继承的虚函数表

5.1 子类新增的虚函数

我们来看看这样一段代码

代码语言:javascript
复制
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;
};

如果我们实例化出Derive对象d,那么d必然继承了两个虚表,那在这种情况下,如上图我们在子类多增加了一个func3()的一个虚函数,那这个新增的虚函数是存在第一个虚表还是第二个虚表呢????

我们可以通过内存窗口来观察

接下里我们通过打印虚表来观察

第一张虚表可以沿用第单继承的虚表打印的调用,使用PrintVFTable((VF_PTR*)(*(int*)&d))或者 PrintVFTable(*(VF_PTR**)&d)

而第二张虚表则需要先跳过base1对象大小的字节,才能找到base2的虚表,但是如果我们在d的地址上加上sizeof(Base1)的话,由于d是一个Derive对象,那么+1会跳过Derive对象对象的大小,并不符合我们的要求,因此我们需要先将&b强转成char*类型去进行指针偏移,然后再进行操作。因此调用方法可以是PrintVFTable((VF_PTR*)(*(int*)((char*)&d + sizeof(Base1))))或者是PrintVFTable(*(VF_PTR**)((char*)&d+sizeof(Base1)))

还有另外一种方法,在使用这个方法之前,我们先来看看一道多继承中指针偏移问题

下面说法正确的是( ) A:p1 == p2 == p3 B:p1 < p2 < p3 C:p1 == p3 != p2 D:p1 != p2 != p3

代码语言:javascript
复制
class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };
int main(){
Derive d;
Base1* p1 = &d;
Base2* p2 = &d;
Derive* p3 = &d;
return 0;
}

所以打印Base2的虚表也可以利用一个Base2的指针帮助我们自动定位在d中的位置!!

总结: (1)父类的指针或引用指向子类对象的时候会自动定位到其继承了自己的那部分区域!! (2)多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中!!

5.2 底层剖析

我们会发现虚继承后,虚表中重写的func1的地址不同!!!下面我们来分析底层的原因——

通过汇编我们可以知道,多态并不是直接去调用对应的函数,而是要先通过对象的this指针去拿到对象的虚函数指针,然后通过虚函数指针再去找对应的函数。而ptr1恰好是this指针的位置,所以就可以直接拿到base1对象的虚表指针然后找到虚表,而ptr2必须要将this指针修正sizeof(base1)的大小才能拿到对应的虚函数!! 所以地址不相同的原因是编译器需要去通过其封装的方法来修正this指针,这样才能正确地取到对象的虚函数指针,实现多继承的多态。

思考题:下面代码中的ptr->func1()是多态调用吗??

代码语言:javascript
复制
class A
{
public:
	virtual void func1()
	{
		cout << "A::func1" << endl;
	}

	void func2()
	{
		cout << "func2" << endl;
	}
};

int main()
{
	// 多态调用  -- 去虚表中找虚函数地址
	A* ptr = &aa;
	ptr->func1();
	return 0;
}

思考: 我们会发现这里并没有出现继承和虚函数的重写,但是还是出现了多态调用!!!为什么呢??? 答:因为编译器做的是一种傻瓜式的判断(因为如果编译器还要判断是否出现继承和重写的话代价太高了),并且其实只要发现满足多态的两个条件,即(1)必须是父类的指针或者引用去调用。(2)是一个虚函数。 就会把他推断成多态(多态调用的本质是能够在虚表中找得到,即使没有重写的虚函数也会进虚表,那么调用的话就是多态调用。)

再看看下面的代码:

代码语言:javascript
复制
class A
{
public:
	virtual void func1()
	{
		cout << "A::func1" << endl;
	}
	void func2()
	{
		cout << "func2" << endl;
	}
};
class B : public A
{
public:
	virtual void func1()
	{
		cout << "B::func1" << endl;
	}
	void func2()
	{
		cout << "func2" << endl;
	}
};
int main()
{
     A aa;
	// 多态调用  -- 去虚表中找虚函数地址
	A* ptr = &aa;
	ptr->func1();
}

此时我们可以证明是否出现继承和重写并不是多态最本质的条件,只要是父类的指针或者引用去调用,并且该虚函数可以在虚表中找得到,那么就构成了多态调用!!!(所以不管是否重写编译器都会按照多态调用去走)。之所以要强调要完成虚函数的重写,是因为只有虚函数重写了才有实际意义,可以看得出来。 总结:多态存在的意义就是得重写虚函数,但是底层多态的调用只要是父类的指针和引用对应的虚表找得到,就会出现多态的调用,只不过不重写的话表面上很难观察并且也就失去意义了!! 所以我们要将多态的现象(需要两个调用去对比)和多态的调用(原理层)区分开来!!

5.3 菱形虚拟继承

代码语言:javascript
复制
class A
{
public:
	virtual void func1()
	{}
public:
	int _a;
};

class B : virtual public A
{
public:
	virtual void func1()
	{}

	virtual void func2()
	{}
public:
	int _b;
};

class C : virtual public A
{
public:
	virtual void func1()
	{}

	virtual void func3()
	{}
public:
	int _c;
};

class D : public B, public C
{
public:
	virtual void func1()
	{

	}
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;
}

总结: (1)菱形虚拟继承的D对象会有三张虚表,分别是A/B/C的,其中由于虚继承的存在,所以A虚表指针以及其成员变量会被存在公共部分。 (2)虚基表的第一行存的偏移量可以找到其虚函数表指针,而第二行存的偏移量可以找到公共部分

实际中我们不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的模型,访问基类成员有一定得性能损耗。

附上两篇文章: C++ 虚函数表解析 | 酷 壳 - CoolShell

C++ 对象的内存布局 | 酷 壳 - CoolShell

六、继承和多态的相关面试题

1. 什么是多态?

2. 什么是重载、重写(覆盖)、重定义(隐藏)?

3. 多态的实现原理?

4. inline函数可以是虚函数吗?

答:可以,不过为了能够将该虚函数放进虚表中,编译器会忽略掉inline的特性,因为本身inline就是一个建议。在某些函数必须出现在代码段(常量区)的时候,inline会妥协并失去作用

5. 静态成员可以是虚函数吗?

答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。

6. 构造函数、拷贝构造可以是虚函数吗?

答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。

7、赋值重载可以是虚函数吗??

答:可以,但是不建议,因为这样的话如果没有触发多态,父类的赋值重载就会被隐藏,子类就调用不了父类的赋值重载了。

8. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?

答:可以,并且最好把基类的析构函数定义成虚函数。因为析构函数都会被编译器处理为destruct,如果没有多态会使得父类的析构函数被隐藏。而有多态的话子类可以重写父类的析构函数,这样子类就调子类的析构,父类调父类的析构,互相不会冲突更不会造成内存泄露。

9. 对象访问普通函数快还是虚函数更快?

答:不一定,因为虚函数只不过是构成多态的条件之一,如果其他条件没满足的话其实也相当于普通对象。如果是普通对象,是一样快的。如果是父类指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。

10. 虚函数表是在什么阶段生成的,存在哪的?

答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。

11. C++菱形继承的问题?虚继承的原理?

12. 什么是抽象类?抽象类的作用?

答:抽象类强制子类重写了虚函数,另外抽象类体现出了接口继承关系。

13.虚基表和虚函数表的区别

答:虚函数表存的是虚函数的地址,是为了多态的实现。而虚基表存的是偏移量,是为了解决数据冗余和二义性。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-05-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、多态的概念
  • 二、多态的定义和实现
    • 2.1 构成多态的条件
      • 2.2 虚函数的重写
        • 2.3 C++11 override 和 final
          • 2.4 重载、覆盖(重写)、隐藏(重定义)的对比
          • 三、抽象类
            • 3.1 什么是抽象类
              • 3.2 理解接口继承和实现继承
              • 四、多态的底层原理
                • 4.1 虚函数表
                  • 4.2 多态的原理
                    • 4.3 实现虚函数表的打印
                      • 4.4 多态的思考
                        • 4.5 动态绑定与静态绑定
                        • 五、多继承的虚函数表
                          • 5.1 子类新增的虚函数
                            • 5.2 底层剖析
                              • 5.3 菱形虚拟继承
                              • 六、继承和多态的相关面试题
                              领券
                              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档