探究类的内存分布

探究类的内存分布

标签(空格分隔): 虚函数表、虚表指针、类的内存分布、继承、多态

1、普通类的内存分布

C++示例:

#include <iostream>
#include <Windows.h>
using namespace std;
class Father {
public:
	Father() {}
	~Father() {}
	void func() { cout << "Father::func" << endl; } //普通方法
private:
	int a = 1;   //普通成员变量
	static int b;//静态数据成员

};
int Father::b = 2; //静态数据成员的初始化

int main(void) {
	Father father;
	system("pause");
	return 0;
}

右击源文件属性,先选择左侧的C/C+±>命令行,然后在其他选项这里写上/d1 reportAllClassLayout,它可以看到所有相关类的内存布局,如果写上/d1 reportSingleClassLayoutXXX(XXX为类名),则只会打出指定类XXX的内存布局。如下:

Father类的内存分布如下:

从输出结果可以看出,只有类的普通数据成员才会占据对象内存空间,类的普通成员函数是不占用对象内存空间的,其中 class Father size(4) 代表类的对象所占用的内存空间为4字节。静态数据成员还有普通成员函数属于类所有,并不属于对象,更不会单独为他分配内存空间。

2、含虚函数的类的内存分布情况

在上面代码的基础上,将func添加virtuel声明,此函数就被定义成虚函数

class Father {
public:
	Father() {}
	~Father() {}
	virtual void func() { cout << "Father::func" << endl; } //父类的虚函数
private:
	int a = 1;   //普通成员变量
	static int b;//静态数据成员

};

重新生成源文件,查看类的内存分布情况:

由输出结果可以看出,类的对象内存中存放的首先虚函数表指针vfptr,他就指向下面Father类的虚函数表 Father::func$vftable@,虚函数表中存放的是所有虚函数的地址,可以理解成函数指针,vfptr前面的0代表其在对象内存当中的起始偏移量,虚函数表是独立于对象内存空间的一块内存,不占用对象内存空间,由于我的电脑是64位处理器,DebugX64情况下要为虚表指针分配8字节的内存空间,可以看到对象的普通数据成员的a的起始偏移量为8,对象所占据总的内存空间大小为16字节,而其中a通过sizeof(a)测出大小只有4字节,这其中涉及到内存对齐的问题,具体不展开了。总之:含有虚函数的基类(注意这里的Father还没有派生子类)对象内存中存放依次是虚表指针、普通的数据成员

3、 派生类的内存分布(这里派生类还没有重写父类的虚方法)

在2的基础上增加如下代码:

class Son :public Father 
{
public:
	Son(){}
	~Son(){}
	void dispaly() { cout << "Son::display" << endl; } //子类的方法
private:
	int c = 3;
};

同样右键源文件->属性->c/c++ ->命令行中的其他选项添加如下代码:/d1 reportSingleClassLayoutSon ,重新生成源文件,观察生成的输出窗口。

由输出结果可以看出:子类继承了父类的虚表指针vfptr以及它的普通数据成员,由于没有重写父类的虚函数,所以子类完全继承父类的虚函数表,子类对象内存中存放的依次是父类的虚表指针、父类的普通数据成员、子类的普通数据成员,注意子类也可以自己定义虚函数,改变的只是子类虚函数表中的内容,这时表中存放的依次是父类的虚函数地址、子类自己的虚函数地址。

4、重写父类虚函数的子类(子类自己也有虚方法)的内存分布

修改的代码如下:

class Son :public Father 
{
public:
	Son(){}
	~Son(){}
	void func() { cout << "Son::func" << endl; } //子类重写父类的虚方法,这里func 加不加 virtual都行
	virtual void dispaly() { cout << "Son::display" << endl; } //display()改为虚方法
private:
	int c = 3;
};

重新生成源文件,查看输出结果:

可以看到子类的虚函数表发生了变化,子类虚函数表中首先存放的是重写的父类虚函数的地址,再者是自己定义的虚函数地址。可以这样理解:虚函数表在编译阶段就确定了,子类先是完整的继承了父类的虚函数表和普通数据成员,然后编译器会查看子类是否重写了父类的方法,假如重写了,则把虚函数表中父类的虚方法替换成子类重写的虚方法,然后再是子类自己定义的虚方法。

5、在继承关系中实现多态

通过定义基类的指针指向子类的对象,通过指针调用被子类重写过的虚方法,实现动态调用子类的方法。这里的动态指的是预编译阶段根本不知道通过P->func()调用的是子类的方法还是父类的方法,预编译阶段的工作只是对词法、语法分析,宏替换等,只有在程序运行时才能确定具体调用的子类还是父类的虚方法。代码如下:

#include <iostream>
#include <Windows.h>
using namespace std;
/************************************************
*  多态:定义基类的指针指向子类的对象,当调用重写
*  的虚函数就能动态调用子类的方法,实现多态。
*  本质:在构造子类对象时,子类的虚表指针指向的是
*  子类的虚函数表,假如子类重写了父类的方法,此时
*  子类的虚函数表中存放的是重写过后子类虚函数的地
*  址,当定义 Father * p=new Son();此时并没有改变
*  子类对象虚表指针的指向,仍是指向子类的虚函数表,
*  P->func() 自然调用的是子类的虚方法。
* ***********************************************/
class Father {
public:
	Father() {}
	~Father() {}
	virtual void func() { cout << "Father::func" << endl; } //虚函数
private:
	int a = 1;   //普通成员变量
	static int b;//静态数据成员

};
int Father::b = 2; //静态数据成员的初始化

class Son :public Father 
{
public:
	Son(){}
	~Son(){}
	void func() { cout << "Son::func" << endl; } //子类重写父类的虚方法,这里func 加不加 virtual都行
	virtual void dispaly() { cout << "Son::display" << endl; } //子类的方法
private:
	int c = 3;
};

int main(void) {
	Father* p = new Son(); //定义父类的指针指向子类的对象
	p->func(); //调用虚方法
	delete p;  
	p = NULL;
	system("pause");
	return 0;
}

运行结果:

程序输出结果符合预期,果然调用的是子类重写的虚方法。

6、多重继承关系中子类的内存分布情况

添加如下代码:

class Mother {
public:
	Mother(){}
	~Mother(){}
	virtual void test() { cout << "Mother::test" << endl; } //虚方法
private:
	int m = 5;

};
class Son :public Father,Mother //继承Father类,同时继承Mother类
{
public:
	Son(){}
	~Son(){}
	void func() { cout << "Son::func" << endl; } //子类重写父类的虚方法,这里func 加不加 virtual都行
	virtual void dispaly() { cout << "Son::display" << endl; } //子类的方法
private:
	int c = 3;
};

添加了基类Mother类,其中由虚方法test和普通成员变量m,将Son类的继承属性改为class Son :public Father,Mother,其他可保持不变。
子类的内存分布如下:

从图中我们可以清晰看到:子类对象内存中先是存放Father类的虚表指针、成员变量a;再存放的是Mother类的虚表指针、成员变量M;最后再是自己类的数据成员c;至于为什么先是存放Father类的数据,再是Mother类的数据,子类的虚函数表也是一样,这与开始Son类的继承顺序有关,读者可以自行试试。

7、菱形继承下子类的内存分布

类的继承关系如下,箭头指向那个的类为父类,如下图所示关系:

相应的代码:

#include <iostream>
#include <Windows.h>
using namespace std;
class Human {
public:
	Human() {}
	~Human(){}
	virtual  void sex() { cout << "Human::sex" << endl; }
private:
	int h = 1;
	

};
class Father :  public Human
{
public:
	Father() {}
	~Father() {}
	virtual void func() { cout << "Father::func" << endl; } //虚函数
private:
	int a = 1;   //普通成员变量
	

};

class Mother:  public Human
{
public:
	Mother(){}
	~Mother(){}
	virtual void test() { cout << "Mother::test" << endl; } //虚方法
private:
	int m = 5;

};
class Son :public Father,Mother //继承Father类,同时继承Mother类
{
public:
	Son(){}
	~Son(){}
	void func() { cout << "Son::func" << endl; } //子类重写父类的虚方法,这里func 加不加 virtual都行
	virtual void dispaly() { cout << "Son::display" << endl; } //子类的方法
private:
	int c = 3;
};

int main(void) {

	system("pause");
	return 0;
}

Human类内存分布:其中vfptr为基类虚表指针

Father类的内存分布:Father继承Human类的虚表指针,Mother类的内存分布类似

Son子类的内存分布:

显然,在菱形继承的情况下,子类对象内存中有两份基类的虚表指针和数据成员h以及虚函数表,这样会增加子类对象创建时所需分配的内存,其次菱形继承还会带来二义性,可以参考 菱形继承带来的二义性(转载) 的链接。
解决办法:把Father\Mother类继承Human类的属性改为虚继承就可以

class Father : virtual public Human
{
public:
	Father() {}
	~Father() {}
	virtual void func() { cout << "Father::func" << endl; } //虚函数
private:
	int a = 1;   //普通成员变量
	

};

class Mother: virtual public Human
{
public:
	Mother(){}
	~Mother(){}
	virtual void test() { cout << "Mother::test" << endl; } //虚方法
private:
	int m = 5;

};
class Son :public Father,Mother //继承Father类,同时继承Mother类
{
public:
	Son(){}
	~Son(){}
	void func() { cout << "Son::func" << endl; } //子类重写父类的虚方法,这里func 加不加 virtual都行
	virtual void dispaly() { cout << "Son::display" << endl; } //子类的方法
private:
	int c = 3;
};

总结:
1、只有普通的数据成员占用对象的内存空间,静态数据成员、普通成员函数(包括常成员函数)不占据对象的内存空间。
2、在含有虚函数的类中,对象内存中存放的是依次是虚表指针、普通的数据成员,虚表指针指向类的虚函数表,虚函数表中存放的是虚函数的地址。
3、在继承关系中,当基类含有虚函数,在构造子类的对象时,子类对象先是完整地继承父类的虚表指针和虚函数表,假如子类重写了基类的虚函数,则子类的虚函数表中的内容要发生改变,把基类虚函数的地址替换为被子类重写的虚函数地址。通过定义基类指针指向子类的对象,就可以动态调用子类的方法,从而实现多态。
4、虚继承解决菱形继承带来的二义性问题。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值