# 内存大小问题

有时候,我们在不同的编译环境,或者不同的机子上测试编译,会呈现不同的结果,于是我们会陷入疑问,内存的大小是谁分配的呢?

在系统中,系统对内存的识别是以 Byte(字节)为单位,每个字节由 8 位二进制数组成,即 8bit(比特,也称 “位” )。按照计算机的二进制方式,1Byte = 8bit;1KB = 1024Byte;1MB = 1024KB;1GB = 1024MB;1TB = 1024GB。

我们都知道 Byte(字节)的大小是固定的,但是,我们目前接触的都是以 Word(字长)、HalfWord(半字),而他的解释是:在电脑领域,对于某种特定的计算机设计而言,(英语:word)是用于表示其自然的数据单位的术语。在这个特定电脑中,字是其用来一次性处理事务的一个固定长度的位(bit)组。一个字的位数(即字长)是电脑系统结构中的一个重要特性。

来源:字 (计算机) - 维基百科

# 分配问题

我们之所以有可能出现编译的数据内存大小不一样,是因为在不同的处理器和编译器中它所呈现的结果是不一样的,直白的说就是所对应的 Word(字长)不一样,其实字长并非一个十分严格的概念,要从汇编语言的角度理解,就是指令集里面的运算和内存操作时操作数的长度,具体还是看一下关于计算机底层的东西。

在 32 位处理器中 32 位指的就是 CPU GPRs(General-Purpose Registers,通用寄存器)的数据宽度,当然 64 位 CPU 的数据宽度为 64 位,所以 32 位 CPU 的数据宽度指的是 32 位了。

64 位指令集就是运行 64 位数据的指令,也就是说处理器一次可以运行 64Bit 数据。这样一来 32 位处理器一次最多只能处理 32 位,也就是 4 个字节的数据,而 64 位处理器一次就能处理 64 位,即 8 个字节的数据。

按照上面总的来说:各种类型的存储大小与系统位数有关。

而至于为什么又跟编译器有关呢?

假设我们在 64 位处理器上运行 32 位的编译器来写代码的,这样编译器就只会默认我们的程序是在 32 位系统下运行,因为这编译器最多只能处理 32 位,多了它处理不来啊 [哭笑]

所以,一般严格来说,在进行编译测试后,想要告诉别人结果,也应该把你当时所测试的操作系统以及编译系统告诉别人,同样的例子也有:就像我们在下载一个程序的时候,你可能会看到程序文件名后面带有 x32 或者 x64 等一些标志性文本,这就是要告诉别人它支持的操作环境。

img

# 结构体分配的空间

在进行讨论之前先来看一下程序

/* 
 * 操作系统:基于 x64 的处理器
 * 编译环境:Dev-C++ V5.11 
 */
#include <stdio.h> 
#define SIZE		1
struct s {
    char a;
    short b;
    int c;
    double d;
    char* e;
    char f[SIZE];
//    double i;
};
struct s temp;
char *p = NULL;
int main(void)
{
	printf("char:%d,short:%d,int:%d,double:%d\n\n",sizeof(char),sizeof(short),sizeof(int),sizeof(double));
	printf("sizeof = %d\n",sizeof(char *));
	printf("sizeof = %d\n\n",sizeof(temp));
	
	printf("temp addr = %p\n",&temp);
	printf("a    addr = %p,%2d\n",&temp.a,sizeof(temp.a));
	printf("b    addr = %p,%2d\n",&temp.b,sizeof(temp.b));
	printf("c    addr = %p,%2d\n",&temp.c,sizeof(temp.c));
	printf("d    addr = %p,%2d\n",&temp.d,sizeof(temp.d));
	printf("e    addr = %p,%2d\n",&temp.e,sizeof(temp.e));
	printf("\nf    addr = %p\n",&temp.f);
	printf("f    sizeof = %d\n",sizeof(temp.f));
	
//	printf("\ni addr = %p,%d\n",&temp.i,sizeof(temp.i));
	
	return 0;
}

点击并拖拽以移动

我们定义了一个 struct s 的结构体,里面有不同数据类型的结构体成员 .a /.b /.c /.e /.f (成员 i 我们先屏蔽先,这个是关于后面对齐问题测试的);成员 f 为数组,特殊点我们把地址跟大小分开打印;指针我们不知道他的大小,也打印出来;最后我们打印一下输出看下结果:

img

先看第一行,我们打印出了 char /short /int /double 所对应的数据类型大小;

第二行我们是打印了指针的数据大小,大小为 8Byte(因为指针在实质上是一个内存地址,内存地址的长度跟 CPU 的寻址有关;在 32 位系统上, CPU 用 32 位表示一个内存地址,这样的系统上一个指针占据 4 个字节(32 / 8 = 4);在 64 位系统上, CPU 用 64 位表示一个内存地址,这样的系统上一个指针占据 8 个字节(64 / 8 = 8))

第三行是打印出结构体总的大小为 32

再到后面的,当前的结构体地址对应着第一个结构体成员地址,这个没问题;后面每个结构体成员所对应的数据类型大小也没问题;但是,当我们一加起来 1 + 2 + 4 + 8 + 8 + 1 = 24 ???跟打印出来的总大小不一样啊,这就关系到数据对齐了。

# 内存大小对齐原则

  1. 结构体变量的首地址能够被其最宽基本类型成员的大小所整除。
  2. 结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding)。即结构体成员的末地址减去结构体首地址(第一个结构体成员的首地址)得到的偏移量都要是对应成员大小的整数倍。
  3. 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在成员末尾加上填充字节。

看了以上的原则后,我们来继续分析一下上面的数据类型大小问题

实际上:结构体 s = a (1byte) + 空闲 (1byte) + b (2byte) + c (4byte) + d (8byte) + e (8byte) + f * SIZE (1byte *1) + 空闲 (7byte) = 32 (byte)。

解释一下:上面输出的结构体变量的首地址为 0x0407A20,对于第一个成员 a 的地址就是结构体的首地址,占用 1 个字节(符合第一个的原则);因为成员 a 只用了 1 个字节,而成员 b 地址要 2 的倍数,中间隔着一个字节,那么就需要把这个一个字节填充进去,为后面的成员 b 的地址制造出 2 的倍数的地址数);成员 b 自己的大小为 2byte,他所对的地址必须是 2 的倍数,那么在填充完那一个字节后,他的地址排下去就是 0x0407A22,对应上了 2 的倍数(即上面的第二点原则);成员 c 大小为 4,同样的他所对的地址就得要 4 的倍数,算一下前面一共占用的地址数:a (1byte) + 空闲 (1byte) + b (2byte) = 4 (byte),当前的地址就已经是 4 的倍数了,所以不需要填充;后面两个成员也一样;然后再到最后一个成员 f 的分析,因为这是最后一个成员了,这样就已经知道在整个结构体 s 中他的最宽基本类型成员大小是多小了,没错,是 8byte(double 类型或者指针的内存地址大小);那么按照第三点的原则,我们先能加起来的先加起来,先不考虑最后一项成员需要填充多少个字节: a (1byte) + 空闲 (1byte) + b (2byte) + c (4byte) + d (8byte) + e (8byte) + f (1byte) = 25 (byte);25byte 明显不是最宽基本类型成员大小 8byte 的倍数了,那么距离 25 最近的 8 的倍数有两个 24 和 32,若是选 24,内存明显不够放,所以只能取大进行填充 7byte,以便总大小成为 32byte。

因此简单总结一下(在 x64 位机,64 位编译器中):

类型对齐方式(变量存放的起始地址相对于结构的起始地址的偏移量)
Char偏移量必须为 sizeof (char) 即 1 的倍数
Short偏移量必须为 sizeof (short) 即 2 的倍数
int偏移量必须为 sizeof (int) 即 4 的倍数
float偏移量必须为 sizeof (float) 即 4 的倍数
double偏移量必须为 sizeof (double) 即 8 的倍数

各成员变量在存放的时候根据在结构中出现的顺序依次申请空间,同时按照上面的对齐方式调整位置,空缺的字节会自动填充。同时为了确保结构的大小为结构的字节边界数(即该结构中占用最大空间的类型所占用的字节数)的倍数,所以在为最后一个成员变量申请空间后,还会根据需要自动填充空缺的字节。

# 其他

案例一(结构体中的成员数组不指定大小,只限于成员数组在结构体尾部):

/* 
 * 操作系统:基于 x64 的处理器
 * 编译环境:Dev-C++ V5.11 
 */
#include <stdio.h> 
#define SIZE		1
struct s {
    char a;
    short b;
    int c;
    double d;
    char* e;
    char f[];
//    double i;
};
struct s temp;
char *p = NULL;
int main(void)
{
	printf("char:%d,short:%d,int:%d,double:%d\n\n",sizeof(char),sizeof(short),sizeof(int),sizeof(double));
	printf("sizeof = %d\n",sizeof(char *));
	printf("sizeof = %d\n\n",sizeof(temp));
	
	printf("temp addr = %p\n",&temp);
	printf("a    addr = %p,%2d\n",&temp.a,sizeof(temp.a));
	printf("b    addr = %p,%2d\n",&temp.b,sizeof(temp.b));
	printf("c    addr = %p,%2d\n",&temp.c,sizeof(temp.c));
	printf("d    addr = %p,%2d\n",&temp.d,sizeof(temp.d));
	printf("e    addr = %p,%2d\n",&temp.e,sizeof(temp.e));
	printf("\nf    addr = %p\n",&temp.f);
//	printf("f    sizeof = %d\n",sizeof(temp.f));
	
//	printf("\ni addr = %p,%d\n",&temp.i,sizeof(temp.i));
	
	return 0;
}

运行结果:

img

可以发现在程序中,由于没有为结构体成员数组 f 指定大小,将不为其分配空间

案例二(在案例一下,再追加一个结构体成员 i (double 类型) ):

#include <stdio.h> 
#define SIZE		1
struct s {
    char a;
    short b;
    int c;
    double d;
    char* e;
    char f[];
    double i;
};
struct s temp;
char *p = NULL;
int main(void)
{
	printf("char:%d,short:%d,int:%d,double:%d\n\n",sizeof(char),sizeof(short),sizeof(int),sizeof(double));
	printf("sizeof = %d\n",sizeof(char *));
	printf("sizeof = %d\n\n",sizeof(temp));
	
	printf("temp addr = %p\n",&temp);
	printf("a    addr = %p,%2d\n",&temp.a,sizeof(temp.a));
	printf("b    addr = %p,%2d\n",&temp.b,sizeof(temp.b));
	printf("c    addr = %p,%2d\n",&temp.c,sizeof(temp.c));
	printf("d    addr = %p,%2d\n",&temp.d,sizeof(temp.d));
	printf("e    addr = %p,%2d\n",&temp.e,sizeof(temp.e));
	printf("\nf    addr = %p\n",&temp.f);
//	printf("f    sizeof = %d\n",sizeof(temp.f));
	
	printf("\ni addr = %p,%d\n",&temp.i,sizeof(temp.i));
	
	return 0;
}

这下编译直接报错: [Error] flexible array member not at end of struct,因为成员数组 f 并不知道他的大小。