【C++进阶篇】学习C++就看这篇--->多态超详解

主页:HABUO🍁主页:HABUO

🍁C++入门到精通专栏🍁

🍁如果再也不能见到你,祝你早安,午安,晚安🍁


目录

📕1、多态的概念及定义

[✨1.1 多态的概念](#✨1.1 多态的概念)

[✨1.2 多态的定义及实现](#✨1.2 多态的定义及实现)

[🌟1.2.1 多态的构成条件](#🌟1.2.1 多态的构成条件)

[🌟1.2.2 虚函数](#🌟1.2.2 虚函数)

[🌟1.2.3 虚函数的重写](#🌟1.2.3 虚函数的重写)

📕2、多态的实例

📕3、构成多态的几个特例

📕4、多态的原理

[✨4.1 虚函数表](#✨4.1 虚函数表)

[✨4.2 多态的理解](#✨4.2 多态的理解)

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

[✨5.1 单继承中的虚函数表](#✨5.1 单继承中的虚函数表)

[✨5.2 多继承中的虚函数表](#✨5.2 多继承中的虚函数表)

📕6、抽象类(了解)

📕7、总结


前言

上篇博客我们深入了解学习了继承的相关知识,从这篇博客开始我们就进入多态的学习,多态在校招笔试和面试中这些内容考察的也很多!,知识大多非常琐碎,需要我们多加记忆,希望通过本篇博客的学习,大家有所收获!
本章重点

本篇文章着重讲解

1. 多态的概念和定义
2. 多态的实现(多态的构成条件、虚函数与虚函数的重写)
3. 多态的原理(虚函数表)(包含单继承虚函数表和多继承虚函数表)

其中讲解过程中会简单介绍:虚函数重写的两个例外:协变与析构函数的重写C++11 override 和 final抽象类

注:如果你不知道什么是继承,或继承``的知识掌握的还不牢固,请先阅读下面文章:

【C++进阶篇】学习C++就看这篇--->继承超详解


📕1、多态的概念及定义

1.1 多态的概念

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

举个例子:我们放假买票,当普通人买票时,是全价买票;学生买票时,是半价买票;军人 买票时是优先买票。(同样是买票这样的一个行为,但是不同的人去买却体现不同的情况)

1.2 多态的定义及实现

🌟1.2.1 多态的构成条件

在继承中构成多态要有两个条件:

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

🌟1.2.2 虚函数

关键字virtual加在成员函数前
这个成员函数就是虚函数!

注意只能是成员函数,其它的一概不行

virtual关键字

  1. 可以修饰成原函数,为了完成虚函数的重写,满足多态的条件之一.
  2. 可以在菱形继承中,去完成虚继承,解决数据冗余和二义性。

两个地方使用了同一个关键字,但是他们互相之间没有一点关联

🌟1.2.3 虚函数的重写

虚函数的重写(也叫覆盖):

要实现有效的重写,必须同时满足以下几个条件:

  1. 基类函数必须声明为 virtual :这是重写的基石。virtual 关键字告诉编译器,这个函数需要动态绑定。

  2. 派生类函数必须与基类函数完全一致

    • 函数名相同

    • 参数列表相同(参数的类型、数量、顺序)

    • 常量性 相同(即是否被 const 修饰)

    • 返回类型相同(有一个例外情况,见下文"协变返回类型")

  3. 重写关系发生在继承体系中:派生类继承自基类。

cpp 复制代码
class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
};

注意:

在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因 为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议 这样使用

虚函数重写的两个例外:

1. 协变(基类与派生类虚函数返回值类型不同)

派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指 针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。

cpp 复制代码
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. 析构函数的重写(基类与派生类析构函数的名字不同)

基类的析构函数为虚函数,子类析构函数只要定义就一定构成了重写,看起来函数名不一样,这里编译器在背后进行了处理,背后统一变成了destructor

cpp 复制代码
class Person {
public:
	virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
	virtual ~Student() { cout << "~Student()" << endl; }
};

这里就会常有的一个问题:析构函数是否需要定义成虚函数?

答案:是的。

cpp 复制代码
class Person {
public:
	~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
	 ~Student() { cout << "~Student()" << endl; }
};
int main()
{
	Person* p1 = new Person;
	Person* p2 = new Student;
	delete p1;
	delete p2;
	return 0;
}

如上述代码,如果子类里有资源需要释放,那么这就会引起内存泄漏,因为这里没有构成多态,调用的指针类型是谁就调用谁的析构函数,这里子类的析构函数并不会被调用。

C++11 override 和 final

final:

  • 用于 :表示这个类不能被继承。
    class MyFinalClass final { ... };

  • 用于虚函数 :表示这个虚函数在派生类中不能再被重写

cpp 复制代码
class Car
{
public:
	virtual void Drive() final {}
};
class Benz :public Car
{
public:
	virtual void Drive() { cout << "Benz-舒适" << endl; }//报错无法重写"final"函数
};

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

  • 编译器检查 :如果派生类中标记了 override 的函数不符合重写条件(例如,拼写错误、参数不匹配),编译器会报错。这可以避免因疏忽而意外创建新函数(隐藏)而不是重写。

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

特性 重写 (Override) 重载 (Overload) 隐藏 (Hide)
作用域 不同类(继承体系) 同一个类 不同类(继承体系)
函数名 相同 相同 相同
参数列表 必须相同 必须不同 可以不同
virtual 基类函数必须是 不要求 不要求
目的 实现多态 提供同名函数的不同版本 派生类函数屏蔽了基类的同名函数

📕2、多态的实例

上面我们了解了多态的构成条件,弄段实例代码体会一下:

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

p1和p2是基类指针,它们调用的函数恰好还被重写了,所以这里符合多态,p1指针指向的内容是Person所以它调用Person中的函数,然而p2指针指向的内容是Student,所以它调用的是Student中的函数!
上面就是一段多态的实例代码

  • 满足多态:跟调用对象的类型无关.跟指向对象有关,指向哪个对象调用就是他的虚函数
  • 不满足多态:跟调用对象的类型有关,类型是什么调用的就是谁的虚函数

相信这里大家看到为什么给Person,其它的不行?

一句话请你去看上篇博客:【C++进阶篇】学习C++就看这篇--->继承超详解

子类天然的可以给父类因此是Person但是反过来不行,前面讲到过

📕3、构成多态的几个特例

前面我们介绍了虚函数重写的几个例外,那这里用到多态里

  • 特例一: 子类的虚函数不写virtual依旧构成多态
cpp 复制代码
class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
	void BuyTicket() { cout << "买票-半价" << endl; }
};
  • 特例二:基类与派生类虚函数返回值类型不同也可以构成多态(返回值必须满足某种条件)
cpp 复制代码
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; }
};

父类的返回值要返回父类,子类的返回值要返回子类

  • 注意事项1:父类不写virtual,而子类的同名函数写了virtual,这是不构成多态的!
cpp 复制代码
class Person {
public:
	void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
  • 注意事项2:在继承体系中,父子类的同名函数不构成重写就构成隐藏,不可能构成重载!

📕4、多态的原理

如果你单纯的认为Base类只有一个整型变量占用空间的话,那你就上当啦!

事实上在32位机器下,这里的结果是8。在64位机器下,这里的结果是16!

这是因为它除了有一个变量外,还有``一个指针,此指针指向一个虚函数表

4.1 虚函数表

下面我们来认识这个虚函数表

cpp 复制代码
class A
{
public:
	virtual void func1()
	{
		cout << "父类func1";
	}
private:
	int _a;
};
class B : public A
{
public:
	virtual void func1()
	{
		cout << "子类func1";
	}
private:
	int _b;
};

int main()
{
	A a;
	B b;
	return 0;
}

此指针叫虚表指针:vfptr,也就是``virtual function ptr

这个指针并不是直接指向虚函数的地址,而是指向一个虚函数表,可以理解为一个函数指针数组,此数组中存放着此对象中所有的虚函数的地址,一般情况这个数组最后面放了一个nullptr它们的关系可以用下图表示:

注:不管有没有继承体系或多态,``只要有虚函数就有虚表!

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

4.2 多态的理解

所以,从上面的例子我们可以去揣摩父类和子类的虚表指针和指向
的内容有什么不同或相同处吗?多态的原理到底是什么?

通过下面的代码来观察内存情况,``得出父子类虚表的关联:

cpp 复制代码
class A
{
public:
	virtual void func1()
	{
		cout << "父类func1";
	}
	virtual void func2()
	{
		cout << "父类func2";
	}
private:
	int _a;
};
class B : public A
{
public:
	virtual void func1()
	{
		cout << "子类func1";
	}
private:
	int _b;
};

结论

父类和子类的虚表指针是不同的,证明父子类各有一张虚函数表!
函数func1在子类中被重写了,所以父子类虚表中的func1函数地址是不同的
函数func2没有被子类重写,所以父子类虚表中的func2函数地址是相同的

这也就是为什么能有多态,多态的根本原理就是因为虚函数的重写,使得相互继承的双方拥有不同的虚表,通过调用不同的类的虚函数体现不同的行为,虽然虚函数的函数名一样,但是结果不同,这就是多态

拓展结论:同一个类的不同对象共用一个虚表
在这里不得不提到一个问题:虚函数存在哪的?虚表存在哪的?

答:虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的 ,只是 他的指针存到了虚表中 。另外对象中存的不是虚表,存的是虚表指针。虚表是存在代码段(常量区)的。

需要注意:满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的

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

5.1 单继承中的虚函数表

单继承中的虚函数表事实上就是上述我们所讲述的虚函数表,不再赘述。

但这里需要注意,有时候编译器只将子类继承父类的虚表体现出来,而将自己的虚函数隐藏了起来,这并不是说没有,而是编译器没有体现出来,这时候我们就只能通过代码的形式将其打印出来,如下所示:

cpp 复制代码
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 f = vTable[i];
		f();
	}
	cout << endl;
}
int main()
{
	Base b;
	Derive d;
	VFPTR* vTableb = (VFPTR*)(*(int*)&b);
	PrintVTable(vTableb);
	VFPTR* vTabled = (VFPTR*)(*(int*)&d);
	PrintVTable(vTabled);
	return 0;
}

可以利用上述代码将其打印下来。

// 思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数 指针的指针数组,这个数组最后面放了一个nullptr

// 1.先取b的地址,强转成一个int*的指针

// 2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针

// 3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。

// 4.虚表指针传递给PrintVTable进行打印虚表

// 5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最 后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的-生成-清理解决方案,再编译就好了。

5.2 多继承中的虚函数表

cpp 复制代码
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;
};

如上述所示的多继承唯一需要注意的是:多继承的虚函数表,子类的虚函数会先放到先继承的那个虚函数表里。如下图所示:

对于这一部分想进一步了解的可以参考学习下面这篇博客:

【C++】多继承的多态

📕6、抽象类(了解)

在虚函数的后面写上=0 ,则这个函数为纯虚函数。

包含纯虚函数的类叫做抽象类(也叫接口 类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。

纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

1、纯虚函数的作用,强制子类去完成重写
2、表示抽象的类型,抽象就是在现实中没有对应的实体的。

接口继承和实现继承:

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


几个小结论

  1. 析构函数最好定义为虚函数
  2. 构造函数不能定义为虚函数
  3. 静态成员函数不能是虚函数
  4. 内联函数(inline)不能是虚函数,(内联函数直接拷贝到对应位置,是没有地址的)

📕7、总结

本篇博客我们深入了解了多态的相关知识点,我们可以将知识点进行如下的串联:

上面的知识,你了解了吗?我们还要继续前进,加油,兄弟!


相关推荐
掘金者阿豪19 小时前
高可用读写分离实战(二):我把数据库主库停了,结果整个集群的反应和我想象的不一样
后端
掘金者阿豪19 小时前
《高可用读写分离集群实战》系列(一)
后端
Dilee19 小时前
Spring AI 2.0.0 Prompt 最小 Demo:system、user、template 到底怎么分工
后端
未秃头的程序猿19 小时前
Java 26正式发布!这3个新特性,让代码量直接减半
java·后端·面试
小旭Coding20 小时前
卧靠!Go 传给前端的 int64 竟然变成了这个?
后端
用户2986985301420 小时前
Word 文档文本查找与替换的 Java 实现方案
java·后端
kunge201320 小时前
深度剖析Claude Code 的CLAUDE.md加载逻辑
后端·vibecoding
米沙AI20 小时前
MSYS2 快速使用版本
后端