C++及反汇编 剖析多态及虚表(上:多态的实现)
多态
什么是虚函数,有什么用途?
虚函数主要用于多态中。
◼ 多态是面向对象非常重要的一个特性
同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果
在运行时,可以识别出真正的对象类型,调用对应子类中的函数
注意:此处非常重要!!!!!!!!!!!!!
◼ 多态的要素
子类重写父类的成员函数(override)
父类指针指向子类对象
利用父类指针调用重写的成员函数
非多态的弊
比如我有一个 猫, 狗, 猪三个类,它们都具有speak(叫) 和run(跑)的功能,即他们都具有相同的成员函数speak和run。我要实现分别调用每个动物的speak和run的功能,让他们一边叫,一边跑,那么你该如何创建类来调用他们呢?
这简单啊,三个动物类,每个类都有speak和run成员函数,在创建每个类的对象,分别调用不就好了??
如果我们不知道多态,我们确实会这样实现他们的功能:(但是这样有什么弊端呢??)
class Dog //狗 { public: void speak() { cout << "Dog::speak()\n"; } void run() { cout << "Dog::ran()\n"; } }; class Cat //猫 { public: void speak() { cout << "Cat::speak()\n"; } void run() { cout << "Cat::ran()\n"; } }; class Pig //猪 { public: void speak() { cout << "Pig::speak()\n"; } void run() { cout << "Pig::ran()\n"; } }; //每个动物都具有特定的行为 void action(Dog* d) { d->speak(); d->run(); } void action(Cat* d) { d->speak(); d->run(); } void action(Pig* d) { d->speak(); d->run(); } int main() { action(new Dog); action(new Cat); action(new Pig); return 0; }
如上所示,我们定义了三个子函数,为了实现每一个动物的speak和run的功能,每一个action函数的形参都是特定化的,我们只需要写调用三次action函数,传递每一个对象都对应相应的指针形参,就好了:
这样我们就可以实现分别调用每个动物的speak和run的功能。
但是,这样写你会不会觉得太麻烦了,如果我们有100个动物,你岂不是要写100个不同的action函数? 这样我们就引入了多态。
多态的利
我们能否把好多个动物都具有的相似功能集中到一个类上去? 这样我们就不必写100个action函数,然后每一个action都指定一只动物了。
多态的重要知识点:
- 父类指针可以指向子类对象
- 子类对象不可以指向父类指针
父类指针可以指向子类对象: 啥意思???
例如:
class Father { public: int money; //钱 }; class Child :public Father { public: int toys; //玩具 }; int main() { Father* a = new Child(); //父类指针指向子类对象 Child* b = new Father(); //错误(ERROR): 子类指针指向父类对象 return 0; }
-
父类指针指向一个孩子,孩子有的,父亲也有,父类指针没有影响
-
子类指针指向父类,子类有的,父类不一定有,父类没有此物品,但是孩子指针可以指向此物品,这就是造成了
访问越界
的问题。
我们来调用一下:
Father* a = new Child(); cout<< a->money<<endl; Child* b = (Child* )new Father(); cout << b->money << " " << b->toys << endl;
子类继承的父类指针,父类指针没有toys的属性,但是子类对象可以访问这个属性,相当于子类指针只存储前四个字节(即父类有的属性),而父类没有的则不会存储。因此,子类对象一旦调用这个父类没有的属性则就会往后面找四个,但是此时它访问的已经越界了!!!
-33686019:即是它访问的越界
的后面的字节组成的垃圾值
因此:我们可以得出结论:
- 父类指针可以指向子类对象(而且还经常使用)
- 子类指针不可以指向父类对象。
这就是多态实现的本质:
即通过继承同一个父类,让父类指针指向多个不同的子类对象,通过父类对象来调用他们公共的成员函数。
代码实现多态
现在,我们来实现多态:
即分别调用猫,狗,猪的各自的行为,使用多态形式:
我们引入了虚函数 virtual:
class Animals { public: virtual void speak() { cout << "Animal::speak()\n"; } virtual void run() { cout << "Animal::ran()\n"; } };
我们定义了一个动物们的基类:他们都具有speak和run的功能,所以我们把他们放在一个统一的基类Animals中。
接着我们为每个动物来继承这个基类:
class Dog :public Animals { public: void speak() { cout << "Dog::speak()\n"; } void run() { cout << "Dog::ran()\n"; } }; class Cat :public Animals { public: void speak() { cout << "Cat::speak()\n"; } void run() { cout << "Cat::ran()\n"; } }; class Pig :public Animals { public: void speak() { cout << "Pig::speak()\n"; } void run() { cout << "Pig::ran()\n"; } };
然后我们再利用一个统一的action函数,只需要一个就行
,我们它的参数写作父类指针的形式:
//一个统一的动物行为 void action(Animals* ani) //注意形参为父类指针 使用父类指针调用子类对象 { ani->speak(); ani->run(); }
注意:我们的形参为Animals,他们共同的父类,所以我们就可以利用我们刚才讲的那一个重要的特性:
父类指针可以指向子类对象,来调用他们:
int main() { action(new Dog); action(new Cat); action(new Pig); return 0; }
注意,我们所写的这个实现代码和上面有什么不同? 我们把action函数的用作多态的形式,通过调用父类指针来指向他们不同的成员函数,这样便实现了一个函数根据形参的不同实现了多个不同的函数行为,本质就是父类指针可以指向子类的对象
,并且调用子类的特定函数:
如果有1000个动物,他们都具有speak和run,如果不用多态,我们岂不是要写action函数1000次? 现在,我们只需要写1次action函数就好了(但是你的类还是要写1000次,这个没办法,我们只需要把他们都继承自父类Animals类)。
virtual是啥 ---->虚表的引入
我们可能会好奇,virtual是啥,为啥要在父类的函数前面加上virtual??
注意:不是每个成员函数我们都加virtaul,只有多个子类同时具有的行为函数,我们才在他们的父类中加上virtaul,表示的大概含义就是说: 我这个函数实际上是虚的,空的,具体的实现行为还是要看子类的此函数的具体实现,父类中利用virtaul只是让你知道有这个函数,它在多个子类中都同时具有。
加不加virtaul有什么区别??
如果我们把virtual去掉:
class Animals { public: void speak() { cout << "Animal::speak()\n"; } void run() { cout << "Animal::ran()\n"; } }; ....此处省略动物类 int main() { //如果不是多态 Animals* dog = new Dog; //父类指针调用子类对象 dog->speak(); dog->run(); return 0; }
注意我们仍然子类继承父类Animals,我们查看一下反汇编:
看不懂的可以先看我的这篇博客:反汇编分析C/C++常见语句的底层实现
我们可以看到,虽然你使用了父类指针指向子类对象这一多态的基本条件,但是call的却是一个准确的地址,我们相想一想,不应该call的是一个随着你创建的对象不同而不同的地址吗??
如果我们在加上virtaul ,在Animals的准备要虚的函数里面:
那么你就会发现,惊喜的一幕:
WDF?????????????
这他妈什么鬼
怎么莫名其妙加上了这么多行??????? 闹鬼了???
我们产生了虚表!!!
并不是:这就是多态virtual实现的妙处:我们只要加上virtual,我们就为这个类创建了一张** 虚表 **,使得它可以根据虚表和创建的对象的类型来查找对应的speak和run函数 所在的位置,然后根据他们对应的地址,call到目标位置
,是不是听不太懂??
没关系,我们下一节在讲解什么是虚表,以及虚表的实现原理。
本文来自博客园,作者:hugeYlh,转载请注明原文链接:https://www.cnblogs.com/helloylh/p/17209702.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)