第4章:数组与指针的纠缠
数组和指针,这对冤家,是C语言里最容易让人头大的地方。我见过不少写了三五年C的工程师,面试时照样在这上面翻车。说白了,它们俩长得像,但骨子里又不一样。今天咱们就把这层窗户纸捅破。
4.1 数组名:它到底是不是指针?
很多人张口就来:「数组名就是常量指针」。嗯,这话说对了一半,但另一半会坑死人。
先看个例子:
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr; // 这行没问题
printf("%lu\n", sizeof(arr)); // 输出 20(5个int)
printf("%lu\n", sizeof(p)); // 输出 8(64位系统)
看到了吗?sizeof(arr) 是整个数组的大小,而 sizeof(p) 只是指针的大小。如果数组名真的是指针,那这两个结果应该一样才对。所以——数组名不是指针,它只是在大多数表达式中会「退化」成指向首元素的指针。
核心结论:数组名是「数组类型的标识符」,不是指针变量。你不能写 arr++ 或 arr = &arr[2],因为它没有自己的存储空间,它代表的是整个数组块。
我在项目中遇到过有人试图用 arr++ 来遍历数组,编译直接报错。他一脸懵:「指针不是可以自增吗?」我说:「你那是变量,这是常量,能一样吗?」
4.2 指针运算:遍历数组的正确姿势
既然数组名不能自增,那怎么遍历?用指针变量呗。
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;
for (int i = 0; i < 5; i++) {
printf("%d ", *(p + i)); // 等价于 p[i]
}
// 或者
for (p = arr; p < arr + 5; p++) {
printf("%d ", *p);
}
第二种写法里,p++ 每次跳过一个 int 的大小(4字节)。这就是指针运算的精髓——它知道指向的是什么类型,所以加减操作会自动按类型大小调整。
个人习惯:我一般用 p[i] 这种下标写法,可读性更好。只有在需要频繁移动指针位置时,才用 p++ 或 p += n。你想想看,代码是写给人看的,不是写给编译器看的。
指针相减也很有用:
int *start = arr;
int *end = arr + 5;
printf("元素个数: %ld\n", end - start); // 输出 5
两个指针相减,结果不是字节数,而是元素个数。这个特性在实现缓冲区、环形队列时特别方便。
4.3 数组作为函数参数:无声的退化
这是最容易被忽视的坑。你写了个函数:
void print_array(int arr[], int size) {
printf("sizeof(arr) = %lu\n", sizeof(arr)); // 输出 8,不是 20!
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
}
int main() {
int data[5] = {1, 2, 3, 4, 5};
printf("sizeof(data) = %lu\n", sizeof(data)); // 输出 20
print_array(data, 5);
return 0;
}
为什么会这样?因为 int arr[] 作为函数参数时,编译器自动把它解释成 int *arr。这就是所谓的「数组参数退化」。说白了,你传进去的不是整个数组,只是一个指针。
我曾经踩过的坑:有一次写一个排序函数,在函数内部用 sizeof(arr)/sizeof(arr[0]) 来算数组长度,结果死活不对。查了半天才发现,arr 已经退化成指针了,sizeof 返回的是指针大小。从那以后,我所有接收数组的函数,都老老实实多传一个 size 参数。
多维数组的情况更微妙:
void func(int arr[3][4]) { // 等价于 int (*arr)[4]
// arr 是一个指向「包含4个int的数组」的指针
}
void func2(int arr[][4], int rows) { // 更常见的写法
// 必须指定第二维大小
}
记住一条:只有第一维可以省略,后面的维度必须明确。因为编译器需要知道每行有多少个元素,才能正确计算偏移量。
4.4 知识体系一览
下面这张图把数组和指针的关系梳理清楚了:
4.5 实用技巧与避坑指南
- 传递数组时永远带上 size:别指望在函数内部用 sizeof 算长度,那是个幻觉。
- 区分 &arr 和 arr:
&arr的类型是int (*)[5],指向整个数组;arr退化为int*,指向首元素。两者数值相同,但类型不同,加减时的步长不一样。 - 指针加减要小心越界:C语言不检查边界,你
p += 100它也不报错,但运行时可能崩得很难看。 - const 修饰数组参数:如果函数不修改数组内容,写成
const int arr[],既安全又明确。
我个人的一个小技巧:在写嵌入式驱动时,经常需要操作寄存器数组。我会用 typedef 给数组类型起个别名,比如 typedef int buffer_t[256];,这样函数参数写 buffer_t buf 看起来更清晰,而且 sizeof 不会退化。当然,这只是语法糖,底层还是指针。
数组和指针的纠缠,说到底就是「名」与「实」的关系。数组名是个名字,指针是个变量。名字在某些场合可以当变量用,但终究不是变量。搞懂这一点,后面再学动态内存、字符串处理、函数指针,都会顺畅很多。
公众号:蓝海资料掘金营,微信deep3321