C++特性之多态

C++作为面向对象的语言,三大特性之一多态在平时的编程中使用频率特别高。

本篇文章就来详细讲解一下多态。

什么是多态

不同的对象相同的一件事会出现不同的状态,这就是多态

举个列子:比如普通人买车票要全价购买,而军人只用半价,这就是多态的一种体现。

多态的定义及实现

多态的构成条件

想要实现多态,需要以下条件

  • 通过基类的指针或者引用来调用子类对象的虚函数
  • 被调用的函数必须是虚函数

虚函数:类成员函数前用 virtual 关键字修饰的函数

 我们可以实际看看如何实现多态。

#include<iostream>using namespace std;class Person {
public:virtual void Ticket(){cout << "全价" << endl;}
};
class Student : public Person {
public:virtual void Ticket(){cout << "半价" << endl;}
};int main()
{Person* p1;Person p2;Student s1;p1 = &p2;p1->Ticket();p1 = &s1;p1->Ticket();
}

我们发现,通过基类的指针调用虚函数能够实现多态。

我们还能通过引用实现多态。

#include<iostream>using namespace std;class Person {
public:virtual void Ticket(){cout << "全价" << endl;}
};
class Student : public Person {
public:virtual void Ticket(){cout << "半价" << endl;}
};
void Func(Person& p)
{p.Ticket();
}int main()
{Person p;Student s;Func(p);Func(s);return 0;
}

多态的调用更关注的是指针指向的类型,和普通调用不同,普通调用更关注的是调用的类型

虚函数的重写

在继承中,若是派生类继承了基类,那么基类的同名成员会和派生类的同名成员构成隐藏关系

而在多态中, 这种虚函数之间的同名成员则称作重写(覆盖)。

比如上面的例子中, Student 类就重写了Person类的 Ticket 函数。

一般虚函数构成重写需要三个条件:函数名相同,返回值相同,参数相同。

只要基类和派生类有同名函数,且基类函数前加了virtual,那么派生类的函数也会成虚函数。

不过虚函数的重写也有例外。

协变

重写的虚函数的返回值不同,即基类的虚函数返回基类对象的指针派生类返回派生类对象的指针时,即构成协变。

#include<iostream>using namespace std;class A{};
class B : public A {};class Person {
public:virtual A* Ticket(){cout << "全价" << endl;return nullptr;}
};
class Student : public Person {
public:virtual B* Ticket(){cout << "半价" << endl;return nullptr;}
};int main()
{Person* p1;Person p2;Student s1;p1 = &p2;p1->Ticket();p1 = &s1;p1->Ticket();return 0;
}

 比如这里,Ticket 虚函数的重写就构成了协变。

Person 类的 Ticket 函数返回 A 类的指针,Student 类的 Tciket 函数返回 B类的指针。

而如果不是基类返回基类的指针,派生类返回派生类的指针,则不会构成协变,还会报错。

 析构函数的重写

若是基类的析构函数是虚函数,那么派生类的析构函数无论加不加virtual 关键字修饰都会是虚函数。

虽然两个析构函数的名称不同,但是二者之间依旧构成重写。

析构函数构成重写的原因

编译器对析构函数做了特殊处理,所有析构函数都会在编译后转换为 destructor;

 override 和 final

由于多态会导致函数重写,有时又会因为程序员的原因导致函数字母出错,从而无法重写函数,这类错误只能在运行的时候发现,因此C++提供了两个关键字用来检测函数是否重写。

  • override : 检查派生类函数是否重写,没重写则报错。
  • final : 修饰虚函数,表示该虚函数不能被重写,修饰普通函数,表示该函数不能被继承

override 示例 

 final 示例

抽象类 

由于多态特性的存在,C++还有一个抽象类的概念。

概念:类中有一个函数是纯虚函数。包含纯虚函数的类就是抽象类,这种类无法实例化对象

纯虚函数:虚函数后面加上 =0 ,就是纯虚函数。

 派生类继承了抽象类后也无法实例化对象,必须重写纯虚函数才能实例化对象。

像纯虚函数这样的函数体现了接口继承。

像图中的 Animal 类,Animal 肯定不能作为一个对象存在在自然界中,而 Cat 当然存在于自然中,C++的抽象类也是类似,无法实例化对象。

接口继承和实现继承

普通类的继承都是实现继承,派生类继承了基类的函数,能够使用基类的函数,而虚函数的继承则体现了接口继承,虚函数的存在是为了让派生类重写,达成多态。因此除非是为了实现多态,一般是不用虚函数的。

多态原理

一般编译器对于只有函数的空类只会给它一个字节的标记位,但是这里 A 类有一个虚函数,它的却有8字节的大小。 

这是因为当一个类含有虚函数时,编译器就会给该类提供一个虚函数表该类实例化的对象中都会有一个指针用来指向该虚函数表

这个指针一般就在对象的头4个或8个字节中;

根据编译器的环境,这个指针大小可能为4或8字节大小。

多态的原理就是建立在虚函数表之上的。

class A
{
public:virtual void Test1(){}virtual void Test2(){}void Test3(){}
};class B : public A
{
public:virtual void Test1(){}
};
int main()
{B b;A a;return 0;
}

我们写下以上的代码,然后通过监视窗口,可以看到,b对象和a对象都分别有一个虚函数表(_vfptr),其中 B 类 重写了 Test1 函数,因此它的虚表中 Test1 函数就是 B类中的Test1函数,而Test2 没有重写,因此就是 A 类中的 Test2 函数。 

观察总结:

  • 派生类生成虚表会先将基类虚表内容保存到派生类的虚表内容中
  • 若是派生类已经重写了基类的某个虚函数,则会将重写的虚函数覆盖到虚表中基类的虚函数。
  • 而派生类自己新增的虚函数则按派生类的声明顺序新增到派生类虚表的最后。

从观察总结中延伸,我们就明白了多态的原理。

  • 基类的指针或引用的是派生类时它的虚表指针指向的就是派生类的虚表,其中保存的虚函数已经覆盖或者重写完毕了。
  • 当基类指针或引用的是基类时,它的虚表指针指向的就是基类的虚表,其中保存的虚函数也是基类的虚函数。
  • 这样就实现了一个对象去完成同一行为时展现的不同形态。

 动态绑定和静态绑定

动态绑定:程序运行期间根据具体的类型确定行为,调用具体的函数。

静态绑定:编译期间,就已经确定了程序的行为。

而多态调用就是典型的动态绑定,普通调用就是静态绑定。

通过查看多态调用和普通调用的汇编代码就能发现,多态调用只有在运行的时候才会确定调用的是哪个函数。

通过反汇编也能够看到多态调用和普通调用不同。

普通调用直接就找到了函数位置,而多态调用在运行的时候才会去连接派生类的虚函数表,再通过虚函数表找到对应的函数位置。

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

单继承的虚函数表

class A
{
public:virtual void Test1(){}virtual void Test2(){}void Test3(){}
};class B : public A
{
public:virtual void Test1(){}virtual void Test3(){}virtual void Test4(){}
};
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()
{B b;A a;PrintVTable((VFPTR*)(*(int*)&b));PrintVTable((VFPTR*)(*(int*)&a));return 0;
}

在 x86 环境下查看虚函数表的内容。

其中 B 类重写了 A 类的 Test1 函数,Test2 函数未重写。

来看看 a 和 b 的虚函数有什么不同。

我们发现, B 类的 Test1 函数是重写后的地址, 而 Test2 函数的地址和 A 类的地址相同。

而 B 类后续的虚函数则添加到 B 类虚表的后面。 

多继承的虚函数表


class A
{
public:virtual void Test1(){cout << "A::Test1()" << endl;}virtual void Test2(){cout << "A::Test2()" << endl;}void Test3(){cout << "A::Test3()" << endl;}
};class B
{
public:virtual void Test1(){cout << "B::Test1()" << endl;}virtual void Test2(){cout << "B::Test2()" << endl;}virtual void Test4(){cout << "B::Test4()" << endl;}
};class C : public A, public B
{
public:virtual void Test1(){cout << "C::Test1()" << endl;}virtual void Test3(){cout << "C::Test3()" << endl;}
};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()
{C c;PrintVTable((VFPTR*)(*(int*)&c));PrintVTable((VFPTR*)*(int*)((char*)&c + sizeof(A)));return 0;
}

我们发现,如果一个类继承的两个函数都有虚函数,那么派生类就会创立两个虚表,分别保存基类的虚函数并且进行了重写操作。

比如这里 C类的 Test1 函数就分别重写在 A类的虚表上和 B 类的虚表上。

而没有重写的Test2函数就被保存了下来。

总结

当一个类有虚函数时,编译器就会为这个类创建一个虚表该类实例化的对象都共享这个虚表,每个对象的头4个(或8个)字节就保存了这个虚表的地址。

虚函数表存在只读代码段中,虚函数也是,而虚函数表指针是在类的构造函数中赋值的,因此类的构造函数不能为虚函数。

一个类继承了一个有虚表的类通过基类的指针引用来调用构成重写的虚函数时,就能实现多态。

一个类继承多个有虚表的类,该类就有多个虚表有重写的虚函数就会分别添加到对应的虚函数,比如 A 类的虚表中有函数被派生类重写了,那就添加到 A 类的虚表中。而没有重写的虚函数添加到继承顺序中第一个有虚表的基类的虚表中。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/258615.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

每天学习一点shell系列(2)—函数的参数传递

参考博客&#xff1a;shell 脚本-10函数_eno_zeng的博客-CSDN博客 $n 或 ${n} &#xff1a;函数内使用 $n 或 ${n} 访问对应的参数, 数字代表参数的前后顺序, $1 代表第一个参数, $2 代表第三个参数, $n 代表第n个参数&#xff1b;当n>10时&#xff0c;需要使用${n}来获取参…

苹果Vision Pro即将量产

据界面新闻消息&#xff0c;苹果公司将在今年12月正式量产第一代MR&#xff08;混合现实&#xff09;产品Vision Pro。苹果公司对Vision Pro寄予了厚望&#xff0c;预计首批备货40万台左右&#xff0c;2024年的销量目标是100万台&#xff0c;第三年达到1000万台。 苹果的供应…

Linux中的文件系统

本章主要介绍文件系统的管理 了解什么是文件系统对分区进行格式化的操作挂载分区查找文件 在Windows系统中&#xff0c;买了一块新的硬盘加到电脑后&#xff0c;需要对分区进行格式化才能使用&#xff0c;Linux系统中也是一样&#xff0c;首先我们需要了解什么是文件系统 1.…

python数据分析总结(pandas)

目录 前言 df导入数据 df基本增删改查 数据清洗 ​编辑 索引操作 数据统计 行列操作 ​编辑 df->types 数据格式化 ​编辑 日期数据处理 前言 此篇文章为个人python数据分析学习总结&#xff0c;总结内容大都为表格和结构图方式&#xff0c;仅供参考。 df导入数…

初识人工智能,一文读懂人工智能概论(1)

&#x1f3c6;作者简介&#xff0c;普修罗双战士&#xff0c;一直追求不断学习和成长&#xff0c;在技术的道路上持续探索和实践。 &#x1f3c6;多年互联网行业从业经验&#xff0c;历任核心研发工程师&#xff0c;项目技术负责人。 &#x1f389;欢迎 &#x1f44d;点赞✍评论…

【分享】我想上手机器学习

目录 前言 一、理解机器学习 1.1 机器学习的目的 1.2 机器学习的模型 1.3 机器学习的数据 二、学习机器学习要学什么 2.1 学习机器学习的核心内容 2.2 怎么选择模型 2.3 怎么获取训练数据 2.4 怎么训练模型 三、机器学习的门槛 3.1 机器学习的第一道门槛 3.2 机器…

学习pytorch19 pytorch使用GPU训练

pytorch使用GPU进行训练 1. 数据 模型 损失函数调用cuda()2. 使用谷歌免费GPU gogle colab 需要创建谷歌账号登录使用, 网络能访问谷歌3. 执行4. 代码 B站土堆学习视频&#xff1a; https://www.bilibili.com/video/BV1hE411t7RN/?p30&spm_id_frompageDriver&vd_sourc…

防水,也不怕水。Mate X5是如何做到让你湿手湿屏也不影响操作的?

相信不少人都碰到过当手机屏幕存在小水珠时&#xff0c;触控变得不灵敏&#xff0c;或者出现“幽灵触屏”&#xff0c;指东打西的情况。 尤其是在洗澡、做饭&#xff0c;或者在户外遇到下雨天气时&#xff0c;如果打湿的手机收到重要聊天消息或者电话&#xff0c;却因为湿屏导…

循环结构中 break、continue、return 和exit() 的区别

循环结构中 break、continue、return 和exit() 的区别 文章目录 循环结构中 break、continue、return 和exit() 的区别一、break语句二、continue语句三、return 语句四、exit() 函数 说明&#xff1a;本文内容参考牟海军 著《C语言进阶&#xff1a; 重点、难点与疑点解析》&a…

交换机与VLAN

交换机简介 交换机的作用 如今随着计算机速度不断提高&#xff0c;以及网络应用越来越多&#xff0c;局域网的负载变得越来越大了&#xff0c;交换机的使用也变得更有必要。交换机的作用主要有两个&#xff1a;一个是维护CAM&#xff08;Context Address Memory&#xff09;表…

WGCLOUD v3.5.0 新增支持监测交换机的接口状态UP DOWN

WGCLOUD v3.5.0开始 可以监测交换机或SNMP设备的接口状态了&#xff0c;直接上图

实时动作识别学习笔记

目录 yowo v2 yowof 判断是在干什么,不能获取细节信息 yowo v2 https://github.com/yjh0410/YOWOv2/blob/master/README_CN.md ModelClipmAPFPSweightYOWOv2-Nano1612.640ckptYOWOv2-Tiny