探究类的内存分布
标签(空格分隔): 虚函数表、虚表指针、类的内存分布、继承、多态
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、虚继承解决菱形继承带来的二义性问题。