C++继承,多继承,虚继承,菱形继承的内存分布

参考:

【C++拾遗】 从内存布局看C++虚继承的实现原理

c++多重继承的内存分布

C++继承,多继承,虚继承,菱形继承的内存分布

VS开发人员命令界面查看C++类内存布局


一:无虚函数篇:

1.普通的继承(无虚函数):

class A {

};

class B : public A {

};

B b_object;
A *pA = &b_object;
B *pB = &b_object;
cout << "pA:" << pA << endl << "pB:" << pB << endl;

含有数据成员时pA与pB仍然相等

class A {
protected:
	void fun_A() {}
private:
	int m_A;
};

class B : public A {
private:
	int m_B;
};

B b_object;
A *pA = &b_object;
B *pB = &b_object;
cout << "pA:" << pA << endl << "pB:" << pB << endl;

B类内存分布图

class B中继承自classA,且均没有虚函数,故其对象模型内存是先是A的数据成员,然后B的数据成员 。

2,子类中有其他类的成员(无虚函数):

class A {
public:
	A() : m_A(1) {}
protected:
	void fun_A() {}
private:
	char m_A;
};

class Other {
public:
	Other() : m_other(2) {}
private:
	char m_other;
};

class B : public A {
public:
	B() : A(), m_B(3) {}
private:
	char m_B;
	Other o1;
	Other o2;
};

B b_object;
A *pA = &b_object;
B *pB = &b_object;
cout << "pA:" << pA << endl << "pB:" << pB << endl;

内存结构同样是父类最前,其余成员按照声明顺序排序

 

二,有虚函数篇:

class Person {
public:
	Person() : m_person(1) { cout << "Person:Person();" << endl; }
	virtual ~Person() {}
	virtual void Speak() { cout << "Person:Speak();" << endl; }

private:
	char m_person;
};

class Chinese : public Person {
public:
	Chinese() : m_chinese(2) { cout << "Chinese:Chinese();" << endl; }
	virtual ~Chinese() {}
	virtual void Speak() {
		cout << "Chinese speak" << endl;
	}

private:
	char m_chinese;
};

class Canadian : public Person {
public:
	Canadian() : Person(), m_canadian(3) { cout << "Canadian:Canadian();" << endl; }
	virtual ~Canadian() {}
	virtual void Speak() {
		cout << "Canadian speak" << endl;
	}

private:
	char m_canadian;
};


        Chinese ch;
	Canadian ca;
	
	Person *p1 = &ch;
	Chinese *p2 = &ch;
	cout << "p1:" << p1 << endl << "p2:" << p2 << endl;

	cout << "---------------" << endl;
	
	Person *p3 = &ca;
	Canadian *p4 = &ca;
	cout << "p3:" << p3<< endl << "p4:" << p4 << endl;

 

这里的01和02没有接在一起与类的内存对齐机制有关。

继承的类的父类有虚函数时候就会在首地址加上一个vtablePtr(虚表指针),然后才是父类成员,子类成员。

虚表指针是什么,查看内存:

其实就是我们父类按照顺序所定义的虚函数的函数指针,虚表中的第一个成员为析构函数,第二成员为Speak函数,如何用虚表来调用虚函数,以及如何构造函数和析构函数的变化又会有哪些,直接看反汇编代码吧。

在init之前将,将父类的虚表地址赋给对象前8字节(对于64位编译器)或者前4字节(对于32位编译器),此时的虚表指针指向虚函数表。

子类的内存分布图如下:

析构函数一般声明为虚函数的原因:

在使用父类指针指向子类的时候,进行delete操作,会调用析构函数,如果不是虚析构函数就会调用到父类的析构函数,是虚构函数就会调用子类的虚构函数

三,多继承篇:

有三个类 Book , Note , BookNote类,class NoteBook:public Book,public Note

class Book {
public:
	Book() : m_bookname(1) { cout << "Book:构造函数" << endl; }
	virtual ~Book() { cout << "Book:析构函数" << endl; }
	virtual void Read() { cout << "Book:Read();" << endl; }

private:
	char m_bookname;
};

class Note {
public:
	Note() : m_NoteColor(2) { cout << "Note:构造函数" << endl; }
	virtual ~Note() { cout << "Note:析构函数" << endl; }
	virtual void Write() { cout << "Note:Write();" << endl; }

private:
	char m_NoteColor;
};

class NoteBook : public Book, public Note {
public:
	NoteBook() : Book(),Note(), m_NoteBook(3) { cout << "NoteBook:构造函数" << endl; }
	virtual ~NoteBook() { cout << "NoteBook:析构函数" << endl; }
	virtual void Read() { cout << "NoteBook:Read()" << endl; }
	virtual void Write() { cout << "NoteBook:Write();" << endl; }

private:
	char m_NoteBook;
};

NoteBook Object;
Book* p1 = &Object;
Note* p2 = &Object;
NoteBook* p3 = &Object;

cout << "--------------" << endl;
p1->Read();
p2->Write();
cout << "--------------" << endl;

cout << "p1:" << p1 << "  p2:" << p2 << "  p3:" << p3 << endl;

p1=p3 != p2 

NoteBook类的内存分布图:

首先明确的是指针都是指向class NoteBook,这里需要知道class NoteBook中的内存分布情况;class NoteBook中继承顺序为Book,Note,而且不是虚继承,故其对象模型内存是先是,Book的内容(虚函数指针),然后Note的内容,最后 NoteBook对象自己的内存 。这样就知道p1=p3!=p2。

 


准备工作

1、VS2012使用命令行选项查看对象的内存布局

微软的Visual Studio提供给用户显示C++对象在内存中的布局的选项:/d1reportSingleClassLayout。使用方法很简单,直接在[工具(T)]选项下找到“Visual Studio命令提示(C)”后点击即可。切换到cpp文件所在目录下输入如下的命令即可

      c1 [filename].cpp /d1reportSingleClassLayout[className]

其中[filename].cpp就是我们想要查看的class所在的cpp文件,[className]指我们想要查看的class的类名。(下面举例说明...)

2、查看普通多继承子类的内存布局

既然我们今天讲的是虚基类和虚继承,我们就先用上面介绍的命令提示工具查看一下普通多继承子类的内存布局,可以跟后文虚继承子类的内存布局情况加以比较。

我们新建一个名叫NormalInheritance的cpp文件,输入一下内容。
 

/**
	普通继承(没有使用虚基类)
*/
 
// 基类A
class A
{
public:
	int dataA;
};
 
class B : public A
{
public:
	int dataB;
};
 
class C : public A
{
public:
	int dataC;
};
 
class D : public B, public C
{
public:
	int dataD;
};

上面是一个简单的多继承例子,我们启动Visual Studio命令提示功能,切换到NormalInheritance.cpp文件所在目录,输入一下命令:
c1  NormalInheritance.cpp /d1reportSingleClassLayoutD

我们可以看到class D的内存布局如下:

从类D的内存布局可以看到A派生出B和C,B和C中分别包含A的成员。再由B和C派生出D,此时D包含了B和C的成员。这样D中就总共出现了2个A成员。大家注意到左边的几个数字,这几个数字表明了D中各成员在D中排列的起始地址,D中的五个成员变量(B::dataA、dataB、C::dataA、dataC、dataD)各占用4个字节,sizeof(D) = 20。

为了跟后文加以比较,我们再来看看B和C的内存布局:

虚继承的内存分布情况

上面我们看到了普通多继承子类的内存分布情况,下面我们进入主题,来看看典型的菱形虚继承子类的内存分布情况。

我们新建一个名叫VirtualInheritance的cpp文件,输入一下内容:

/**
	虚继承(虚基类)
*/
 
#include <iostream>
 
// 基类A
class A
{
public:
	int dataA;
};
 
class B : virtual public A
{
public:
	int dataB;
};
 
class C : virtual public A
{
public:
	int dataC;
};
 
class D : public B, public C
{
public:
	int dataD;
};

VirtualInheritance.cpp和NormalInheritance.cpp的不同点在与C和C继承A时使用了virtual关键字,也就是虚继承。同样,我们看看B、C、D类的内存布局情况:

我们可以看到,菱形继承体系中的子类在内存布局上和普通多继承体系中的子类类有很大的不一样。对于类B和C,sizeof的值变成了12,除了包含类A的成员变量dataA外还多了一个指针vbptr,类D除了继承B、C各自的成员变量dataB、dataA和自己的成员变量外,还有两个分别属于B、C的指针。

那么类D对象的内存布局就变成如下的样子:

vbptr:继承自父类B中的指针

int dataB:继承自父类B的成员变量

vbptr:继承自父类C的指针

int dataC:继承自父类C的成员变量

int dataD:D自己的成员变量

int A:继承自父类A的成员变量

显然,虚继承之所以能够实现在多重派生子类中只保存一份共有基类的拷贝,关键在于vbptr指针。那vbptr到底指的是什么?又是如何实现虚继承的呢?其实上面的类D内存布局图中已经给出答案:
 

实际上,vbptr指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚表(virtual table),虚表中记录了vbptr与本类的偏移地址;第二项是vbptr到共有基类元素之间的偏移量。在这个例子中,类B中的vbptr指向了虚表D::$vbtable@B@,虚表表明公共基类A的成员变量dataA距离类B开始处的位移为20,这样就找到了成员变量dataA,而虚继承也不用像普通多继承那样维持着公共基类的两份同样的拷贝,节省了存储空间。

为了进一步确定上面的想法是否正确,我们可以写一个简单的程序加以验证:
 

int main()
{
	D* d = new D;
	d->dataA = 10;
	d->dataB = 100;
	d->dataC = 1000;
	d->dataD = 10000;
 
	B* b = d; // 转化为基类B
	C* c = d; // 转化为基类C
	A* fromB = (B*) d;
	A* fromC = (C*) d;
 
	std::cout << "d address    : " << d << std::endl;
	std::cout << "b address    : " << b << std::endl;
	std::cout << "c address    : " << c << std::endl;
	std::cout << "fromB address: " << fromB << std::endl;
	std::cout << "fromC address: " << fromC << std::endl;
	std::cout << std::endl;
 
	std::cout << "vbptr address: " << (int*)d << std::endl;
	std::cout << "    [0] => " << *(int*)(*(int*)d) << std::endl;
	std::cout << "    [1] => " << *(((int*)(*(int*)d)) + 1)<< std::endl; // 偏移量20
	std::cout << "dataB value  : " << *((int*)d + 1) << std::endl;
	std::cout << "vbptr address: " << ((int*)d + 2) << std::endl;
	std::cout << "    [0] => " << *(int*)(*((int*)d + 2)) << std::endl;
	std::cout << "    [1] => " << *((int*)(*((int*)d + 2)) + 1) << std::endl; // 偏移量12
	std::cout << "dataC value  : " << *((int*)d + 3) << std::endl;
	std::cout << "dataD value  : " << *((int*)d + 4) << std::endl;
	std::cout << "dataA value  : " << *((int*)d + 5) << std::endl;
}

 

得到结果为:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值