C++类对象的内存布局
1、C++类对象的内存布局
在C++的类对象中,有两种类的成员变量:static和⾮static,有三种成员函数:static、⾮static和virtual。那么,它们在C++的内存中是如何分布的呢?
C++程序的内存格局通常分为四个区:全局数据区(data area),代码区(code area),栈区(stack area),堆区(heap area)(即⾃由存储区)。全局数据区存放全局变量,静态数据和常量。所有类成员函数和⾮成员函数代码存放在代码区;为运⾏函数⽽分配的局部变量、函数参数、返回数据、返回地址等存放在栈区;余下的空间都被称为堆区。
在类的定义时,
类的成员函数被放在代码区。
类的静态成员变量在全局数据区。
⾮静态成员变量在类的实例内,实例在栈区或者堆区。
虚函数指针、虚基类指针在类的实例内,实例在栈区或者堆区。
类的实例如果是定义的类变量,则在栈内存区,如果是new出来的类指针,则在堆内存区,同时引⽤会保存在栈⾥。
为何这样设计?其实这是从c语⾔发展⽽来的。类的成员变量相当于c的结构体,类的成员函数类似于c的函数,类的静态变量类似于c的静态或全局变量,⾄于虚函数,函数体还是放在代码区,但虚函数的指针和成员变量⼀起放在数据区,这是因为虚函数的函数体有多个,不同的⼦类调⽤同⼀虚函数实则调⽤的不同函数体,因此需要在类的数据区保持真正的虚函数的指针。
类的成员函数为什么不需要在类的数据区保持指针?因为类的成员函数是唯⼀的,在编译时,编译器会为每个类的成员函数改头换⾯,如函数名加上类名,参数加上this类指针。这样类的成员函数和c的普通函数就⼀样了。虚函数由于其多态的特殊性,⽆法这样处理,所以需要保持在类的数据区。
下⾯就⼀个⾮常简单的类,通过逐渐向其中加⼊各种成员,来逐⼀分析上述两种成员变量及三种成员函数对类的对象的内存分布的影响。
注:以下的代码的测试结果均是基于Ubuntu 14.04 64位系统下的G++ 4.8.2,若在其他的系统上或使⽤其他的编译器,可能会运⾏出不同的结果。
2、含有⾮static成员变量及成员函数的类的对象的内存分布
类Persion的定义如下:
class Person
{
public:
Person():mId(0), mAge(20){}
void print()
{
cout << "id: " << mId
<< ", age: " << mAge << endl;
}
private:
int mId;
int mAge;
};
Person类包含两个⾮static的int型的成员变量,⼀个构造函数和⼀个⾮static成员函数。为弄清楚该类的对象的内存分布,对该类的对象进⾏⼀些操作如下:
int main()
{
Person p1;
cout << "sizeof(p1) == " << sizeof(p1) << endl;
int *p = (int*)&p1;
cout << "p.id == " << *p << ", address: "  << p << endl;
++p;
static修饰的变量
cout << "p.age == " << *p << ", address: " << p << endl;
cout << endl;
Person p2;
cout << "sizeof(p2) == " << sizeof(p1) << endl;
p = (int*)&p2;
cout << "p.id == " << *p << ", address: " << p << endl;
++p;
cout << "p.age == " << *p << ", address: " << p << endl;
return 0;
}
其运⾏结果如下:
从上图可以看到类的对象的占⽤的内存均为8字节,使⽤普通的int*指针可以遍历输出对象内的⾮static成员变量的值,且两个对象中的相同的⾮static成员变量的地址各不相同。
据此,可以得出结论,在C++中,⾮static成员变量被放置于每⼀个类对象中,⾮static成员函数放在类的对象之外,且⾮static成员变量在内存中的存放顺序与其在类内的声明顺序⼀致。即person对象的内存分布如下图所⽰:
3、含有static和⾮static成员变量和成员函数的类的对象的内存分布
向Person类中加⼊⼀个static成员变量和⼀个static成员函数,如下:
class Person
{
public:
Person():mId(0), mAge(20){ ++sCount; }
~Person(){ --sCount; }
void print()
{
cout << "id: " << mId
<< ", age: " << mAge << endl;
}
static int personCount()
{
return sCount;
}
private:
static int sCount;
int mId;
int mAge;
};
测试代码不变,与第1节中的代码相同。其运⾏结果不变,与第1节中的运⾏结果相同。 据此,可以得出:static成员变量存放在类的对象之外,static成员函数也放在类的对象之外。
其内存分布如下图所⽰:
4、加⼊virtual成员函数的类的对象的内存分布
在Person类中加⼊⼀个virtual函数,并把前⾯的print函数修改为虚函数,如下:
class Person
{
public:
Person():mId(0), mAge(20){ ++sCount; }
static int personCount()
{
return sCount;
}
virtual void print()
{
cout << "id: " << mId
<< ", age: " << mAge << endl;
}
virtual void job()
{
cout << "Person" << endl;
}
virtual ~Person()
{
--sCount;
cout << "~Person" << endl;
}
protected:
static int sCount;
int mId;
int mAge;
};
为了查看类的对象的内存分布,对类的对象执⾏如下的操作代码,如下:
int main()
{
Person person;
cout << sizeof(person) << endl;
int *p = (int*)&person;
for (int i = 0; i < sizeof(person) / sizeof(int); ++i, ++p)
{
cout << *p << endl;
}
return 0;
}
其运⾏结果如下:
从上图可以看出,加virtual成员函数后,类的对象的⼤⼩为16字节,增加了8。通过int*指针遍历该对象的内存,可以看到,最后两⾏显⽰的是成员数据的值。
C++中的虚函数是通过虚函数表(vtbl)来实现,每⼀个类为每⼀个virtual函数产⽣⼀个指针,放在表格中,这个表格就是虚函数表。每⼀个类对象会被安插⼀个指针(vptr),指向该类的虚函数表。vptr的设定和重置都由每⼀个类的构造函数、析构函数和复制赋值运算符⾃动完成。
由于本⼈的系统是64位的系统,⼀个指针的⼤⼩为8字节,所以可以推出,在本⼈的环境中,类的对象的安插的vptr放在该对象所占内存的最前⾯。其内存分布图如下:
注:虚函数的顺序是按虚函数定义顺序定义的,但是它还包含其他的⼀些字段,本⼈还未明⽩它是什么,在下⼀节会详细说明虚函数表的内容。
5、虚函数表(vtbl)的内容及函数指针存放顺序
在第3节中,我们可以知道了指向虚函数表的指针(vptr)在类中的位置了,⽽函数表中的数据都是函数指针,于是便可利⽤这点来遍历虚函数表,并测试出虚函数表中的内容。
测试代码如下:
typedef void (*FuncPtr)();
int main()
{
Person person;
int **vtbl = (int**)*(int**)&person;
for (int i = 0; i < 3 && *vtbl != NULL; ++i)
{
FuncPtr func = (FuncPtr)*vtbl;
func();
++vtbl;
}
while (*vtbl)
{
cout << "*vtbl == " << *vtbl << endl;
++vtbl;
}
return 0;
}
代码解释:
由于虚函数表位于对象的⾸位置上,且虚函数表保存的是函数的指针,若把虚函数表当作⼀个数组,
则要指向该数组需要⼀个双指针。我们可以通过如下⽅式获取Person类的对象的地址,并转化成int**指针:
Person person;
int **p = (int**)&person;
再通过如下的表达式,获取虚函数表的地址:
int **vtbl = (int**)*p;

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。