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

bol*_*lov 21 c arrays pointers dereference

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

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

//file file1.c

int a[2] = {800, 801};
int b[2] = {100, 101};
Run Code Online (Sandbox Code Playgroud)
//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;
}
Run Code Online (Sandbox Code Playgroud)

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

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

所以我想问题是在访问元素时,数组如何与指针区别对待?(因为我认为无论是数组还是指针p[1]都转换为(程序集等价)- 我显然是错的).*(p + 1)p


由两个解除引用(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  
Run Code Online (Sandbox Code Playgroud)

bol*_*lov 15

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

  • 宣布时int a[2]:
    • 编译器具有a存储此变量的地址.该地址也是数组的地址,因为变量的类型是数组.
    • 访问a[1]手段:
      • 检索该地址,
      • 添加偏移和
      • 在此计算的新地址访问内存.
  • 宣布时int *b:
    • 编译器也有一个地址,b但这是指针变量的地址,而不是数组.
    • 所以访问b[1]意味着:
      • 检索该地址,
      • 访问该位置以获取值b,即数组的地址
      • 然后向此地址添加偏移量
      • 访问最终的内存位置.


aja*_*jay 12

// 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
Run Code Online (Sandbox Code Playgroud)

链接器将它们链接到相同的存储器地址,但是由于符号b具有不同的类型,file1.c并且file2.c相同的存储器位置被不同地解释.

// in file2.c

int x2;  // assuming sizeof(int) == 4
x2 = b[1]; // b[1] == *(b+1) == *(100 + 1) == *(104) --> segfault
Run Code Online (Sandbox Code Playgroud)

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

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

int a[] = {100, 800};
int *b = a;
Run Code Online (Sandbox Code Playgroud)

a是一个2整数数组,b是一个指向初始化为第一个元素地址的整数的指针a.现在,当a[1]被访问时,就意味着得到什么是有偏移量1从地址a[0],地址(和下一个块),其中符号a必然.这是一个汇编指令.就好像某些信息嵌入到数组符号中一样,这样CPU就可以在一条指令中从数组的基址偏移一个元素.当您访问*bor b[0]或者b[1],首先获取其内容b是地址时,然后执行指针算法以获取新地址,然后获取该地址处的任何内容.因此,CPU必须首先加载内容b,评估b+1(for b[1])然后在地址加载内容b+1.这是两个装配说明.

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

extern int a[2];  // equivalent to the below statement
extern int a[];
Run Code Online (Sandbox Code Playgroud)

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

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


Flo*_*ris 5

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

//file1.c

int a[2] = {800, 801};
int b[2] = {255, 255};

#include <stdio.h>

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;
}
Run Code Online (Sandbox Code Playgroud)

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

allocated x1 OK
a is 0x10afa4018
c is 0xff000000ff
Segmentation fault: 11
Run Code Online (Sandbox Code Playgroud)

指针C的值是不是您所期望的:不是为指针数组的开始b(这将是一个明智的存储位置接近a),它似乎包含内容数组b ...(0xff255十六进制,当然).

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