C语言中,指针是核心概念之一,抽象且语法复杂,导致部分工程师在项目开发中倾向于绕开它。然而,既然它被设计出来,必然有其独特的应用价值。特别是在资源受限、强调直接硬件交互的嵌入式系统中,指针绝非可有可无的高级特性,而是实现高效控制和资源管理的必备工具。
1. 指针的基本概念
C语言中的变量有多种类型,如 char、int、long 等。指针也是一种变量类型,只不过它存储的内容比较特殊——不是具体的数据,而是内存地址。
在相同的CPU架构下,不同类型的指针变量所占用的存储单元长度是相同的,而存放数据的变量则因数据类型不同占用不同大小的空间。下面通过一个例子来理解。

如上图代码所示:
001 行:声明一个字符型变量 a 并赋值为 0x0A。
002 行:声明一个整型变量 b 并赋值为 0x0B。
003 行:声明一个字符型指针 p1,并让它指向变量 a 的地址 (&a)。
004 行:声明一个整型指针 p2,并让它指向变量 b 的地址 (&b)。
这里的 * 用于声明指针变量,& 是取地址运算符。
假设是在一个32位系统架构下,char 类型占用1个字节,int 类型占用4个字节。那么指针变量 p1 和 p2 各占用几个字节呢?内存分布图可以清晰地展示这一点。

如上图所示,左侧是内存地址,中间是存储的数值,右侧是对应的变量名。
- 变量
a 占用1个字节(地址 0x20 00 00 00)。
- 变量
b 占用4个字节(地址 0x20 00 00 01 到 0x20 00 00 04)。
- 指针
p1 存储的数值是 0x20 00 00 00,正是变量 a 的地址。
- 指针
p2 存储的数值是 0x20 00 00 01,对应变量 b 的首字节地址(注:图中假设为小端存储模式)。
可以看到,无论是字符型指针 p1 还是整型指针 p2,它们本身都占用了4个字节。这是因为32位系统的地址总线宽度就是4个字节,所有指针变量都需要这么大的空间来存储一个地址信息。指针变量本身也是一个变量,占用内存空间,只是它存储的是一个地址,而非普通数据。
2. 指针与寄存器操作:嵌入式开发的刚需
上面的例子用内存变量解释了指针的概念。在这种场景下,我们也可以这样使用指针:

这段代码中,(*p1)++ 表示先解引用指针 p1(即访问 a 的值),然后将该值加1。执行后,a 从 0x0A 变为 0x0B。同理,(*p2)++ 将 b 的值从 0x0B 变为 0x0C。
从理论上讲,这段代码没问题,但在实际中意义不大,因为用 a++ 和 b++ 就能轻松实现,无需引入指针的复杂性。原因在于,这些变量定义在内存(RAM)中,通过变量名可直接操作,指针的优势并未体现。
然而,在单片机或嵌入式系统中,情况截然不同。除了RAM,系统还有一片特殊的“内存”——寄存器。系统内许多外设模块(如GPIO、UART、定时器等)的初始化、配置和控制,都依赖于对这些寄存器的读写操作。每个寄存器在内存映射中都有一个固定的物理地址。

如上图所示,通过 #define 宏可以清晰定义GPIO模块的基地址和寄存器偏移量。例如,GPIOA_ODR 是端口A的输出数据寄存器。如果想通过 PA0 引脚输出高电平来点亮一个LED,就需要向 GPIOA_ODR 寄存器的第0位写入1。
如何写?必须通过指针。因为C语言不允许直接对内存地址进行赋值或读取,访问任何地址(包括寄存器地址)都必须借助指针。
uint32_t *gpio_odr = (uint32_t *)0x4001080C;
*gpio_odr |= 1;

如上图代码所示:
- 定义一个
uint32_t 类型的指针变量 gpio_odr。
- 将寄存器地址
0x4001080C 强制转换为 (uint32_t *) 类型后赋值给该指针。这一步至关重要,它告诉编译器这个地址应被视作一个指向32位无符号整数的指针。
- 通过解引用运算符
* 访问该指针指向的地址(即寄存器),并使用 |= 1 操作将其第0位置1。
也可以不定义中间变量,直接进行操作:
*( (uint32_t*)0x4001080C ) |= 1;

这条语句从右向左看:(uint32_t*)0x4001080C 将地址强制转换为指针;最前面的 * 是解引用运算符,访问该地址;|= 1 执行置位操作。这样就把地址为 0x4001080C 的寄存器第0位置为了1。
因此,在嵌入式系统,尤其是底层驱动开发中,对寄存器的操作必须使用指针。即使你使用的是厂商提供的库函数(如HAL库中的 HAL_GPIO_WritePin()),这些函数在底层封装中,最终仍然是通过指针来访问寄存器的。
3. 传递大容量函数参数:提升效率的关键
除了操作硬件寄存器,指针在软件层面也能发挥巨大作用,尤其是在函数参数传递时。
通常,函数参数采用“值传递”方式。看看下面的例子:
#include <stdio.h>
void increment(int num) {
num = num + 1; // 修改形参
printf("函数内部: num = %d\n", num);
}
int main() {
int x = 10;
increment(x); // 调用函数,实参为x
printf("函数外部: x = %d\n", x);
return 0;
}

运行结果将是:
函数内部: num = 11
函数外部: x = 10
这说明,子函数内部修改的是形参 num(实参 x 的一个副本),不会影响主函数中的实参 x。这种机制保护了原始数据,但会带来一个问题:当需要传递大型数据结构(如包含成百上千个元素的数组或大型结构体)时,在栈上复制一份副本会消耗可观的内存和时间,在资源紧张的嵌入式系统中这是不可接受的。
此时,指针传递就成为了更优选择。它传递的是数据的地址,而非数据本身,避免了不必要的拷贝。
int Max(int *p) {
int i;
int max=p[0];
for(i=1;i<10;i++) {
if(p[i]>max) {
max=p[i];
}
}
return max;
}
int main(void) {
int a[]={1,2,3,4,5,6,7,8,9,10};
int MaxValue;
MaxValue=Max(a);
return 0;
}

在这个例子中,main 函数有一个包含10个整数的数组 a。Max 函数用于找出其中的最大值。它的形参 p 是一个整型指针。调用时,我们将数组名 a 传入。虽然数组名 a 本身不是指针变量,但在绝大多数上下文中,它会退化为指向其首元素的地址。
于是,在 Max 函数内部,通过 p[i] 就能直接访问 main 函数中数组 a 的第 i 个元素,整个过程没有发生任何数据复制。这种方式不仅节约了内存,也提升了程序运行效率,是嵌入式内存管理中优化性能的常见手段。此外,通过指针参数还能实现函数返回多个值的功能。
4. 小结
综上所述,指针在嵌入式软件开发中扮演着不可或缺的角色,其必要性根植于系统对效率、资源直接控制和底层硬件交互的硬性要求。
- 硬件交互的唯一桥梁:嵌入式系统的核心是控制物理硬件。这些硬件的控制寄存器被映射到内存中的固定地址,而指针是C语言中直接访问这些物理地址的唯一合法且高效的机制。
- 提升内存与运行效率:嵌入式系统资源(RAM和CPU算力)通常有限。指针通过传递地址而非拷贝数据,极大地减少了在函数调用、处理大型数据结构时的内存开销和时间消耗。
- 实现灵活软件架构:指针为动态内存分配、回调函数、数据结构(如链表、树)的实现提供了基础,有助于构建更灵活、高效的软件架构。
因此,深入理解并合理运用指针,是每一位嵌入式开发者提升代码性能与底层控制力的必经之路。如果你想就C语言或嵌入式开发中的其他问题进行更深入的探讨,欢迎到云栈社区交流分享。