解除引用指针和访问数组元素之间的区别

我记得一个示例,其中演示了指针和数组之间的区别。

当作为函数参数传递时,数组衰减到指向数组中第一个元素的指针,但它们不等效,如下所示:

//file file1.c int a[2] = {800, 801}; int b[2] = {100, 101}; 
 //file file2.c extern int a[2]; // here b is declared as pointer, // although the external unit defines it as an array extern int *b; int main() { int x1, x2; x1 = a[1]; // ok x2 = b[1]; // crash at runtime return 0; } 

链接器不会对外部变量进行类型检查,因此在编译时不会生成错误。 问题是b实际上是一个数组,但编译单元file2并不知道这一点并将b视为指针,导致在尝试取消引用它时发生崩溃。

我记得当这被解释时它是完全合理的,但现在我不记得解释,也不能靠自己来解决。

所以我想问题是在访问元素时,数组与指针的处理方式有何不同? (因为我认为无论p是数组还是指针, p[1]都转换为(汇编等价于) *(p + 1) – 我显然是错的)。


由两个解引用产生的集合(VS 2013):
注意: 1158000h1158008h分别是ab的内存地址

  12: x1 = a[1]; 0115139E mov eax,4 011513A3 shl eax,0 011513A6 mov ecx,dword ptr [eax+1158000h] 011513AC mov dword ptr [x1],ecx 13: x2 = b[1]; 011513AF mov eax,4 011513B4 shl eax,0 011513B7 mov ecx,dword ptr ds:[1158008h] 011513BD mov edx,dword ptr [ecx+eax] 011513C0 mov dword ptr [x2],edx 

感谢@tesseract在评论中提供的链接: 专家C编程:Deep C Secrets (第96页),我想出了一个简单的答案(书中解释的简单愚蠢版本;完整的学术答案阅读这本书):

  • 当声明为int a[2]
    • 编译器具有存储此变量的地址。 该地址也是数组的地址,因为变量的类型是数组。
    • 访问a[1]意味着:
      • 检索该地址,
      • 添加偏移和
      • 在此计算的新地址访问内存。
  • 当声明int *b
    • 编译器也有b的地址,但这是指针变量的地址,而不是数组。
    • 因此访问b[1]意味着:
      • 检索该地址,
      • 访问该位置以获取b的值,即数组的地址
      • 然后向此地址添加偏移量
      • 访问最终的内存位置。
 // in file2.c extern int *b; // b is declared as a pointer to an integer // in file1.c int b[2] = {100, 101}; // b is defined and initialized as an array of 2 ints 

链接器将它们链接到相同的内存地址,但由于符号bfile1.cfile2.c具有不同的类型,因此相同的内存位置的解释方式不同。

 // in file2.c int x2; // assuming sizeof(int) == 4 x2 = b[1]; // b[1] == *(b+1) == *(100 + 1) == *(104) --> segfault 

b[1]首先评估为*(b+1) 。 这意味着获取内存位置b绑定的值,向其中添加1 (指针算术)以获取新地址,将该值加载到CPU寄存器中,将该值存储在x2绑定的位置。 因此,位置b值绑定为100 ,将其加1得到104 (指针算术; sizeof *b为4)并获取地址104的值! 这是错误的和未定义的行为,很可能会导致程序崩溃。

如何访问数组元素以及如何访问指针指向的值有所不同。 我们来举个例子吧。

 int a[] = {100, 800}; int *b = a; 

a2整数的数组, b是指向初始化为a的第一个元素的地址的整数的指针。 现在当访问a[1] ,它意味着从a[0]的地址得到偏移1处的任何内容,即符号a所绑定的地址(和下一个块)。 这是一个汇编指令。 就好像某些信息嵌入到数组符号中一样,这样CPU就可以在一条指令中从数组的基址偏移一个元素。 当你访问*bb[0]b[1] ,首先得到b的内容,这是一个地址,然后做指针算术得到一个新的地址,然后得到那个地址的任何东西。 因此,CPU必须首先加载b的内容,计算b+1 (对于b[1] ),然后加载地址b+1的内容。 这是两个assembly说明。

对于extern数组,您不需要指定其大小。唯一的要求是它必须与其外部定义匹配。 因此,以下两个陈述是等效的:

 extern int a[2]; // equivalent to the below statement extern int a[]; 

您必须将其声明中的变量类型与其外部定义相匹配。 在解析符号引用时,链接器不检查变量的类型。 只有函数具有编码到函数名称中的函数类型。 因此,您不会得到任何警告或错误,它会编译得很好。

从技术上讲,链接器或某些编译器组件可以跟踪符号所代表的类型,然后给出错误或警告。 但是标准没有要求这样做。 你需要做正确的事。

这并没有完全回答你的问题,但它会给你一个暗示正在发生的事情。 稍微修改你的代码给予

 //file1.c int a[2] = {800, 801}; int b[2] = {255, 255}; #include  extern int a[2]; // here b is declared as pointer, // although the external unit declares it as an array extern int *b; int *c; int main() { int x1, x2; x1 = a[1]; // ok c = b; printf("allocated x1 OK\n"); printf("a is %p\n", a); printf("c is %p\n", c); x2 = *(c+1); printf("%d %d\n", x1, x2); return 0; } 

现在,当你运行它时,你仍然会遇到段错误。 但就在您做之前,您可以了解原因:

 allocated x1 OK a is 0x10afa4018 c is 0xff000000ff Segmentation fault: 11 

指针c的值不是你所期望的:它不是指向数组b的开头的指针(它是一个接近a的合理内存位置),它似乎包含数组b内容 ……( 0xff是当然是255 (hex)。

我无法清楚地解释为什么会这样 – 为此,请参阅评论中@tesseract给出的链接 (实际上第4章的所有内容都非常有用)。