C++类、结构对象「内存布局/内存结构」浅析

4.50 avg. rating (91% score) - 2 votes
转载请注明: 吹水小镇 | reetsee.com
原文链接地址: http://blog.reetsee.com/archives/207

最近面试多,出的题目很多都有如下形式,给定一个class或者struct的定义,例如这样:

struct node {
	int a;
	char b;
	int c;
	char d;
};

问题是:sizeof(node) = ?

之前了解过对齐的概念,但是不深入,所以在这里自己做了一些小测试,说一下自己的看法。先告诉大家吧,上面那题答案是16。

如果用“对齐”的说法,那么就是先找到这个struct中占内存最大的一个类型——“int”,然后简单地进行“变量个数 * sizeof(int) = 4 * 4 = 16”即可。但是这样的做法其实有点问题,下面会继续探讨。

先上一段代码(可以自己先想一下这段代码的输出是什么?):

#include<iostream>
#include<stdio.h>
using namespace std;
struct node {
	int a;
	char b;
	int c;
	char d;
	node(){a=1,b=2,c=3,d=4;}
};
int main()
{
	node n;
	printf("a:%d b:%d c:%d d:%d\n", n.a, n.b, n.c, n.d);
	
	void *p = &n;
	*((int*)p) = 12345;
	cout<<n.a<<endl;

	p = ((int*)p) + 1;
	*((char*)p) = 107;
	cout<<(int)n.b<<endl;
    
	p = ((char*)p) + 1;
	*((int*)p) = 0x80000000;
	cout<<(unsigned int)n.c<<endl;
	cout<<(int)n.d<<endl;

	cout<<"size:"<<sizeof(node)<<endl;
	int stop;
	cin>>stop;    
}

代码输出:

a:1 b:2 c:3 d:4
12345
107
128
4
size:16

解析如下(以下所有分析仅为本人推测所得,实际是否如此请参见更权威的资料):

void *p = &n这句,使得p指针指向了n的起始地址,然后我们将void*类型的指针转换为int*类型,并对其赋值12345,然后发现a变成了12345,说明a的首地址和n的首地址是相同的。

接着我们让指针p移动一个int的距离,向高地址方向移动,如果猜测没错,现在应该移动到了b的首地址,那么赋值操作将会使b的值变为107,输出也验证了这个猜测。

接下来就是重点了,我们让p移动一个char大小的距离(即1个字节),如果struct内的成员变量是密集排布的,那么现在应该移动到了c的位置,赋值0x80000000,换成二进制就是1000 0000 0000 0000 0000 0000 0000 0000,即便int的最小值-2147483648。然后输出c的值时发现其等于128。

什么时候int的值是128?只有其二进制为0000 0000 0000 0000 0000 0000 1000 0000的时候。红色字的部分就是被改变的部分,也就是说只有低地址的一个字节(8bit)被赋值了,另外的24bit内容丢失了吗?结合之前所说的“对齐”问题,那么就可以大胆猜测其实b和c之间隔了3个字节,所以进行赋值的时候,低位的24个bit作用在变量b后面的3个字节中,而高位的1000 0000作用在变量c的低位第一个字节上。

如果用一张图去形象地描述就如下:

内存布局

其中一个方格(不是很方……)代表一个字节,红色的部分是a占用的内存空间,绿色是b,蓝色是c,黄色是d,而带斜线的部分则是被0x80000000赋值的内存区域。

好了,现在给出另外一个问题,如果struct是这样的:

struct node {
	int a;
	char b;
	char d;
	int c;
};

 

那么问sizeof(node) = ?

 给个提示,现在的话,内存占用示意图会变成这样:

内存结构2

那么结果各位应该也知道了。好了,一些拓展问题,上面这个struct的内存是在栈中分配的,如果是用new运算符,使其在堆上面分配,那么结果是什么?各位可以亲手试一下,直接把答案全部说出来,就有点没有意思了,哈哈大笑

好啦,其实到这里,文章还没完,要再进一步讲一些

下面要讨论类,即class。

按照惯例,继续先上代码,请猜猜下面各个类其sizeof的返回值是什么:

#include<iostream>
#include<stdio.h>
using namespace std;
class TwoIntNoVirtual {
	int a;
	int b;
	void function() {
		cout<<"Hello"<<endl;
	}
};
class TwoIntOneVirtual {
	int a;
	int b;
	virtual void function() {

	}
};
class TwoIntTwoVirtual {
	int a;
	int b;
	virtual void function() {
	}
	virtual void function2() {
	}
};
class TwoIntOneVirtualDerived : public TwoIntOneVirtual {
	void function2() {
	}
};
class TwoIntTwoVirtualDerived : public TwoIntTwoVirtual {
	void function() {
	}
	void function2() {
	}
};
class OtherTest {
	char e,f,g,h,i,j,k;
	int a;
	int b;
	int c;
	long long d;
	virtual void function() {}
	virtual void function2() {}
};
int main()
{
	cout<<"TwoIntNoVirtual: "<<sizeof(TwoIntNoVirtual)<<endl;
	cout<<"TwoIntOneVirtual: "<<sizeof(TwoIntOneVirtual)<<endl;
	cout<<"TwoIntTwoVirtual: "<<sizeof(TwoIntTwoVirtual)<<endl;
	cout<<"TwoIntOneVirtualDerived: "<<sizeof(TwoIntOneVirtualDerived)<<endl;
	cout<<"TwoIntTwoVirtualDerived: "<<sizeof(TwoIntTwoVirtualDerived)<<endl;
	cout<<"OtherTest: "<<sizeof(OtherTest)<<endl;
	cout<<"Long long: "<<sizeof(long long)<<endl;

	int stop;
	cin>>stop;    
}

 

其实从上面讲完struct,现在就是多加了两点:1 函数对size有什么影响? 2 虚函数对size有什么影响?

首先,对于32位的机器,指针的size是4个字节,其次虚函数有虚表的概念,神马是虚表?可以google一下,学C++一定要知道的基础知识啊。

这个程序的输出如下:

TwoIntNoVirtual: 8
TwoIntOneVirtual: 12
TwoIntTwoVirtual: 12
TwoIntOneVirtualDerived: 12
TwoIntTwoVirtualDerived: 12
OtherTest: 32
Long long: 8

成员变量对内存的占用和struct一样,普通的函数不会影响size,但是虚函数会使得size至少增加4,因为指向虚表的指针占用4个字节,然而要注意,1个虚函数或者n个虚函数对这个类取size是得到相同的结果(为什么?请参见拓展阅读1)。更多实验结果,各位要亲自动手试一试,最后再增加一个结论,注意以下两个struct(得到的size是相同的,都为16,感谢旧博客中一楼指出原本的错误!)

struct node1 {
	int a;
	char b, c, d, e;
	long long f;
};

struct node2 {
	int a;
	int b;
	long long c;
};

但是,下面这个struct,得到的size和上面是不同的(结果为24):

class node0 {
	int a;
	char b;
	short c;
	char d;

	long long f;
};

虽然a,b,c,d加起来是8个字节的宽度,但是其中d会被拥到“下一行”,可以用以下这段程序检验(同时发现了你可以在类的外部访问这个类的private成员了,神奇的指针啊):

#include<iostream>
using namespace std;
class node0 {
	int a;
	char b;
	short c;
	char d;

	long long f;
	public:
	node0(): a(1), b(5), c(10), d(15), f(20) {}
};
struct node1 {
	int a;
	char b;
        char c;
        char d;
        char e;
	long long f;
};

struct node2 {
	int a;
	int b;
	long long c;
};
int main()
{
    cout<<sizeof(node0)<<endl;
    cout<<sizeof(node1)<<endl;
    cout<<sizeof(node2)<<endl;
    
    node0 n0;
    void *p = &n0;
    p = ((long long*)p) + 1;
    cout<<(int)(*((char*)p))<<endl;
    
    int stop;
    cin>>stop;
}

输出结果是:

24

16

16

15

 

到底对齐要怎么对?经过查百度百科后(搜索“内存对齐”),发现其中有一条:结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节

这篇文章很多都是个人通过实验的猜想,各位务必亲测一下或者做进一步的研究,免得误导大家那就遗臭万年了!如果纰漏,欢迎指出,多谢!

相信上面的内容,对于我来说,应该能应付大部分sizeof题目,后来看了一些资料,发现有牛人已经对这个问题作过文,这里附上拓展阅读的链接:

拓展阅读1:C++虚函数表解释

拓展阅读2:C++对象的内存布局(上)

拓展阅读3:C++对象的内存布局(下)

 

 

3 条思考于 “C++类、结构对象「内存布局/内存结构」浅析

发表评论

电子邮件地址不会被公开。 必填项已用*标注