程序的机器级表示——数据
数组
数组空间的分配
对于一个类型为T
,长度为L
的数组来说,C语言编译器会在内存中分配一片连续的内存区域来储存这个数组,这段内存空间的长度是L * sizeof(T)
。
数组中值的访问
在C语言中,这个数组的标识符可以被用作一个访问这个数组中首个元素的指针,而实际上,C语言也通过计算数组中每个元素的地址来直接访问。比如C语言代码z[digit]
对应的汇编代码为movl (%rdi,%rsi,4),%eax
,其中%rdi
寄存器是数组的首地址,%rsi
中的值是digit
。
但是C语言编译器在编译的过程中不会检查数组的访问是否越界,它只会傻傻的给出这个内存访问式子。
可以通过下面这个例子来理解数组和指针之间的联系和区别。
我们通过下面的语句声明两个变量
int A1[3];
int *A2;
这两个语句都可以通过编译。A1
和A2
都可以访问而不引发空指针异常,他们的字长分别为12和8。*A1
和*A2
则是字长均为4,而对于A2
的取值会引发空指针异常。
多维数组
对于一个类型为T
,M
行N
列的二维数组,C语言编译器会在内存中分配一片连续的内存空间来储存这个数组,直接将这个二维数组一行行的放在这篇内存区域中。
对于多维数组的数据访问也是类似于普通数组。直接计算需要访问元素的地址,比如对于一个二维的int
类型的数组A[i][j]
,汇编代码中的地址计算为A+(i * size + j) * 4
。
结构体
- 一个结构体代表了内存中的一块内存
- 在这块内存中按照声明的顺序安排每个元素的内存空间
- 编译器在编译的时候会确定整个结构体的大小和其中每个元素的位置,机器级代码中对于结构体这些内容一无所知。
结构体的例子——链表
我们先给出这样一段C语言代码:
struct rec {
int a[4];
int i;
struct rec* next;
}
void set_val(struct rec* r, int val)
{
while(r)
{
int i = r->i;
r->a[i] = val;
r = r->next;
}
}
其中核心的循环代码为
.L11:
movsalq 16(%rdi), %rax
movl %esi, (%rdi, %rax, 4)
movq 24(%rdi), %rdi
testq %rdi, %rdi
jne .L11
我们不难从汇编代码中发现,无论是对数组的访问还是对于结构体中元素的访问,都是利用地址的运算来实现的。
结构体和对齐
为了提高内存访问的效率,内存中指针的地址应该被对齐至4或者8的倍数。而在结构体中,元素的大小又是任意的,因此,在编译含有结构体的代码时,编译器可能在结构体的中间或者前后安排一些“多余的”空间,使结构体和结构体中元素的地址都是4或者8的倍数。
对于每一种数据结构:
- 仅有1位的
byte
之类的数据类型,没有特别的对齐要求 - 2位的数据结构,比如
short
,我们希望他们的地址是2的倍数 - 对于4位的数据结构,比如
int
,那最好是4的倍数 - 对于8位的数据结构,比如
double
,那么地址最好是8的倍数