找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1544

积分

0

好友

200

主题
发表于 7 天前 | 查看: 32| 回复: 0

C语言中,指针是核心概念之一,抽象且语法复杂,导致部分工程师在项目开发中倾向于绕开它。然而,既然它被设计出来,必然有其独特的应用价值。特别是在资源受限、强调直接硬件交互的嵌入式系统中,指针绝非可有可无的高级特性,而是实现高效控制和资源管理的必备工具。

1. 指针的基本概念

C语言中的变量有多种类型,如 charintlong 等。指针也是一种变量类型,只不过它存储的内容比较特殊——不是具体的数据,而是内存地址

在相同的CPU架构下,不同类型的指针变量所占用的存储单元长度是相同的,而存放数据的变量则因数据类型不同占用不同大小的空间。下面通过一个例子来理解。

C语言指针声明与初始化代码示例

如上图代码所示:

  • 001 行:声明一个字符型变量 a 并赋值为 0x0A
  • 002 行:声明一个整型变量 b 并赋值为 0x0B
  • 003 行:声明一个字符型指针 p1,并让它指向变量 a 的地址 (&a)。
  • 004 行:声明一个整型指针 p2,并让它指向变量 b 的地址 (&b)。

这里的 * 用于声明指针变量,& 是取地址运算符。

假设是在一个32位系统架构下,char 类型占用1个字节,int 类型占用4个字节。那么指针变量 p1p2 各占用几个字节呢?内存分布图可以清晰地展示这一点。

32位系统下变量与指针的内存分布示意图

如上图所示,左侧是内存地址,中间是存储的数值,右侧是对应的变量名。

  • 变量 a 占用1个字节(地址 0x20 00 00 00)。
  • 变量 b 占用4个字节(地址 0x20 00 00 010x20 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。执行后,a0x0A 变为 0x0B。同理,(*p2)++b 的值从 0x0B 变为 0x0C

从理论上讲,这段代码没问题,但在实际中意义不大,因为用 a++b++ 就能轻松实现,无需引入指针的复杂性。原因在于,这些变量定义在内存(RAM)中,通过变量名可直接操作,指针的优势并未体现。

然而,在单片机或嵌入式系统中,情况截然不同。除了RAM,系统还有一片特殊的“内存”——寄存器。系统内许多外设模块(如GPIO、UART、定时器等)的初始化、配置和控制,都依赖于对这些寄存器的读写操作。每个寄存器在内存映射中都有一个固定的物理地址。

使用宏定义GPIO寄存器地址的代码示例

如上图所示,通过 #define 宏可以清晰定义GPIO模块的基地址和寄存器偏移量。例如,GPIOA_ODR 是端口A的输出数据寄存器。如果想通过 PA0 引脚输出高电平来点亮一个LED,就需要向 GPIOA_ODR 寄存器的第0位写入1。

如何写?必须通过指针。因为C语言不允许直接对内存地址进行赋值或读取,访问任何地址(包括寄存器地址)都必须借助指针。

uint32_t *gpio_odr = (uint32_t *)0x4001080C;
*gpio_odr |= 1;

定义指针变量并操作寄存器的代码示例

如上图代码所示:

  1. 定义一个 uint32_t 类型的指针变量 gpio_odr
  2. 将寄存器地址 0x4001080C 强制转换(uint32_t *) 类型后赋值给该指针。这一步至关重要,它告诉编译器这个地址应被视作一个指向32位无符号整数的指针。
  3. 通过解引用运算符 * 访问该指针指向的地址(即寄存器),并使用 |= 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个整数的数组 aMax 函数用于找出其中的最大值。它的形参 p 是一个整型指针。调用时,我们将数组名 a 传入。虽然数组名 a 本身不是指针变量,但在绝大多数上下文中,它会退化为指向其首元素的地址

于是,在 Max 函数内部,通过 p[i] 就能直接访问 main 函数中数组 a 的第 i 个元素,整个过程没有发生任何数据复制。这种方式不仅节约了内存,也提升了程序运行效率,是嵌入式内存管理中优化性能的常见手段。此外,通过指针参数还能实现函数返回多个值的功能。

4. 小结

综上所述,指针在嵌入式软件开发中扮演着不可或缺的角色,其必要性根植于系统对效率、资源直接控制和底层硬件交互的硬性要求。

  1. 硬件交互的唯一桥梁:嵌入式系统的核心是控制物理硬件。这些硬件的控制寄存器被映射到内存中的固定地址,而指针是C语言中直接访问这些物理地址的唯一合法且高效的机制。
  2. 提升内存与运行效率:嵌入式系统资源(RAM和CPU算力)通常有限。指针通过传递地址而非拷贝数据,极大地减少了在函数调用、处理大型数据结构时的内存开销和时间消耗。
  3. 实现灵活软件架构:指针为动态内存分配、回调函数、数据结构(如链表、树)的实现提供了基础,有助于构建更灵活、高效的软件架构。

因此,深入理解并合理运用指针,是每一位嵌入式开发者提升代码性能与底层控制力的必经之路。如果你想就C语言或嵌入式开发中的其他问题进行更深入的探讨,欢迎到云栈社区交流分享。




上一篇:SALA稀疏-线性混合注意力架构解析:9B模型实现3.5倍推理加速与超长上下文处理
下一篇:深入解析ELF中的.data.rel.ro节:只读重定位数据与RELRO安全机制
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-23 09:03 , Processed in 0.572873 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表