访问虚函数表++ (菱形继承(public))

本文通过一个C++实例,详细解析了在菱形继承结构中虚函数表的布局和访问机制,展示了如何通过指针遍历虚函数表并调用各个类的虚函数。

访问菱形继承时的虚函数表

在这里插入图片描述

#include <iostream>
#include <cstdio>
#include <iomanip>
#include <string>
#include <cstdlib>
#include <cstring>
#include <queue>
#include <set>
#include <vector>
#include <map>
#include <algorithm>
#include <cmath>
#include <stack>

#define INF 0x3f3f3f3f
#define LINF 0x3f3f3f3f3f3f3f3f
#define ll long long
#define ull unsigned long long
#define uint unsigned int
#define l(x) ((x)<<1)
#define r(x) ((x)<<1|1)
#define lowbit(x) ((x)&(-(x)))
#define abs(x) ((x)>=0?(x):(-(x)))
#define ms(a,b) memset(a,b,sizeof(a))

using namespace std;

class A {
public:
	virtual void getA() {
		cout << "A" << endl;
	}
	virtual void get() {
		cout << "A get" << endl;
	}
};

class B {
public:
	virtual void get() {
		cout << "B get" << endl;
	}
	virtual void getB() {
		cout << "B" << endl;
	}
};

class C : public A, public B {
public:
	virtual void get() {
		cout << "C get" << endl;
	}
	virtual void getC() {
		cout << "C" << endl;
	}
};

class D : public A, public B {
public:
	virtual void get() {
		cout << "D get" << endl;
	}
	virtual void getD() {
		cout << "D" << endl;
	}
};

class Child : public C, public D {
public:
	virtual void get() {
		cout << "Child get" << endl;
	}
	virtual void getChild() {
		cout << "Child" << endl;
	}
};

typedef void(*FUNC)();


int main() {
	Child *child = new Child();

	int *vt = (int *)child;

	cout << "in begin" << endl;
	for (int i = 0; vt[i] != 0; i++) {
		cout << "第"<<i<<"张虚函数表" << endl;
		int *fun = (int *)(vt[i]);
		for (int i = 0; fun[i] != 0; ++i) {
			printf("function : %d :0X%x->", i, fun[i]);
			FUNC f = (FUNC)fun[i];
			f();
		}
		cout << endl;
	}

	return 0;
}

狗头保命(我没有嘲讽14nm++)

<think> 我们正在讨论C++中的菱形继承问题,特别是派生类虚函数表的处理方式。根据引用内容,菱形继承涉及虚继承,以解决基类重复的问题。在虚继承下,虚函数表的处理更为复杂。 引用[1]提到:在菱形继承中,派生类(D)在构造时,修改的是第一个继承的基类(假设为B)的虚函数表指针。因为虚继承保证了虚基类(A)在派生类中只有唯一实例,所以B和C的虚函数表在D中被合并成一个,并被D改写为指向自己的虚函数表。 引用[2]的目录显示,菱形继承和菱形虚拟继承是重要内容,其中虚函数表是关键。 引用[3]展示了单继承的虚函数表布局,但菱形继承更复杂。 引用[4]提供了一个虚函数的简单示例,但未直接涉及菱形继承。 因此,我们重点参考引用[1]和引用[2]来总结菱形继承下派生类虚函数表的处理过程。 处理过程如下: 1. **菱形继承结构**: 考虑以下类结构: A (虚基类) / \ B C (虚继承A) \ / D (派生类) 其中,B和C都虚继承A,D继承B和C。 2. **虚基类唯一性**: 由于虚继承,A在D中只有一份副本。这意味着D对象中只有一个A子对象。 3. **虚函数表指针的初始情况**: - 在B和C中,它们都包含一个指向自己虚函数表的指针(vptr)。但是,由于它们虚继承自A,所以它们并没有自己的A子对象副本,而是共享最终派生类D中的A子对象。 - 根据引用[1],当B和C继承A时,它们使用的是原本属于A的虚函数表指针(即它们没有生成自己的虚函数表指针?这里需要准确理解)。实际上,在虚继承的情况下,B和C会包含一个虚基类指针(vbptr)来指向虚基类子对象,但这里我们关注虚函数表指针(vptr)。 4. **派生类D的构造过程**: - 当创建D对象时,首先调用A的构造函数(由D直接调用,因为虚继承),然后调用B和C的构造函数,最后调用D的构造函数。 - 关键点:在D的构造函数执行期间,它会修改继承而来的虚函数表指针(vptr)。根据引用[1],D修改的是第一个继承的基类的虚函数表指针(假设D先继承B,则修改B的vptr)。这是因为在对象的内存布局中,D对象最前面的部分是其第一个基类B的部分(包括vptr)。 5. **虚函数表的合并与覆盖**: - 引用[1]指出:在D中,B和C的虚函数表被合成了一个。这是因为虚继承要求虚基类A在D中只有一份,所以B和C的虚函数表需要统一。具体来说,D会创建一个新的虚函数表,其中包含: - 继承自A的虚函数(如果有覆盖,则覆盖) - 继承自B的虚函数(未被覆盖的部分)以及B中覆盖的虚函数(如果D再次覆盖,则使用D的版本) - 继承自C的虚函数(同样处理) - D新增的虚函数 - 由于B和C的虚函数表在D中被合并,因此D对象只需要一个虚函数表指针(指向合并后的虚函数表)?注意:实际上,在多重继承中,可能会有多个虚函数表指针。 6. **内存布局与虚函数表指针**: - 在多重继承(包括菱形继承)中,派生类对象可能包含多个虚函数表指针。每个基类(非虚继承)都有自己的虚函数表指针,但虚基类的情况特殊。 - 在菱形虚继承中,D对象的内存布局通常如下(以典型的编译器实现为例): - 首先,是B子对象(包括B的vptr,指向D中为B部分准备的虚函数表) - 然后,是C子对象(包括C的vptr,指向D中为C部分准备的虚函数表) - 然后,是D自己的数据成员 - 最后,是虚基类A子对象(共享一份) - 注意:实际上,由于虚继承,B和C部分会包含指向虚基类子对象的指针(vbptr),而不是直接包含A子对象。而虚函数表指针(vptr)和虚基类指针(vbptr)是两个不同的概念。 7. **虚函数表的内容**: - D类会有多个虚函数表:一个主表(与B关联)和一个次表(与C关联)。主表包含: - B的虚函数(如果D覆盖了B中的虚函数,则主表中对应项指向D的覆盖版本) - D新增的虚函数 - 次表包含C的虚函数(同样,覆盖的会替换)。此外,虚基类A的虚函数会放在主表中(因为A是虚基类,通常通过主表访问)。 8. **关键结论(引用[1]的核心观点)**: - 在菱形虚继承中,D类在构造时,会将第一个基类(B)的虚函数表指针修改为指向D自己的虚函数表(主表)。同时,由于虚继承的唯一性,B和C的虚函数表在D中被合并(实际上是D为每个基类部分分别维护虚函数表,但通过覆盖实现了统一的多态行为)。 9. **注意事项**: - 不同的编译器(如gcc和msvc)可能有不同的实现方式,但基本原理相似。 - 虚继承会增加对象的内存开销(因为需要存储虚基类指针)和访问开销(通过指针间接访问虚基类成员)。 因此,我们可以这样总结: 在菱形虚继承中,派生类D的虚函数表处理过程如下: 1. 编译器为D生成多个虚函数表:一个主表(对应第一个基类B)和一个或多个次表(对应其他基类,如C)。 2. D中的虚函数覆盖规则:如果D覆盖了从基类继承的虚函数,则在主表和次表中都会替换为D的版本(确保通过任何基类指针调用时都正确调用D的覆盖函数)。 3. 虚基类A的虚函数被放在主表中,因为A在D中只有一份,且通常通过主表的指针访问。 4. D对象在构造过程中,会初始化它的虚函数表指针(vptr),其中第一个基类B的vptr被设置为指向D的主表,第二个基类C的vptr被设置为指向D的次表。 5. 由于虚继承,B和C共享同一个虚基类A子对象,所以它们的虚函数表中关于A的部分最终都指向同一个实现(即D中覆盖的版本或A的原始版本)。 引用[1]所说的“B和C的虚函数表被诡异的合成一个”,实际上是指D将两个基类的虚函数表整合到自己的虚函数表布局中,使得通过B*或C*指针调用虚函数时,都能正确调用D的覆盖函数(如果存在覆盖)。 示例(简化): 假设A有虚函数vfa,B覆盖了vfa(假设B覆盖了A的虚函数),C没有覆盖,D又覆盖了vfa。 则: - 当通过B*指针调用vfa时,会使用D的主表(其中vfa指向D::vfa) - 当通过C*指针调用vfa时,由于C没有覆盖,所以原本应该调用A::vfa,但因为D覆盖了,所以在C的次表中,vfa也会指向D::vfa(因为D覆盖了,所以整个对象中只有D的版本) 因此,在D对象中,虽然存在多个虚函数表(主表和次表),但它们对于同一个虚函数的指向是一致的(即指向派生类D的覆盖版本),从而保证了多态的正确性。 以上过程确保了在菱形继承中,虚函数的调用能够正确工作,同时满足了虚基类的唯一性要求。 注意:具体的内存布局和虚函数表结构可能因编译器而异,但基本原理相同。 最后,根据引用[1],我们了解到在菱形虚继承中,虚函数表的处理是编译器自动完成的,开发者只需正确使用虚继承和虚函数覆盖。 引用标识:[^1] 生成相关问题: 1. 在多重继承(非菱形)中,虚函数表的结构是怎样的? 2. 虚继承和非虚继承在内存布局上有哪些区别? 3. 如何通过代码验证编译器对菱形继承虚函数表处理? 注意:由于引用内容有限,以上总结是基于引用的合理推断,具体实现细节可能因编译器不同而有所差异。 </think> ### C++菱形继承中派生类虚函数表的处理过程 在菱形继承场景中,虚函数表的处理涉及特殊机制来保证虚基类唯一性和多态正确性。以下是详细处理过程(以类结构 `A`←`B`/`C`←`D` 为例): #### 1. **虚基类唯一性保证** - 虚继承确保虚基类 `A` 在派生类 `D` 中仅存一份实例 - 编译器为 `D` 生成**单个共享的虚基类子对象** $$ \text{内存布局} = [\text{D数据}] + [\text{B数据}] + [\text{C数据}] + \color{red}{1\times[\text{A数据}]} $$ - 引用说明: > "虚继承保证了虚基类在派生类中只有唯一一个实例" [^1] #### 2. **虚函数表指针(vptr)的特殊处理** - **基类构造阶段**: - `B` 和 `C` 在构造时**复用 `A` 的 vptr** $$ \text{vptr}_\text{B} = \text{vptr}_\text{C} = \text{原A的虚表地址} $$ - *关键机制*:`B`/`C` 不生成独立 vptr,直接使用 `A` 的指针 - **派生类构造阶段**: - `D` 的构造函数**重定向共享 vptr** $$ \text{vptr}_\text{新} \rightarrow \text{D的虚函数表} $$ - 覆盖效果: ```cpp // 伪代码示意 D::D() { vptr = &D_vtable; // 所有基类vptr统一指向D的虚表 } ``` #### 3. **虚函数表的合并与覆盖** - 编译器为 `D` 生成**单一整合的虚函数表** - 处理规则: 1. 继承 `A` 的所有虚函数项 2. 用 `B::func` 覆盖 `A::func`(若 `B` 重写) 3. 用 `C::func` 覆盖 `A::func`(若 `C` 重写) 4. **`D` 的覆盖优先级最高** $$ \text{D_vtable}[i] = \begin{cases} \text{D::func} & \text{若D重写} \\ \text{最后继承类的实现} & \text{否则} \end{cases} $$ - 引用佐证: > "B和C的虚函数表被诡异的合成了一个,且被D改写为指向自己的虚函数表" [^1] #### 4. **内存布局示例** 假设以下场景: ```cpp class A { virtual void f(); }; class B : virtual A { virtual void f(); }; // 重写A::f class C : virtual A { }; // 不重写 class D : public B, public C { virtual void f(); }; // 重写B::f ``` 内存结构: ``` +-------------+ +---------------------+ | D实例 | --> | D虚函数表 | | vptr_B ---|---->| [0]: D::f() | // 覆盖所有父类 | vptr_C ---|--+ +---------------------+ | A数据 | | | B数据 | | | C数据 | | +-------------+ | | +---------------------+ +->| C虚函数表 (共享) | | [0]: D::f() | // 与D虚表相同 +---------------------+ ``` #### 5. **多态调用的实现** - 通过 `B*` 或 `C*` 调用虚函数: ```cpp B* ptr = new D; ptr->f(); // 调用过程: // 1. 通过ptr->vptr_B 访问虚表 // 2. 查表跳转 D::f() ``` - **关键特性**:所有基类 vptr 最终指向同一虚表,确保调用统一 #### 典型编译器行为(GCC/Clang) 1. 为含虚函数的基类生成初始虚表 2. 菱形继承时合并冲突项,优先最新派生类 3. 在派生类构造函数中重设所有 vptr 4. 虚表项按声明顺序排列,重复项被覆盖 > 不同编译器实现存在差异,但均遵循C++标准的多态要求[^2][^3]
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值