C++多态

前言: 

 

在C++编程中,多态是一种强大而重要的概念,它可以帮助我们实现更灵活和可扩展的代码。多态允许不同类型的对象对相同的消息做出不同的响应,这在实际应用中非常有用。

多态的实际应用非常广泛,尤其在面向对象的程序设计中。通过使用多态,我们可以编写通用的代码,而不需要考虑对象的具体类型。这种灵活性使得我们可以更容易地扩展和修改代码,而不会影响其他部分的功能。

在C++语法中,多态具有特殊的地位。C++通过在基类中声明虚函数,并使用动态绑定和虚函数表来实现多态。这使得C++成为一种非常适合面向对象编程的语言,能够充分发挥多态的优势。

尽管多态在编程中非常有用,但它也具有一定的难度。理解和正确使用多态需要掌握一些复杂的概念和技术,如虚函数、虚函数表和动态绑定。此外,多态还需要注意一些细节,如构造函数不能是虚函数,静态成员函数也不能是虚函数等。因此,学习和应用多态需要一定的时间和专注力。

多态的理解和应用在笔试面试中也具有重要性。很多公司在面试中会考察对多态的理解和使用能力。掌握多态不仅能够展示出对面向对象编程的深刻理解,还能够解决一些复杂的问题,并写出更优雅和高效的代码。

在本博客系列中,我们将深入探讨C++中多态的原理和实际应用。我们将介绍虚函数、动态绑定、虚函数表等概念,并通过具体的示例代码来说明多态的使用方法和技巧。无论你是初学者还是有一定经验的开发者,通过学习多态,你将能够写出更灵活和可扩展的代码,并在面试中展现自己的优势。

个人主页:Oldinjuly的个人主页

收录专栏:数据结构

欢迎各位点赞收藏⭐关注❤️

C++多态_第1张图片

 

目录

1.多态的概念

2.多态的定义及实现

2.1 虚函数

2.2 虚函数重写

2.3 多态的构成条件

2.4 C++11 override和final关键字

2.5 重载,覆盖(重写),隐藏(重定义)的对比

3.抽象类

3.1.抽象类和纯虚函数

3.2.接口继承和实现继承

4.多态的原理

4.1 虚函数表

4.2 多态的原理

4.3 动态绑定与静态绑定

5.单继承和多继承关系中的虚函数表

5.1 单继承

5.2 多继承

5.3 菱形虚拟继承中的多态

6.继承和多态常见的面试题

6.1 什么是多态?

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

6.3 多态的实现原理?

6.4 inline函数可以是虚函数吗?

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

6.6 构造函数可以是虚函数吗?

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

6.8 拷贝构造和operator=()可以是虚函数吗?

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

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

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

6.12 什么是抽象类?抽象类的作用?


1.多态的概念

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

举例:

  • 比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票。
  • 最近为了争夺在线支付市场,支付宝年底经常会做诱人的扫红包-支付-给奖励金的活动。那么大家想想为什么有人扫的红包又大又新鲜8块、10块...,而有人扫的红包都是1毛,5毛....。其实这背后也是一个多态行为。支付宝首先会分析你的账户数据,比如你是新用户、比如你没有经常支付宝支付等等,那么你需要被鼓励使用支付宝,那么就你扫码金额 =random()%99;比如你经常使用支付宝支付或者支付宝账户中常年没钱,那么就不需要太鼓励你去使用支付宝,那么就你扫码金额 = random()%1;总结一下:同样是扫码动作,不同的用户扫得到的不一样的红包,这也是一种多态行为。

总结:不同的对象完成同一个行为会有不同的效果。

2.多态的定义及实现

在介绍多态的实现条件前,先引入两个概念:虚函数和虚函数重写


2.1 虚函数

虚函数:即被virtual修饰的类成员函数

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

2.2 虚函数重写

虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),并且派生类重写了基类的函数实现,称子类的虚函数重写了基类的虚函数。

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

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

总结:虚函数重写/覆盖条件:

  • 基类和派生类都是虚函数
  • 三同(函数名,函数参数,返回值相同)(返回值和参数不同,只构成隐藏)
  • 派生类重写基类虚函数的实现

三个例外:

1.子类虚函数重写时可以不加virtual关键字,依旧构成重写,原理和后面要介绍的接口继承有关。

2.协变:基类和派生类虚函数的返回值类型可以不同

重写时,基类和派生类虚函数的返回值可以不同,但必须要求返回的是父子关系类型的指针或者引用(父返回父,子返回子)。这里的父子关系类型可以是其他父子关系的类,不仅仅是这里的基类和派生类。

class A {};
class B : public A {};

class Person {
public:
	virtual A* f() { return new A; }
};
class Student : public Person {
public:
	virtual B* f() { return new B; }
};

3.析构函数的重写:

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

2.3 多态的构成条件

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 ps;
	Student st;
	Func(ps);
	Func(st);
	return 0;
}

​​C++多态_第2张图片​​

C++多态_第3张图片 

 

​构成多态的两个条件:

  • 必须通过基类的指针或者引用来调用虚函数。
  • 被调用的必须是虚函数,并且派生类对基类的虚函数进行重写。

其实还有一个隐性支持多态的语法机制:赋值兼容转换(切片原理)。父类的指针或者引用可以指向子类对象。

那么这个调用虚函数的基类指针或者引用既可以指向基类对象,也可以指向派生类对象。基类指针或者引用指向的是谁,就会调用谁重写的虚函数。

2.4 C++11 override和final关键字

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

1.final:修饰虚函数,表示该虚函数不能被重写。(很扯淡的关键字,极少用)

C++多态_第4张图片

 

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

class Car {
public:
	virtual void Drive() {}
};

class Benz :public Car 
{
public:
	virtual void Drive() override { cout << "Benz-舒适" << endl; }
};

2.5 重载,覆盖(重写),隐藏(重定义)的对比

C++多态_第5张图片

 

3.抽象类

3.1.抽象类和纯虚函数

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类)抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

纯虚函数的出现,也使得派生类间接强制重写。

3.2.接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了对实现进行重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。

所以上面所说的虚函数重写条件中,派生类的虚函数可以不加virtual,因为接口继承,已经自带virtual了。

4.多态的原理

4.1 虚函数表

想了解虚函数表,先来看一道常考的笔试题:

class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	int _b = 1;
};

​​C++多态_第6张图片​​

C++多态_第7张图片 

我们发现sizeof(Base)=8(x86环境下测试)

通过监视窗口,我们来观察基类对象模型:除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,这个指针指向一个虚函数表(简称虚表),虚函数表中存放的是类中虚函数的指针

注意几点:

  • 虚函数表的本质是函数指针数组。虚函数表指针是函数指针数组的指针。
  • 虚表指针是放在一个对象的头部的。也就是对象内存模型的前4个字节(x86环境)。
  • 虚表中存放的是虚函数指针,并不是虚函数本身,虚函数是放在公共代码段中的。

派生类中的虚表又是个什么情况?

这里我们为了观察细致,类中多写几个虚函数。

// 针对上面的代码我们做出以下改造
// 1.我们增加一个派生类Derive去继承Base
// 2.Derive中重写Func1
// 3.Base再增加一个虚函数Func2和一个普通函数Func3
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;
	}
private:
	int _d = 2;
};

int main()
{
	Base b;
	Derive d;
	return 0;
}

​​C++多态_第8张图片​​

 

通过监视窗口的观察,我们发现了以下几个问题:

  1. 派生类对象d中的内容包括两部分:1.从基类继承下来的,包括虚表指针;2.自己的成员。但是继承下来的虚表指针和基类的虚表指针不是同一个!
  2. d对象的虚表内容和b对象的虚表内容比较一下,发现重写的Func1的函数地址发生了变化,但是未重写的Func2的函数地址一模一样。
  3. 普通函数Func3不在虚表中。

总结:

1.派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是,另一部分是自己的成员。


2.基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法层的叫法,覆盖是原理层的叫法。

补充:

a.同一个类型的对象共用一个虚表;

b.不管是否重写,子类虚表和父类虚表都不是同一个


3.另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。

注意:只要是虚函数,就要放进虚表中。


4.虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。


5.总结一下派生类的虚表生成:

  • a.先将基类中的虚表内容拷贝一份到派生类虚表中
  • b.如果派生类重写了基类中某个虚函数,派生类自己重写的虚函数覆盖虚表中基类的虚函数
  • c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后(这里后面进行验证)

只要重写,派生类 重写的虚函数地址  就会覆盖  从基类拷贝过来的虚函数地址,函数地址就会改变。

不重写,虚函数地址就是从基类拷贝过来的,函数地址和基类的一样。


6.虚函数存在哪的?虚表存在哪的? 注意:虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?实际我们去验证一下会发现vs下是存在于代码段的(后面验证)。

前面说过,派生类自己新增的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

如何验证呢?我们可以尝试打印出虚表中的函数

class Base1 {
public:
	virtual void func1() { cout << "Base1::func1" << endl; }
	virtual void func2() { cout << "Base1::func2" << endl; }
private:
	int b1;
};

class Derive : public Base1
{
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]);
		VFPTR f = vTable[i];
		f();
	}
	cout << endl;
}

int main()
{
	Derive d;
	VFPTR* vTableb = (VFPTR*)(*(int*)&d);
	PrintVTable(vTableb);
	return 0;
}

我们发现,这里的代码对指针掌握程度的要求极高,具体复习详见:指针进阶

我们针对晦涩代码进行解读:

1.函数指针类型重命名

typedef void(*VFPTR) ();

typedef对函数指针类型的重命名和创建函数指针类型的变量一样,要符合语法规范。

2.数组指针和数组名:

void PrintVTable(VFPTR vTable[]);
VFPTR* vTableb = (VFPTR*)(*(int*)&d);
PrintVTable(vTableb);
  • 这里的函数形参是VFPTR vTable[],其实就是数组首元素的地址(虚表就是函数指针数组,那么首元素就是函数指针),也就是函数的二级指针,所以实参类型是VFPTR*;
  • &d是对象d的地址,强转(int*)即是取头部四个字节的地址,前面说过,虚表指针放在一个对象的头部,所以(int *)&d就是取到了虚表指针,也是就(函数指针)数组的指针。
  • 再来一层*((int *)&d),就是数组指针解引用,得到的是数组本身,也是数组首元素地址(函数的二级指针)。
  • 最后再强转VFPTR* ,不然默认*((int *)&d)为整型。

C++多态_第9张图片

最终我们发现:派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

4.2 多态的原理

上面研究了这么长时间的虚函数表,那么多态的原理到底是什么?

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

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

//多态调用
void Func(Person& p)
{
	p.BuyTicket();
}

//普通调用
//void Func(Person p)
//{
//	p.BuyTicket();
//}

int main()
{
	Person Mike;
	Func(Mike);
	Student Johnson;
	Func(Johnson);
	return 0;
}

前面我们说过多态的一个条件就是父类的指针或者引用调用虚函数。而且这个父类指针或者引用可以指向父类对象自己,也可以指向子类对象。

C++多态_第10张图片

  • 观察上图的橙色箭头我们看到,p是指向Mike对象时,p->BuyTicket在Mike对象的虚表中找虚函数:Person::BuyTicket。
  • 观察下图的绿色箭头我们看到,p是指向Johnson对象时,p->BuyTicket在Johson对象的虚表中找虚函数:Student::BuyTicket。
  • 这样就实现出了不同对象去完成同一行为时,展现出不同的形态。

总结:多态的本质原理:

到父类指针(或者引用)指向对象的虚表中找到要调用的虚函数地址,进行调用。

多态的函数调用和普通函数有什么区别呢?

多态调用:运行时到指向对象的虚表中找到要调用的函数地址,进行调用。

普通调用:编译链接时确定函数地址,运行时直接调用。

我们可以用汇编代码进行分析:

C++多态_第11张图片

4.3 动态绑定与静态绑定

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

5.单继承和多继承关系中的虚函数表

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

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]);
		VFPTR pf = vTable[i];
		pf();
	}
	cout << endl;
}

int main()
{
	Base b;
	Derive d;
	VFPTR* vTableb1 = (VFPTR*)(*(int*)&b); 
	VFPTR* vTableb2 = (VFPTR*)(*(int*)&d);
	cout << "Base1:" << endl;
	PrintVTable(vTableb1);
	cout << "Derive:" << endl;
	PrintVTable(vTableb2); 

	return 0;
}

​​C++多态_第12张图片​​

观察下图中的监视窗口中我们发现看不见func3和func4。这里是编译器的监视窗口故意隐藏了这两个函数,也可以认为是他的一个小bug。那么我们如何查看d的虚表呢?下面我们使用代码打印出虚表中的函数。

C++多态_第13张图片

 

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]);
		VFPTR pf = vTable[i];
		pf();
	}
	cout << endl;
}

int main()
{
	Derive d;
	VFPTR* vTableb1 = (VFPTR*)(*(int*)&d); 
	//VFPTR* vTableb2 = (VFPTR*)(*(int*)((Base2*)&d));//代码一
	VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d+sizeof(Base1)));//代码二,和代码一效果一样
	PrintVTable(vTableb1);
	PrintVTable(vTableb2); 

	return 0;
}

C++多态_第14张图片

 C++多态_第15张图片

(注意:监视窗口的地址和打印的地址不一样,是因为两次调用是分开的。)


如何分开打印两个虚表的内容呢?

VFPTR* vTableb2 = (VFPTR*)(*(int*)((Base2*)&d));//代码一
VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d+sizeof(Base1)));//代码二,和代码一效果一样

通过上面两个对象内存成员模型中可以看出:

  • 多继承的派生类同时继承了两个虚表。
  • 多继承派生类新增加的虚函数放在第一个继承基类部分的虚函数表中。

细心的人会发现,两个虚表中func1的地址不一样,这是为什么?

其实我们再打印一下func1的地址,发现三个地址都不一样:

printf("func1:0X%x\n", &Derive::func1); 
//注意:打印成员函数地址要加&,并且要指定类域

C++多态_第16张图片

 这是为什么呢?我么可以通过底层汇编代码进行观察:

int main()
{
	Derive d;
	VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
	VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
	PrintVTable(vTableb1);
	PrintVTable(vTableb2);

	d.func1();
	Base1* ptr1 = &d;
	ptr1->func1();

	Base2* ptr2 = &d;
	ptr2->func1();

	return 0;
}

C++多态_第17张图片

 通过汇编代码分析得出:其实三个地址都能调用函数,只是有的地址需要绕很多层,有的函数可以直接call来调用,形象来说,他们都能吃到蛋糕,只不过有的人只要拆一层,有的人要拆两层的道理。

5.3 菱形虚拟继承中的多态

实际中我们不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的模型,访问基类成员有一定得性能损耗。所以菱形继承、菱形虚拟继承我们的虚表我们就不看了,一般我们也不需要研究清楚,因为实际中很少用。如果好奇心比较强,可以去看下面的两篇链接文章。

  1. C++ 虚函数表解析
  2. C++ 对象的内存布局

注意:D类中一定要重写公共基类A的虚函数,因为为了解决数据冗余和二义性问题,D类中只有一个公共的A类成员,B和C都重写的A类的虚函数,A类不知道从哪里继承,所以要重写。

菱形虚拟继承的多态会出现三个虚表,相当复杂,所以不会使用菱形虚拟继承。

6.继承和多态常见的面试题

6.1 什么是多态?


不同的对象完成同一件事情会产生不同的效果。

多态的构成条件:父类指针或者引用调用虚函数。派生类重写虚函数。

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


C++多态_第18张图片

 

6.3 多态的实现原理?


6.4 inline函数可以是虚函数吗?


答:可以,不过编译器就忽略inline属性(inline本身也是个建议性的关键字),这个函数就不再是inline。因为虚函数的地址要放到虚表中去,但是inline函数没有地址,他是直接展开的,所以inline函数和虚函数是互斥的。多态调用中inline会失效。

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


答:不能,因为静态成员函数没有this指针,都是在编译时决议。无法访问虚函数表,所以静态成员函数无法放进虚函数表。

6.6 构造函数可以是虚函数吗?


答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。如果构造函数是虚函数,就要到虚函数表中找到构造函数的地址,但是此时并没有虚表指针,找不到虚表。

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


答:可以,并且最好把基类的析构函数定义成虚函数。(前面有介绍)

主要用于以下场景:

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

如果这里不是用多态,delete p2时调用的是Person的析构函数,无法正确析构。

6.8 拷贝构造和operator=()可以是虚函数吗?


拷贝构造不可以是虚函数,答案和构造一样。

operator=() 可以,但是没有实际价值。

class A
{
public:
	A()
		:_a(0)
	{}

	virtual A& operator=(const A& aa)
	{
		return *this;
	}

public:
	int _a;
};

class B : public A
{
public:
	virtual B& operator=(const B& aa)//这里不构成重写,参数不同,因此不构成多态
	{
		return *this;
	}

	virtual B& operator=(const A& aa)//基类对象可以赋值给派生类对象
	{
		return *this;
	}
};

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


答:

首先如果是普通对象,是一样快的。

如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。

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


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

可以用以下代码验证:

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

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

int main()
{
	Student s;
	printf("虚表:%p\n", *((int*)&s));

	static int x = 0;
	printf("static变量:%p\n", &x);

	const char* ptr = "hello world";
	printf("常量:%p\n", ptr);

	int y = 0;
	printf("局部变量:%p\n", &y);

	printf("new变量:%p\n", new int);


	return 0;
}

C++多态_第19张图片

 我们发现虚表的地址和static变量、常量的地址很相近,所以虚函数表大概率是存放在代码段了。

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


答:

问题:数据冗余和二义性的问题。

虚拟继承相比普通继承,普通继承会从公共基类中继承多个成员,造成了数据冗余和二义性问题。而虚拟继承的内存对象模型中只有一个公共基类的成员,从而避免了数据冗余问题;虚拟继承又会在继承体系中设置虚基表和虚基表指针,虚基表中存放着偏移量,通过偏移量可以找到对应类所继承的公共基类成员,解决二义性问题。

6.12 什么是抽象类?抽象类的作用?


在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类);

作用:给其他类当做接口。实现多态。约束派生类(派生类必须强制重写虚函数)。

你可能感兴趣的:(C++,c++,多态,虚函数,重写覆盖,抽象类,多态面试题)