数组是指针吗?

24

在C和C++中,数组和指针的实现方式是否不同?我之所以遇到这个问题,是因为在两种情况下,我们都是从一个元素的起始地址访问元素。因此,它们之间应该有密切关系。请解释它们之间的确切关系。谢谢。

在C和C++中,数组和指针的实现方式是不同的。虽然数组名和指向数组第一个元素的指针之间存在一些相似之处,但它们是不同的类型。特别是,数组名不能被赋值,而指针可以被赋值。另外,在函数中传递数组时,它们也有不同的行为。在C中,传递一个数组的名称将传递一个指向数组第一个元素的指针;而在C++中,传递一个数组的名称将传递整个数组。

从低级汇编的角度来看,数组只是一个内存分配,通常第一个位置定义了后面有多少空间。此时访问元素仅仅是从起始地址偏移。另一方面,指针是保存另一个内存位置的内存位置。因此,当您访问指针时,您得到的是该内存地址的值,它只是实际数据的另一个内存地址。如果您有一个指向数组的指针,则指针的内存地址存储数组的起始位置(如上所述)。 - Chris
你可能会觉得这个问题很有趣:So you think you know pointers (and arrays)? - Cristian Ciupitu
7个回答

71

首先我们需要澄清一件重要的事情:数组不是指针。数组类型和指针类型是完全不同的类型,并且编译器会对它们进行不同的处理。

混淆发生的地方在于C语言如何处理数组表达式。N1570

6.3.2.1 左值、数组和函数设计者

...
3 对于一个具有数组类型的表达式,在以下情况下除外:作为 sizeof 运算符、_Alignof 运算符或一元 & 运算符的操作数,或者是用于初始化数组的字符串字面量。此时,该表达式被转换为一个类型为“指向数组对象的第一个元素的指针”的表达式,而不是左值。如果该数组对象有 register 存储类别,则其行为未定义。

让我们来看一下以下声明:

int arr[10] = {0,1,2,3,4,5,6,7,8,9};
int *parr = arr;

arr是一个10个元素的int数组,它指向足够大的连续内存块,以存储10个int值。在第二次声明中,表达式arr属于数组类型,但因为它不是&sizeof操作符的操作数,并且它不是字符串字面量,所以表达式的类型变为“指向int的指针”,并且其值是第一个元素的地址,即&arr[0]

parr是一个指向int的指针,它引用了一个足够大的内存块,可以存储单个int对象的地址。如上所述,它被初始化为指向arr中的第一个元素。

下面是一个假设的内存映射,显示两者之间的关系(假设使用16位的int和32位的地址):

Object           Address         0x00  0x01  0x02  0x03
------           -------         ----------------------
   arr           0x10008000      0x00  0x00  0x00  0x01
                 0x10008004      0x00  0x02  0x00  0x03
                 0x10008008      0x00  0x04  0x00  0x05
                 0x1000800c      0x00  0x06  0x00  0x07
                 0x10008010      0x00  0x08  0x00  0x09
  parr           0x10008014      0x10  0x00  0x80  0x00

类型对于sizeof&等操作至关重要;在这种情况下,sizeof arr == 10 * sizeof (int),即20,并且sizeof parr == sizeof (int *),即4。同样地,表达式&arr的类型是int (*)[10],即指向一个包含10个int元素的数组的指针,而&parr的类型是int **,即指向指向int的指针。

请注意,表达式arr&arr将产生相同的arr中第一个元素的地址),但表达式的类型不同(分别为int *int (*)[10])。这在使用指针算术时会有所不同。例如,给定:

int arr[10] = {0,1,2,3,4,5,6,7,8,9};
int *p = arr;
int (*ap)[10] = &arr;

printf("before: arr = %p, p = %p, ap = %p\n", (void *) arr, (void *) p, (void *) ap);
p++;
ap++;
printf("after: arr = %p, p = %p, ap = %p\n", (void *) arr, (void *) p, (void *) ap);
"before" 那行应该对所有三个表达式打印相同的值(在我们假设的映射中为 0x10008000)。"after" 那行应该显示三个不同的值:0x100080000x10008002(基地址加上 sizeof(int)),以及 0x10008014(基地址加上 sizeof(int [10]))。
现在让我们回到上面第二段中说的:在大多数情况下,数组表达式会被转换为指针类型。让我们看一下下标表达式 arr[i]。由于表达式 arr 既不是出现在 sizeof& 操作符的操作数位置上,也不是用作初始化另一个数组的字符串字面量,所以它的类型从 "10 个元素的 int 数组" 转换成了 "int 指针",并且下标操作被应用于这个指针值。实际上,当你看 C 语言定义时,会看到以下语言:
6.5.2.1 数组下标
...
2. 后缀表达式后跟方括号内的表达式 [] 是数组对象的带下标的指定。下标运算符 [] 的定义是 E1[E2] 等同于 (*((E1)+(E2)))。由于适用于二元 + 运算符的转换规则,如果 E1 是数组对象(或等效地,是数组对象的初始元素的指针),而 E2 是整数,则 E1[E2] 指代 E1 的第 E2 个元素(从零开始计数)。
实际上,这意味着你可以将下标运算符应用于指针对象,就好像它是一个数组一样。这就是为什么像下面这样的代码能够工作的原因:
int foo(int *p, size_t size)
{
  int sum = 0;
  int i;
  for (i = 0; i < size; i++)
  {
    sum += p[i];
  }
  return sum;
}

int main(void)
{
  int arr[10] = {0,1,2,3,4,5,6,7,8,9};
  int result = foo(arr, sizeof arr / sizeof arr[0]);
  ...
}

这个方法之所以能够正常工作,是因为main处理的是一个int数组,而foo处理的是一个int指针,但两者都可以像处理数组类型一样使用下标操作符。

这也意味着数组下标操作是可交换的:假设a是一个数组表达式,i是一个整数表达式,则a[i]i[a]均为有效表达式,并且两者将产生相同的值。


2
特别是对于 arr&arr,加上 +1 - pmg
完美。但是只能点赞一次。 - Daniel Fischer
2
只是好奇,为什么你使用了一个假设的例子 sizeof(int)==2,虽然标准允许,但在任何主流平台/编译器上都已经很久没有看到过了? - Baruch
4
大多是为了让示例的内存映射大小合理化。并且因为我年纪大了。 - John Bode
1
你能否举个例子来说明 "或者是一个字符串字面量用来初始化数组"? - Suraj Jain
显示剩余2条评论

23

对于C++我不熟悉,但是关于C语言的问题,c-faq 的回答比我更好。

c-faq中的小片段:

6.3 C语言中“指针和数组的等价性”是什么意思?

[...]

具体来说,等价性的基础是这个重要定义:

在表达式中出现的类型为array-of-T的对象引用(有三个例外)会衰减为指向其第一个元素的指针;结果指针的类型是pointer-to-T。

[...]


我以为数组是常量指针!我错了吗? - Amir Zadeh
@绿色代码:显然你是的。 :) - Armen Tsirunyan
@Green:是的。数组不是指针。 - GManNickG
有哪些被特意保留的代码片段?有哪三个例外使得它们与众不同? - Luis Colorado
@LuisColorado:当数组与sizeof&一起使用时,以及在数组初始化中用作字符串字面量时,有3个例外。 - pmg
我知道...谢谢你澄清 :) 这些异常是让它们与众不同的。 - Luis Colorado

8

根据C++标准4.2节:

类型为“N个T的数组”或“未知大小的T数组”的左值或右值可以转换为类型为“指向T的指针”的右值。结果是指向数组第一个元素的指针。


@Armen:我认为三元运算符可能可以做到这一点。 - Ben Voigt
@Armen:我猜它仍然是一个lvalue。http://ideone.com/WfLiQ - Ben Voigt
@Armen,请查看这个问题。 - Kirill V. Lyadvinsky
1
@Armen:我看到了你找到了数组rvalue的代码示例。 - Ben Voigt
1
@Ben:是的,几天前完成了,我为此感到自豪 :) - Armen Tsirunyan
显示剩余4条评论

8
不,它们的实现方式并不不同。两者都通过相同的计算查找元素:a[i] 的地址为 a + i*sizeof(a[0]),而 p[i] 的地址为 p + i*sizeof(p[0])
但是,它们在类型系统中被不同地处理。C++ 对数组有类型信息,可以通过sizeof 运算符(像 C 语言一样),模板推导,函数重载,RTTI 等来查看。基本上,在语言中使用类型信息的任何地方,指针和数组可能会表现出不同的行为。
在 C++ 中,有很多例子表明两个不同的语言概念具有相同的实现方式。只举几个例子:数组与指针、指针与引用、虚函数与函数指针、迭代器与指针、for 循环与 while 循环、异常与 longjmp。
在每种情况下,都有不同的语法和不同的思考方式,但最终结果是相同的机器代码。

6
在C++中(我认为在C语言中也是如此),数组不是指针,可以通过以下方式证明。
#include <iostream>
int main()
{
   char arr[1000];
   std::cout << sizeof arr;
}

如果arr是一个指针,那么这个程序将打印sizeof(char *),通常为4。但它打印1000。
另一个证明:
template <class T>
void f(T& obj)
{
   T x = obj; //this will fail to compile if T is an array type
}

int main()
{
   int a[30] = {};
   int* p = 0; 
   f(p); //OK
   f(a); //results in compile error. Remember f takes by ref therefore needs lvalue and no conversion applies
}

正式来讲,在左值到右值转换期间,数组会被转换为指向其第一个元素的指针。也就是说,当以数组类型的左值在需要右值的上下文中出现时,该数组会被转换为指向其第一个元素的指针。
此外,以传值方式声明的函数等同于以指针方式接受数组的函数,即:
void f(int a[]);
void f(int a[10]);
void f(int* a);

以下是三个等价的声明。希望有所帮助。

1
在C语言中,void f(int a[]);void f(int a[10]);void f(int* a);是完全等效的声明。 - pmg
我知道在C语言中它们是等效的,但我认为C++编译器足够聪明以区分它们。实际上只有在通过引用传递数组时才会有区别,在这种情况下,即使原型指定所需的缓冲区,也完全没有防止传递错误大小的缓冲区的保护措施。 - Ben Voigt
@Ben:请不要使用像“无意义”这样的词语。这并不是无意义的。正如我的第二个示例所清楚显示的那样,就像第一个示例一样,它表明数组不是指针。如果数组是指针,模板函数将被实例化为T = int,并且将成功编译。然而,由于参数是通过引用传递的,因此对f的第二次调用会实例化T = int[30]的f,这会导致编译时错误,因为初始化语法错误。如果参数是按值传递的,则对f的第二次调用将使用已经实例化的T = int的f。 - Armen Tsirunyan
@Ben:这个注释是正确的。如果函数采用值传递方式,并且传递了一个左值,则会进行左值到右值转换(包括数组到指针)。如果函数使用引用传递(包括对const的引用)并且传递了一个左值,则不会发生这样的转换。我知道rvalue可以绑定到const引用,但该事实与示例无关。还有其他异议吗? - Armen Tsirunyan
@pmg,作为形式参数声明是等价的,标准中有一段摘录作为该规则的例外。标准说所有内容都会被实现为指针,但是如果您将指向表达式的大小打印为printf,则会怎样呢? - Luis Colorado
显示剩余8条评论

2
在C++中,数组类型具有“大小属性”,因此可以使用该属性来获取数组的大小。
T a[10];
T b[20];

ab有不同的类型。

这使得可以使用以下代码:

template<typename T, size_t N>
void foo(T (&a)[N])
{
   ...
}

1
数组和指针之间最大的混淆点源于K&R(注:为The C Programming Language一书的作者)将声明为数组类型的函数参数的行为视为指针。以下这些声明
void foo(int a[]);
void foo(int *a);
是等效的,就我所知
void foo(int a[5]);
也是等效的,尽管我不确定编译器是否需要接受对后者函数内的a[6]的引用。在其他情况下,数组声明会为指定数量的元素分配空间。请注意:

typedef int foo[1];

任何类型为foo的声明都将为一个元素分配空间,而任何试图将foo作为函数参数传递的尝试都将传递地址。 这是我在研究va_list实现时学到的有用技巧。


这三个函数声明是相同的:它们都接受一个指向整数的指针。任何整数类型的数组或引用或指向整数的指针都可以传递给该函数,因为该函数通过值接受指针。 - Armen Tsirunyan
1
@Armen Tsirunyan:在我使用的所有编译器上,我知道这三个实际上是相同的;我不知道编译器是否允许检查下标的有效性;完全忽略数组边界规范似乎很愚蠢。边界是否需要为非负数? - supercat
好问题,我从未想过。我现在找不到标准中相关的条款,但是MSVC9.0和在线Comeau都要求边界为正数。 - Armen Tsirunyan

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