C 语言结构体、数组的内存布局
申明一个结构体:
下面是一些对 fraction
结构体的操作:
fraction f;
f.denom = 22;
f.num = 7;
((fraction*)&(f.num))->denom = 12;
std::cout << f.num << std::endl;
((fraction*)&(f.num))->num = 33;
std::cout << (&f)[1].denom << std::endl;
下面逐行解释下上面的程序。
首先我们声明了类型是 fraction
的变量 f
,在栈上分配了两个 4 字节去存储,低位是 denom
,高位是 num
:
mem |
---|
num |
denom |
接下来是两个赋值语句,内存变成了:
mem |
---|
7 |
22 |
接下来是个复杂的赋值语句,&(f.num)
拿到了 num
所在内存的地址,(fraction*)
告诉编译器这个地址是上放的是 fraction
结构体,->denom
是修改这个虚拟结构体的 denom
字段。好了,内存变成了:
mem |
---|
12 |
22 |
所以紧接着那句输出是 12。接下来又是个类似的复杂赋值语句,不再赘述,内存变成了:
mem |
---|
33 |
12 |
22 |
使用原来的 f
能不能访问到 33 这个值呢?能,方法就是最后一个语句。(&f)
取到 f
的地址,(&f)[1]
,编译器会向后偏移 f
指向的对象的大小,fraction
大小是八字节,所以这句就是往后偏移八个字节,于是乎指向 33 所在的内存。最后,拿出 denom 的值。
数组在内存中的布局比较简单,很直接。需要注意的是C、C++并不检查数据越界,访问外面的数据会破坏上下文的数据或者访问不能访问的内存,很危险。
总结下数组和指针操作的等价性:
其中,k
可以为负数。
下面看个示例:
int arr[5];
((short*)(((char*)(&arr[1])) + 8))[2] = 100;
((short*)(((char*)(&arr[1])) + 8))[3] = 1;
std::cout << arr[4] << std::endl;
int
的数组,&arr[1]
拿到第二个元素的地址, (char*)
让编译器认为当前指针是 char*
类型,+8
操作就是往后偏移 8 个字节,这时,指针指向了 arr[3]
。(short*)
让编译器认为当前指针是 short*
类型,[2]
和 +2
是等价的,移动 个字节,现在指针指向的是 arr[4]
,但是编译器会认为这里是个 short
并赋值为 100。所以 arr[4]
的内存就是 0x64 0x00 0x00 0x00
(小端)。下面一句类似,[3]
就是移动 个字节,指向了 arr[4]
的后半段,赋值 1,内存就是 0x64 0x00 0x01 0x00
。输出语句就是把 arr[4]
里面所有的内容以 int
的方式输出 。
最后,看一个综合的示例:
struct student {
char* name;
char suid[8];
int numUnits;
};
student pupils[2];
pupils[0].numUnits = 21;
pupils[1].name = pupils[0].suid + 6;
strcpy(pupils[0].suid, "40415xx");
std::cout << pupils[0].suid << std::endl;
strcpy(pupils[1].name, "123456");
std::cout << pupils[0].suid << std::endl;
std::cout << std::hex << pupils[0].numUnits << std::endl;
mem | 4 bytes | per | row |
---|---|---|---|
numUnits | |||
s | s | u | u |
i | i | d | d |
name | |||
name | |||
numUnits | |||
s | s | u | u |
i | i | d | d |
name | |||
name |
下面的是 pupils[0]
上面的是 pupils[1]
。经过第一个输出之前的三个赋值语句后:
mem | 4 bytes | per | row |
---|---|---|---|
numUnits | |||
s | s | u | u |
i | i | d | d |
name | |||
name | |||
21 | |||
5 | x | x | \0 |
4 | 0 | 4 | 1 |
name | |||
name |
另外,pupils[1].name
指向的是倒数第四行第二个 x
所在的内存。
第一个输出语句很简单,40415xx
,遇到 \0
就结束了。
下面看第二个 strcpy
。
这个语句的操作结果是:
mem | 4 bytes | per | row |
---|---|---|---|
numUnits | |||
s | s | u | u |
i | i | d | d |
name | |||
\0 | name | ||
3 | 4 | 5 | 6 |
5 | x | 1 | 2 |
4 | 0 | 4 | 1 |
name | |||
name |
这时,输出的 pupils[0].suid
是 40415x123456
,遇到 pupils[1]
的 \0
才结束。pupils[0].numUnits
也已经被修改了,16 进制输出是 36353433,数字 6 的 ASCII 码对应的就是 0x36,其他类似。