为什么数组变量的地址与其本身相同?

9
在C语言中,如果我们有一个数组像是a[10],那么a&a会有相同的指针值(但不是相同的类型)。我想知道为什么C会设计成这样?
这是为了节省存储&a所需的额外空间吗?当你思考到a永远不可能指向其他位置时,这就是有意义的。因此存储&a是没有意义的。

2
C语言实际上经历了很多次迭代来确定代码中&a的含义。至于dmr最初的意图 - 请问他。 - user2100815
a是一个地址。数组的第零个元素的地址和数组的地址是相同的。这有帮助吗? - Scott C Wilson
1
@Scott:a不是一个地址,而是一个数组。数组变量可以隐式转换为其初始元素的地址,并且在大多数情况下会发生这种隐式转换,但这并不意味着a是一个指针。 - James McNellis
@nbt:唉,现在已经不可能了。 :-( - Omnifarious
5个回答

13
事实并非如此。如果a是数组,则a不指向任何地方,因为a不是指针。例如int a [42];中,a表示一个由42个整数对象组成的数组;它不是指向一个由42个整数对象组成的数组的指针(那将是int (*a) [42];)。
&x给出变量x的地址;如果x是数组类型变量,则&x给出数组的地址;如果没有其他内容,则这与任何其他对象的&行为一致。
更好的问题应该是“为什么大多数情况下,数组(如a)在使用时会衰减为其初始元素的指针?”虽然我不能确切地知道为什么语言是这样设计的,但它确实使许多事物的规范化更加简单明了,特别是,使用数组进行算术运算与使用指针进行算术运算实际上是相同的。

因为a不是一个由42个整数组成的数组,而是一个指向至少有42 * sizeof(int)大小的内存位置的指针。 :) 数组是人类的概念。C语言并没有它们。a[21]只是a[0]地址加上21 * sizeof(int)的结果。 - Mel
4
声明int a[42];声明了一个数组,它没有声明指针。C确实有数组。 - James McNellis
我仍然建议您将其视为数组。实际发生的是,在编译时创建一个地址范围,其大小被固定,并且指针被混淆。因此,a [0] = 1与a = 1相同,a [1]是(a + 1)。但是,我也可以将单词“数组”别名为指向多个类型大小的内存块的隐藏指针。 :) - Mel
@Mel 我建议你阅读http://webcache.googleusercontent.com/search?q=cache:8Z51QkvTsYcJ:plan9.bell-labs.com/who/dmr/chist.html - Johannes Schaub - litb
1
我怀疑数组衰减为指针设计的主要好处是,它意味着以“数组”作为参数的函数实际上接受一个指针,这使得同一函数可以操作不同大小的数组。回过头来看,允许数组作为一等公民传递,并在幕后传递指针和大小可能会更好。 - caf
@JohannesSchaub-litb 这个链接已经失效了,我对内容很感兴趣。你能否在评论中提供一个更新的链接? - AMVaddictionist

4
设计相当优雅,考虑到在汇编层面如何引用数组,这是非常必要的。使用x86汇编,考虑以下C代码:
void f(int array[]) { return; }
void g(int (*array)[]) { return; }

int main()
{
    int a[5];

    f(a);
    g(&a);

    return 0;
}

数组a将占用20个字节的堆栈空间,因为在大多数平台上,int类型通常占用4个字节。使用寄存器EBP指向堆栈激活记录的基地址,您将查看上面main()函数的以下汇编代码:

//subtract 20 bytes from the stack pointer register ESP for the array
sub esp, 20

//the array is now allocated on the stack

//get the address of the start of the array, and move it into EAX register   
lea eax, [ebp - 20]

//push the address contained in EAX onto the stack for the call to f()
//this is pretty much the only way that f() can refer to the array allocated
//in the stack for main()
push eax
call f

//clean-up the stack
pop eax

//get a pointer to the array of int's on the stack
//(so the type is "int (*)[]")
lea eax, [ebp - 20]

//make the function call again using the stack for the function parameters
push eax
call g

//...clean up the stack and return

汇编指令LEA,或者“加载有效地址”,计算第二个操作数的表达式的地址并将其移动到由第一个操作数指定的寄存器中。因此,每次调用该命令时,它就像C语言中的取地址运算符。您会注意到,数组开始的地址(即[ebp-20],或从寄存器EBP中的堆栈指针地址的基础上减去20个字节)始终传递给每个函数fg。这是在机器码级别上唯一能够在不必复制数组内容的情况下引用在一个函数的堆栈中分配的内存块的另一个函数的方法。
结论是数组与指针不同,但是在右侧赋值运算符或传递给函数中引用数组的唯一有效方法是通过引用传递它,这意味着按名称引用数组在机器级别上实际上与获取数组指针相同。因此,在机器码级别上,这些情况下的a&a甚至&a[0]都会转化为相同的一组指令(在本例中为lea eax,[ebp-20])。但是请注意,数组类型不是指针,a&a也不是相同类型。但是,由于它指定了一块内存,因此获得对其的引用最简单有效的方法是通过指针。

1
事实上,a[0] 实际上是与 a 相同的内存位置。 &a 表示存储 a 的地址。
这是表示相同符号的不同方式。
访问数组的索引 3(a[2])与执行 a + sizeof( typeof(a) ) * 3 相同,其中 typeof(a) 是变量的类型。

真的吗?&aa是一样的,我以为&a是指针a所在地址。 - Dan F
3
@a 不是指针,而是数组。&a 给出了数组的地址。在大多数表达式中,数组的名字 a 会被隐式地转换("退化")为指向其初始元素的指针,因此在这些表达式中,a 实际上相当于 &a[0]。&a[0] 和 &a 指向相同的位置,但它们的类型不同(前者的类型是 int,而后者的类型是 int ()[N],其中 N 是数组中元素的数量)。 - James McNellis
按照标准和我的理解,在 int a[10] 中,a 是一个指针,编译器会将其视为指针,它只是在堆栈上分配了 10 个对象,而不是在堆上分配。而 &a 将指向该堆栈分配变量的地址,而不是第一个元素存储的堆栈位置。数组在 C/C++ 中不是一种类型。 - Dan F

1

是的

你的解释基本正确,尽管我怀疑空间的数量是否是问题,而更多地是需要分配它的特殊情况。通常,C处理的每个对象都有一个值(或多个值)和一个地址。因此,实际分配的指针已经有了自己的地址,并且在真正的指针中具有值和地址是有意义的。

但是,数组引用已经是一个地址。对于C通过&操作符制作双重间接指针将需要在某个地方分配空间,这将代表简单早期dmr C编译器中哲学上的巨大分歧。

它将存储这个新指针的位置是一个很好的问题。与数组相同的存储类?如果它是一个参数呢?这是Pandora's box,最简单的解决方法是定义掉这个操作。如果开发人员想要一个间接指针,他总可以声明一个。

此外,&返回数组对象的地址是有意义的,因为这与其在其他地方的使用一致。

一个好的看待这个问题的方式是将对象视为具有值和地址,而数组引用只是一种简写语法。实际上,强制要求使用&a可能有点学究式,因为引用a无论如何都不会有其他解释。

0
B是C的直接祖先。它是一种无类型语言,其语法非常简单。
tab[10];

大致上的意思是

Word tab_[10];
Word tab = (Word)&tab_;

在 C 语言中,即便你定义的是一个数组变量(顺带一提不仅仅是数组变量,任何数组值,哪怕是指针和多维数组),它都会自动地“衰变”为指向第一个元素的指针。

当 C 语言发展至今,人们认为保持这个特性很有用。它可以让代码更加简洁高效。

如果您想了解更多 C 和 Unix 的历史信息,可以参考 B Manual 或者 Dennis Ritchie home page


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接