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++之多态<polymorphism> -> 正文阅读

[C++知识库]C++之多态<polymorphism>

🌈前言

本篇文章进行C++中类多态(polymorphism)的学习!!!
本文章代码在x64程序中测试,涉及指针是8bytes的,如果为x86,涉及指针则为4bytes


🌸 多态

🌷1、多态的概念

  • 多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为时,不同的对象去完成会产生出不同的状态
  • 举个栗子1??:比如坐地铁买票时,普通人买票是全价;而学生买票时,是半价买票;军人买票时,是优先买票
  • 举个栗子2??:比如过年时,有些软件就会搞一些刷视频邀请人就能获得红包的活动,邀请新人获得的红包可能是0-99r,新人刷视频得到的金额搞一些,老用户少一些,这也是多态的一种

🌸2、多态的定义及实现

🌹2.1、多态的构成条件

  • 前言:多态是在不同继承关系的类对象,去调用同一个函数,产生不同的行为。比如:Student继承了Person,则Person对象买票全价,Student对象买票半价

在继承中构成多态的条件有两种:

  • 1、必须通过基类的指针或者引用调用虚函数
  • 2、被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写(覆盖)
    在这里插入图片描述

🌺2.2、虚函数

  • 虚函数:即被virtual修饰的类成员函数称为虚函数
class Person
{
public:
	virtual void BuyTicket() { cout << "买票全价" << endl; }
};

🌻2.3、虚函数的重写

跳转到多态的原理

  • 虚函数的重写(覆盖):派生类中有一个与基类完全相同的虚函数,即派生类虚函数与基类虚函数的返回值类型、函数名、参数列表完全相同,称派生类的虚函数重写了基类的虚函数
class Person
{
public:
	virtual void BuyTicket() { cout << "买票全价" << endl; }
};

class Student : public Person
{
  /*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后
	基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用*/
  /*void BuyTicket() { cout << "买票-半价" << endl; }*/
	virtual void BuyTicket() { cout << "买票半价" << endl; }
};

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

int main()
{
	Person p;
	Func(p);

	Student s;
	Func(s);
	return 0;
}

在这里插入图片描述


虚函数重写的二个例外:(1)

  • 协变(基类与派生类虚函数的返回值类型不同)
    -派生类重写基类虚函数时,与基类虚函数返回值类型不同,即基类虚函数返回基类对象的指针或引用,派生类虚函数返回派生类的指针或引用时,称为协变
class A {};
class B : public A {};

// 协变 --- 派生类虚函数返回类型与基类虚函数不同
class Person
{
public:
	virtual A* f() { return new A; }
};

class Student : public Person
{
public:
public:
	virtual B* f() override { return new B; }
};

void Test(Person& p)
{
	A* ps = p.f();
}

int main()
{
	Person p;
	Student s;
	Test(p);
	Test(s);
	return 0;
}

2. 析构函数的重写(基类与派生类析构函数的名称不同)

  • 如果基类的析构函数为虚函数,则派生类析构函数只需要定义就行了,无论是否添加virtual,都与基类的析构函数构成重写
  • 虽然函数名不同,看起来违背了重写规则,但是,这里可以理解为编译器对析构函数做了特殊处理,编译后析构函数的名称统一处理成destructor(析构函数)
class Person
{
public:
	virtual ~Person() { cout << "~Person()" << endl; }
};

class Student :public Person
{
public:
	virtual ~Student() { cout << "~Student()" << endl; }
};

void Test()
{
	// 首先开辟sizeof(Person)的堆空间 随后调用Person构造函数
	Person* p = new Person;
	Person* ps = new Student;
	// 首先调用p的析构函数 随后free堆空间
	delete p;
	delete ps;
}

在这里插入图片描述


🌼2.4、C++11中override 和 final

🌈前言:

  • 从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失
  • 因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写

1. final:修饰虚函数,表示该虚函数不能再被重写

class A
{
public:
	virtual void Test() final {}
};

class B : public A
{
public:
	virtual void Test() {}
};

在这里插入图片描述

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

class A
{
public:
	virtual void Test(){}
};

class B : public A
{
public:
	virtual void Test() override {}
};

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

在这里插入图片描述


🌿3、抽象类

🍀3.1、概念

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

  • 抽象类:包含纯虚函数的类叫做抽象类(接口类),抽象类不能实例化出对象。
  • 只有重写纯虚函数,派生类才能实例化出对象
  • 纯虚函数规范了派生类必须重写它,另外纯虚函数更体现除了接口继承
class Base
{
public:
	// 纯虚函数
	virtual void Show() = 0;
};

class Derive1 : public Base
{
public:
	virtual void Show() { cout << "Derive1::Show()" << endl; }
};

class Derive2 : public Base
{
public:
	virtual void Show() { cout << "Derive2::Show()" << endl; }
};

void Test()
{
	Base* pb1 = new Derive1;
	pb1->Show();
	Base* pb2 = new Derive2;
	pb2->Show();

	delete pb1;
	delete pb2;
}

在这里插入图片描述


🍁3.2、接口继承和实现继承

接口继承和继承的区别:

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

🍂4、多态的原理

🍃 4.1、虚函数表

// 这里常考一道笔试题:sizeof(Test)为多少bytes?
class Test
{
public:
	virtual void f1() {}
private:
	int _a = 0;
};

int main()
{
	Test t;
	cout << sizeof(t) << endl;
	return 0;
}
  • 通过观察我们发现t的对象所占内存是16bytes,除了_a成员,还多一个_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 _a = 1;
};

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

void Test(Base* b)
{
	b->Func1();
	b->Func2();
	b->Func3();
}

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

在这里插入图片描述

通过调试,我们发现了一下几点问题:

  1. 派生类对象d中也有一个虚表指针,d对象由二部分构成,一部分是父类继承下来的成员,虚表指针也是继承的一部分,另一部分是派生类自己的成员
  2. 基类b对象和派生类d对象的虚表是不一样的,这里我们发现Func1完成了重写,所以虚函数的重写也叫做覆盖。覆盖是指虚表中虚函数的覆盖
  3. Func2继承下来后是虚函数,放进了虚表,Func3也继承下来了,但不是虚函数,没有放进虚表,而是存储在公共代码区中
  4. 虚表本质是一个存储虚函数指针的数组,一般这个数组最后面放了一个nullptr
  5. 派生类虚表的生成:1、先将基类中虚表拷贝一份到派生类虚表中 2、如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 3、派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后
  6. 虚函数存在哪?虚表存在哪?很多人说:虚函数存在虚表中,虚表存在对象中,注意这是错误的说法。注意:虚表存的是虚函数指针,虚函数和普通函数一样存在代码段中,只是它的指针又存到了虚表中,另外对象存的不是虚表,存的是虚表指针
  7. 虚表在vs中是存在代码段中的,Linux存放在可执行文件的只读数据段中(rodata),这与微软的编译器将虚函数表存放在常量段存在一些差别

🍄 4.2、多态的原理

多态的原理:

  • 还记得上面的买票例子吗?跳转到前面例子
  • Func函数传Person调用的Person::BuyTicket,传Student调用的时Student::BuyTicket
    在这里插入图片描述
  1. 观察下图红色箭头可以发现,p指向mike对象时,p->BuyTicket在mike的虚表中找到的是Person::BuyTicket
  2. 观察下图蓝色箭头可以发现,p指向johnson对象时,p->BuyTicket在johnson虚表中找到的是Student::BuyTicket

3.

  1. 通过下面的汇编代码分析,看出满足多态以后的函数调用,不是在编译时确定的,是在运行起来后到对象到找的。不满足多态的函数调用时编译阶段就确认好了
void Func(Person* p)
{
	p->BuyTicket();
}
int main()
{
	Person mike;
	Func(&mike);
	mike.BuyTicket();
	return 0;
}

// 以下汇编代码中跟你这个问题不相关的都被去掉了
void Func(Person* p)
{
	...
	p->BuyTicket();
	// p中存的是mike对象的指针,将p移动到eax中
	001940DE mov eax, dword ptr[p]
	// [eax]就是取eax值指向的内容,这里相当于把mike对象头4个字节(虚表指针)移动到了edx
	001940E1 mov edx, dword ptr[eax]
	// [edx]就是取edx值指向的内容,这里相当于把虚表中的头4字节存的虚函数指针移动到了eax
	00B823EE mov eax, dword ptr[edx]
	// call eax中存虚函数的指针。这里可以看出满足多态的调用,不是在编译时确定的,是运行起来以后到对象的中取找的。
	001940EA call eax
	001940EC cmp esi, esp
}

int main()
{
	...
	// 首先BuyTicket虽然是虚函数,但是mike是对象,不满足多态的条件,所以这里是普通函数的调用转换成地址时,是在编译时已经从符号表确认了函数的地址,直接call地址
	mike.BuyTicket();
	00195182 lea ecx, [mike]
	00195185 call Person::BuyTicket(01914F6h)
		...
}

🍅 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 = 0;
};

class Derive : public Base
{
public:
	virtual void func1() { cout << "Derive::func1" << endl;}
	virtual void func2() { cout << "Derive::func2" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
	virtual void func4() { cout << "Derive::func4" << endl; }
private:
	int b = 0;
};

// 重命名函数指针类型,返回值为void,参数为空
typedef void (*VFPTR) ();
void PrintVTable(VFPTR* vTable)
{
	// 依次取虚表中的虚函数指针打印并调用,查看调用哪个函数
	cout << "虚表地址>" << vTable << endl;
	for (int i = 0; vTable[i] != nullptr; ++i)
	{
		std::printf("第%d个虚函数地址: 0x%x,->", i, vTable[i]);
		VFPTR f = vTable[i];
		f();
	}
	cout << endl;
}

int main()
{
	Base b;
	Derive d;

	// 32位下指向虚表的指针偏移量是4,用int
	// 64位下指向虚表的指针偏移量是8,需要使用long long
	VFPTR* vTableb = (VFPTR*)(*(long long*)&b);
	PrintVTable(vTableb);

	VFPTR* vTabled = (VFPTR*)(*(long long*)&d);
	PrintVTable(vTabled);
	return 0;
}

在这里插入图片描述


🍈 5.2、多重继承中的虚表

这里在x86下测试

class Base1
{
public:
	virtual void Fun1() { cout << "Base1::Fun1()" << endl; }
	virtual void Fun2() { cout << "Base1::Fun2()" << endl; }
private:
	int b1 = 0;
};

class Base2
{
public:
	virtual void Fun1() { cout << "Base2::Fun1()" << endl; }
	virtual void Fun2() { cout << "Base2::Fun2()" << endl; }
private:
	int b2 = 1;
};

class Derive : public Base1, public Base2
{
public:
	virtual void Fun1() { cout << "Derive::Fun1()" << endl; }
	virtual void Fun3() { cout << "Derive::Fun3()" << endl; }
private:
	int d = 2;
};

using VFPTR = void(*) (); // 类型重命名
void PrintVTable(VFPTR vTable[]) // 参数类型:指向函数指针的指针(虚表首地址)
{
	cout << " 虚表地址>" << vTable << endl;
	for (int i = 0; vTable[i] != nullptr; ++i)
	{
		std::printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
		VFPTR f = vTable[i];
		f();
	}
	cout << endl;
}
int main()
{
	Derive d;
	// 第一个虚表
	VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
	PrintVTable(vTableb1);
	// 第二个虚表
	VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
	PrintVTable(vTableb2);
	return 0;
}

在这里插入图片描述


🍈5.3.、菱形继承和菱形虚拟继承

  • 实际中我们不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的模型,访问基类成员有一定得性能损耗,这里就不对其进行讲解了
  1. C++ 虚函数表解析
  2. C++ 虚表的内存布局

感谢支持!!!

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-07-03 10:32:27  更:2022-07-03 10:32:52 
 
开发: 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年4日历 -2024/4/28 5:50:14-

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