解除引用指针和访问数组元素之间的区别
我记得一个示例,其中演示了指针和数组之间的区别。
当作为函数参数传递时,数组衰减到指向数组中第一个元素的指针,但它们不等效,如下所示:
//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):
注意: 1158000h
和1158008h
分别是a
和b
的内存地址
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
链接器将它们链接到相同的内存地址,但由于符号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
b[1]
首先评估为*(b+1)
。 这意味着获取内存位置b
绑定的值,向其中添加1
(指针算术)以获取新地址,将该值加载到CPU寄存器中,将该值存储在x2
绑定的位置。 因此,位置b
值绑定为100
,将其加1
得到104
(指针算术; sizeof *b
为4)并获取地址104
的值! 这是错误的和未定义的行为,很可能会导致程序崩溃。
如何访问数组元素以及如何访问指针指向的值有所不同。 我们来举个例子吧。
int a[] = {100, 800}; int *b = a;
a
是2
整数的数组, b
是指向初始化为a
的第一个元素的地址的整数的指针。 现在当访问a[1]
,它意味着从a[0]
的地址得到偏移1
处的任何内容,即符号a
所绑定的地址(和下一个块)。 这是一个汇编指令。 就好像某些信息嵌入到数组符号中一样,这样CPU就可以在一条指令中从数组的基址偏移一个元素。 当你访问*b
或b[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章的所有内容都非常有用)。