C++:多态-虚函数

news/2024/9/23 11:20:54/

C++ 中的多态性是面向对象编程中的一个重要概念,它允许在运行时选择不同的函数实现,以适应不同类型的对象。

多态的种类
编译时多态性(Compile-time Polymorphism):也称为静态多态性或早期绑定,指在编译时确定程序应该调用的函数或运算符版本的能力;主要通过函数重载(Function Overloading)和运算符重载(Operator Overloading)来实现。
​
运行时多态性(Runtime Polymorphism):也称为动态多态性或晚期绑定,指在程序运行时根据对象的实际类型来确定调用的函数版本的能力,主要通过虚函数(Virtual Functions)和继承来实现,在运行时根据对象的实际类型来确定调用哪个函数。
​
参数多态性(Parametric Polymorphism):也称为泛型编程(Generic Programming);是指一种通用的编程技术,它允许在编写代码时不指定具体的数据类型,而是以一般的方式编写代码,稍后根据需要使用具体的类型实例化代码。

关于函数重载、运算符重载和继承/虚继承在之前的文章就已经有阐述过了,接下去说一下运行时多态性中的虚函数。

虚函数

虚函数(Virtual Function)是在基类中声明为虚函数的成员函数,它的特点是可以被派生类重写(覆盖)。虚函数为实现运行时多态性提供了基础,它允许在派生类中重新定义基类的函数,并通过基类指针或引用调用时动态地选择调用哪个函数版本。

以下是一个简单的示例:

代码定义了一个基类 Animal 和一个派生类 Cat,其中 CatAnimal 的子类。每个类都有构造函数和析构函数,并且 Animal 类中定义了一个 eat() 函数,Cat 类中重写了这个函数。

//父类
class Animal
{
public:Animal(){};~Animal(){};void eat(){std::cout << "Animal Eat 函数" << std::endl;};
};
​
//派生类
class Cat : public Animal
{
public:Cat(){};~Cat(){};void eat(){std::cout << "cat Eat 函数" << std::endl;};
}
​
int main() {
​Animal * catObj = new Cat;catObj->eat();system("pause");return 0;
}

main() 函数中,创建了一个指向 Cat 对象的 Animal 指针 catObj。这是因为派生类对象可以被赋值给基类指针,因为派生类对象包含了基类对象的所有成员。然后,通过这个指针调用 eat() 函数,因为这是一个Cat对象所以在结果出来之前我会认为运行的时Cat类中的eat()方法,但事实上此时程序的输出内容为:

可以看到此时输出的内容时Animal类中的eat()函数而不是Cat类中的eat()函数,这是由于 eat() 函数在基类中被声明为非虚函数,而派生类中重新定义了这个函数,所以在运行时,尽管 catObj 指向的是 Cat 对象,但实际上调用的是基类 Animal 中的 eat() 函数,而不是派生类 Cat 中的版本。这是因为非虚函数的调用是静态绑定的,编译器在编译时就已经确定了调用的函数版本。

这个结果很明显时不符合我们的预期的,这个时候如果希望运行的是子类中的方法,那么此时我们可以将父类中的eat()函数设置为虚函数:

在 C++ 中,将一个成员函数声明为虚函数的方法是在函数声明前面加上 virtual 关键字。
//父类
class Animal
{
public:Animal(){};~Animal(){};//虚函数virtual void eat(){std::cout << "Animal Eat 函数" << std::endl;};
};
​
//派生类
class Cat : public Animal
{
public:Cat(){};~Cat(){};void eat(){std::cout << "cat Eat 函数" << std::endl;};
}
​
int main() {
​Animal * catObj = new Cat;catObj->eat();system("pause");return 0;
}

将父类中的eat()方法设置为虚函数后,此时再进行程序的运行,得到的结果为:

因为 eat() 函数在基类中被声明为虚函数,而且派生类中重新定义了这个函数,所以在运行时,通过指向派生类对象的基类指针调用 eat() 函数时,实际上会调用派生类 Cat 中的版本。这是因为虚函数的调用是动态绑定的,会根据对象的实际类型来确定调用的函数版本。

虚函数的使用原理涉及到动态绑定(Dynamic Binding)和虚函数表(Virtual Function Table)。
动态绑定:
动态绑定是指在运行时确定应该调用的函数版本,而不是在编译时确定;当通过基类指针或引用调用虚函数时,实际调用的是对象的实际类型对应的函数版本。

虚函数表
虚函数表(Virtual Function Table,简称 vtable)是 C++ 实现运行时多态性的重要机制之一;每个含有虚函数的类都有一个虚函数表,其中存储了指向各个虚函数的指针,当对象被创建时,会包含一个指向正确虚函数表的指针,通过这个指针,程序能够在运行时根据对象的实际类型来确定调用的虚函数版本。

我们们可以描绘出虚函数表的示意图,以便更好地理解虚函数的工作原理。

虚函数表示例:

每个含有虚函数的类都有一个虚函数表,其中存储了指向各个虚函数的指针,其中的指针顺序与虚函数在类定义中的声明顺序相同。要注意:当一个虚函数在父类中声明为虚函数时,它会自动成为子类中的虚函数。

1.Animal类虚函数表
+----------------------------------------+
|             虚函数表 (Animal)          |
+----------------------------------------+
|  指向 Animal::eat() 的指针             |
+----------------------------------------+
|  指向 类中某虚函数 的指针                |
+----------------------------------------+
|               ....                   |
+----------------------------------------+
​
2.Cat类虚函数表
+----------------------------------------+
|             虚函数表 (Cat)             |
+----------------------------------------+
|  指向 Cat::eat() 的指针                |
+----------------------------------------+
|  指向 类中某虚函数 的指针                |
+----------------------------------------+
|                    ....              |
+----------------------------------------+

含有虚函数的类实例化的对象:对象的前四个字节存储空间存储的是指向虚函数表的指针(对指针取值即可获得到对应类虚函数表的地址)

Animal对象
+-----------------------+
|       Animal 对象      |
+-----------------------+
|  指向虚函数表 (Animal) |
+-----------------------+
|    其他成员变量        |
+----------------------+
​
​
Cat对象
+-----------------------+
|         Cat 对象       |
+-----------------------+
|  指向虚函数表 (Cat)     |
+-----------------------+
|    其他成员变量         |
+-----------------------+

现在让我们来解释一下虚函数调用的过程:

int main() {Animal * catObj = new Cat;catObj->eat();system("pause");return 0;
}
  1. 创建对象:首先,我们创建了一个 Cat 类的对象,并将其地址赋给了一个 Animal 类型的指针 catObj。由于 eat() 函数在基类 Animal 中声明为虚函数,因此在 Cat 类的对象中,会包含一个指向正确虚函数表的指针,这个指针会指向 Cat 类的虚函数表。

  2. 调用虚函数:当我们通过 catObj 指针调用 eat() 函数时,编译器会根据指针所指向的对象的实际类型来决定应该调用哪个虚函数版本。然后,程序会使用对象中存储的虚函数表指针来找到正确的虚函数表。

  3. 查找函数指针:在找到了正确的虚函数表后,程序会在虚函数表中查找 eat() 函数对应的函数指针。由于 Cat 类中重写了 eat() 函数,因此在 Cat 类的虚函数表中,指向 Cat::eat() 函数的指针会被存储在相应位置上。

  4. 调用函数:最后,程序会通过找到的函数指针来调用 Cat::eat() 函数,输出 "cat Eat 函数"。

根据上面的虚函数的调用过程和原理我们也可以不使用->调用符号,而通过手动地址寻找得到循行的函数。

int main() {
​Animal * catObj = new Cat;//手动调用虚函数typedef void(*MyEat)();MyEat  myeat = (MyEat)*(int *)*(int *)catObj;myeat();
​delete catObj;system("pause");return 0;
}
1.*(int *)*(int *)catObj;解释:

虚函数表指针通常位于对象的内存布局的开始位置(可能是第一个成员或对象的隐藏成员);

(int *)catObj:这一步是将指向对象的指针 catObj 进行了类型转换,将其转换为 int* 类型指针

*(int *)catObj:接着,我们对转换后的指针进行了解引用操作;根据 C++ 中的指针运算规则,解引用操作会取出指针所指向的虚函数表内存地址处的值。

(int *)*(int *)catObj:将虚函数表内存地址转化为int* 类型指针,此时指针指向虚函数表内存地址。

*(int *)*(int *)catObj:最后,我们对转换后的整数地址进行解引用操作,得到的是该地址存储的值,也就是Cat类虚函数表中的第一个虚函数eat()的函数指针地址。

因为我们通过上述方法获得到了虚函数的函数地址,所以此时需要使用一个函数指针去指向该地址,对该虚函数进行调用。

2.typedef void(*MyEat)();解析:

这段代码定义了一个函数指针类型 MyEat,它可以指向一个没有参数且返回类型为 void 的函数。

void(*MyEat)();:这是一个函数指针的声明。在 typedef 关键字后面,我们声明了一个名为 MyEat 的新类型,它是一个指向函数的指针。括号中的 *MyEat 表示这是一个指针类型,而括号外的 () 表示这个指针所指向的函数的参数列表。(如果指向的函数地址有参数,那么再进行函数指针类型定义的时候也需要跟上参数列表)
3.(MyEat)*(int *)*(int *)catObj;解析:

此时我们将上述获得到的虚函数eat()的函数指针地址(此时类型为整型)类型强制转化为函数指针类型MyEat

4.MyEat myeat = (MyEat)*(int *)*(int *)catObj;解析:

并声明一个函数指针类型MyEat对象myeat,接着将该指针指向虚函数eat()的函数指针地址。

5.myeat();解析:

最后运行myeat()函数获得最后的结果:

最后得到的结果也是cat对象的eat()方法。

再此处下断点,查看函数指针的地址值;

在反汇编窗口查看该地址值的相关汇编代码,可以看到该地址指向的就是Cat类的Eat函数。


http://www.ppmy.cn/news/1458446.html

相关文章

数据仓库与数据挖掘实验练习3-4(实验二2024.5.8)

练习3 1.简单文件操作练习 import pandas as pd # 读取文件 pd.read_csv(pokemon.csv) # 读取 CSV 文件的函数调用&#xff0c;它将文件中的数据加载到 DataFrame 中&#xff0c;并指定了 Pokemon 列作为索引列。 pd.read_csv(pokemon.csv,index_colPokemon)#查看类型 type(p…

Flutter想转鸿蒙开发但遭频繁却退?这些真实经历打破你的认知

前言 不知不觉已经干了好几年Flutter开发了。 首先来感叹一下近年来HarmonyOS的发展速度之快吧&#xff01; 但凭着我对新领域的高度热情&#xff0c;还是决定打破自己的舒适圈了… 鸿蒙OS&#xff08;HarmonyOS&#xff09;的崛起确实引起了我的注意&#xff0c;做了很久的挣…

【Python】在Windows Server上部署Flask后端服务器

想要在Windows Server上部署flask应用&#xff0c;当然不能只下一个anaconda配完环境之后直接启动py文件&#xff0c;这样的话后台会有一段警告&#xff1a; * Serving Flask app app* Debug mode: off WARNING: This is a development server. Do not use it in a production …

W801学习笔记二十四:NES模拟器游戏

之前已经实现了NES模拟器玩游戏。W801学习笔记九&#xff1a;HLK-W801制作学习机/NES游戏机(模拟器) 现在要在新版本掌机中移植过来。 1、把NES文件都拷贝到SD卡中。 这回不会受内存大小限制了。我这里拷贝了4个&#xff0c;还可以拷贝更多。 2、应用初始化中&#xff0c;加载…

人工智能培训讲师咨询叶梓介绍及智能医疗技术与ChatGPT临床应用三日深度培训提纲

1、授课老师简介 叶梓&#xff0c;上海交通大学计算机专业博士毕业&#xff0c;高级工程师。主研方向&#xff1a;数据挖掘、机器学习、人工智能。历任国内知名上市IT企业的AI技术总监、资深技术专家&#xff0c;市级行业大数据平台技术负责人。 长期负责城市信息化智能平台的…

第50期|GPTSecurity周报

GPTSecurity是一个涵盖了前沿学术研究和实践经验分享的社区&#xff0c;集成了生成预训练Transformer&#xff08;GPT&#xff09;、人工智能生成内容&#xff08;AIGC&#xff09;以及大语言模型&#xff08;LLM&#xff09;等安全领域应用的知识。在这里&#xff0c;您可以找…

使用自关联方法处理多表关系

使用自关联方法处理多表关系 这里通过省市区之间的关系来解释自关联的情况 在设置地址的过程中 , 不可避免的需要设置 , 省份 ,市以及区 而省市区三者之间的具有一定的关联关系 一个省份对应多个市 一个市对应多个区 如果通过设置主表从表关系则需要设置三张标分别对应省…

2024-经济学宏观微观CFA金融工程公式汇总/本科研究生专业考试/考研/论文/重点考点汇总

### 经济学/CFA/金融工程公式汇总 http://deepnlp.org/equation/category/economics Simple Interest\ http://www.deepnlp.org/equation/simple-interest Compound Interest\ http://www.deepnlp.org/equation/compound-interest Effective Rate\ http://www.de…