C++ 多态详解

1. 多态的概念

多态是面向对象编程中的一个重要概念,通俗来说,多态就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态
举个例子:比如我们在12306买票,对于买票这个行为,成人买成人票就是全价,学生买学生票就是半价。
在这里插入图片描述

2. 多态的定义及实现

2.1 多态的构成条件

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

  1. 必须通过基类的指针或者引用调用虚函数
  2. 被调用的函数必须是虚函数且派生类必须对基类的虚函数进行重写
    在这里插入图片描述
    多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如上面图片中的例子:Student继承了Person。Person对象买票全价,Student对象买票半价。

代码如下:

class Person
{
public:
	virtual void buy_ticket()
	{
		cout << "全价" << endl;
	}
};
class Student : public Person
{

public:
	virtual void buy_ticket()
	{
		cout << "半价" << endl;
	}

};
void func(Person& p)
{
	p.buy_ticket();
}
void func(Person* p)
{
	p->buy_ticket();
}
int main()
{
	Person p;
	Student s;
	//基类的引用
	func(p);
	func(s);
	//基类的指针
	func(&p);
	func(&s);
	return 0;
}

2.2 虚函数

virtual修饰的类成员函数称为虚函数。

class Person
{
public:
	//buy_ticket()就是虚函数,被virtual修饰的类成员函数
	virtual void buy_ticket()
	{
		cout << "全价" << endl;
	}
};

2.3 虚函数的重写

虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
重写虚函数的目的是为了在派生类中提供一个特定于派生类的实现,从而实现特定的行为。重写后,当通过基类指针或引用调用虚函数时,如果指向或引用的是派生类对象,将会调用派生类中的虚函数,如果指向或引用的是基类对象,将会调用基类中的虚函数,这样就可以做到不同的派生类的对象在调用同一个函数时,能表现出不同的行为。
在这里插入图片描述

2.3.1 虚函数重写的两个例外

  1. 协变(基类与派生类虚函数返回值类型不同)
    派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
    在这里插入图片描述
  2. 析构函数的重写(基类与派生类析构函数的名字不同)
    如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
    这里我们可以间接验证一下:
    在这里插入图片描述
    因此在涉及到资源管理的时候,基类的析构函数最好加上virtual关键字修饰,否则可能在某些情况下,造成无法正确调用析构函数而造成内存泄漏。
    比如下面这种情况:
    在这里插入图片描述

2.4 C++11 override 和 final

  1. final:修饰虚函数,表示该虚函数不能再被重写
    在这里插入图片描述
  2. final:修饰类,表示该类不能被继承
    在这里插入图片描述
  3. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
    在这里插入图片描述

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

在这里插入图片描述

3. 多态的原理

3.1 虚函数表

我们先来看一道题:

#include<iostream>
using namespace std;
class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	int _b = 1;
};

int main()
{
	cout << sizeof(Base) << endl;
	return 0;
}

运行结果:(32位平台下是8,64位平台下位16。)
在这里插入图片描述
我们发现,Base类只有一个整型变量,就算考虑内存对齐,结果应该是4呀,这里为啥会输出8呢?
下面我们打开监视窗口来看下Base类对象的模型:
在这里插入图片描述
通过观察测试我们发现b对象是8bytes,除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。

针对上面的代码我们做出以下改造
1.我们增加一个派生类Derive去继承Base
2.Derive中重写Func1
3.Base再增加一个虚函数Func2和一个普通函数Func3

#include<iostream>
using namespace std;

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. 虚函数存在哪的?虚表存在哪的?
    答:虚函数存在虚表,虚表存在对象中。注意上面的回答的错的。注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?
    在这里插入图片描述
    上面分析了这个半天了那么多态的原理到底是什么?下面我们来具体分析一下:

3.2多态的原理

现在我们再来看下之前写的买票的代码,Func函数传Person调用的Person::BuyTicket,传Student调用的是Student::BuyTicket:

#include<iostream>
using namespace std;
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 p;
	Func(p);
	Student s;
	Func(s);
	return 0;
}

在这里插入图片描述
上面我们分析出,当p指向谁就去谁的虚函数表中去取对应虚函数的地址,因此就实现出了不同对象完成同一行为时,展现出不同的形态。
而我们要达到多态,有两个条件,一个是虚函数覆盖,一个是对象的指针或引用调用虚函数。这是为什么呢?我们来反思一下:
在这里插入图片描述
我们分析出,赋值不会拷贝虚函数指针,因此要实现出不同对象完成同一行为时,展现出不同的形态,有两个条件,一个是虚函数覆盖,一个是对象的指针或引用调用虚函数。
下面我们在来分析一下多态调用和普通函数调用有什么区别呢?
在这里插入图片描述
通过上面的汇编代码分析,看出满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时编译链接时确认好的

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

在单继承和多继承关系中,下面我们去关注的是派生类对象的虚表模型,因为基类
的虚表模型前面我们已经看过了,没什么需要特别研究的。

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

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

分析如下:
在这里插入图片描述
我们通过监视窗口中我们发现看不见func3和func4。这里是编译器的监视窗口故意隐藏了这两个函数,也可以认为是他的一个小bug。而且通过内存窗口只能看清虚函数的个数,那么我们如何查看d的虚表呢?下面我们使用代码打印出虚表中的函数。

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 PrintVFTable(VFPTR* vftptr)
{
	for (int i = 0; vftptr[i] != 0; ++i)
	{
		printf("第%d个虚函数的地址:%p------>", i + 1, vftptr[i]);
		vftptr[i]();
	}
	printf("\n");
}
int main()
{
	Base b;
	Derive d;
//思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数指针的指针数组,
// 这个数组最后面放了一个nullptr
// 1.先取b的地址,强转成一个int*的指针
// 2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
// 3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
// 4.虚表指针传递给PrintVTable进行打印虚表
	VFPTR* vftptr = (VFPTR*)(*(int*)&b);
	PrintVFTable(vftptr);
	vftptr = (VFPTR*)(*(int*)&d);
	PrintVFTable(vftptr);
	return 0;
}

运行结果如下:
在这里插入图片描述
因此我们可以推断出派生类的虚函数表模型:
在这里插入图片描述

4.2 多继承中的虚函数表

分析如下代码:

#include <iostream>
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 = 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 = 0;
};
typedef void(*VFPTR) ();
void PrintVFTable(VFPTR* vftptr)
{
	for (int i = 0; vftptr[i] != 0; ++i)
	{
		printf("第%d个虚函数的地址:%p------>", i + 1, vftptr[i]);
		vftptr[i]();
	}
	printf("\n");
}
int main()
{
	Derive d;
	VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
	PrintVFTable(vTableb1);
	VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
	PrintVFTable(vTableb2);
	return 0;
}

通过监视创窗口看下d对象的模型:
在这里插入图片描述
通过监视窗口可以看出,d对象有两个虚函数指针和从两个基类继承下来的成员以及自己的成员,但是从监视窗口无法看出两个虚函数表具体放了哪几个虚函数,因此下面我们打印一下两个虚函数表:
观察下图可以看出:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中!!!
在这里插入图片描述

从上图中可以看出,两个虚函数表中的func1是同一个函数,但是两张表中的地址不一样这是为啥呢?
在这里插入图片描述
下面从汇编代码看下,这个函数是如何调用的:
在这里插入图片描述
从上表中看出,两个表中func1的地址不同,是因为第二个虚函数表中的func1在调用的时候要修正this指针。

至此,本片文章就结束了,若本篇内容对您有所帮助,请三连点赞,关注,收藏支持下。

创作不易,白嫖不好,各位的支持和认可,就是我创作的最大动力,我们下篇文章见!

如果本篇博客有任何错误,请批评指教,不胜感激 !!!
在这里插入图片描述

相关推荐

  1. C++基础-详解

    2024-05-01 18:40:03       59 阅读
  2. C++ 详解(14)

    2024-05-01 18:40:03       71 阅读
  3. c++详细学习

    2024-05-01 18:40:03       21 阅读
  4. C++详解:静态与动态的实现

    2024-05-01 18:40:03       31 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-05-01 18:40:03       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-05-01 18:40:03       100 阅读
  3. 在Django里面运行非项目文件

    2024-05-01 18:40:03       82 阅读
  4. Python语言-面向对象

    2024-05-01 18:40:03       91 阅读

热门阅读

  1. qt对话框功能介绍

    2024-05-01 18:40:03       35 阅读
  2. 2024年五一数学建模竞赛赛题浅析-助攻快速选题

    2024-05-01 18:40:03       35 阅读
  3. 学习mysql相关知识记录

    2024-05-01 18:40:03       32 阅读
  4. 大模型LoRA微调调参的实战技巧

    2024-05-01 18:40:03       36 阅读
  5. 在编程中,方法和函数都是什么意思

    2024-05-01 18:40:03       33 阅读
  6. C语言创建文件夹和多级目录

    2024-05-01 18:40:03       33 阅读
  7. DB-GPT源码阅读1-数据库表读取

    2024-05-01 18:40:03       32 阅读
  8. 2024 c/c++A组填空第一题--选择与篮球

    2024-05-01 18:40:03       30 阅读
  9. 网络安全思考题

    2024-05-01 18:40:03       34 阅读