1 引言
考虑下面这段代码:
c
#include <stdio.h> int main(void) { int arr[5] = {10, 20, 30, 40, 50}; int *p = arr; /* p 指向 arr[0] */ printf("arr[2] = %d\n", arr[2]); /* 使用下标 */ printf("*(arr + 2) = %d\n", *(arr + 2)); /* 使用指针运算 */ printf("p[2] = %d\n", p[2]); /* 指针也可以使用下标 */ printf("*(p + 2) = %d\n", *(p + 2)); /* 标准的指针运算 */ return 0; }四种方式都输出30。这说明数组和指针在访问元素时可以互换使用。但它们完全等同吗?数组名是指针吗?本章将回答这些问题。
2 数组名的本质
2.1 数组名不是指针变量
数组名在大多数表达式中被当作指向数组首元素的指针,但它不是指针变量,而是一个地址常量。
c
int arr[5] = {1, 2, 3, 4, 5}; int *p = arr; /* 合法:arr 被当作指向首元素的指针 */ arr = p; /* 错误!arr 是常量,不能被赋值 */ arr++; /* 错误!arr 是常量,不能自增 */关键区别:
指针是变量,可以修改(指向别处)
数组名是常量,始终指向固定的地址(数组的首地址)
2.2 两个例外情况
数组名不是在任何情况下都等同于指针,有两个重要的例外:
例外1:对数组名使用 sizeof
c
int arr[5]; int *p = arr; printf("sizeof(arr) = %zu\n", sizeof(arr)); /* 20(整个数组的大小) */ printf("sizeof(p) = %zu\n", sizeof(p)); /* 8(指针本身的大小) */例外2:对数组名使用取地址 &
c
int arr[5]; int *p = arr; int (*parr)[5] = &arr; /* &arr 是指向整个数组的指针 */ printf("arr = %p\n", arr); /* 数组首元素地址 */ printf("&arr = %p\n", &arr); /* 也是数组首地址,但类型不同 */ printf("arr + 1 = %p\n", arr + 1); /* 增加 4 字节(一个 int) */ printf("&arr + 1 = %p\n", &arr + 1); /* 增加 20 字节(整个数组) */arr的类型是int*,指向第一个元素&arr的类型是int (*)[5],指向整个长度为5的数组
3 通过指针访问数组元素
3.1 基本方式
假设有:
c
int arr[5] = {10, 20, 30, 40, 50}; int *p = arr; /* p 指向 arr[0] */访问数组元素的几种等价方式:
| 访问方式 | 含义 | 示例结果 |
|---|---|---|
arr[i] | 数组下标 | arr[2] = 30 |
*(arr + i) | 指针运算 | *(arr + 2) = 30 |
p[i] | 指针下标 | p[2] = 30 |
*(p + i) | 指针运算 | *(p + 2) = 30 |
3.2 指针移动访问
c
#include <stdio.h> int main(void) { int arr[5] = {10, 20, 30, 40, 50}; int *p = arr; /* 方式1:通过移动指针遍历 */ for (int i = 0; i < 5; i++) { printf("%d ", *p); /* 输出当前指向的元素 */ p++; /* 指向下一个元素 */ } printf("\n"); /* 注意:p 已经指向数组末尾之后,需要重新指向开头 */ p = arr; /* 方式2:通过偏移量访问 */ for (int i = 0; i < 5; i++) { printf("%d ", *(p + i)); /* p 本身不变,通过偏移访问 */ } printf("\n"); return 0; }3.3 指针加减的步长
指针加减运算的步长由指向的类型决定:
c
char *cp; int *ip; double *dp; cp + 1; /* 地址增加 1 字节 */ ip + 1; /* 地址增加 4 字节(假设 int 占4字节) */ dp + 1; /* 地址增加 8 字节(假设 double 占8字节) */
这正是为什么*(p + i)能正确访问第 i 个元素的原因——编译器会自动根据类型计算偏移量。
4 下标运算的本质
4.1 下标运算的等价性
C语言标准规定:a[i]等价于*(a + i)。
这是一个非常重要的等价关系,它意味着:
下标运算是通过指针运算实现的
a[i]只是*(a + i)的语法糖
c
int arr[5] = {10, 20, 30, 40, 50}; /* 下面三行完全等价 */ arr[2] = 100; *(arr + 2) = 100; 2[arr] = 100; /* 是的,这也可以!但千万别这么写 */4.2 为什么可以写成 2[arr]?
根据等价关系:
arr[2]→*(arr + 2)2[arr]→*(2 + arr)
加法满足交换律,所以*(arr + 2)和*(2 + arr)是一样的。因此2[arr]在语法上是合法的,但这种写法极其不推荐,会严重降低代码可读性。
c
/* 合法但反人类的写法 */ int arr[5] = {10, 20, 30, 40, 50}; printf("%d\n", 2[arr]); /* 输出 30 */4.3 对指针使用下标
因为p[i]等价于*(p + i),所以指针也可以使用下标:
c
int arr[5] = {10, 20, 30, 40, 50}; int *p = arr + 2; /* p 指向 arr[2] */ printf("%d\n", p[0]); /* 30,即 arr[2] */ printf("%d\n", p[-1]); /* 20,即 arr[1] —— 负下标也是合法的! */ printf("%d\n", p[2]); /* 50,即 arr[4] */重要:指针可以使用负下标,只要结果指针仍在数组范围内。这给了我们很大的灵活性,但也要格外小心越界。
5 数组作为函数参数
5.1 数组参数的退化
当数组作为函数参数时,它退化为指针:
c
#include <stdio.h> void func1(int arr[]) /* 这里的 arr 实际上是指针 */ { printf("在 func1 中,sizeof(arr) = %zu\n", sizeof(arr)); /* 8(指针大小) */ } void func2(int *arr) /* 和上面完全等价 */ { printf("在 func2 中,sizeof(arr) = %zu\n", sizeof(arr)); /* 也是 8 */ } int main(void) { int arr[10] = {0}; printf("在 main 中,sizeof(arr) = %zu\n", sizeof(arr)); /* 40 */ func1(arr); func2(arr); return 0; }重要结论:
在函数内部无法通过
sizeof(arr)/sizeof(arr[0])获取数组长度必须额外传递一个参数表示数组长度
5.2 函数参数的几种等价写法
c
/* 这三种写法在函数内部完全等价 */ void print_array(int arr[], int size) void print_array(int *arr, int size) void print_array(int arr[10], int size) /* 10 被忽略,只是文档作用 */ { for (int i = 0; i < size; i++) { printf("%d ", arr[i]); /* 或 *(arr + i) */ } }5.3 为什么要传递长度
c
#include <stdio.h> /* 错误的尝试:试图在函数内计算长度 */ void bad_print(int arr[]) { int size = sizeof(arr) / sizeof(arr[0]); /* size = 8/4 = 2,错误! */ for (int i = 0; i < size; i++) { printf("%d ", arr[i]); } } /* 正确的方式 */ void good_print(int arr[], int size) { for (int i = 0; i < size; i++) { printf("%d ", arr[i]); } } int main(void) { int arr[10] = {1,2,3,4,5,6,7,8,9,10}; bad_print(arr); /* 只输出前两个元素! */ printf("\n"); good_print(arr, 10); /* 正确输出全部 */ return 0; }6 指针数组 vs 数组指针
这两个概念容易混淆,需要区分清楚:
6.1 指针数组
指针数组:数组的每个元素都是指针。
c
int *arr[5]; /* 包含5个 int* 的数组 */
c
int a = 10, b = 20, c = 30; int *arr[3] = {&a, &b, &c}; for (int i = 0; i < 3; i++) { printf("%d ", *arr[i]); /* 输出 10 20 30 */ }6.2 数组指针
数组指针:指向整个数组的指针。
c
int (*p)[5]; /* p 是一个指针,指向包含5个int的数组 */
c
int arr[5] = {1,2,3,4,5}; int (*p)[5] = &arr; /* p 指向整个数组 */ for (int i = 0; i < 5; i++) { printf("%d ", (*p)[i]); /* 通过指针访问数组元素 */ }6.3 区别总结
| 写法 | 名称 | 含义 |
|---|---|---|
int *p[5] | 指针数组 | 一个数组,包含5个int*元素 |
int (*p)[5] | 数组指针 | 一个指针,指向包含5个int的数组 |
7 综合示例
7.1 数组逆序(使用指针)
c
#include <stdio.h> void reverse_array(int *start, int *end) { /* end 指向最后一个元素 */ while (start < end) { int temp = *start; *start = *end; *end = temp; start++; end--; } } int main(void) { int arr[10] = {1,2,3,4,5,6,7,8,9,10}; printf("原数组:"); for (int i = 0; i < 10; i++) { printf("%d ", arr[i]); } printf("\n"); reverse_array(arr, arr + 9); /* 传递首尾指针 */ printf("逆序后:"); for (int i = 0; i < 10; i++) { printf("%d ", arr[i]); } printf("\n"); return 0; }7.2 数组的切片访问
c
#include <stdio.h> void print_slice(int *start, int *end) { printf("["); while (start < end) { printf("%d ", *start++); } printf("%d]\n", *end); } int main(void) { int arr[10] = {1,2,3,4,5,6,7,8,9,10}; print_slice(arr, arr + 4); /* 前5个元素 [1 2 3 4 5] */ print_slice(arr + 3, arr + 7); /* 中间5个元素 [4 5 6 7 8] */ print_slice(arr + 5, arr + 9); /* 后5个元素 [6 7 8 9 10] */ return 0; }7.3 字符串数组与指针
c
#include <stdio.h> int main(void) { /* 二维字符数组:每个字符串存储在连续的内存中 */ char names1[][10] = {"Alice", "Bob", "Charlie"}; /* 指针数组:每个元素指向字符串常量 */ char *names2[] = {"Alice", "Bob", "Charlie"}; /* 访问方式相同 */ for (int i = 0; i < 3; i++) { printf("names1[%d] = %s\n", i, names1[i]); printf("names2[%d] = %s\n", i, names2[i]); } /* 重要区别:能否修改 */ names1[0][0] = 'a'; /* 可以修改(在栈上) */ /* names2[0][0] = 'a'; */ /* 危险!字符串常量在只读区,不能修改 */ return 0; }8 常见错误与注意事项
8.1 混淆数组和指针的 sizeof
c
void func(int arr[]) { int size = sizeof(arr) / sizeof(arr[0]); /* 错误!arr 是指针 */ }8.2 数组名自增
c
int arr[5]; arr++; /* 错误!数组名是常量 */
8.3 指针越界
c
int arr[5] = {1,2,3,4,5}; int *p = arr + 5; /* 指向数组末尾之后(允许) */ int x = *p; /* 危险!越界访问 */8.4 混淆指针数组和数组指针
c
int *p1[5]; /* 指针数组 */ int (*p2)[5]; /* 数组指针 */ p2 = &arr; /* 正确 */ p1 = &arr; /* 错误!类型不匹配 */
8.5 试图返回局部数组
c
int* get_array(void) { int arr[5] = {1,2,3,4,5}; return arr; /* 危险!arr 是局部变量 */ }9 本章小结
本章系统介绍了指针与数组的关系:
1. 数组名的本质
大多数情况下被当作指向首元素的指针
但它是常量,不能修改
两个例外:
sizeof和&运算符
2. 访问元素的等价方式
a[i]等价于*(a + i)指针也可以用下标:
p[i]等价于*(p + i)甚至
i[a]也合法(但千万别用)
3. 指针加减的步长
由指向的类型决定
p + i增加i * sizeof(指向类型)字节
4. 数组作为函数参数
退化为指针
必须额外传递长度
三种写法等价:
int arr[]、int *arr、int arr[10]
5. 容易混淆的概念
指针数组:
int *p[5],数组元素是指针数组指针:
int (*p)[5],指向整个数组的指针
6. 核心结论
text
a[i] == *(a + i) == i[a] p[i] == *(p + i)
理解这个等价关系,就掌握了 C 语言中数组和指针的精髓。