使用函数指针访问虚函数

Windows10下Clion中的C++虚函数与内存布局探索
本文展示了在Windows1064位环境下,使用Clion进行C++编程时,对类的继承和虚函数的探究。通过创建一个基类`Foo`和派生类`Sun`,并讨论了对象内存布局,特别是虚函数表(vtable)的访问。通过指针和汇编指令直接调用vtable中的函数,揭示了C++多态性在底层的工作原理。

环境 windows10 64位 clion64

class Foo {

public:
    virtual int add(){
        return 0;
    }
};

class Sun : public Foo{
public:
    int add() {
        printf("\n%s\n", "class Sun : public Foo add ");
        return 99;
    }
};

int main(){
    Sun *a =new Sun();
    long * vt = (long*)(*(long *)a);
    printf("vt =%lu", *vt);
    __asm__("call *%0"::"r"((vt[0])));//这里也可以用函数指针访问 但是比较麻烦所以直接用call指令调用了 下面为函数指针调用
    //    int (*addP)()=reinterpret_cast<int (*)(void)>(((long *) (*(long *) a))[0]);
	//    (*addP)();

}


在这里插入图片描述

<think>我们正在讨论虚函数指针和虚函数表的相关知识。根据引用内容,虚函数表(vtable)是一个存储虚函数地址的指针数组,每个包含虚函数的类(或从包含虚函数的类派生)都有一个虚函数表。每个对象在创建时,会包含一个指向该虚函数表的指针(vptr),通常位于对象内存布局的起始位置(前4或8字节,取决于系统架构)。 ### 虚函数指针(vptr)和虚函数表(vtable)的概念 1. **虚函数指针(vptr)**: - 每个对象在创建时,如果其类有虚函数(包括继承的),则该对象会包含一个隐藏的成员——虚函数指针(vptr)。 - vptr指向该对象所属类的虚函数表(vtable)。 - 通常位于对象内存布局的最前面(也可以在其他位置,但常见的是在前面)。 2. **虚函数表(vtable)**: - 每个类(不是每个对象)有一个虚函数表,该表在编译期间由编译器生成。 - 虚函数表是一个函数指针数组,存放该类中所有虚函数的地址(包括继承的和新增的)。 - 虚函数表在内存中通常位于程序的只读数据段(如代码段)[^3]。 ### 工作原理 1. **对象创建时**: - 当创建一个对象时,构造函数会初始化该对象的vptr,使其指向正确的虚函数表。 2. **通过基类指针调用虚函数**: - 当通过基类指针(或引用)调用虚函数时,程序会: a. 通过该指针找到对象(因为对象内存布局的前几个字节就是vptr)。 b. 通过vptr找到虚函数表。 c. 在虚函数表中查找该虚函数的地址。 d. 调用该地址指向的函数。 3. **动态绑定(运行时多态)**: - 由于vptr在运行时才指向实际对象的虚函数表,因此能够根据对象的实际类型调用正确的函数版本。 ### 示例代码解析 以下引用中的示例代码展示了如何通过指针访问虚函数表: ```cpp int main() { Student S; Person P; Person* PP = &P; int* ptr = (int*)PP; // 将对象地址转为int*(假设vptr在前4字节) printf("取到虚函数指针的值:%p", *ptr); // 输出vptr的值(即虚函数表地址)[^2] } ``` ### 虚函数表在继承中的行为 1. **派生类虚函数表的构建**: - 编译器在构建派生类的虚函数表时: a. 先将基类的虚函数表内容拷贝一份到派生类虚函数表中。 b. 如果派生类重写了某个虚函数,则用派生类的函数地址覆盖基类的对应项。 c. 派生类新增的虚函数地址按声明顺序添加到虚函数表末尾[^3]。 2. **示例**: 假设基类`Base`有两个虚函数`func1`和`func2`,派生类`Derived`重写了`func1`并新增了`func3`: - 基类虚函数表:`Base::func1`, `Base::func2` - 派生类虚函数表:`Derived::func1`(覆盖), `Base::func2`(继承未覆盖), `Derived::func3`(新增) ### 虚函数表内存位置 - 虚函数表存储在代码段(只读数据段),因为它在编译时确定,且所有同类型对象共享同一个虚函数表[^3]。 - 虚函数指针(vptr)存储在对象实例中,每个对象有自己的vptr(但指向相同的虚函数表)。 ### 手动模拟虚函数调用 以下代码模拟了通过虚函数表调用虚函数的过程(仅用于理解原理,实际编程中不需要这样调用): ```cpp CTest tst; // 获取对象起始地址,并解释为int*(假设vptr在对象起始位置) int** __vfptr = (int**)*(int*)&tst; // 将虚函数表的前三项解释为函数指针 void(*m_pfun1)() = (void (*) ())__vfptr[0]; void(*m_pfun2)() = (void (*) ())__vfptr[1]; void(*m_pfun3)() = (void (*) ())__vfptr[2]; // 通过函数指针调用 (*m_pfun1)(); // 调用第一个虚函数 (*m_pfun2)(); // 调用第二个虚函数 (*m_pfun3)(); // 调用第三个虚函数 [^4] ``` ### 注意事项 1. **虚析构函数**: - 若类可能被继承,且会通过基类指针删除派生类对象,则基类析构函数必须是虚函数。否则只会调用基类析构函数,导致派生类部分内存泄漏[^1]. 2. **虚函数表大小**: - 虚函数表的大小取决于虚函数的数量(包括继承的和新增的)。 - 每个派生类有自己的虚函数表(即使与基类相同)。 3. **性能考虑**: - 虚函数调用比普通函数调用多一次间接寻址(通过vptr和虚函数表),因此有轻微性能开销。 - 虚函数不能被内联(因为调用在运行时决定)[^5]. ```mermaid graph TD A[对象实例] --> B[vptr] --> C[虚函数表] C --> D[虚函数1地址] C --> E[虚函数2地址] C --> F[...] ``` §§相关问题§§ 1. 虚函数表在多重继承中的结构是怎样的? 2. 如何通过调试工具查看虚函数表的内容? 3. 虚函数调用与普通函数调用的性能差异有多大? 4. C++11中override和final关键字如何影响虚函数表? 5. 虚函数表中的析构函数是如何处理的? [^1]: 虚函数指针和虚函数表实现动态绑定的核心机制 [^2]: 通过指针访问虚函数表地址的示例 [^3]: 派生类虚函数表构建规则及存储位置 [^4]: 手动模拟虚函数调用的代码示例 [^5]: 虚函数调用的性能考虑 </think>### 虚函数指针(vptr)与虚函数表(vtable)详解 #### 一、核心概念 1. **虚函数指针(vptr)** - 每个包含虚函数的对象内部**隐藏一个指针成员**(通常位于对象内存起始位置) - 大小与普通指针相同(32位系统4字节,64位系统8字节) - **作用**:指向该类对应的虚函数表[^1][^2] 2. **虚函数表(vtable)** - 编译器为**每个类**(非每个对象)生成的一张**静态函数指针表** - 存储该类所有虚函数的实际地址 - 内存位置:通常位于程序的**只读数据段**(代码段)[^3][^5] ```cpp class Base { public: virtual void func1() {} // 虚函数 virtual void func2() {} // 虚函数 }; // 内存布局示意: // [ vptr ] -> 指向Base类的vtable // [其他成员变量...] ``` #### 二、工作原理 **运行时动态绑定流程**: 1. 对象创建时,构造函数初始化其 vptr 2. 通过对象调用虚函数时: - 通过 vptr 定位到 vtable - 在 vtable 中查找函数偏移位置 - 跳转到实际函数地址执行 ```mermaid graph LR A[对象] --> B[vptr] --> C[vtable] C --> D[虚函数1地址] C --> E[虚函数2地址] C --> F[...] ``` #### 三、继承中的行为特点 1. **派生类 vtable 构建规则**: - 复制基类 vtable 内容 - **覆盖**重写的虚函数地址 - **追加**新增的虚函数地址[^3] ```cpp class Derived : public Base { public: virtual void func1() override {} // 覆盖基类func1 virtual void func3() {} // 新增虚函数 }; ``` 内存布局变化: ``` Base vtable: [ &Base::func1, &Base::func2 ] Derived vtable: [ &Derived::func1, &Base::func2, &Derived::func3 ] // 覆盖基类func1 继承基类func2 新增函数 ``` 2. **覆盖的本质** 虚函数重写实质是 **vtable 中函数指针的替换**,因此也称"覆盖"(原理层术语)[^2] #### 四、手动操作示例 通过指针访问 vtable(演示原理,实际开发慎用): ```cpp class Test { public: virtual void f1() { cout << "f1"; } virtual void f2() { cout << "f2"; } }; int main() { Test obj; // 1. 获取vptr地址(对象首地址) int* vptr = reinterpret_cast<int*>(&obj); // 2. 获取vtable首地址 int* vtable = reinterpret_cast<int*>(*vptr); // 3. 转换为函数指针并调用 using Func = void(*)(); Func f1 = reinterpret_cast<Func>(vtable[0]); // 第一个虚函数 f1(); // 输出"f1" Func f2 = reinterpret_cast<Func>(vtable[1]); // 第二个虚函数 f2(); // 输出"f2" } ``` > 注意:不同编译器实现可能有差异(如末尾是否添加 nullptr)[^4] #### 五、关键特性总结 | 特性 | 说明 | |---------------------|----------------------------------------------------------------------| | **vptr 存储位置** | 对象实例内部(每个对象独立) | | **vtable 存储位置** | 代码段(所有同类对象共享)[^3] | | **内存开销** | 每个对象增加一个指针大小;每个类增加一个 vtable | | **动态绑定代价** | 多一次指针寻址(通常1-2个CPU周期)[^5] | | **调试可见性** | IDE调试器通常隐藏派生类新增的虚函数项(需查看内存)[^2] | #### 六、核心作用 1. 实现 **C++ 运行时多态**(动态绑定) 2. 支持面向对象的 **接口抽象**(通过基类指针操作派生类) 3. 奠定 **RTTI(运行时类型识别)** 的基础
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值