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

时间:2014-02-23 18:22:18

标签: c arrays pointers dereference

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

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

//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)被转换为(程序集等价于)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  

3 个答案:

答案 0 :(得分:15)

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

    声明int a[2]
    • 编译器为a存储了此变量的地址。该地址也是数组的地址,因为变量的类型是数组。
    • 访问a[1]表示:
      • 检索该地址,
      • 添加偏移量和
      • 以此计算的新地址访问内存。
  • 声明int *b
    • 编译器也有b的地址,但这是指针变量的地址,而不是数组。
    • 因此,访问b[1]意味着:
      • 检索该地址,
      • 访问该位置以获取b的值,即数组的地址
      • 向此地址添加偏移量,然后
      • 访问最终内存位置。

答案 1 :(得分:11)

// 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]时,它意味着从1的地址获取偏移a[0]处的任何内容,即符号a的地址(和下一个块)受约束。这是一个汇编指令。就好像某些信息嵌入到数组符号中一样,这样CPU就可以在一条指令中从数组的基址偏移一个元素。当您访问*bb[0]b[1]时,首先获取b的内容,这是一个地址,然后执行指针算术以获取新地址,然后获取无论那个地址在那里。因此,CPU必须首先加载b的内容,评估b+1(对于b[1]),然后在地址b+1加载内容。这是两个装配说明。

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

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

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

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

答案 2 :(得分: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;
}

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

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

指针c的值不是您所期望的:它不是指向数组b的开头的指针(这将是一个接近a的合理内存位置),它似乎包含数组 b 内容 ...(0xff当然是255十六进制)。

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