【C++】多态

目录

一、多态的概念

二、多态的定义及实现

2.1、多态的满足条件

2.2、多态的使用条件

2.3、虚函数

2.4、虚函数的重写

2.5、虚函数重写的特殊情况:协变

2.6、析构函数的重写

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

三、抽象类

四、多态的原理

4.1、虚函数表

[4.2 、多态的原理](#4.2 、多态的原理)

4.3、静态绑定和动态绑定

五、单继承和多继承关系中的虚函数表

5.1、单继承中的虚函数表

5.2、多继承中的虚函数表

六、常见面试题(题目)


一、多态的概念

具体来讲,多态就是一件事情由不同的类型对象去完成,不同对象会由不同的行为状态。

比如买票,例如学生买票是半价,军人买票时优先购买,普通人买票是正常全价购买。

对于不同的对象去买票这个行为,会有不同的结果

二、多态的定义及实现

2.1、多态的满足条件

有继承关系,子类重新父类中的虚函数

2.2、多态的使用条件

1、必须提供基类的指针或者引用去调用虚函数

2、被调用的函数必须是虚函数,并且派生类必须对基类的虚函数进行重写。

2.3、虚函数

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

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

2.4、虚函数的重写

虚函数的重写:派生类中有一个跟基类完全相同的虚函数**(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同)**,则称派生类的虚函数重写了基类的虚函数

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

class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
	
	/*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因
	为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性)*/
	/*void BuyTicket() { cout << "买票-半价" << endl; }*/
};
void Func(Person& p)
{
	p.BuyTicket();
}
int main()
{
	Person ps;
	Student st;
	Func(ps);
	Func(st);
	return 0;
}

2.5、虚函数重写的特殊情况:协变

该情况下、派生类重写基类虚函数时,派生类虚函数可以和基类虚函数的返回值类型不同。

基类虚函数返回基类回基类对象的指针或者引用。(返回其它基类对象的指针和引用也可以)

派生类虚函数返回派生类对象的指针或者引用。(返回其它派生类对象的指针和引用也可以)

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

2.6、析构函数的重写

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

//总结
//1、虚析构或纯虚析构就是用来解决通过父类指针释放子类对象
//2、如果子类中没有堆区数据,可以不写虚析构或纯虚析构
//3、虚析构和纯虚析构都要有具体的函数实现
//4、拥有纯虚析构函数的类也属于抽象类
class Animal
{
public:
	Animal()
	{
		cout << "Animal的构造函数" << endl;
	}
	//纯虚函数 不用具体的实现
	virtual void speak()=0;
 
	//利用虚析构可以解决 父类指针释放子类对象时不干净的问题
	//virtual ~Animal()
	//{
	//	cout << "Animal的析构函数" << endl;
	//}
	//纯虚析构 需要声明也需要实现
	// 有了纯虚析构之后,这个类也属于抽象类,无法实例化对象
	//需要代码实现 父类也可能有些属性开辟到堆区
	virtual ~Animal() = 0;
};
Animal:: ~Animal()
{
	cout << "Animal纯虚析构函数调用" << endl;
}
class Cat : public Animal
{
public:
	Cat(string name)
	{
		cout << "Cat的构造函数" << endl;
		m_Name=new string(name);
	}
	virtual void speak()
	{
		cout <<*m_Name <<"小猫在说话" << endl;
	}
	~Cat()
	{
		if (m_Name != NULL)
		{
			cout << "Cat的析构函数" << endl;
			delete m_Name;
			m_Name = NULL;
		}
	}
	//小猫的名称创建在堆区 用string*指针维护
	string* m_Name;
};
void test01()
{
	Animal* animal = new Cat("Tom");
	animal->speak();
	//父类指针在析构时候 不会调用子类析构函数 导致子类如果有堆区属性 出现内存泄漏
	delete animal;
}

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

三、抽象类

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

四、多态的原理

3.1、接口继承和实现继承
1、普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实
现。
2、虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成
多态,继承的是接口。

四、多态的原理

4.1、虚函数表

//sizeof(Base)
class Base
{
public:
	virtual void Func1()
	{
		cout << "virtual void Func1()" << endl;
	}
private:
	int _b = 1;
};
int main()
{
	Base b;
	cout << sizeof(Base) << endl;
	//输出结果8
}

通过代码结果我们发现 b 对象是 8bytes , 除了 _b 成员,还多一个 __vfptr 放在对象的前面 ( 注意有些
平台可能会放到对象的最后面,这个跟平台有关 ) ,对象中的这个指针我们叫做虚函数表指针 (v
virtual f 代表 function) 。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数
的地址要被放到虚函数表中,虚函数表也简称虚表,

// 针对上面的代码我们做出以下改造
// 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;
}

通过观察和测试,我们发现了以下几点问题:

  1. 派生类对象 d 中也有一个虚表指针, d 对象由两部分构成,一部分是父类继承下来的成员,虚
    表指针也就是存在部分的另一部分是自己的成员。
  2. 基类 b 对象和派生类 d 对象虚表是不一样的,这里我们发现 Func1 完成了重写,所以 d 的虚表
    中存的是重写的 Derive::Func1 ,所以虚函数的重写也叫作覆盖 ,覆盖就是指虚表中虚函数
    的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
  3. 另外 Func2 继承下来后是虚函数,所以放进了虚表, Func3 也继承下来了,但是不是虚函
    数,所以不会放进虚表。
  4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个 nullptr 。
  5. 总结一下派生类的虚表生成: a. 先将基类中的虚表内容拷贝一份到派生类虚表中 b. 如果派生
    类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c. 派生类自己
    新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
  6. 这里还有一个很容易混淆的问题: 虚函数存在哪的?虚表存在哪的? 答:虚函数存在
    虚表,虚表存在对象中。注意上面的回答的错的 。我们很多时候都是这样深以为然的。注意
    虚表存的是虚函数指针,不是虚函数 ,虚函数和普通函数一样的,都是存在代码段的,只是
    他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。

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();
}
int main()
{
	Person ps;
	Student st;
	Func(ps);
	Func(st);
	return 0;
}
  1. 父类 p指针 是指向ps(Person类型) 对象时, p->BuyTicket 在ps(Person类型) 的虚表中找到虚
    函数是 Person::BuyTicket 。
  2. 父类P指针 是指向st(Student类型) 对象时, p->BuyTicket 在(Student类型) 的虚表中
    找到虚函数是 Student::BuyTicket 。
  3. 这样就实现出了不同对象去完成同一行为时,展现出不同的形态。
  4. 反过来思考我们要达到多态,有两个条件,一个是虚函数覆盖,一个是对象的指针或引用调
    用虚函数。
  5. 再通过下面的汇编代码分析, 看出满足多态以后的函数调用,不是在编译时确定的,是运行
    起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的

4.3、静态绑定和动态绑定

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

五、单继承和多继承关系中的虚函数表

5.1、单继承中的虚函数表

从监视窗口来看我们看不见Derive类中的Func3()和Func4()函数,原因是编译器隐藏了这两个函数,可以通过虚表打印查看该虚函数

//执行以下代码并调试查看
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;
}

虚函数地址打印

//单继承
class Base {
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
private:
	int a=1;
};
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=2;
};

typedef void(*VFPTR)();
//打印虚函数表中的虚函数地址,并且调用虚函数
void PrintVFTable(VFPTR* table)
{
	cout << "虚表地址>" << table << endl;
	for (size_t i = 0; table[i] != nullptr; i++)
	{
		printf("VFT[%d]:%p->", i, table[i]);
		VFPTR pf = table[i];
		pf();
	}
	//只要有函数指针+括号 就能调用对应的函数

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

	VFPTR* Table = (VFPTR*)(*(int*)&b);
	PrintVFTable(Table);

// 思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数
指针的指针数组,这个数组最后面放了一个nullptr
// 1.先取b的地址,强转成一个int*的指针
// 2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
// 3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
// 4.虚表指针传递给PrintVTable进行打印虚表
// 5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最
后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的-生成-清理解决方案,再
编译就好了

	PrintVFTable((VFPTR*)(*(int*)&d));
	return 0;
}
//虚表地址 > 00229B34
//VFT[0]:002212AD->Base::func1
//VFT[1] : 00221113->Base::func2
//虚表地址 > 00229B64
//VFT[0]:0022123F->Derive::func1
//VFT[1] : 00221113->Base::func2
//VFT[2] : 0022122B->Derive::func3
//VFT[3] : 00221168->Derive::func4

5.2、多继承中的虚函数表

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

class Base2 {
public:
	virtual void func1() { cout << "Base2::func1" << endl; }
	virtual void func2() { cout << "Base2::func2" << endl; }
private:
		int b2=2;
};

class Derive : public Base1, public Base2 {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
private:
	int d1=3;
};

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]);
		printf("VFT[%d]:%p->", i, vTable[i]);
		VFPTR f = vTable[i];
		f();
	}
	cout << endl;
}
int main()
{
	Derive d;

	VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
	PrintVTable(vTableb1);

	//切片&d地址偏移一个Base1对象
	VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
	PrintVTable(vTableb2);

	return 0;
}
//运行结果
//虚表地址 > 009E9B94
//VFT[0]:009E1244->Derive::func1
//VFT[1] : 009E12E9->Base1::func2
//VFT[2] : 009E1230->Derive::func3
//
//虚表地址 > 009E9BA8
//VFT[0]:009E1357->Derive::func1
//VFT[1] : 009E10B9->Base2::func2

六、常见面试题(题目)

1. 什么是多态?
2. 什么是重载、重写(覆盖)、重定义(隐藏)?
3. 多态的实现原理?
4. inline函数可以是虚函数吗?
5. 静态成员可以是虚函数吗?
6. 构造函数可以是虚函数吗?
7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
8. 对象访问普通函数快还是虚函数更快?
9. 虚函数表是在什么阶段生成的,存在哪的?
10. C++菱形继承的问题?虚继承的原理?
11. 什么是抽象类?抽象类的作用?

相关推荐
一点媛艺1 小时前
Kotlin函数由易到难
开发语言·python·kotlin
姑苏风1 小时前
《Kotlin实战》-附录
android·开发语言·kotlin
奋斗的小花生2 小时前
c++ 多态性
开发语言·c++
魔道不误砍柴功2 小时前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
闲晨2 小时前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
老猿讲编程2 小时前
一个例子来说明Ada语言的实时性支持
开发语言·ada
UestcXiye3 小时前
《TCP/IP网络编程》学习笔记 | Chapter 3:地址族与数据序列
c++·计算机网络·ip·tcp
Chrikk3 小时前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*3 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue3 小时前
go语言连续监控事件并回调处理
开发语言·后端·golang