面向对象三大特性之一——多态详解

2023-11-19

目录

前言

一、多态的概念

二、多态的定义及实现

 1、虚函数的概念

2、虚函数的重写

2.1概念

2.2虚函数重写的两个例外 

3、多态的构成条件

4、c++ 11 override和final

4.1 final

4.2 override

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

三、抽象类

1、概念

2、接口继承和实现继承 

四、多态的原理

1、虚函数表

2、多态的原理

3、动态绑定和静态绑定

 五、继承的虚函数表

1、单继承中的虚函数表

 2、多继承中的虚函数表

六、常见面试题

总结


前言

哈喽,小伙伴们大家好。上一章我们一起学习了继承,今天我们继续来学习面向对象三大特性中的最后一个特性——多态。事不宜迟,快拿起小本本,跟我一起开始吧。


一、多态的概念

概念:多态,顾名思义,也就是多种状态。通俗点说就是不同的对象去干同一件事情时会产生不同的状态。以买火车票为例,普通人买票就要付全款,学生买票就可以出半价,军人虽然不能优惠但是可以优先买票。

放到我们程序中就是不同继承关系的类对象去调用同一个函数,会产生不同的状态。

二、多态的定义及实现

 1、虚函数的概念

在了解多态的构成条件前,我们先来了解一个概念,虚函数。

虚函数:被virtual修饰的类成员函数称为虚函数。要注意,虽然虚函数的关键字和虚拟继承的关键字相同,但实际它们没有任何关系。

2、虚函数的重写

2.1概念

虚函数的重写(覆盖):派生类中有一个和基类中完全相同的虚函数(函数名,返回值,参数列表都相同),则成为子类对基类的虚函数进行了重写。

注意:如果基类的函数为虚函数,不管派生类的相同函数前加不加virtual,都默认是虚函数,进行了重写。

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

2.2虚函数重写的两个例外 

虚函数重写一般要严格遵守函数完全相同的条件,但是有两个例外情况下这个规则被打破。

(1)协变(返回值不同)

派生类重写基类的虚函数时,与基类虚函数返回值不同。如果派生类虚函数返回的是派生类对象的引用或指针,基类虚函数返回的是基类对象的引用或指针,则构成协变。这种情况下可以看作进行了虚函数重写。虚函数一般很少使用,了解即可。

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。、

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

// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函
//数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{
    Person* p1 = new Person;
    Person* p2 = new Student;
    delete p1;
    delete p2;
    return 0;
}

注意:我们在写多态时,尽量不要出现协变,并且基类和派生类的虚函数前面都加上virtual,使代码更规范。

3、多态的构成条件

多态的构成条件:

  • 被调用的必须是虚函数,且派生类必须对基类的虚函数进行了重写。
  • 必须通过基类的指针或引用调用虚函数。

我们以普通人和学生买票为例,代码如下:

4、c++ 11 override和final

在实际写代码的过程中,我们很可能因为不小心把虚函数的名字母敲错或敲反,导致无法构成重写,所以c++11加了两个关键字来帮我们检查这个错误。

4.1 final

final修饰虚函数,表示该函数不能再被重写。如果发生了重写会在编译阶段报错。

class A
{
public:
	virtual void fun() final {}
};
class B :public A
{
public:
	virtual void fun() { cout << "重写成功" << endl; } //编译的时候报错
};

4.2 override

override函数用来检查派生类的函数是否虚写了基类的某个虚函数,如果没有则发生报错。

class A
{
public:
	virtual void fun();
};
class B :public A
{
public:
	virtual void fun() override { cout << "重写成功" << endl; }
};

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

重载:

  • 在同一个作用域。
  • 函数名相同,参数不同。

重写:

  • 在基类和派生类两个作用域。
  • 必须是虚函数。
  • 返回值,函数名,参数列表必须都完全相同。(协变除外)。

隐藏:

  • 在基类和派生类两个作用域。
  • 函数名相同。
  • 两个基类和派生类的同名函数不构成重写就构成隐藏。

三、抽象类

1、概念

在虚函数的后面加上=0,这个函数被称为纯虚函数,包含纯虚函数的类被称为抽象类(也叫做接口类)。抽象类的派生类依旧是抽象类。只有重写了纯虚函数,派生类才能够实例化对象。纯虚函数规范了派生类需要重写,并且很好的体现出了接口继承的特性。

class A
{
public:
virtual void func() = 0;//纯虚函数
};

2、接口继承和实现继承 

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

四、多态的原理

1、虚函数表

大家先看这样一道题,sizeof(A)的值是多少。

class A
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _a = 0;
int _b = 1;
};

int main()
{
	A a;
	cout << sizeof(a);
	return 0;
}

 表面上看上去只有两个成员变量a和b,根据内存对齐规则,这个类的大小为8Bytes,但实际通过测试,这个类的大小是12Bytes。我们打开监视窗口发现,这个类中隐藏了一个_vfptr指针。这个指针叫做虚函数表指针(v是virtual,f是function),指向虚函数表,简称虚表。虚函数表中存放的是类中虚函数的地址。每个含有虚函数的类都至少有一个虚函数表指针,如果A这个类里没有虚函数,那sizeof(A)就是8Bytes。

 那么派生类中这个表里放了什么呢?我们继续往下研究。

class Person
{
public:
	virtual void Func1()
	{
		cout << "Person::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Person::Func2()" << endl;
	}
	void Func3()
	{
		cout << "Person::Func3()" << endl;
	}
private:
	string name="zxy";
};

class Student : public Person
{
public:
	virtual void Func1()
	{
		cout << "Student::Func1()" << endl;
	}
private:
	int _id = 100;
};

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

经过调试我们发现,在派生类中也有一个虚函数表。

 我们从上面的表中可以得出以下信息。

  • 由于虚函数表指针是二级指针,说明虚函数表中存的并不是虚函数,而是虚函数的地址。虚函数表的本质是一个存了虚函数指针的指针数组,一般数组的最后放了一个nullptr。
  • 由于Func3是普通函数,所以虚函数表中只存了Func1和Func2两个函数的地址。
  • 基类的虚函数表和派生类的虚函数表不是同一张。
  • 派生类中虚函数表的生成过程:先将基类的虚函数表拷贝一份放到派生类虚表中。如果在派生类中对某个虚函数进行了重写,则用派生类自己的虚函数覆盖表中基类的虚函数。
  • 派生类中自己新添加的虚函数按照声明顺序依次加到派生类虚表的后面。(这个通过监视窗口看不到,需要看内存窗口)。
  • 虚表是存在代码段中的。

2、多态的原理

还是以最开始提到的买票的例子为例。

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

 下面这张图很好的体现了多态的原理,编译器可以通过基类指针指向对象的不同分别调用不同的函数,主要是因为虚函数表指针指向了不同的虚函数表,根据虚函数表中的地址,调用不同的函数。

如果满足多态条件构成多态后,调用虚函数就不是编译时决定的了,而是运行时到指定的对象的虚表中去找相应的函数。所以指向父类对象,调用的就是父类的虚函数。指向子类对象,调用的就是子类的虚函数。

相反,如果不构成多态,那调用哪个函数就是编译时决定的了,取决于p的类型,跟p指向的对象没关系。

 这时候我们反过来思考一下,多态要求必须通过基类的指针或引用去调用,为什么不能通过基类对象去调用呢?

如果是通过引用或指针调用,指针(引用的本质也是指针)直接指向父类对象或子类对象切片出来的那一部分,即可找到相应的虚表。

而如果通过父类对象调用,则需要子类对象切片拷贝到父类对象中,但是要注意一点,子类的虚表指针是不会拷贝到父类对象中去的。因为同类型的对象,虚表是一样的,共享一张虚表,把子类的虚表指针拷贝到父类中不合理。

3、动态绑定和静态绑定

  • 静态绑定指的是在程序编译期间就确定了程序的行为,也叫做静态多态,比如函数重载。
  • 动态绑定指的是在程序运行阶段,根据具体拿到的数据类型调用相应的函数,也叫做动态多态。

 我们平时说的多态一般都指动态多态,本章介绍的也主要围绕动态多态。

 五、继承的虚函数表

1、单继承中的虚函数表

我们来进一步研究一下虚函数表模型,假设在派生类中又增加了虚函数,那他应该存在哪里呢?

class Person
{
public:
	virtual void Func1()
	{
		cout << "Person::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Person::Func2()" << endl;
	}
	void Func3()
	{
		cout << "Person::Func3()" << endl;
	}
private:
	string name="zxy";
};

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

	virtual void Func4()
	{
		cout << "Student::Func4()" << endl;
	}
private:
	int _id = 100;
};

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

我们发现监视窗口中看不到func4,可以认为是编译器隐藏了起来。

通过内存窗口观察发现,派生类中新增加的虚函数会按声明顺序依次加在派生对象虚表的后面。

 2、多继承中的虚函数表

这里不再演示具体过程,只需要记住结论,多继承派生类新增加的虚函数会放在第一个基类部分的虚表中。

六、常见面试题

1. 什么是多态?答:参考本章内容
2. 什么是重载、重写(覆盖)、重定义(隐藏)?答:参考本章内容
3. 多态的实现原理?答:参考本章内容
4. inline函数可以是虚函数吗?答:可以,虽然inline函数没有地址,但是编译器编译时会忽略inline属性,这个函数就不再是inline,可以放到虚表中去。
5. 静态成员可以是虚函数吗?答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
6. 构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。构造函数之前没有虚函数指针。
7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?答:可以,并且最好把基类的析构函数定义成虚函数。否则如果用基类指针指向派生类对象,调用析构函数时不会形成多态,会默认调用基类的析构函数,造成内存泄漏。
8. 对象访问普通函数快还是虚函数更快?答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
9. 虚函数表是在什么阶段生成的,存在哪的?答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。
10. C++菱形继承的问题?虚继承的原理?答:参考之前写的继承博客。注意这里不要把虚函数表和虚基表搞混了。
11. 什么是抽象类?抽象类的作用?答:参考本章内容。抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系。


 总结

以上就是今天要讲的全部内容,本章主要介绍了多态的相关概念和原理。多态的知识还是比较复杂的,还有很多地方博主也没有理解的很透彻。在以后的日子里我会坚持学习和探索,争取把更好的作品带给大家。感谢阅读,来日方长,期待和大家再次见面。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

面向对象三大特性之一——多态详解 的相关文章

  • Windows 10 Mobile (10.0.14393) 地理围栏后台任务 (LocationTrigger)

    自从10 0 14393 周年纪念更新 LocationTrigger似乎不起作用 我有 Windows Phone 8 1 应用程序 也适用于 UWP 应用程序 输出到的便携式库Windows Runtime Component图书馆 w
  • 为什么存在 async 关键字

    浏览 msdn 9 频道视频时 我发现以下未答复的评论 希望有人能解释一下 我不明白 async 关键字的意义 为什么不直接允许 任何时候方法返回任务时都会使用await关键字 就像迭代器一样 可以在任何返回 IEnumerable 的方法
  • 分段错误(核心转储)错误

    我的程序编译罚款 但在输入文件时出现 分段错误 核心转储 错误 我没有正确处理 ostream 吗 include
  • 内联函数/方法

    声明 内联函数必须在调用之前定义 这个说法正确吗 EDIT 该问题最初是德语 内联功能穆森 弗 伊赫雷姆 奥夫鲁夫定义 sein 也许它对任何人都有帮助 是的 它是正确的 但只是部分正确 它可能正确地重新构建如下 内联函数必须在每个翻译单位
  • C 程序从连接到系统的 USB 设备读取数据

    我正在尝试从连接到系统 USB 端口的 USB 设备 例如随身碟 获取数据 在这里 我可以打开设备文件并读取一些随机原始数据 但我想获取像 minicom teraterm 这样的数据 请让我知道我可以使用哪些方法和库来成功完成此操作以及如
  • 从多线程程序中调用 system()

    我们正在开发一个用 C 编写的多线程内存消耗应用程序 我们必须执行大量的 shellscript linux 命令 并获取返回码 读完之后article http www linuxprogrammingblog com threads a
  • 在 C++ 中将成对向量转换为两个独立向量的最快方法

    假设我有一个vector of pair
  • 为什么大多数平台上没有“aligned_realloc”?

    MSVC有自己的非标准函数 aligned malloc aligned realloc and aligned free C 17和C11引入了 std aligned alloc 其结果可以是de分配有free or realloc B
  • 将字符串中的“奇怪”字符转换为罗马字符

    我需要能够将用户输入仅转换为 a z 罗马字符 不区分大小写 所以 我感兴趣的角色只有26个 然而 用户可以输入他们想要的任何 形式 的字符 西班牙语 n 法语 e 和德语 u 都可以包含用户输入中的重音符号 这些重音符号会被程序删除 我已
  • 如何在 C++ 中将 CString 转换为 double?

    我如何转换CString to a double在 C 中 Unicode 支持也很好 Thanks A CString可以转换为LPCTSTR 这基本上是一个const char const wchar t 在 Unicode 版本中 知
  • 从成员函数指针类型生成函子

    我正在尝试简化 通过make fn 预处理参数的函子的生成 通过wrap 对于 arity 的成员函数n 生成函子基本上可以工作 但到目前为止只能通过显式指定成员函数的参数类型来实现 现在我想从它处理的成员函数类型生成正确的函子 struc
  • 为什么 clang 使用 -O0 生成低效的 asm(对于这个简单的浮点和)?

    我正在 llvm clang Apple LLVM 版本 8 0 0 clang 800 0 42 1 上反汇编此代码 int main float a 0 151234 float b 0 2 float c a b printf f c
  • libxml2 xmlChar * 到 std::wstring

    libxml2似乎将所有字符串存储在 UTF 8 中 如xmlChar xmlChar This is a basic byte in an UTF 8 encoded string It s unsigned allowing to pi
  • C++11 动态线程池

    最近 我一直在尝试寻找一个用于线程并发任务的库 理想情况下 是一个在线程上调用函数的简单接口 任何时候都有 n 个线程 有些线程比其他线程完成得更快 并且到达的时间不同 首先我尝试了 Rx 它在 C 中非常棒 我还研究了 Blocks 和
  • 如何随着分辨率的变化自动调整大小和调整表单控件

    我注意到某些应用程序会更改控件的位置以尽可能适应当前的分辨率 例如 如果窗口最大化 则控件的设置方式应使整个 GUI 看起来平衡 是否可以使用 C 在 Visual studio 2010 中制作或实现此功能 Use Dock http m
  • tabcontrol selectedindex 更改事件未被触发 C#

    嘿伙计们 我有一个很小的问题 请参阅下面的代码 this is main load private void Form1 Load object sender EventArgs e tabAddRemoveOperator Selecte
  • 使用 mingw32 在 Windows 上构建 glew 时“DllMainCRTStartup@12”的多个定义

    我关注了这个主题 使用 mingw 使建筑物在 Windows 上闪闪发光 https stackoverflow com questions 6005076 building glew on windows with mingw 6005
  • 0-1背包算法

    以下 0 1 背包问题是否可解 浮动 正值和 浮动 权重 可以是正数或负数 背包的 浮动 容量 gt 0 我平均有 这是一个相对简单的二进制程序 我建议用蛮力进行修剪 如果任何时候你超过了允许的重量 你不需要尝试其他物品的组合 你可以丢弃整
  • 如何引用解决方案之外的项目?

    我有一个 Visual Studio C 解决方案 其中包含一些项目 其中一个项目需要引用另一个不属于解决方案的项目 一开始我引用了dll
  • 为什么文件更新时“如果较新则复制”不复制文件?

    我在 Visual Studio Express 中有一个解决方案 如下所示 The LogicSchemaC 中的类 将在运行时解析指定的 XML 文件 以下是在main的方法Program cs LogicSchema ls new L

随机推荐

  • c++ Sigmoid/Softmax/Argmax

    Sigmoid float sigmoid float x return 1 1 exp x float sigmoid dy dz float x return x 1 0 x float tanh dy dz float x retur
  • ES按日期滚动索引

    按日期滚动索引 环境 ES 6 9 ES 7 Centos 7 配置过程 创建索引 PUT localhost 9200 index 20210915 设置索引别名 写入别名和读取别名 PUT localhost 9200 index 20
  • 网络 -- n/24 计算IP范围

    1 IP地址 共分为四类 A B C D类 A类 从1 0 0 0 到 126 255 255 255 B类 从128 0 0 0 到 191 255 255 255 C类 从192 0 0 0 到 223 255 255 255 其中12
  • 关于输入、输出电容在 LDO 应用中的重要性

    关于输入 输出电容在 LDO 应用中的重要性 如何让LDO 产品在应用中达到更佳的稳定性 则用户在设计电路时 最好根据芯片 datasheet 的说明文档而定 下面以LP2985 3 3这个LDO为例 LP2985 3 3是低功耗 低压差
  • Vue基础(二)——模板语法

    一 指令 1 v bind 绑定属性 2 v on 绑定事件 3 v if和v show 1 介绍
  • tcp/ip在物理层/数据链路层 实现简单抓包

    socket的精妙之处在于协议族的横向转换和地址族的纵向转换 我们也可在更底层实现对流经host的数据流的监督和修改 尤其是监察数据 十分简单 这里是混杂模式实现对ip数据流的监察与对tcp数据流的简单查看 需要root权限 这里忽略了tc
  • 整理一下go的ci工具

    代码格式化 go fmt fileName go goimports 自动格式化import goimports w fileName go mod 自动更新 删除包 go mod tidy 检查注释是否符合导出 1 安装revive go
  • 关于如何修复烧写镜像文件失败的SD卡

    前言 使用某些软件 比如 win32 Disk Imager 向SD卡烧写镜像文件时 很有可能出现烧写失败的情况 通常如果烧写失败 系统会弹出请求格式化SD卡的提示框 此时不要点格式化 点了可能会造成不可挽救的结果 也可能不会 而是进行以下
  • 【C库函数】memcpy函数详解

    目录 memcpy 函数原型 参数讲解 返回值讲解 函数讲解 三个注意点 memcpy 拷贝内存块到目标空间 函数原型 void memcpy void dest const void src size t count 参数讲解 参数 de
  • 百度AI──自然语言处理使用教程

    百度AI 自然语言处理使用教程 情感倾向分析 创建自己的应用 python方式调用 安装Python SDK 创建一个 Python SDK客户端 配置AipNlp 调用接口 情感倾向分析 需要注意的几个点 完整代码 参考 创建自己的应用
  • Linux 配置 PaddleOCR环境

    配置环境 1 准备好CUDA和cudnn 安培架构GPU需配置CUDA 11 2 CUDNN 8 1 1 以下文档以安培架构GPU的为例 找到对应的版本下载CUDA https developer nvidia com cuda downl
  • 一位数组返回id和pid通过这两个参数转换为树形结构数据,和树形结构的渲染

    废话不多说直接上代码 html代码我是引用了一个jq的插件作为样式插件名字为 jOrgChart 具体内容大家可以评论到下方 div class com div class TheEditor 编辑 div div div div js代码
  • Java 实体设置指定日期格式

    import com fasterxml jackson annotation JsonFormat JsonFormat pattern yyyy MM dd HH mm ss timezone GMT 8 private Date cr
  • nginx 代理图片服务器

    location gif jpg jpeg png expires 24h root home sk ftp 指定图片存放路径 proxy store on proxy store access user rw group rw all r
  • MATLAB BP神经网络 笔记整理

    1 如何更改输出层的激活函数 传递函数 对于有两层神经网络结构 可以通过调用以下函数 net layers 1 or 2 transferFcn for the hidden net layers 3 transferFcn for the
  • C#实现遍历文件夹获取指定后缀名文件

    问题描述 项目需要 要进行某文件夹下所有shp数据的读取 解决方法 using System using System Collections Generic using System ComponentModel using System
  • Python机器学习/数据挖掘项目实战 波士顿房价预测 回归分析

    Python机器学习 数据挖掘项目实战 波士顿房价预测 回归分析 此数据源于美国某经济学杂志上 分析研究波士顿房价 Boston HousePrice 的数据集 在这个项目中 你将利用马萨诸塞州波士顿郊区的房屋信息数据训练和测试一个模型 并
  • Qt之一个类成员函数调用另一个类成员的方法

    原文 https blog csdn net qq 35721743 article details 83592415 在继承之外 在C 中一个类成员函数调用另一个类成员的方法主要有 类的组合 友元类 类的前向声明 单例模式等 下面主要讲讲
  • gym 101512 BAPC 2014 I Interesting Integers

    Problem codeforces com gym 101512 attachments vjudge net contest 186506 problem I Meaning 给出一个 正整数 n 要找尽量小的 a 和 b a lt b
  • 面向对象三大特性之一——多态详解

    目录 前言 一 多态的概念 二 多态的定义及实现 1 虚函数的概念 2 虚函数的重写 2 1概念 2 2虚函数重写的两个例外 3 多态的构成条件 4 c 11 override和final 4 1 final 4 2 override 5