文中代码

对于如下代码:

int array[] = { 0, 1, 2, 3, 4 };
printf("%p\n", &array);
printf("%p\n", array);

可以看到输出的地址是相同的.

而对于如下代码:

int **array = (int **)malloc(sizeof(int *) * 5);
for (int i = 0; i < 5; i++)
  array[i] = (int *)malloc(sizeof(int));
printf("%p\n", &array);
printf("%p\n", array);

可以看到输出的地址是不同的. 其实对于这一段代码是没有任何疑问的, 可以很轻松的 明白为什么输出的地址不同.

那么编译器是如何处理第一部分代码,使输出的地址相同的。

可以通过gdb调试可以很清晰的看到编译器是如何处理这种情况的。

int array[] = { 0, 1, 2, 3, 4 };
void *a1 = (void *)&array, *a2 = (void *)array;

可以看到void这一行的汇编代码为:

0x565561e4 <main+71>:        lea    0x8(%esp),%eax
0x565561e8 <main+75>:        mov    %eax,(%esp)
0x565561eb <main+78>:        lea    0x8(%esp),%eax
0x565561ef <main+82>:        mov    %eax,0x4(%esp)
0x565561f3 <main+86>:        mov    $0x0,%eax

可以看到不管是 array 还是 &array 的操作都是使用的 lea 操作将 0x8(%esp) 移动到 %eax 中然后在 mov 操作赋值到相关变量中.

而对于另一段代码:

int **array = (int **)malloc(sizeof(int *) * 5);
for (int i = 0; i < 5; i++)
  array[i] = (int *)malloc(sizeof(int));
void *a1 = (void *)&array, *a2 = (void *)array;
0x56556214 <main+103>:       lea    -0x2c(%ebp),%eax
0x56556217 <main+106>:       mov    %eax,-0x24(%ebp)
0x5655621a <main+109>:       mov    -0x2c(%ebp),%eax
0x5655621d <main+112>:       mov    %eax,-0x20(%ebp)
0x56556220 <main+115>:       mov    $0x0,%eax

可以看到在为 a2 赋值是使用的是 mov 指令而不是 lea 指令.

;; mov 和 lea 指令的区别
;; 假设在地址`0x80`中有内容`1234`
mov 0x80, %eax // 执行完后eax内容为1234
lea 0x80, %eax // 执行完后eax内容为0x80

而编译器为什么会有这个行为呢?

Except when it is the operand of the sizeof operator or the unary & operator, or is a string literal used to initialize an array,an expression that has type “array of type” is converted to an expression with type “pointer to type” that points to the initial element of the array object and is not an lvalue. If the array object has register storage class, the behavior is undefined. (C99 [6.3.2.1])

A character string literal is a sequence of zero or more multibyte characters enclosed in double-quotes, as in “xyz”. A wide string literal is the same, except prefixed by the letter L. (C99 [6.4.5])

The multibyte character sequence is then used to initialize an array of static storage duration and length just sufficient to contain the sequence. (C99 [6.4.5])

第一段的主要意思是当一个表达式是一个数组并且操作它的操作符不是 sizeof 或 一元操作符 & 时,这个表达式会被转化为指向数组第一个元素的指针。下面两段是解释 为什么要提 string literal 的,有兴趣可以自己看看。

int array[] = { 0, 1, 2, 3, 4 };
array == &array // is true

因此根据 C99 标准的说法,array&array 虽然数值是相同的但表达的含义却不相同, array 是指向数组第一个元素的指针,而 &arrayarray 这个 object 的地址。

我们可以通过下面这段代码来验证上面的解释:

#include <stdio.h>

int main(void) {
    int a[5] = {1, 2, 3, 4, 5};
    int *p1 = (int *)(a + 1);
    int *p2 = (int *)(&a + 1);
    printf("%d\n", *(p1 - 1));
    printf("%d\n", *(p2 - 1));
    return 0;
}

上面代码的输出为:

1
5

当单独使用 a 时,它代表的是一个 int 类型的指针,因此对它加一,只是移动一个 int 的大小,所以 p1 指向的为数组中的 2,在 printfp1 - 1 又指回了 1

&a 代表的为 int[5] 类型的指针,因此 &a + 1 会指向 5 后面的那个地址, 而我们又把 p2 强制转换成了 int *,所以 p2 - 1 会指向 5

当然,&a 可以被理解为 pointer to pointer,但我认为上面的解释更清晰,更符合 C99 标准的解释