# 内存大小问题
有时候,我们在不同的编译环境,或者不同的机子上测试编译,会呈现不同的结果,于是我们会陷入疑问,内存的大小是谁分配的呢?
在系统中,系统对内存的识别是以 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 等一些标志性文本,这就是要告诉别人它支持的操作环境。
# 结构体分配的空间
在进行讨论之前先来看一下程序
/* | |
* 操作系统:基于 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
为数组,特殊点我们把地址跟大小分开打印;指针我们不知道他的大小,也打印出来;最后我们打印一下输出看下结果:
先看第一行,我们打印出了 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 ???跟打印出来的总大小不一样啊,这就关系到数据对齐了。
# 内存大小对齐原则
- 结构体变量的首地址能够被其最宽基本类型成员的大小所整除。
- 结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding)。即结构体成员的末地址减去结构体首地址(第一个结构体成员的首地址)得到的偏移量都要是对应成员大小的整数倍。
- 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在成员末尾加上填充字节。
看了以上的原则后,我们来继续分析一下上面的数据类型大小问题
实际上:结构体 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; | |
} |
运行结果:
可以发现在程序中,由于没有为结构体成员数组 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
并不知道他的大小。