深入理解c++多态实现原理

本文深入探讨C++中多态的实现原理,包括虚函数表的构造与使用过程,以及如何通过基类引用调用派生类成员函数。通过具体实例及反汇编分析,揭示多态背后的技术细节。

引言

       多态是指通过基类的指针或者引用,在运行时动态调用实际绑定对象函数的行为。与之相对应的编译时绑定函数称为静态绑定。多态是面向对象编程的核心思想之一,因此我们有必要深入探索一下它的实现原理。理解了原理才能更好的使用。

前置条件

        现有代码如下所示,非常简单的例子。通过基类的引用调用recv函数来触发多态。接下来的分析涉及汇编知识,如果还没熟悉汇编,可以看另外一篇文章<深入理解c++函数调用的参数传递与局部变量申请>

#include <stdio.h>
#include <string>

#define trace(fmt, ...) printf("[trace] %s:%s:%d " fmt, __FILE__, __FUNCTION__, __LINE__, ##__VA_ARGS__)

class IClient
{
	public:
		IClient(){};
		virtual ~IClient(){};
		virtual ssize_t recv(char *buff, size_t len) = 0;
};
class CStreamClient: public IClient
{
	public:
		CStreamClient(){};
		~CStreamClient(){};
		ssize_t recv(char *buff, size_t len)
		{
			trace("recv %d bytes in %p\n", len, buff);
			return len;
		}
};
int main(int argc, char **argv)
{
	CStreamClient streamclient;
	IClient &client = streamclient;
	client.recv(NULL, 0);
	return 0;
}
分析

        我们都知道拥有虚函数的类都有属于自己的虚表存放在.text段中,实例化后对象拥有一个内建变量_vptr虚表指针,它指向了实际对象的虚表。那么这个_vptr在哪里初始化呢?当然是构造函数。如下图所示,CStreamClient与IClient都会在自己的构造函数中初始化_vptr为自己的虚表地址。不过父类构造先于子类构造函数执行,因此CStreamClient对象指向的是自己的虚表。


        接下来我们对main反汇编代码分析如下所示,client对象处于栈中。首先计算出client对象的地址。接下来解引用得到_vptr的值。值得注意的是_vptr并非指向虚表起始地址,而是+0X08。起始4字节猜测是保留,毕竟全是0x00;接下来4字节存放的是对象的类型信息,反汇编typeid()函数你就会发现它就是在读取这个地址指向的typeinfo。

        得到_vptr地址后+0x08的偏移得到虚表存放recv地址的地址,再解引用就得到实际绑定对象的recv函数地址了。接下来是各个参数入栈,再跳转执行。


        那么,这就结束了吗?眼尖的你肯定发现了,为什么会有两个的析构函数?函数声明还是一个模子出来的。那么我们对这两个析构反汇编分析对比一下有什么不同。有注意到什么不同吗?

        左侧的析构函数赋值eax为零,eax分为[16位ax|8位ah|8位al]。那么test指令将会置位ZF标志位,因此接下来的je指令将会跳转,也就是不进行free的操作。右侧的析构函数在调用左侧析构的基础上进行了释放内存。那么就是说它们分别是为栈上和堆上的对象准备的。


<think>嗯,用户想了解C++多态实现原理,特别是虚函数表和虚函数指针的部分。首先,我需要回忆一下多态的基本概念。多态分为编译时多态和运行时多态,这里应该是指运行时多态,也就是通过虚函数实现的动态绑定。 根据用户提供的引用资料,引用1提到每个包含虚函数的类都有一个虚函数表(VTable),编译器会把虚函数表的指针放在对象实例的最前面。引用2则详细说明了虚函数表是函数指针数组,每个元素对应虚函数的地址,并且类中存在虚函数时,对象大小会增加4字节(32位系统),也就是一个指针的大小。 接下来需要整理这些信息,分步骤解释多态实现机制。首先,虚函数表的结构,每个类有一个,存储虚函数地址。然后,对象实例中的虚函数表指针(vptr),指向对应的vtable。当调用虚函数时,通过vptr找到vtable,再找到对应的函数地址,实现动态绑定。 需要注意的是,用户可能对虚函数表的内存布局不太清楚,可以结合引用的例子,比如CTest类的大小变化,说明vptr的存在。此外,可能需要解释继承情况下的虚函数表,比如派生类如何覆盖虚函数,以及多重继承下的情况,不过用户的问题可能更关注基础的单继承情况。 另外,用户可能想知道为什么虚函数能够实现多态,这里需要强调动态绑定和静态绑定的区别。普通函数调用在编译时确定地址,而虚函数在运行时通过vtable查找,这才能实现根据对象类型调用正确的函数。 还需要注意代码示例是否必要,用户提供的引用中有C++代码,可以引用来说明类的结构,但用户的问题是关于原理,可能需要用更简洁的示例或者描述来解释。 最后,生成相关问题时要围绕多态、虚函数表、内存布局、性能影响等方面提问,确保问题有助于用户深入理解。</think>### C++多态实现原理 #### 1. 虚函数表(vtable)的核心作用 每个包含虚函数的类都会自动生成一个**虚函数表**(Virtual Table),这个表本质上是一个**函数指针数组**。数组中每个元素对应类中虚函数的地址,例如: ```cpp class Animal { public: virtual void speak() { cout << "Animal sound" << endl; } virtual ~Animal() {} }; ``` 此时`Animal`类的虚函数表中包含`speak()`和析构函数的地址[^2]。 #### 2. 虚函数指针(vptr)的内存布局 当类包含虚函数时: - 编译器会在**每个对象实例头部**插入一个隐藏的虚函数表指针(vptr) - 该指针指向对应类的虚函数表 - 验证方法: ```cpp cout << sizeof(Animal) << endl; // 输出4(32位系统)或8(64位系统) ``` 当类没有虚函数时,对象大小为1字节(空对象占位符),添加虚函数后大小变为指针尺寸。 #### 3. 多态调用过程 ```cpp Animal* animal = new Dog(); animal->speak(); // 动态绑定到Dog::speak() ``` 具体执行步骤: 1. 通过对象地址找到vptr 2. 通过vptr访问虚函数表 3. 在虚函数表中找到speak()的偏移位置 4. 调用对应的函数指针 #### 4. 继承体系下的虚函数表 当存在继承关系时: ```cpp class Dog : public Animal { public: void speak() override { cout << "Woof!" << endl; } }; ``` - `Dog`类会复制父类的虚函数表 - 覆盖重写的虚函数条目 - 新增的虚函数会追加到虚函数表末尾 #### 5. 虚函数表特性总结 | 特性 | 说明 | |-----------------------|----------------------------------------------------------------------| | 编译期生成 | 每个类有且仅有一个虚函数表 | | 内存位置 | 通常存储在程序的只读数据段 | | 动态绑定成本 | 多一次指针解引用操作(约2-3个CPU周期) | | 多重继承 | 每个基类对应独立的虚函数表指针 |
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值