您这个说法非常准确,可以说是C语言中一个核心且重要的概念,我们可以从几个层面来深入理解“数组名是一个不可变的左值(const lvalue)”这句话。

(图片来源网络,侵删)
数组名的本质:指向首元素的“常量”指针
在大多数情况下,数组名会被“衰变”(decay)为其首元素的地址,这个地址值被存储在一个指针常量中。
让我们看一个例子:
int arr[5] = {10, 20, 30, 40, 50};
在这个例子中:
arr是一个包含5个整数的数组。- 当我们使用
arr时(在printf("%p", arr);中),它会被编译器视为一个指向第一个元素arr[0](值为10)的指针。 - 这个指针的值是固定的,因为它代表了数组在内存中的起始位置,你不能改变这个起始位置。
为什么说它是“不可变的”?
“不可变”主要体现在你不能对数组名本身进行赋值运算,这会导致编译错误。

(图片来源网络,侵删)
1 尝试修改数组名的地址(直接赋值)
这是最直接的错误尝试,你无法让数组名指向一个不同的内存地址。
int arr[5] = {1, 2, 3, 4, 5};
int another_arr[5] = {6, 7, 8, 9, 10};
// 以下代码是错误的,会导致编译错误
arr = another_arr; // 错误: assignment to expression with array type 'int[5]'
编译错误信息:GCC/Clang 会报错 error: assignment to expression with array type 'int[5]',这清晰地表明,你不能给一个数组类型的表达式(即数组名)赋值。
2 尝试修改数组名的值(通过指针算术)
即使你将数组名赋给一个指针,你也不能通过这个指针来修改原始数组名的“地址”值。
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr; // p 指向 arr 的首元素,p == arr
// 以下代码是错误的,但错误可能不那么直观
p++; // p 现在指向 arr[1],这是合法的指针操作
// 这并没有改变 arr 本身!
// arr 仍然指向 arr[0] 的原始地址
printf("p = %p\n", (void*)p); // 输出: p = (地址+4)
printf("arr = %p\n", (void*)arr); // 输出: arr = (原始地址)
在这个例子中,p 是一个普通指针,可以被修改,但 arr 本身没有被修改,它依然牢牢地“钉”在数组的起始位置上。p = arr 只是复制了地址值,p 和 arr 是两个不同的变量,p 是可变的,而 arr 是不可变的。
数组名与指针的关键区别
尽管数组名在很多情况下会衰变成指针,但它们在C语言中不是一回事,理解它们的区别是掌握C语言的关键。
| 特性 | 数组名 | 指针 |
|---|---|---|
| 本质 | 代表整个数组,是一个符号常量,其值是数组首地址。 | 一个变量,存储一个内存地址。 |
sizeof |
sizeof(arr) 返回整个数组占用的字节大小。int arr[5]; 则 sizeof(arr) 是 5 * sizeof(int)。 |
sizeof(ptr) 返回指针变量本身占用的字节大小(在32位系统上是4字节,64位上是8字节)。 |
| 地址 | &arr 和 arr 的值(地址)可能相同,但类型不同。&arr 的类型是 "指向数组 N 个 int 的指针" (int (*)[N])。 |
&ptr 是取指针变量的地址,其类型是 "指向指针的指针" (int **)。 |
| 可变性 | 不可变,不能被重新赋值指向别处。 | 可变,可以指向任何兼容类型的内存地址(ptr++, ptr = another_ptr 都是合法的)。 |
示例:sizeof 的区别
int arr[5] = {0};
int *ptr = arr;
printf("sizeof(arr) = %zu\n", sizeof(arr)); // 输出: sizeof(arr) = 20 (假设int为4字节)
printf("sizeof(ptr) = %zu\n", sizeof(ptr)); // 输出: sizeof(ptr) = 8 (在64位系统上)
这个例子最能体现数组名和指针的根本不同。
哪些情况下数组名不会“衰变”为数组指针?
有两种主要情况数组名会保持其“数组”身份,不会衰变:
sizeof操作符:如上所述,sizeof(arr)计算的是整个数组的大小。&(取地址) 操作符:&arr返回的是指向整个数组的指针,而不是指向首元素的指针。
int arr[5] = {10, 20, 30, 40, 50};
// 情况1: sizeof
size_t arr_size = sizeof(arr); // arr_size 是 5 * sizeof(int)
// 情况2: &
int (*p_arr)[5] = &arr; // p_arr 是一个指向包含5个int的数组的指针
// 注意:这里的类型是 int (*)[5],而不是 int*
// 解引用 &arr 和 arr 的结果是一样的
printf("%d\n", *arr); // 输出 10 (arr[0])
printf("%d\n", **(&arr)); // 输出 10 (先通过 &arr 找到整个数组,再解引用得到首元素)
尽管 *arr 和 **(&arr) 的结果一样,但它们的操作过程和类型是不同的。
- 核心观点:数组名是一个不可变的左值,它的值(即数组的首地址)是固定的,在程序的整个生命周期中都不能被改变。
- 衰变现象:在大多数表达式(如作为函数参数、进行指针算术等)中,数组名会“衰变”为一个指向其首元素的指针常量。
- 根本区别:数组名和指针是两种不同的类型。
sizeof操作符是区分它们最简单的方法,数组名代表整个数组,而指针只是一个地址变量。 - 例外情况:当数组名作为
sizeof或&操作符的操作数时,它不会衰变,而是保持其完整的数组身份。
理解了这一点,你就能明白为什么不能 arr = ptr,也能更好地理解C语言中数组与指针之间复杂而微妙的关系。
