C++内存布局之虚拟继承

虚拟继承是个稍微有点复杂的机制,它的出现是为了防止在多重继承中出现同一个基类的多个副本,举例如下:


#include <iostream>
#include <stdio.h>
using namespace std;

class A {
public:
	int a;
	A(int i = 100) : a(i) {}
};

class B : public  A {};

class C : public  A {};

class D : public B, public C {};

int main(){ return 0; }

直观上我们可能认为D中会包含两份A,但有时候编译器会直接报错,错误如下:(D的基类A是模棱两可的)


error: ‘A’ is an ambiguous base of ‘D’


引入虚拟继承后,只需对上面的代码做少许改动,便可使D只包含一份A,改动后的代码如下(先不用管 main() 中的代码):


#include <iostream>
#include <stdio.h>
using namespace std;

class A {
public:
	int a;
	A(int i = 100) : a(i) {}
};

class B : virtual public  A {};

class C : virtual public  A {};

class D : public B, public C {};

int main(){

	printf("sizeof A = %d\nsizeof B = %d\nsizeof C = %d\nsizeof D = %d\n", sizeof(A),sizeof(B),sizeof(C),sizeof(D));

	D d;
	D *dp = &d;
	B *bp = dp;
	C *cp = dp;
	A *ap = dp;

	printf("addr of D = %p\naddr of B = %p\naddr of C = %p\naddr of A = %p\n",dp,bp,cp,ap);

	return 0;
}

可以看到,只要在定义 B 和 C 的时候引入一个关键字 virtual 便可,这样当 D 继承自 B 和 C 时,只保留一份 A, 从这以后,B 、C 访问的 A 也就是这份被 D 保存的 A,也就是说 B、C、D 共享一份 A。 上面的代码会输出如下结果:



首先,A 的大小为4,这不足为奇; 其次,注意到 B、C 的大小均为8,这是因为就 B、C而言,他们会保留一份 A 的子对象,同时保留一个指针,指向这个 A 的 子对象(这个指针现在貌似有点多余,但考虑到 B 或 C将来会被 D 虚拟继承,这个指针还是有用的,稍后会介绍);最后,D 的大小为 12,因为 D 中会保留一份 A 的子对象、B 中指向这个子对象的指针、C 中指向这个子对象的指针。下面我们来讲一讲 B 、C 中这两个指针的用途:当我们用 B 或 C 的指针指向 D 的对象时,就像程序中的 bp 和 cp,我们该如何访问 a 呢?此时 a 相对于 bp 的偏移量是8,而相对于 cp 的偏移量为 4,在编译器编译的时候,它实际上不能确定 bp 指向的对象是 D 类的对象还是 B 类的对象,如果是 B类的对象,那么 a 相对于 bp 的偏移量就应该为4,因此,为了兼容这种情况,就不能使用内存布局中的偏移量来访问 a ,只能给 B 和 C 各引入一个指针,来指向 A,这个指针的值会在D类对象定义的时候或B类对象定义的时候由编译器设置好,之后就可以统一由这个指针来访问 A 的子对象。


上面的程序还说明了一个问题,就是 D 类的指针在向上类型转换时,编译器会自动调整其值,使得转换后的指针指向基类对象的首地址,因此从上面的输出也可以一窥 D 的内存布局。如果大家还有耐心的话可以再对代码做稍微的修改,如下(我们在 B 和 C 中引入了虚函数,众所周知,引入虚函数后会引入一个指针,来指向一个虚函数表):


#include <iostream>
#include <stdio.h>
using namespace std;

class A {
public:
	int a;
	A(int i = 100) : a(i) {}
};

class B : virtual public  A {virtual void fun(){}};

class C : virtual public  A {virtual void fun(){}};

class D : public B, public C {};

int main(){

	printf("sizeof A = %d\nsizeof B = %d\nsizeof C = %d\nsizeof D = %d\n", sizeof(A),sizeof(B),sizeof(C),sizeof(D));

	D d;
	D *dp = &d;
	B *bp = dp;
	C *cp = dp;
	A *ap = dp;

	printf("addr of D = %p\naddr of B = %p\naddr of C = %p\naddr of A = %p\n",dp,bp,cp,ap);

	return 0;
}

输出结果如下:



可以看到 A、B、C、D的大小是不变的,因此指向虚函数表的指针与指向 A 类子对象的指针合二为一了,实际情况是这个指针指向了虚函数表,而虚函数表中还额外包含了 A 类子对象的偏移量等信息。



评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值