多态

多态的概念

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

多态的定义及实现

多态构成的条件

1、必须通过基类的指针或者引用调用虚函数

2、子类必须对基类的虚函数进行重写

虚函数

被关键字virtual修饰的类成员函数称为虚函数

虚函数的重写

子类中有一个和基类完全相同的虚函数(子类的虚函数和基类的虚函数返回类型、函数名、参数列表完全相同)称为子类的虚函数重写(覆盖)基类的虚函数

cpp 复制代码
#include<iostream>
using namespace std;

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

class Student : public Person 
{
public:
	// 在重写基类虚函数时,子类的虚函数也可以不叫virtual关键字
	// 因为子类会将基类的虚函数继承下来,在子类中依然会保留有虚函数性质
	// 但是这种写法不规范  void ButTicket ()
	virtual void BuyTicket() 
	{
		cout << "买票-半价" << endl; 
	}
};

void Func(Person& p)
{
	p.BuyTicket();
}

int main()
{
	Person ps;
	Student st;

	Func(ps);
	Func(st);
	return 0;
}

虚函数重写的两个例外

协变

当子类重写基类的虚函数时,与基类的虚函数返回值类型不同。(这里的不同意思是:基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用)

析构函数的重写

当基类的析构函数是虚函数时,子类想去重写基类的虚函数的析构函数,只需要定义就行,不需要加关键字virtual,也不需要同名。 因为编译器对析构函数名做了特殊处理,统一将析构函数名处理为destructor。

C++11 关键字override 和 final

final

修饰虚函数,表示该虚函数不能再被重写

class Car

{

public: virtual void Drive() final

{}

};

class Benz :public Car

{

public: virtual void Drive()

{

cout "Benz-舒适" endl;

}

};

override

检查子类虚函数是否重写了基类的某个虚函数,如果没写重写就报错。

class Car

{

public:

virtual void Drive()

{}

};

class Benz :public Car

{

public:

virtual void Drive() override

{

cout "Benz-舒适" endl;

}

};

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

|---------|----------------------------------------------------------|
| 重载 | 1、两个函数在同一作用域 2、函数名相同;参数有三不同(顺序不同or类型不同or数量不同) |
| 重写(覆盖) | 1、两个函数分别在基类和子列的作用域 2、函数名/参数/返回值都必须相同(协变例外) 3、两个函数必须是虚函数 |
| 重定义(隐藏) | 1、两个函数分别在基类和子列的作用域 2、函数名相同 3、基类和子类的两个同名函数不是构成重写就是重定义 |
[概念对比]

抽象类

纯虚函数:在虚函数后面加上 =0。**一个类中有纯虚函数就称为抽象类(也称接口类);抽象类不能实例化出对象。**子类继承后也不能实例化出对象,只有重写纯虚函数,子类才能实例化出对象。

纯虚函数的作用:(纯虚函数不需要实现函数体)
1、强制子类去完成重写
2、表示抽象的类型
3、更能体现出接口继承

接口继承和实现继承

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

多态的原理

虚函数表

如果一个类中有虚函数,那么当这个类实例化出对象时,该对象中不但有成员变量,还会有一个虚函数表指针(_vfptr)(这个_vfptr可能在对象的内存模型最前面,也可能在对象的内存模型最后面;和编译器有关) 虚函数表指针指向的是虚函数表(简称虚表),这个虚表内存的是虚函数的地址。

cpp 复制代码
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、可以看到子类是重写了基类的第一个虚函数(Func1),所以子类的虚表中覆盖类基类的第一个虚函数。

2、Func2在子类中没有重写,子类继承下来了,所以也放在了虚表中,Func3不是虚函数,所以不会放入虚表中

3、虚函数表本质是一个存虚函数指针的指针数组,一般情况会在这个数组最后放一个nullptr

4、虚函数存在代码段中 ,每一个虚函数都有一个虚函数指针,这些虚函数指针存在虚表 中,虚表存在代码段中 ,这个虚表有一个首地址,这个首地址也叫虚表指针 就在存在实例化出的对象中。
总结子类虚表的生成:

  1. 先将基类中的虚表内容拷贝一份到子类虚表中
  2. 如果子类重写了基类的某个虚函数,那么就用子类自己的虚函数覆盖虚表中基类的被重写的虚函数
  3. 子类自己新增的虚函数按照它们在子类中声明次序添加到虚表后面

多态的原理

有虚函数表知识的铺垫,多态的原理就很容易理解了。

cpp 复制代码
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 Mike;
 Func(Mike);
 
 Student Johnson;
 Func(Johnson);
 return 0;
}

这段代码,基类和子类对象都调用了Func函数,然后再Func函数中都调用了BuyTicket函数,但是分别调用自己类中的BuyTicket函数。这样就是多态。

展示一下流程,当一个对象去调用BuyTicket函数时,会去自己的虚表中寻找这个虚函数。

这样可以看出满足多态的函数调用,不是在编译时确定的,是运行起来以后到对象中去寻找。不满足多态的函数调用,是在编译时确定好的。

动态绑定和静态绑定

静态绑定:静态的多态 (静态:编译时确定函数地址)

动态绑定:动态的多态(动态:运行时到虚表中找虚函数地址)

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

cpp 复制代码
#include<iostream>
using namespace std;

class Base
{
public:
	virtual void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	
private:
	int _b = 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 _d = 2;
};
int main()
{
	Base b;
	Derive d;
	return 0;
}

根据上面的代码和对象内存模型可以知道编译器隐藏了子类对象的Func3和Func4这两个虚函数。

我们也可以通过它们在虚表中去将它们打印出来。

cpp 复制代码
class Base
{
public:
	virtual void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	
private:
	int _b = 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 _d = 2;
};
// void(*p)() // 定义一个函数指针
typedef void(*VFPTR)(); // 函数指针类型重定义

void PrintVTable(VFPTR* VTable)
{
	cout << "虚表地址:" << VTable << endl;
	for (int i = 0; VTable[i] != nullptr; ++i)
	{
		printf("第%d个虚函数地址:%p", i, VTable[i]);
		cout << endl;
	}
}

int main()
{
	Base b;
	Derive d;
	// 取出d对象的前4个字节,就是虚表指针,虚表的本质就是一个存虚函数地址的指针数组,这个数组的最后有一个nullptr
	//强转int*,就是取出了前4个字节
	//解引用就是拿到了对象的前4个字节的值,这个值就是指向虚表的指针
	//再强转VFPTR*,因为虚表就是存VFPTR类型的数组
	PrintVTable( (VFPTR*)(*(int*)&d) );
	return 0;
}

多继承

cpp 复制代码
#include<iostream>
#include<stdio.h>
using namespace std;

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 = 1;
};
class Derive : public Base1, public Base2
{
public:
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}
	virtual void Func3()
	{
		cout << "Derive::Func3()" << endl;
	}
	virtual void Func4()
	{
		cout << "Derive::Func4()" << endl;
	}
private:
	int _d = 2;
};
// void(*p)() // 定义一个函数指针
typedef void(*VFPTR)(); // 函数指针类型重定义

void PrintVTable(VFPTR* VTable)
{
	cout << "虚表地址:" << VTable << endl;
	for (int i = 0; VTable[i] != nullptr; ++i)
	{
		printf("第%d个虚函数地址:%p -> ", i, VTable[i]);
		VFPTR f = VTable[i];
		f();
		cout << endl;
	}
}

int main()
{
	Derive d;
	// 取出d对象的前4个字节,就是虚表指针,虚表的本质就是一个存虚函数地址的指针数组,这个数组的最后有一个nullptr
	//强转int*,就是取出了前4个字节
	//解引用就是拿到了对象的前4个字节的值,这个值就是指向虚表的指针
	//再强转VFPTR*,因为虚表就是存VFPTR类型的数组
	PrintVTable( (VFPTR*)(*(int*)&d) );
	// 想获取Base2中的虚表地址
	PrintVTable( (VFPTR*) ( *(int*) ((char*)&d + sizeof(Base1)))); 
	return 0;
}

多继承的基类中未重写的虚函数放在第一个继承的基类的虚函数表中。

相关推荐
Mr.Z.41120 分钟前
【历年CSP-S复赛第一题】暴力解法与正解合集(2019-2022)
c++
Death20024 分钟前
使用Qt进行TCP和UDP网络编程
网络·c++·qt·tcp/ip
郭二哈35 分钟前
C++——list
开发语言·c++·list
黑不溜秋的1 小时前
C++ 语言特性29 - 协程介绍
开发语言·c++
一丝晨光1 小时前
C++、Ruby和JavaScript
java·开发语言·javascript·c++·python·c·ruby
￴ㅤ￴￴ㅤ9527超级帅2 小时前
LeetCode hot100---二叉树专题(C++语言)
c++·算法·leetcode
_GR2 小时前
每日OJ题_牛客_牛牛冲钻五_模拟_C++_Java
java·数据结构·c++·算法·动态规划
Death2002 小时前
Qt 中的 QListWidget、QTreeWidget 和 QTableWidget:简化的数据展示控件
c语言·开发语言·c++·qt·c#
六点半8882 小时前
【C++】速通涉及 “vector” 的经典OJ编程题
开发语言·c++·算法·青少年编程·推荐算法
coduck_S12004zbj3 小时前
csp-j模拟五补题报告
c++·算法·图论