2. C语言基础回顾:数据类型、运算符与表达式、流程控制语句、数组与指针、函数

各位同学,咱们今天聊点实在的。C语言是嵌入式系统的灵魂,这话一点不夸张。我见过太多人,上来就啃RTOS、啃驱动,结果连个指针都搞不明白,最后代码跑飞了都不知道怎么查。说白了,C语言基础不牢,后面全是坑。

这一章,我带大家快速过一遍嵌入式开发中最常用的C语言核心知识点。别嫌啰嗦,这些都是我踩过坑之后才真正理解的东西。

2.1 数据类型——选对类型,省一半内存

嵌入式系统资源有限,RAM和Flash都是按KB算的。你写PC程序,int随便用,没人管你。但在单片机上,一个int占4字节,一个char占1字节,差别就出来了。

我个人习惯,能用uint8_t绝不用int。为什么?因为明确。uint8_t告诉你,这个变量范围0~255,不会出负数,也不会溢出(如果你小心的话)。

核心原则:嵌入式开发中,尽量使用中定义的类型,如uint8_t、uint16_t、int32_t等。这些类型跨平台、跨编译器,大小固定,不会因为换了个芯片就出问题。

类型 大小(常见) 取值范围 我常用的场景
uint8_t 1字节 0 ~ 255 状态标志、小计数器、GPIO电平
uint16_t 2字节 0 ~ 65535 ADC采样值、PWM占空比
uint32_t 4字节 0 ~ 4.29e9 时间戳、大循环计数
float 4字节 ±1.2e-38 ~ ±3.4e38 传感器数据(温度、电压)

注意:float在有些低端MCU上不支持硬件浮点运算,用起来很慢。我建议能用整数算就别用浮点。比如温度值,直接放大10倍存成整数,显示时再处理小数点。

2.2 运算符与表达式——别让优先级坑了你

运算符优先级,这东西看着简单,但坑起来真要命。我曾经调试一个I2C通信的bug,调了整整两天,最后发现是 if (a & b == 0) 被解析成了 if (a & (b == 0))。你说冤不冤?

我的建议很简单:拿不准优先级,就加括号。括号不花钱,但能省你两天时间。

嵌入式里常用的几个运算符,我列一下:

  • 位运算:&(与)、|(或)、^(异或)、~(取反)、<<(左移)、>>(右移)——寄存器操作全靠它们
  • 自增自减:++i 和 i++ 的区别——前置先加后用,后置先用后加
  • 三目运算符:? : ——写简单判断时很清爽,但别嵌套太多
// 位运算经典用法:操作寄存器
uint8_t reg = 0x00;

// 置位第3位
reg |= (1 << 3);      // reg = 0x08

// 清零第3位
reg &= ~(1 << 3);     // reg = 0x00

// 翻转第3位
reg ^= (1 << 3);      // reg = 0x08

小技巧:用宏定义给位操作起个名字,代码可读性会好很多。比如 #define SET_BIT(reg, bit) ((reg) |= (1 << (bit)))

2.3 流程控制语句——别写又臭又长的if-else

流程控制,说白了就是让程序按你的想法走。if-else、switch-case、for、while,这些大家都会用。但嵌入式里有个特殊要求:代码要快,要确定。

我见过有人写了一个if-else嵌套了七八层,看得我头皮发麻。其实很多时候,用switch-case或者查表法就能解决。

// 不好的写法:多层if-else
if (state == 0) {
    // do something
} else if (state == 1) {
    // do something else
} else if (state == 2) {
    // ...
}

// 好的写法:switch-case
switch (state) {
    case 0:
        // do something
        break;
    case 1:
        // do something else
        break;
    case 2:
        // ...
        break;
    default:
        // 错误处理
        break;
}

为什么推荐switch?因为编译器会把switch优化成跳转表,执行时间固定,不会像if-else那样一个个判断。在实时性要求高的地方,这点差别很关键。

注意:while(1)是嵌入式主循环的标配,但别忘了在里面加个看门狗喂狗操作,不然程序跑飞了都不知道。

2.4 数组与指针——嵌入式开发的灵魂

指针,C语言的精髓,也是很多人的噩梦。我刚开始学的时候也觉得指针玄乎,后来发现,说白了就是地址。你家的门牌号是地址,指针就是存这个门牌号的变量。

嵌入式里指针用得最多的地方:

  • 访问寄存器:把外设寄存器地址强制转换成指针,直接读写
  • 传递大块数据:传指针比传整个数组快得多
  • 动态内存管理:malloc/free,但嵌入式里慎用
// 直接操作寄存器地址(以STM32的GPIO为例)
#define GPIOA_BASE     0x40020000
#define GPIOA_ODR      (*(volatile uint32_t *)(GPIOA_BASE + 0x14))

// 置位PA0
GPIOA_ODR |= (1 << 0);

数组和指针的关系,记住一句话:数组名就是首元素的地址。但数组名是常量,不能自增自减;指针是变量,可以随便指。

避坑指南:我曾经在项目里用了一个野指针,结果程序跑着跑着就死机了。查了三天,发现是指针没有初始化,指向了一个随机地址。从那以后,我每次定义指针都会初始化成NULL,用之前再检查一下。

2.5 函数——模块化的基石

函数,就是把一段逻辑封装起来,给它起个名字,需要的时候调用就行。嵌入式里函数设计有几个要点:

  • 函数不宜太长:我一般控制在50行以内,超过就考虑拆分
  • 参数不宜太多:超过3个参数,考虑用结构体传参
  • 返回值要明确:成功返回0,失败返回错误码,这是行业惯例
// 好的函数设计示例
/**
 * @brief 初始化UART
 * @param baudrate 波特率
 * @return 0成功,-1失败
 */
int uart_init(uint32_t baudrate) {
    if (baudrate == 0) {
        return -1;  // 参数错误
    }
    
    // 配置寄存器...
    // 设置波特率...
    
    return 0;  // 成功
}

另外,嵌入式里函数调用是有开销的。每次调用都要压栈、跳转、返回。如果某个函数被频繁调用(比如中断服务函数),可以考虑用宏或者内联函数来减少开销。

小技巧:用static修饰函数,可以限制它的作用域只在当前文件。这样能避免命名冲突,也方便代码管理。我每个.c文件里都会把内部函数声明成static。

本章知识体系

下面这张图,是我自己总结的C语言基础在嵌入式中的核心逻辑。你看一眼,心里就有数了。

C语言基础回顾 数据类型 uint8_t / uint16_t float / 结构体 运算符 位运算 / 优先级 流程控制 if-else / switch-case for / while 数组与指针 地址 / 寄存器操作 函数 模块化 / 封装 核心思想 选对类型 → 写对运算 → 控制流程 用好指针 → 封装函数 → 稳定可靠

嗯,这一章的内容就这些。C语言基础看着简单,但真正用好、用对,需要大量的实践。我建议你每学一个知识点,都去开发板上跑一跑,看看实际效果。纸上得来终觉浅,绝知此事要躬行。

可可精品资料库