IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> C++ 多态详解【用法+原理】 -> 正文阅读

[C++知识库]C++ 多态详解【用法+原理】

?前言:本文会对C++中多态这个概念进行详细介绍,通过用法和原理两个方面进行阐述,主要会对多态的概念多态的实现和原理,以及对单继承和多继承中的虚函数表模型进行分析,还介绍了C++11中的两个关键字overridefinal的使用.

🏝?1.多态的概念

多态:通俗来讲,就是多种形态,同一件事情,由不同类型的对象去完成时,会表现出多种状态.

比如,在日常生活中,当我们去旅游景点游玩时,需要买票,买票时的票价会根据买票人的身份不同,所对应的价格也就不一样,当我们是普通人时,可能会是正常票价,是学生时,可能会打五折,是儿童时,会免门票.
在这里插入图片描述

那么,我们将这个动作,用一个BuyTicket()的函数来表示,当我们用不同的身份(对象)去调用这个函数时,它会表现出三种不同的价格(状态).这就是多态.

🏠2.多态的实现

📖2.1 多态的构成条件

C++多态意味着调用成员函数时,会根据调用成员函数的对象的类型来执行不同的函数,并产生不同的行为,比如Student类继承了Person类,Student类买票半价,Person类买票全价.

看如下代码:

class Person
{
public:
    //加virtual代表虚函数,下文中会讲解
	virtual void BuyTicket()
	{
		cout << "Person::全价" << endl;
	}
};

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

class Children : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "Children::免门票" << endl;
	}
};

int main()
{
    Person* p;
	Student s;
	Children c;

	//指向Student
	p = &s;
	p->BuyTicket();

	//指向Children
	p = &c;
	p->BuyTicket();
	
	return 0;
}

执行结果如下:

可以看到,我们使用同一个父类指针去分别指向不同的子类对象时,并且调用BuyTicket()函数时,分别调用了StudentChildren子类的BuyTicket()函数.这便是多态的实现方式.

所以,我们也可以得到构成多态的两个条件:

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

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

📖2.2 虚函数

在C++中,基类将类型相关的函数和派生类不做改变直接继承的函数区分对待.对于某些函数,基类希望它的派生类各自定义合适自己的版本,此时基类就将这些函数声明成虚函数.

虚函数:被Virtual修饰的函数即为虚函数

📖2.3 虚函数的重写

虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(派生类的虚函数与基类虚函数的返回值函数名形参列表完全相同),称子类的虚函数重写了基类的虚函数.

在这里插入图片描述

📖2.4 C++11 override 和 final

在C++中,我们可以看到,对于虚函数重写的要求是比较严格的,要求三同,但在实际当中,往往会因为不小心可能会将派生类中的要重写的虚函数的函数名或者参数列表没有写成相同的,从而定义了一个新的函数,这种情况编译器也不会报错,只有当我们自己去运行发现结果不对时才能反应到,因此:C++11就提供了两个关键字:override来帮助用户检测是否被重写.

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

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

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

如果我们使用override来标记某个函数,但这个函数并没有重写基类中的某个虚函数,则编译器就会报错.

2.final:修饰虚函数,表明该虚函数不能被重写.

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

class Student : public Person
{
public:
    //会编译报错,Person中的BuyTicket()函数已经被声明成final
	virtual void BuyTicket() override
	{
		cout << "Student::半价" << endl;
	}
};

如果我们把函数定义成了final,之后任何想要覆盖该函数的操作都将引发错误.

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

对于这三种概念的对比,用一张图来表示:

在这里插入图片描述
通过这张图,对于这三个概念的理解可以进一步的加深.

🏖?3. 抽象类

我们来看这样一个场景,比如我们现在要实现一个书店卖书折扣的问题,假如我们需要书店程序支持不同的折扣策略,例如我们可能提供购买量不超过某个限额时,可以享受折扣,一旦超过,均按原价支付,或者只有购买量超过一定数量时,所有书籍均享受折扣,否则全都不打折.

于是我们可以定义一个类Dis_quote来支持不同的折扣策略,Dis_quote负责保存购买量的值和折扣值,其他的表示某种特定策略的类将继承自Dis_quote.
另外,每个派生类通过定义自己的strategy()函数来实现不同的折扣策略.

class Dis_quote
{
public:
	Dis_quote(std::size_t amount = 0, double discount = 0.0)
		: _amount(amount)
		, _discount(discount)
	{}

	virtual void strategy()
	{
		cout << "负责提供给子类继承的购买量和折扣值、不提供折扣策略" << endl;
	}

protected:
	std::size_t _amount;    //购买量
	double _discount;  //折扣值
};

class Bulk_quote : public Dis_quote
{
public:
	Bulk_quote(std::size_t amount = 100, double discount = 8.8)
		:Dis_quote(amount, discount)
	{}

	virtual void strategy()
	{
		cout << "Bulk_quote策略:当购买量超过amount(如果未指定就是100)本时,打8.8折" << endl;
	}
};

class Less_quote : public Dis_quote
{
public:
	Less_quote(std::size_t amount = 50, double discount = 8.8)
		:Dis_quote(amount, discount)
	{}

	virtual void strategy()
	{
		cout << "Less_quote策略:当购买量小于amount(如果未指定就是50)时,打8.8折,一旦大于amount,按原价" << endl;
	}
};

显然,Dis_quote类与任何特定的折扣策略都无关,因此Dis_quote中的strategy()函数是没有实际含义的.

所以对于Dis_quote类,我们并不希望去直接使用它,因为这毫无意义,我们也不希望Dis_quote类创建对象,Dis_quote表示的是一种通用概念,而不是某种具体折扣策略.

此时,我们就可以将Dis_quote类中的strategy()函数设置成纯虚函数,从而实现我们的设计意图.

在函数体的位置(在声明语句的分号之前),写上= 0,这个函数就为纯虚函数. 包含纯虚函数的类叫抽象类,抽象类不能实例化出对象.

注意: 由于虚函数的继承为接口继承,所以继承了具有纯虚函数的基类的派生类也不能实例化对象,只有重写虚函数,派生类才能实例化出对象,纯虚函数规范了派生类必须重写,纯虚函数也体现了虚函数的接口继承.

class Dis_quote
{
public:
	Dis_quote(std::size_t amount = 0, double discount = 0.0)
		: _amount(amount)
		, _discount(discount)
	{}
    
    //纯虚函数
	virtual void strategy() = 0;

protected:
	std::size_t _amount;    //购买量
	double _discount;  //折扣值
};

🏞?4. 多态的原理

📖4.1 虚函数表

class Base
{
public:
	virtual void func1()
	{
		cout << "func1()" << endl;
	}
private:
	int _a;
};

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

运行结果:
在这里插入图片描述

我们先来看一段代码,我们定义了一个Base基类,然后通过sizeof计算基类大小,通过运行结果我们发现基类大小为8bytes也就是说,除了一个_a成员,还多了一个_vfptr放在了对象的前面(某些平台可能会放在对象的最后面,这个与平台有关),对象中的这个指针叫做虚函数表指针(v代表virtual,f代表function).

一个含有虚函数的类中都至少有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表.

在这里插入图片描述

这是在VS监视窗口下观察到的b对象内部结构.

那么如果是一个继承了这个基类的派生类,派生类的虚函数表又是怎样的结构呢?我们将上边的代码进行改造然后接着分析:

通过对上面的代码进行改造:

  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 _a = 1;
};

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

int main()
{
	Base b;
	Derive d;

	return 0;
}

紧接着我们再使用VS的监视窗口去观察b对象和d对象的内部结构:

在这里插入图片描述
通过观察,我们发现了以下几点:

  1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是从Base中继承而来的成员,另一部分是Derive类自己的成员.
  2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现func1完成了重写,所以d的虚表中存的是重写的Derive::func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法.
  3. 另外func2继承下来后是虚函数,所以放进了虚表,func3也继承下来了,但是不是虚函数,所以不会放进虚表.
  4. 虚函数表本质是一个存虚函数指针指针数组,一般情况这个数组最后面放了一个nullptr.
  5. 总结一下派生类的虚表生成:
    a.先将基类中的虚表内容拷贝一份到派生类虚表中
    b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
    c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后
    .

注意:这里有一个容易混淆的地方,虚函数存在哪里?虚表存在哪里?

可能会有类似:虚函数存在虚表中,虚表存在对象中. 而这样的回答是错误的.

注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样,都是存在代码段中的,只是它的指针又存在了虚表中.另外对象中存的不是虚表,存的是虚表指针.那虚表是存在哪里的呢?

我们可以写一段代码来验证:

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 _a = 1;
};

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

int a = 0;
int main()
{
	Base b1;
	
	int b = 0;  //栈区
	static int c = 0;  //静态区
	int* d = new int[10]; //堆区
	const char* str = "hello world"; // 常量区/代码段

	printf("栈区:%p\n", &b);
	printf("静态区/数据段:%p\n", &a);
	printf("静态区/数据段:%p\n", &c);
	printf("堆区:%p\n", d);
	printf("常量区/代码段:%p\n", str);

	printf("虚表:%p\n", (*((int*)&b1)));

	return 0;
}

我们通过查看虚表的地址和哪一个存储区域的数据地址非常接近来判断,在对虚表找地址时,由于虚表指针一般存在对象的前4个字节中,所以我们可以先将b1对象的地址转换为int*的地址以便我们在解引用时能够取出前4个字节的内容也就是虚表指针.

运行结果如下:
在这里插入图片描述
从运行结果我们可以明显可以看出:虚表的地址和常量区/代码段的地址很相近,所以我们也可以推断出虚表是存放在代码段/常量区.

📖4.2 多态的原理

从我们上面的分析,那么多态的原理到底是什么呢?

我们先来看一段代码:

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

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

void Func(Person* people)
{
	people->BuyTicket();
}

int main()
{
	Person p;
	//传入p->调用Person::BuyTicket
	Func(&p);

	//传入s->调用Student::BuyTicket
	Student s;
	Func(&s);

	return 0;
}

运行结果:
在这里插入图片描述
也就是说,对于Func函数内部利用Person类型的指针去调用BuyTicket()函数,当people指针指向p对象时,就调用Person中的BuyTicket,当people指针指向s对象时,就调用Student中的BuyTicket.

在这里插入图片描述

  1. 观察下图的红色箭头我们看到,people是指向p对象时,people->BuyTicketp的虚表中找到虚函数是Person::BuyTicket.
  2. 观察下图的橙色箭头我们看到,people是指向s对象时,people->BuyTickets的虚表中找到虚函数是Student::BuyTicket.
  3. 这样就实现出了不同对象去完成同一行为时,展现出不同的形态.
  4. 我们要达到多态,有两个条件,一个是虚函数覆盖,一个是对象的指针或引用调用虚函数.
  5. 通过下面的汇编代码分析,看出满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中找的. 不满足多态的函数调用时编译时确认好的.

在这里插入图片描述
下面我们来看汇编代码分析:
在这里插入图片描述
传入s对象时也是类似.
下面我们再来看普通函的调用:
我们用p对象调用BuyTicket函数,首先BuyTicket虽然是虚函数,但是p是对象,不满足多态的条件,所以这里是普通函数的调用转换成地址时,是在编译时已经从符号表确认了函数的地址,直接call 地址.

int main()
{
	Person p;
	p.BuyTicket();
	//传入p->调用Person::BuyTicket
	Func(&p);

	//传入s->调用Student::BuyTicket
	Student s;
	Func(&s);

	return 0;
}

这里我们也看见了,直接call地址.
在这里插入图片描述
在这里插入图片描述

📖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 = 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 = 1;
};

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

当我们使用visual studio2019的监视窗口去观察b对象和d对象的内部结构时:
在这里插入图片描述
派生类中func3func4也是虚函数,但是监视窗口中我们发现看不见func3func4,这里是编译器的监视窗口故意隐藏了这两个函数,也可以认为是他的一个小bug。那么我们如何查看d的虚表呢?下面我们使用代码打印出虚表中的函数:

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

typedef void (*VFPTR) ();
void PrintVFTable(VFPTR vfTable[])
{
	printf("vfptr虚表地址:%p\n", vfTable);
	for (size_t i = 0; vfTable[i] != nullptr; i++)
	{
		printf("第[%d]个虚函数地址->%p\n", i + 1, vfTable[i]);
		VFPTR f = vfTable[i];
		f();
	}
}

int main()
{
	Base b;
	Derive d;

	PrintVFTable((VFPTR*)(*((int*)&b)));
	cout << "-------------------------" << endl;
	PrintVFTable((VFPTR*)(*((int*)&d)));
	
	
	return 0;
}

运行结果:
在这里插入图片描述
在这里插入图片描述
结果说明func3func4作为虚函数,它们的地址是被放在虚表中的.

📖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 PrintVFTable(VFPTR vfTable[])
{
	printf("vfptr虚表地址:%p\n", vfTable);
	for (size_t i = 0; vfTable[i] != nullptr; i++)
	{
		printf("第[%d]个虚函数地址->%p\n", i + 1, vfTable[i]);
		VFPTR f = vfTable[i];
		f();
	}
}

int main()
{
	Derive d;

	PrintVFTable((VFPTR*)(*((int*)&d)));
	cout << "---------------------" << endl;
	PrintVFTable((VFPTR*)(*(int*)((char*)&d + sizeof(Base1))));
	
	return 0;
}

在多继承中,Derive类中会存在两个虚表指针,其模型如下:

在这里插入图片描述
也就是说,在d对象中,包含两部分,一部分是从Base1中继承而来的,另一部分从Base2继承来的,并且多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中.

运行上面的代码:
在这里插入图片描述

其中PrintVFTable((VFPTR*)(*(int*)((char*)&d + sizeof(Base1))));这句代码,其实是从d对象的起始地址处,跳过Base1部分,也就是跳转到Base2部分的起始地址处,从而打印Base2部分的虚表.

在这里插入图片描述
Base2虚表的打印结果我们也可以看到,Base2部分的虚函数func1也被Derive::func1重写.
由上述代码的运行结果也验证了我们的结论.

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-08-19 18:45:50  更:2022-08-19 18:49:50 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/13 22:07:50-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码