我们在学习C语言时,常常会听到一个说法:数组名可以用来表示数组的首地址。既然指针就是用来存放地址的,那么数组名是不是就等于指针呢?这个问题困扰了许多初学者,甚至在云栈社区的C/C++板块也屡见不鲜。今天,我们就来彻底厘清它们之间的关系。
看下面这段代码,似乎能支持“数组名是指针”的观点:
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr; //arr表示数组的首地址
void main()
{
int a = *arr; // *arr == arr[0]
}

示例中,我们定义了一个数组 arr,它有5个元素。接着定义了一个指针 p,并将 arr 的值赋给它。因为 arr 在表达式中代表数组的首地址,所以这个赋值是合法的。在主函数里,*arr 是对该地址的解引用,相当于 arr[0],其值 1 被赋给了变量 a。
你看,在这个例子里,数组名 arr 的用法和指针 p 几乎一模一样。那是不是就能证明数组名就是指针了呢?别急,让我们再深入探究一下。
数组名、指针常量与指针变量
数组是在连续内存空间中存放的一组相同类型的数据,而数组名就是这组数据的标识符。编译器确实会将数组名视为指向数组第一个元素的地址。也就是说,在上面的代码中,arr 等价于 &arr[0],它的类型是 int*。在大多数表达式中,数组名会“退化”为指向首元素的指针,因此 *(arr + i) 就等价于 arr[i]。
但是,数组一旦定义,它在内存中的位置就固定了,这个地址是不可更改的。所以,数组名不能被重新赋值。

int arr1[5] = {1, 2, 3, 4, 5};
int arr2[5] = {6, 7, 8, 9, 10};
arr2 = arr1; //错误,数组名arr2是常量地址,不能作为左值被赋值
arr2++; //错误,数组名arr2是常量地址,不能作为左值被修改
如示例所示,虽然数组名在表达式中可以像指针一样用于访问元素,但其本身的值不可更改。而指针(例如 int *p;)是一种专门用于存放地址的变量,其值是可以改变的。
因此,即使说数组名是指针,它也更像一个指向数组首元素的指针常量。但它真的是指针常量吗?
严格来说,也不是。数组名是编译器符号表中的一个地址标记,它本身并不是一个变量,不占用独立的内存空间来存储这个地址值。而指针常量(如 int *const p;)虽然存储的地址不可变,但它本身作为一个变量,是需要占用内存空间、拥有自己地址的。这一点,可以从后续的几个关键区别中得到验证。
数组名不是指针的三种情况
尽管数组名在大多数情况下会“退化”为指针,但它毕竟是“数组”的名字。在以下三种情况下,它不会退化,而是代表整个数组本身。
1. 在 sizeof 运算符中

int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
int a, b;
a = sizeof(p); // 32位系统,指针字节数 a = 4
b = sizeof(arr); // 32位系统,数组总字节数 b = 4 * 5 = 20
指针 p 保存的是地址,在32位系统中,地址长度是4字节,所以 sizeof(p) 返回4。如果 arr 也是指针,它也应该返回4。但实际返回的是20,即5个 int 元素的总大小。这说明在 sizeof 运算中,数组名代表的是整个数组,而非一个地址。
2. 在取地址 & 操作符中

int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
int a, b;
a = &p; // a为指针“p”的地址
b = &arr; // b为数组的首地址,亦即“&arr[0]”的地址
&p 返回的是指针变量 p 自身的地址。而 &arr 的数值虽然等于 &arr[0],但其类型是指向整个数组的指针(int (*)[5]),而不是指向 int 的指针(int*)。&arr 无法返回“指向首元素的指针的地址”,因为数组名本身没有独立的存储空间。
为了更直观地理解,我们来看内存布局:

假设数组 arr 的首地址是 0x20000000,指针 p 中保存的就是这个地址。指针 p 自身占用内存(假设地址是 0x20000014),所以 &p 得到 0x20000014。而 arr 只是一个标识符,没有独立的内存空间来存储 0x20000000 这个值,因此 &arr 直接得到数组的起始地址 0x20000000。
3. 用字符串常量初始化字符数组时
char arr[5] = "Hello";
这里,字符串常量 "Hello" 的内容会被拷贝到数组 arr 的栈内存中。此时,arr 代表的是用于接收这串字符的整个内存空间,不会退化为一个指针。
综上所述,数组名在 sizeof、& 和字符串常量初始化字符数组这三种情况下,不会退化为指针,而是代表整个数组,并且它自身不占用存储地址值的内存空间。
数组名是指针的特例:函数形参
前面提到,数组名在大多数情况下会退化为指针,但它是一个不占内存的“地址常量”。然而,有一种特殊情况会让数组名完全等同于一个真正的指针变量,那就是当它作为函数形参时。
当你在函数声明中使用数组作为形参,例如:

void function(int arr[]) {
...
}
编译器实际上会将其视为:

void function(int *arr) {
...
}
也就是说,当数组作为函数参数传递时,它必然退化为一个真正的指针变量 arr。这个 arr 会在函数调用时被分配内存,用于存储传进来的实参数组首地址。
正因为如此,前面提到的限制在此刻被打破了。我们知道,对数组名进行自增 (arr++) 操作是非法的,因为它是常量。但在函数形参中,这变得合法:

void function(int arr[]) {
arr++; // 合法!因为这里的arr已经是一个指针变量
}
同样,sizeof 的行为也发生了变化。在函数外部,sizeof(arr) 返回整个数组的大小。但在函数内部,由于 arr 已经是一个指针,sizeof(arr) 返回的是指针本身的大小。

int function(int arr[]) {
return sizeof(arr); // 返回指针的大小,不是数组的大小
}
如果这是一个32位系统,上面的函数将返回4,而不是实参数组的总字节数。这也是为什么在函数中处理数组时,通常需要额外传递数组长度的原因。
总结
让我们来梳理一下核心结论,这能帮助我们更好地理解计算机基础中关于内存寻址的核心概念:
-
常态(退化):在绝大多数表达式(如赋值、算术运算、下标访问)中,数组名会隐式转换为指向其首元素的指针(int*类型)。你可以把它想象成一个绑定到固定地址的常量标识符,其行为类似于指针常量,但自身不占用内存存储这个地址值。
-
特例(不退化):在 sizeof、取地址 & 以及用字符串常量初始化字符数组这三种情况下,数组名代表整个数组对象本身,不会退化为指针。
-
特例中的特例(完全成为指针变量):当数组名作为函数形参时,它100%等价于一个指针变量。编译器会为其分配内存来存储地址,因此它可以被赋值、修改(如 arr++),并且 sizeof 操作返回的是指针大小。
所以,简单粗暴地回答“数组名是不是指针”是不准确的。更严谨的说法是:数组名在某些上下文里表现得像指针,但它本质上是数组的标识符;只有在作为函数参数时,它才彻底变成一个纯粹的指针变量。 理解这个细微差别,对于掌握C语言的内存模型和编写正确的程序至关重要。