C++对象内存布局之继承

转载请注明出处http://blog.csdn.net/a827461712/article/details/22726449

感谢在博客写作过程中韩朋程和罗国佳的宝贵意见!!

总述

继承,是C++语言程序设计中重要的组成部分。C++对象模型,简单的理解可以说就是C++中一个对象的内存布局。下面就总结一下类继承对C++对象的内存布局产生的影响。

首先,请记住如下规则(以下规则针对的都是VC编译器):

1、  对于一般的类继承。如果基类和派生类都具有虚函数,在派生类对象的内存布局中,所有的虚函数都存放在一张虚函数表中(多重继承时会有多个虚函数表)。即,派生类中,从基类继承的虚函数与自身新定义的虚函数共用一张虚函数表 

2、  对于虚继承。如果基类和派生类都具有虚函数,在派生类对象的内存布局中,基类和派生类的虚函数存在于不同的虚函数表中。即,派生类中,从基类继承的虚函数在一张虚函数表中,派生类自身新定义的虚函数在另一张虚函数表中。为了支持虚继承,VC编译器在对象模型中加入了一个虚基类指针,虚基类指针在任何情况下都不会共享

3、派生类对象的内存布局中要保证基类子对象空间的完整性

下面对这三条规则进行详细说明。

1.一般继承、不带虚函数

         一般来说,不带虚函数的类继承是没有什么实质性意义的。但是,为了更好的说明本文的主题。对这种情况进行简单的描述。

假如有如下两个类:

class Base
{
	int n1;
};
class Derived:public Base
{
	int n2;
};

定义一个派生类对象b,b的内存布局如下图所示。其中,n1和n2各占4B:


上文举例使用的是单一继承,对于多重继承,内存布局并没有大的改变。还是先放基类成员再放派生类成员,基类成员的排列顺序按照定义派生类时基类的排列顺序来定。

完整代码如下:

#include <iostream>
using namespace std;

class Base
{
public:
	Base(const int n = 3):n1(n){}
	int n1;
};

class Derived:public Base
{
public:
	Derived(const int nlr = 2):n2(nlr){}
	int n2;
};

int main()
{
	cout<<"********Write by a827461712*********"<<endl;
	Derived d;
	cout<<"Size of d is:"<<sizeof(d)<<endl;
	int nTest1 = (int)*((int*)&d);
	cout<<nTest1<<endl;
	int nTest2 = (int)*((int*)&d+1);
	cout<<nTest2<<endl;
	return 0;
}

运行结果如下:


2. 一般继承、带虚函数

对于带虚函数的类继承,单一继承与多重继承存在细小的差别,此处分为单一继承和多重继承两种情况来讲解。
单一继承
单一继承的情况比较简单,类对象中有一个虚函数表指针vptr,该指针指向一个函数指针数组,该数组即为虚函数表。一个类所有的虚函数的函数地址根据虚函数的声明次序存放在虚函数表中(与访问属性public、protected、private无关)。派生类与基类共用一个虚函数表,如果派生类定义了自己的虚函数,这些虚函数放在虚函数表中基类虚函数的后面。如果派生类重写了基类中的某一虚函数,那么在派生类的虚函数表中将会用派生类的虚函数地址覆盖相应的函数指针。具体分析见下面的例子:
假如有下面代码中的两个类:
class Base
{
public:
	Base(const int n = 3):n1(n){}
	~Base(){}
	virtual void fun(){cout<<"Base fun called"<<endl;}
	int n1;
};

class Derived:public Base
{
public:
	Derived(const int nlr = 2):n2(nlr){}
	~Derived(){}
	virtual void myfun(){cout<<"Derived fun called"<<endl;}
	int n2;
};
根据代码可知,定义一个派生类对象d,此时d的内存布局如下图所示:


对象d的内存布局中,首先是虚函数表指针vptr。虚函数表中有三个成员,fun是基类的虚函数,myfun是派生类的虚函数,第三个成员用于表示虚函数表的终止。功能类似于C语言中字符数组的最后一个字节。如果派生类中重写了基类中的fun函数,那么此时虚函数表中的第一个个元素会被派生类中的fun覆盖。虚函数表的出现,对类的安全性也产生了危险,因为如果直接用虚函数地址去调用函数,那么函数定义中给虚函数限定的访问属性完全不起作用。也就是说,可以通过指针访问一个private类型或protected类型的乘以函数。
完整代码如下:
#include <iostream>
using namespace std;

class Base
{
public:
	Base(const int n = 3):n1(n){}
	~Base(){}
	virtual void fun(){cout<<"Base fun called"<<endl;}
	int n1;
};

class Derived:public Base
{
public:
	Derived(const int nlr = 2):n2(nlr){}
	~Derived(){}
	virtual void myfun(){cout<<"Derived fun called"<<endl;}
	int n2;
};

typedef void(*Fun)(void);

int main()
{
	cout<<"********Write by a827461712*********"<<endl;
	Derived d;
	cout<<"Size of d is:"<<sizeof(d)<<endl;
	Fun fun = NULL;
	fun = (Fun)*((int*)*(int*)(&d));
	fun();
	fun = (Fun)*((int*)*(int*)(&d)+1);
	fun();
	int nTest1 = (int)*((int*)&d+1);
	cout<<nTest1<<endl;
	int nTest2 = (int)*((int*)&d+2);
	cout<<nTest2<<endl;
	return 0;
}

运行结果如图,从下图的打印信息可以印证上文中给的对象d的内存布局是正确的:



多重继承
多重继承的情况比单一继承稍微复杂一点,如果每一个基类都具有虚函数,那么派生类对象的内存布局中会有多个虚函数表指针。派生类中新定义的虚函数将放置在第一个派生基类子对象的虚函数表末尾。如果派生类中重写了基类某一虚函数,那将会覆盖虚函数表中相应的函数指针。派生基类子对象在派生类对象中的布局根据派生类定义时派生列表中类的顺序确定。
假设有如下的三个类,派生类Derived派生自基类Base1和Base2:
class Base1
{
public:
	Base1(const int n = 3):nBase1(n){}
	~Base1(){}
	virtual void fun(){cout<<"Base1 fun called"<<endl;}
	int nBase1;
};
class Base2
{
public:
	Base2(const int n = 3):nBase2(n){}
	~Base2(){}
	virtual void fun(){cout<<"Base2 fun called"<<endl;}
	int nBase2;
};
class Derived:public Base1,public Base2
{
public:
	Derived(const int nlr = 2):n(nlr){}
	~Derived(){}
	virtual void myfun(){cout<<"Derived fun called"<<endl;}
	int n;
};
定义一个派生类对象,其内存布局如图所示。由于派生类Derived定义时,Base1位置在Base2前面,因此,派生类对象内存中,Base1子对象在Base2子对象之前:

此时,如果派生类中重写函数fun,那么两个虚函数表中的fun项都会被派生类中的fun覆盖。
完成代码如下:
#include <iostream>
using namespace std;

class Base1
{
public:
	Base1(const int n = 3):nBase1(n){}
	~Base1(){}
	virtual void fun(){cout<<"Base1 fun called"<<endl;}
	int nBase1;
};
class Base2
{
public:
	Base2(const int n = 4):nBase2(n){}
	~Base2(){}
	virtual void fun(){cout<<"Base2 fun called"<<endl;}
	int nBase2;
};
class Derived:public Base1,public Base2
{
public:
	Derived(const int nlr = 2):n(nlr){}
	~Derived(){}
	virtual void myfun(){cout<<"Derived fun called"<<endl;}
	int n;
};

typedef void(*Fun)(void);

int main()
{
	cout<<"********Write by a827461712*********"<<endl;
	Derived d;
	cout<<"Size of d is:"<<sizeof(d)<<endl;
	Fun fun = NULL;
	fun = (Fun)*((int*)*(int*)(&d));//Base1::fun()
	fun();
	fun = (Fun)*((int*)*(int*)(&d)+1);//Derived::myfun()
	fun();
	int nTest1 = (int)*((int*)&d+1);//Base1::nBase1
	cout<<nTest1<<endl;
	fun = (Fun)*((int*)*((int*)(&d)+2));//Base2::fun()
	fun();
	int nTest2 = (int)*((int*)&d+3);//Base2::nBase2
	cout<<nTest2<<endl;
	int nTest3 = (int)*((int*)&d+4);//Derived::n
	cout<<nTest3<<endl;
	return 0;
}
运行结果如图所示,下图中打印的信息与上文中给出的内存布局完全符合:

3. 虚继承

虚继承是为了在类似于“菱形继承”的情况下,在派生类中只保留基类的一份副本。请记住另一条规则“当碰到虚继承的时候,任何规则都变得不一样!”。为了实现虚继承,编译器需要在派生类中加入一个虚基类指针。而且,如果虚基类和派生类都定义有各自的虚函数,此时,派生类和虚基类中的虚函数并不会共享一个虚函数表。这与一般继承时有巨大区别。

此处以最典型的“菱形继承”为例进行讲解。

假设有如下4个类,Left类和Right类虚继承自基类Base,派生类Derived又多重继承自Left和Right。

class Base
{
public:
	Base(const int n = 3):nBase(n){}
	~Base(){}
	virtual void fun(){cout<<"Base fun called"<<endl;}
	int nBase;
};
class Left:virtual public Base
{
public:
	Left(const int n = 4):nLeft(n){}
	~Left(){}
	virtual void leftfun(){cout<<"Left fun called"<<endl;}
	int nLeft;
};
class Right:virtual public Base
{
public:
	Right(const int n = 5):nRight(n){}
	~Right(){}
	virtual void rightfun(){cout<<"Right fun called"<<endl;}
	int nRight;
};
class Derived:public Left,public Right
{
public:
	Derived(const int nlr = 2):n(nlr){}
	~Derived(){}
	virtual void myfun(){cout<<"Derived fun called"<<endl;}
	int n;
};

类继承关系如下图所示:


内存布局如图所示:


从这个内存布局图可知,在虚继承中,虚基类子对象放在对象的最低端。但是对于一般的继承,基类子对象都是放在派生类对象的开始处。上图中,前12字节是类Left的子对象,接下来12字节是Right的子对象,接下来4字节是Derived的空间,最后8字节虚基类子对象空间。

在虚继承中,以上文中的例子来说,假如类Left重写了虚基类中的fun函数。这时,Left中的fun函数会覆盖虚基类虚函数表中的fun函数。更重要的是,Left中重写的那个fun并不会出现在类Left的虚函数表中(此时Left中有两个虚函数表,一个放虚基类中的虚函数,另一个放派生类新定义的虚函数)。

完整代码如下:

#include <iostream>
using namespace std;

class Base
{
public:
	Base(const int n = 3):nBase(n){}
	~Base(){}
	virtual void fun(){cout<<"Base fun called"<<endl;}
	int nBase;
};
class Left:virtual public Base
{
public:
	Left(const int n = 4):nLeft(n){}
	~Left(){}
	virtual void leftfun(){cout<<"Left fun called"<<endl;}
	int nLeft;
};
class Right:virtual public Base
{
public:
	Right(const int n = 5):nRight(n){}
	~Right(){}
	virtual void rightfun(){cout<<"Right fun called"<<endl;}
	int nRight;
};
class Derived:public Left,public Right
{
public:
	Derived(const int nlr = 2):n(nlr){}
	~Derived(){}
	virtual void myfun(){cout<<"Derived fun called"<<endl;}
	int n;
};

typedef void(*Fun)(void);

int main()
{
	cout<<"********Write by a827461712*********"<<endl;
	Derived d;
	cout<<"Size of d is:"<<sizeof(d)<<endl;
	Fun fun = NULL;

	fun = (Fun)*((int*)*(int*)(&d));//Left::leftfun()   Left::vptr
	fun();
	fun = (Fun)*((int*)*(int*)(&d)+1);//Derived::myfun()
	fun();	
	int nTest1 = (int)*((int*)&d+2);//Left::nLeft
	cout<<nTest1<<endl;

	fun = (Fun)*((int*)*((int*)(&d)+3));//Right::rightfun()     Right::vptr 
	fun();
	int nTest2 = (int)*((int*)&d+5);//Right::nRight
	cout<<nTest2<<endl;
	int nTest3 = (int)*((int*)&d+6);//Derived::n
	cout<<nTest3<<endl;

	fun = (Fun)*((int*)*((int*)(&d)+7));//Base::fun()  Base::vptr
	fun();
	int nTest4 = (int)*((int*)&d+8);//Base::nBase
	cout<<nTest4<<endl;
	return 0;
}
运行结果如图所示:

4 GCC编译器实现的虚继承

GCC编译器对继承的实现与VC编译器不太一样。不同之处在于,无论是否是虚继承,派生类中,从基类继承的虚函数与派生类自身新定义的虚函数共享虚函数表。不会像VC编译器,虚继承时虚基类的虚函数表并不会与派生类虚函数表合并。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值