一点一点前进...

0%

结构体和数组在内存中的布局

申明一个结构体:

1
2
3
4
struct fraction{
int denom;
int num;
};

下面是一些对fraction结构体的操作:

1
2
3
4
5
6
7
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++并不检查数据越界,访问外面的数据会破坏上下文的数据或者访问不能访问的内存,很危险。
总结下数组和指针操作的等价性:

1
2
3
4
arr ≡ &arr[0]
arr + k ≡ &arr[k]
*arr ≡ arr[0]
*(arr + k) ≡ *arr[k]

其中,k可以为负数。
下面看个示例:

1
2
3
4
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是等价的,移动2*2个字节,现在指针指向的是arr[4],但是编译器会认为这里是个short并赋值为100。所以arr[4]的内存就是0x64 0x00 0x00 0x00(小端)
下面一句类似,[3]就是移动2*3个字节,指向了arr[4]的后半段,赋值1,内存就是0x64 0x00 0x01 0x00
输出语句就是把arr[4]里面所有的内容以int的方式输出1 * 2 ^ 16 + 100 = 65636

最后,我们看一个综合的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 |
| numUnits |
| s | s | u | u |
| i | i | d | d |
| name |
下面的是[0]上面的是[1]
经过第一个输出之前的三个赋值语句后:
| mem | 4 bytes | per | row |
| — |-|-|-|
| numUnits |
| s | s | u | u |
| i | i | d | d |
| name |
| 21 |
| 5 | x | x | \0 |
| 4 | 0 | 4 | 1 |
| name |
pupils[1].name指向的是倒数第三行第二个x所在的内存
第一个输出语句很简单,40415xx,遇到\0就结束了
strcpy(pupils[1].name, “123456”);
这个语句的操作结果是:
| mem | 4 bytes | per | row |
| — |-|-|-|
| numUnits |
| s | s | u | u |
| \0 | i | d | d |
| 3 | 4 | 5 | 6 |
| 5 | x | 1 | 2 |
| 4 | 0 | 4 | 1 |
| name |
这时,输出的pupils[0].suid是40415x123456,遇到pupils[1]的\0才结束。
pupils[0].numUnits也已经被修改了,16进制输出是36353433,数字6的ASCII码对应的就是0x36,其他类似,为什么是反着的呢?我的机器是小端的。