2. 嵌入式C语言基础回顾

数组和指针,在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],问题解决。

指针与数组核心关系图 数组名 arr 指针变量 p 地址常量,不可修改 地址变量,可修改 函数参数:退化为指针 int arr[] → int *arr 多维数组:数组的数组 int arr[3][4] → int (*arr)[4] 本质不同 核心:数组名不是指针,但大多数情况下会「表现」得像指针

性能调优小贴士:在嵌入式系统中,用指针遍历数组通常比下标法生成的代码更紧凑。但别盲目优化——先写清楚,再考虑优化。

总结一下本章的核心:

  • 数组名是常量,指针是变量——别搞混
  • 函数传数组,长度要另传——别指望sizeof
  • 多维数组就是数组的数组——逐层理解
  • 指针运算的步长由类型决定——算清楚再动