第2章:指针与数组的纠缠
数组和指针,在C语言里就像一对双胞胎。长得像,性格却完全不同。很多嵌入式开发者在这上面栽过跟头,我也不例外。
记得我刚入行时,有个同事调试了整整三天,最后发现是数组名和指针混用导致的bug。从那以后,我对这对「双胞胎」就格外上心。
2.1 数组名的本质
先问一个问题:int arr[10] 里的 arr 到底是什么?
很多人脱口而出:「数组名就是指针啊!」
错。大错特错。
数组名是一个地址常量,不是指针变量。它指向数组的第一个元素,但你不能修改它。
int arr[10];
arr = NULL; // 编译错误!arr是常量
int *p = arr; // 正确,p是指针变量
p = NULL; // 正确,p可以修改
我习惯把数组名理解为「数组的身份证号」——它标识了数组的位置,但你不能把身份证号改成别的号码。
| 特性 | 数组名 | 指针变量 |
|---|---|---|
| 可修改性 | 不可修改 | 可修改 |
| 存储空间 | 无独立存储(仅代表数组首地址) | 有独立存储(4/8字节) |
| sizeof结果 | 整个数组大小 | 指针本身大小 |
| &操作 | 取整个数组的地址 | 取指针变量的地址 |
关键区别:sizeof(arr) 返回 40(10个int),而 sizeof(p) 返回 4 或 8(指针大小)。
2.2 指针运算与数组遍历
指针运算的核心就一句话:指针加1,跳过一个元素。
听起来简单,但实际用起来有很多门道。
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;
// 三种遍历方式,性能一样
for (int i = 0; i < 5; i++) {
printf("%d ", arr[i]); // 下标法
printf("%d ", *(p + i)); // 指针偏移法
printf("%d ", *p++); // 指针移动法(注意p被修改)
}
我在项目中做过性能测试,这三种方式编译后的汇编代码几乎一样。现代编译器优化能力很强,别纠结哪种写法更快。
但有一个坑要注意:指针越界。
我曾经在STM32上写一个环形缓冲区,指针越界后直接写到了相邻的全局变量上。系统运行了3小时才崩溃,排查过程痛不欲生。
指针运算的步长由指向的类型决定:
int *p:p+1 跳 4 字节char *p:p+1 跳 1 字节short *p:p+1 跳 2 字节struct large *p:p+1 跳 sizeof(large) 字节
2.3 数组作为函数参数退化为指针
这是C语言里最容易被误解的特性之一。
当你把数组传给函数时,数组名会退化为指向第一个元素的指针。
void func(int arr[]) {
// 这里的arr已经是指针了!
printf("sizeof(arr) = %zu\n", sizeof(arr)); // 输出4或8
}
int main() {
int data[100];
printf("sizeof(data) = %zu\n", sizeof(data)); // 输出400
func(data);
return 0;
}
为什么会这样?
说白了,C语言设计者觉得「把整个数组拷贝进函数」太浪费了。传指针效率高,但代价就是丢失了数组长度信息。
我的习惯:凡是接收数组的函数,都额外传一个长度参数。别指望在函数内部用sizeof获取数组大小——那只会得到指针大小。
// 正确的做法
void process_array(int *arr, size_t len) {
for (size_t i = 0; i < len; i++) {
arr[i] *= 2;
}
}
// 调用
int buffer[256];
process_array(buffer, 256);
这里有个细节:int arr[] 和 int *arr 在函数参数中是等价的。我个人更倾向于用 int *arr,因为它明确告诉读者「这是个指针,不是数组」。
2.4 多维数组与指针
多维数组的指针操作,是很多人的噩梦。其实只要抓住一个核心:多维数组本质上是数组的数组。
int matrix[3][4]; // 3行4列
// matrix的类型是 int (*)[4]
// matrix[0]的类型是 int *
// matrix[0][0]的类型是 int
用指针访问多维数组:
int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
// 三种访问方式
int a = matrix[1][2]; // 下标法,值=7
int b = *(*(matrix + 1) + 2); // 指针法,值=7
int c = *((int *)matrix + 1*4 + 2); // 线性化访问,值=7
你想想看,第三种方式把二维数组当成一维来访问。这在嵌入式里很常见,比如操作帧缓冲区时,我们经常把二维像素数组当成线性内存来处理。
| 表达式 | 类型 | 值 |
|---|---|---|
| matrix | int (*)[4] | 整个数组首地址 |
| matrix + 1 | int (*)[4] | 第1行首地址(跳16字节) |
| *(matrix + 1) | int * | 第1行第0列地址 |
| *(matrix + 1) + 2 | int * | 第1行第2列地址 |
| *(*(matrix + 1) + 2) | int | 值7 |
避坑指南:多维数组作为函数参数时,除了第一维,其他维度必须指定。
// 正确
void func(int arr[][4], int rows);
// 错误!编译器不知道每行有多少列
void func(int arr[][], int rows);
嗯,这里要注意:int arr[][4] 实际上退化成了 int (*arr)[4],一个指向「包含4个int的数组」的指针。
我曾经在做一个图像处理项目时,需要把二维像素数组传给函数。一开始没指定列数,编译直接报错。后来改成 uint8_t frame[][320],问题解决。
性能调优小贴士:在嵌入式系统中,用指针遍历数组通常比下标法生成的代码更紧凑。但别盲目优化——先写清楚,再考虑优化。
总结一下本章的核心:
- 数组名是常量,指针是变量——别搞混
- 函数传数组,长度要另传——别指望sizeof
- 多维数组就是数组的数组——逐层理解
- 指针运算的步长由类型决定——算清楚再动