C语言基础回顾(2026/7/20)

目录

第八阶段:预处理和模块化

一、预处理是什么

二、#include

三、#define

1. 定义常量

2. 定义位掩码

3. 带参数的宏

4. 多语句宏

四、条件编译

1. #ifdef

2. #ifndef

3. #if、#elif、#else

4. 功能开关

5. 暂时屏蔽代码

五、头文件保护

六、.h 和 .c 的分工

1:.h 文件:对外接口

2:.c 文件:具体实现

七、extern

八、static 文件内封装

1. 局部变量:是否需要记住上次的值

2. 文件中的全局变量:其他文件是否需要访问

3. 函数:它是不是模块的公开接口

4.STM32实例

5.不要滥用局部 static

九、多文件工程如何连接

十、模块接口设计


第八阶段:预处理和模块化

一、预处理是什么

C 代码并不是直接编译的,大致过程是:

C 代码并不是直接编译的,大致过程是: 源代码 ↓ 预处理:处理#include、#define、条件编译 ↓ 编译:转换成汇编代码 ↓ 汇编:生成.o目标文件 ↓ 链接:合并所有.o和库文件 ↓ 生成.axf/.elf ↓ 生成.hex/.bin 预处理发生在正式编译之前,本质上以文本处理为主。

二、#include

#include的作用是把另一个文件的内容引入当前位置

STM32 工程常见写法:

#include "stm32f10x.h" #include "Delay.h" #include "OLED.h" #include "LED.h" #include "LED.c" // 错误习惯

.c文件应当分别编译,模块之间通过.h文件建立联系。

三、#define

1. 定义常量
#define LED_PIN GPIO_Pin_13 #define BUFFER_SIZE 128 #define PI 3.1415926f
uint8_t buffer[BUFFER_SIZE]; 预处理阶段会进行文本替换: uint8_t buffer[128]; 宏本身通常不占用独立运行内存。

实例:

#include <stdio.h> /* ========== 1. 定义常量 ========== */ #define LED_PIN 13 #define UART_RATE 115200 int main(void) { /* 测试 1:常量 */ printf("LED_PIN = %d\n", LED_PIN); printf("UART_RATE = %d\n", UART_RATE); }
2. 定义位掩码
#define MOTOR_ENABLE (1U << 0) #define MOTOR_ALARM (1U << 1) 使用: status |= MOTOR_ENABLE;

实例:

#include <stdio.h> /* ========== 2. 定义位掩码 ========== */ #define BIT0 (1u << 0) // 0000 0001 #define BIT3 (1u << 3) // 0000 1000 #define BIT7 (1u << 7) // 1000 0000 int main() { /* 测试 :位掩码 */ unsigned char reg = 0x00; // 模拟一个 8 位寄存器 reg |= BIT3; // 打开第 3 位 -> 0000 1000 reg |= BIT7; // 打开第 7 位 -> 1000 1000 // 所以此时 reg 的二进制是:1000 1000(十六进制 0x88)。 /*%02X 是什么意思? %X :以十六进制大写打印 2 :最少占 2 位字符宽度。 0 :如果不够 2 位,前面补 0。 reg 现在是 0x88,已经是两位了,所以直接输出:0x88 */ printf("Register value = 0x%02X\n", reg); /*第二句:检查第 0 位是不是开着 BIT0 是define BIT0 (1u << 0,为二进制 0000 0001 按位与上第0位, 只要第 0 位是 1,结果才是非 0(真);现在是 0,所以条件为假。输出:OFF */ printf("Check BIT0: %s\n", (reg & BIT0) ? "ON" : "OFF"); }
3. 带参数的宏
#define SQUARE(x) ((x) * (x)) 使用: result = SQUARE(5); 展开后: result = ((5) * (5)); 宏参数和整个表达式都应该加括号: #define SQUARE(x) ((x) * (x)) // 推荐 #define SQUARE(x) x * x // 容易出错 例如错误宏: #define SQUARE(x) x * x result = SQUARE(1 + 2); 展开成: result = 1 + 2 * 1 + 2; // 结果为5,不是9 还要避免: SQUARE(i++); 因为展开后 i++ 会执行两次。复杂逻辑更适合使用函数或 static inline 函数

宏不是函数,是文本替换:

1: #define SQUARE(x) ((x) * (x)) 你在代码里写: result = SQUARE(5); 编译器在编译前就会把这句话原封不动地替换成: result = ((5) * (5)); 宏不是函数,不会先计算 x=5 再传进去,而是直接抄字符串。这一点一定要刻在脑子里。 2: 为什么参数和整个表达式都要加括号?#define SQUARE(x) ((x) * (x)) ❌ 错误写法 #define SQUARE(x) x * x SQUARE(1 + 2) 错误宏展开: result = 1 + 2 * 1 + 2; C语言里 *(乘)比 +(加)优先级高,所以实际计算顺序是: result = 1 + (2 * 1) + 2; 但我们想要的是: (1 + 2) * (1 + 2) = 3 * 3 = 9 3. 最隐蔽的坑:副作用(Side Effect) SQUARE(i++); 假设 i = 3,展开正确宏:((i++) * (i++)) 这里 i++ 的意思是“先用当前值,再自增1”。但它出现了两次,于是: 第一次用 i=3,然后 i 变成 4 ,第二次用 i=4,然后 i 变成 5 计算结果可能是 3 * 4 = 12(但 C 标准里这是未定义行为,不同编译器结果可能不同,甚至崩溃) 你本来只想让 i 加 1 然后求平方(3 * 3=9,i 变成 4),结果 i 加了两次,数也算错了。 结论:永远不要给宏传递带有自增、自减、赋值等有副作用的表达式。 4.嵌入式里的正解:用函数或 static inline 宏这么容易写错,为啥嵌入式还用?因为宏没有函数调用的开销(不用压栈、跳转),在资源紧张的单片机上很快。 现代编译器给了我们更好的平衡方案: ①:普通函数(最安全) int square(int x) { return x * x; } ②:static inline 函数 static inline int square(int x) { return x * x; } inline 建议编译器把函数体直接展开(类似宏的速度) static 限制只在当前文件可见,避免重复定义 有类型检查,括号不用你操心 传 square(i++) 也只会算一次 i++ 在 STM32 / ESP32 开发里,复杂一点的计算我都推荐用 static inline 代替宏。
4. 多语句宏
#define LED_FLASH() \ do \ { \ LED_On(); \ Delay_ms(100); \ LED_Off(); \ } while (0) 不过初学阶段优先写成普通函数,更容易调试和进行类型检查。

解析:

一:在 #define 里,如果宏体里不止一条语句(比如又要开灯、又要延时、又要关灯),就叫多语句宏。 #define LED_FLASH() \ do \ { \ LED_On(); \ Delay_ms(100); \ LED_Off(); \ } while (0) 这里面有三条语句: LED_On(); Delay_ms(100); LED_Off(); 因为跨了多行,每行末尾用了 ``(反斜杠)告诉预处理器“下一行还是宏的一部分”。 二、为什么不能直接用 { } 包起来? 很多初学者会天然写成: #define LED_FLASH() { LED_On(); Delay_ms(100); LED_Off(); } 看起来没问题,但一放进 if 里就炸了。 ❌ 错误示范 if (flag) LED_FLASH(); else do_something(); 宏展开后变成: if (flag) { LED_On(); Delay_ms(100); LED_Off(); }; else do_something(); 注意看:} 后面有一个分号——这个分号被当成了一条独立的空语句,导致 else 找不到对应的 if,编译直接报错。 正确套路:do { ... } while(0) 把宏写成: do { LED_On(); Delay_ms(100); LED_Off(); } while (0) 展开后: if (flag) do { LED_On(); Delay_ms(100); LED_Off(); } while (0); else do_something(); do ... while(0) 在语法上是一个完整的语句(类似于循环语句),后面必须跟分号。 因此它和 if / else 搭配时,不会多出诡异的分号。 同时,它保证宏里的多条语句被当成一个整体执行,不会被 if 只“吃掉”第一条。 📌 这就是多语句宏必须用 do { ... } while(0) 的根本原因 do { ... } while (0) 先无条件执行一次 {} 里的所有语句 再判断 while (0) —— 因为条件是假,所以不再循环 do { LED_On(); // 第一句:开灯 Delay_ms(100); // 第二句:延时 LED_Off(); // 第三句:关灯 } while (0); 三句统统执行,效果是:灯亮 → 等 100ms → 灯灭。 用do while 它的使命是:把多语句宏伪装成一条正常语句,让它在 if/else 里不翻车。

四、条件编译

条件编译决定某段代码是否参与编译。

1.#ifdef
#define DEBUG #ifdef DEBUG printf("System started\n"); #endif

定义了DEBUG,调试代码才会被编译。

在嵌入式中,这通常被称为“编译开关”。它的核心作用是:决定某段代码是否被交给编译器去编译。 define DEBUG // 第一步:定义了一个叫 DEBUG 的开关(此时开关处于“开启”状态) ifdef DEBUG // 第二步:编译器检查,“哎,DEBUG 这个开关定义了吗?” endif // 结束检查 如果你注释掉第一行 #define DEBUG,那么 printf 就不会被编译,程序里根本不存在这段代码。 如果你保留了 #define DEBUG,那么 printf 就会被编译进固件里,程序运行时会打印信息。 为什么要玩这个“开关”? 在写单片机程序时,硬件资源非常宝贵(Flash 和 RAM 很小)。 假设你写了几千行代码,其中有 10 行是用来打印调试信息的。 如果不使用条件编译:这 10 行代码永远留在芯片里,白白占用宝贵的 Flash 空间。 使用条件编译:等你要发布正式版时,只需要删掉 #define DEBUG 这一行,重新编译。这 10 行代码就会像变魔术一样从芯片里彻底消失,完全不占地方。 条件编译不仅能用来“打开”调试信息,还能用来“关闭”某些功能。 条件编译最核心的用法: 用宏定义来控制代码的“生死”。在接下来的学习中,你会经常看到 #ifdef 配合 #include 来管理不同芯片的头文件

实例:

①: #include <stdio.h> #define DEBUG // 把这行注释掉再试一次 int main(void) { #ifdef DEBUG printf(">>> 调试信息:程序正在运行...\n"); #else printf(">>> 调试信息:未执行DEBUG语句\n"); #endif return 0; } 打印:调试信息:程序正在运行... ② #include <stdio.h> // #define DEBUG // 把这行注释掉再试一次 int main(void) { #ifdef DEBUG printf(">>> 调试信息:程序正在运行...\n"); #else printf(">>> 调试信息:未执行DEBUG语句\n"); #endif return 0; } 打印:调试信息:未执行DEBUG语句
2.#ifndef

与#ifdef 刚好是相反的逻辑,但在嵌入式开发中极其常用。

#ifdef:如果定义了(If Defined)→ 执行代码。
#ifndef:如果没有定义(If Not Defined)→ 执行代码。

#ifndef DEBUG /* 非调试版本代码 */ #endif

表示没有定义DEBUG时才编译。

实例:

#include <stdio.h> // #define DEBUG // 先别取消注释,跑一次;再取消注释,再跑一次 int main(void) { #ifndef DEBUG printf(">>> 未定义DEBUG:打印本行\n"); #else printf(">>> 定义DEBUG:打印else内容\n"); #endif return 0; } 输出内容: 未定义DEBUG:打印本行 #include <stdio.h> #define DEBUG // 先别取消注释,跑一次;再取消注释,再跑一次 int main(void) { #ifndef DEBUG printf(">>> 未定义DEBUG:打印本行\n"); #else printf(">>> 定义DEBUG:打印else内容\n"); #endif return 0; } 输出内容:定义DEBUG:打印else内容
3.#if#elif#else
#define BOARD_VERSION 2 #if BOARD_VERSION == 1 #define LED_PIN GPIO_Pin_0 #elif BOARD_VERSION == 2 #define LED_PIN GPIO_Pin_13 #else #error "Unsupported board version" #endif 1. 为什么要用 #if?(看图说话) 你发的这张图里写得很清楚:“同一份程序可以适配不同版本的硬件”。 举个最实际的例子: 假设你做了一个产品,既卖给了客户A(用的是 V1 版电路板,LED 接在 Pin 0),又卖给了客户B(用的是 V2 版电路板,LED 接在 Pin 13)。 如果只用 #ifdef,你得写两套代码。 但用 #if,你只要改一个数字,编译器就会自动帮你把代码里的引脚改成对的。

同一份程序可以适配不同版本的硬件。

这一节的内容非常硬核,也是真正能让你写出“万能代码”的秘诀。

简单来说,#ifdef只是问“有没有这个开关?”,而#if是问“这个开关的数值是多少?”。

实例:

先听懂:#if 是干什么的? 之前学的 #ifdef 和 #ifndef 只能判断“有没有定义”这个开关。 而 #if 厉害在它能判断“定义的值等于多少”,就像程序里的 if 语句,但它是编译前就定好的。 #if 表达式:如果表达式为真,编译这段 #elif:否则如果(增加分支) #else:否则 #error:如果前面都没中,直接让编译器报错罢工(防止乱填参数) #include <stdio.h> #define SPEED 2 int main(void) { #if SPEED == 1 printf("低速模式\n"); #elif SPEED == 2 printf("中速模式\n"); #elif SPEED == 3 printf("高速模式\n"); #else #error "SPEED 只能填 1、2 或 3!" #endif return 0; } 输出:中速模式
4. 功能开关
#define ENABLE_MOTOR 1 #define ENABLE_SENSOR 0 #if ENABLE_MOTOR Motor_Init(); #endif #if ENABLE_SENSOR Sensor_Init(); #endif 如果宏可能被定义成 0,应该使用: #if ENABLE_SENSOR 而不是: #ifdef ENABLE_SENSOR 因为即使写成 #define ENABLE_SENSOR 0,它依然属于“已经定义”。 1. 它是什么?(为什么要用它?) 在写大型程序(尤其是单片机、嵌入式)时,我们通常希望一份代码能搞定所有事情,而不是为了做个新功能就重新拷贝一份代码去修改。 这时候就需要一个“总控制台”。 我想用电机?把开关拨到 1。 我想用传感器?把开关拨到 1。 我不想用蓝牙了?把开关拨到 0。 #if​ 配合 #define​ 定义的 1 或 0,就是这个控制台。 2. 必须注意的“坑” 这部分最重要的就是图中红字强调的陷阱: #ifdef (或者 #if defined):它只看有没有这个名字。 只要你写了 #define ENABLE_SENSOR 0,#ifdef 一看:“哦,有这个名字!” -> 它就认为条件成立了。 #if:它是真真正正去看数值。 只有写成 #define ENABLE_SENSOR 1,#if 一看:“值是 1,真!” -> 条件才成立。 如果是 0,它就是假。 结论:当你想把某个功能彻底关掉(变成 0)时,绝对不能用 #ifdef,必须用 #if。

实例:

#include <stdio.h> // ================= 这里是控制台,改数字试试看 ================= #define LIGHT_ON 1 // 1开灯,0关灯 #define AIR_CONDITIONER 0 // 1开空调,0关空调 // ============================================================ int main(void) { printf("=== 智能家居系统启动 ==="); // 灯光控制 #if LIGHT_ON printf("[系统] 灯光已开启"); #else printf("[系统] 灯光已关闭"); #endif // 空调控制 #if AIR_CONDITIONER printf("[系统] 空调已开启"); #else printf("[系统] 空调已关闭"); #endif printf("=== 状态更新完毕 ==="); return 0; } 打印:=== 智能家居系统启动 ===[系统] 灯光已开启[系统] 空调已关闭=== 状态更新完毕 === #include <stdio.h> // ================= 这里是控制台,改数字试试看 ================= #define LIGHT_ON 0 // 1开灯,0关灯 #define AIR_CONDITIONER 1 // 1开空调,0关空调 // ============================================================ int main(void) { printf("=== 智能家居系统启动 ==="); // 灯光控制 #if LIGHT_ON printf("[系统] 灯光已开启"); #else printf("[系统] 灯光已关闭"); #endif // 空调控制 #if AIR_CONDITIONER printf("[系统] 空调已开启"); #else printf("[系统] 空调已关闭"); #endif printf("=== 状态更新完毕 ==="); return 0; } === 智能家居系统启动 ===[系统] 灯光已关闭[系统] 空调已开启=== 状态更新完毕 ===
5. 暂时屏蔽代码
#if 0 //这部分代码不会参与编译 #endif

五、头文件保护

假设main.c间接包含了两次LED.h,头文件内容可能重复展开。

标准写法是: #ifndef LED_H #define LED_H void LED_Init(void); void LED_On(void); void LED_Off(void); #endif 第一次包含时: LED_H 未定义 → 进入头文件 → 定义 LED_H → 加载头文件内容 第二次包含时: LED_H 已定义 → 跳过头文件内容 工程中建议使用更独特的名字: #ifndef BSP_LED_H #define BSP_LED_H /* 头文件内容 */ #endif

六、.h.c的分工

1:.h文件:对外接口
通常放置: 函数声明 类型声明 结构体和枚举 必须公开的宏 必须公开的 extern 变量声明 #ifndef BSP_LED_H #define BSP_LED_H typedef enum { LED_OFF = 0, LED_ON } LED_State_t; void LED_Init(void); void LED_Write(LED_State_t state); void LED_Toggle(void); #endif
2:.c文件:具体实现
通常放置: 函数定义 私有变量 私有函数 私有宏 硬件实现细节 #include "LED.h" #include "stm32f10x.h" #define LED_GPIO GPIOC #define LED_PIN GPIO_Pin_13 static LED_State_t s_ledState = LED_OFF; void LED_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Pin = LED_PIN; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(LED_GPIO, &GPIO_InitStructure); LED_Write(LED_OFF); } void LED_Write(LED_State_t state) { s_ledState = state; if (state == LED_ON) { GPIO_ResetBits(LED_GPIO, LED_PIN); // PC13低电平点亮 } else { GPIO_SetBits(LED_GPIO, LED_PIN); } } void LED_Toggle(void) { if (s_ledState == LED_ON) { LED_Write(LED_OFF); } else { LED_Write(LED_ON); } } main.c 不需要知道 PC13 是高电平还是低电平点亮: #include "stm32f10x.h" #include "Delay.h" #include "LED.h" int main(void) { LED_Init(); while (1) { LED_Toggle(); Delay_ms(500); } } 这就是模块封装:调用者只关心功能,不关心内部硬件细节

七、extern

extern用来声明“这个变量定义在其他文件”。

例如,在 SystemState.c 中定义: #include <stdint.h> uint8_t g_SystemReady = 0; 在 SystemState.h 中声明: #ifndef SYSTEM_STATE_H #define SYSTEM_STATE_H #include <stdint.h> extern uint8_t g_SystemReady; #endif 其他文件使用: #include "SystemState.h" void Function(void) { g_SystemReady = 1; } 要区分: extern uint8_t g_SystemReady; // 声明,不分配对象存储 uint8_t g_SystemReady = 0; // 定义,真正创建变量 整个工程只能有一个实际定义,否则链接时会出现“重复定义”。 不推荐直接在头文件中写: uint8_t g_SystemReady = 0; // 容易造成多个定义 工程设计中应尽量少暴露全局变量。更好的方式是提供函数: void System_SetReady(uint8_t ready); uint8_t System_IsReady(void);

八、static文件内封装

在文件作用域使用static,表示变量或函数只允许当前.c文件使用。

static uint8_t s_ledState; 其他 .c 文件无法通过 extern 访问它。 私有函数也可以使用 static: static void LED_SetHardwareLevel(uint8_t level) { /* 内部实现 */ } 这样可以明确区分: 头文件中的普通函数声明 → 模块公开接口 .c中的static函数 → 模块私有实现 注意,函数内部的 static 是另一种含义: void Function(void) { static uint32_t count; } 这里表示 count 具有静态存储期,函数退出后不会销毁。

1. 局部变量:是否需要记住上次的值
普通局部变量: void Function(void) { int count = 0; count++; } 每次进入函数都会重新创建并初始化,函数退出后失效。因此每次调用后 count 都是 1。 加入 static: void Function(void) { static int count = 0; count++; } count 只初始化一次,函数退出后仍然存在: 第1次调用:1 第2次调用:2 第3次调用:3 决策规则: 需要在多次函数调用之间保留数据 → 加 static 每次进入函数都应该重新开始 → 不加 static 适合使用的场景: uint32_t GetCallCount(void) { static uint32_t count = 0; count++; return count; } 不适合使用的场景: int Add(int a, int b) { static int result; // 没必要 result = a + b; return result; } 这里 result 每次都会重新计算,不需要保存上一次的值,普通局部变量更合适。
2. 文件中的全局变量:其他文件是否需要访问
假设在 LED.c 中定义: uint8_t LED_State; 这个变量具有外部链接,其他 .c 文件可以通过 extern 访问: extern uint8_t LED_State; 如果加上 static: static uint8_t LED_State; 它只能在 LED.c 内部访问,其他文件看不到。 决策规则: 只在当前.c文件中使用 → 加 static 确实需要被其他.c访问 → 不加 static 实际工程中,文件级变量建议默认加 static: /* LED.c */ static uint8_t s_ledState; 然后通过函数操作: void LED_SetState(uint8_t state) { s_ledState = state; } uint8_t LED_GetState(void) { return s_ledState; } 这样比直接公开全局变量更可靠,因为其他模块不能随意修改内部状态。
3. 函数:它是不是模块的公开接口
普通函数: void LED_On(void) { } 其他 .c 文件可以通过头文件中的声明调用它: /* LED.h */ void LED_On(void); 如果函数只服务于 LED.c 内部,就应该加 static: static void LED_WritePin(uint8_t level) { } 决策规则: 需要被其他.c调用 → 不加 static,并在.h中声明 只在当前.c内部调用 → 加 static,不放到.h中 例如: /* LED.c */ static void LED_WritePin(uint8_t level) { if (level != 0U) { GPIO_SetBits(GPIOC, GPIO_Pin_13); } else { GPIO_ResetBits(GPIOC, GPIO_Pin_13); } } void LED_On(void) { LED_WritePin(0); } void LED_Off(void) { LED_WritePin(1); } 这里: LED_On、LED_Off → 公开接口,不加static LED_WritePin → 内部实现,加static
4.STM32实例
/* Key.c */ #include "Key.h" static uint8_t s_lastKey; static uint16_t s_debounceCount; static uint8_t Key_ReadPin(void) { return GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1); } void Key_Init(void) { s_lastKey = 0; s_debounceCount = 0; } uint8_t Key_GetNum(void) { uint8_t currentKey = Key_ReadPin(); /* 按键处理 */ return currentKey; }

分类如下:

对象是否加static原因
s_lastKey只属于Key.c
s_debounceCount只属于Key.c
Key_ReadPin内部辅助函数
Key_Init需要由main.c调用
Key_GetNum需要由其他模块调用
currentKey只在一次函数调用中临时使用
5.不要滥用局部 static

static局部变量会让函数带有隐藏状态:

uint8_t Filter(uint8_t input) { static uint8_t lastValue; /* 使用lastValue */ }

这可能带来几个问题:

  • 函数无法轻易恢复初始状态。
  • 多个设备不能共用同一个状态。
  • 递归或重复进入时可能出错。
  • 中断和主程序同时调用时可能产生竞争。
  • FreeRTOS 多任务调用时不一定安全。
如果不同设备需要独立状态,更适合使用结构体: typedef struct { uint8_t lastValue; } Filter_t; uint8_t Filter_Run(Filter_t *filter, uint8_t input) { filter->lastValue = input; return filter->lastValue; }

九、多文件工程如何连接

假设工程包含: main.c LED.c LED.h Delay.c Delay.h 编译器分别处理: main.c → main.o LED.c → LED.o Delay.c → Delay.o 链接器再把它们组合: main.o + LED.o + Delay.o + 库文件 ↓ 最终程序 main.c 只看到 LED.h 中的声明: void LED_On(void); 链接时,链接器会去 LED.o 中寻找 LED_On 的真正实现。 常见错误: 找不到头文件 → include路径或文件名有问题 函数未声明 → 没有包含正确的.h文件 undefined reference / Undefined symbol → 有声明,但对应.c未加入工程或没有实现 multiple definition → 同一个全局变量或函数被定义了多次

十、模块接口设计

一个模块应该尽量做到:

  1. 模块只负责一类功能。
  2. 公开接口尽量少。
  3. 硬件细节放在.c中。
  4. 私有变量和函数使用static
  5. 不轻易公开可写全局变量。
  6. 接口名称带模块前缀。
  7. 参数和返回值含义明确。
  8. 错误应通过返回值报告。
例如串口模块: #ifndef BSP_SERIAL_H #define BSP_SERIAL_H #include <stdint.h> #include <stddef.h> void Serial_Init(uint32_t baudRate); void Serial_SendByte(uint8_t data); void Serial_SendArray(const uint8_t *data, size_t length); #endif

接口使用统一前缀:

Serial_Init Serial_SendByte Serial_SendArray

带错误处理的接口:

typedef enum { SENSOR_OK = 0, SENSOR_ERROR_TIMEOUT, SENSOR_ERROR_NOT_FOUND } Sensor_Status_t; Sensor_Status_t Sensor_Init(void); Sensor_Status_t Sensor_Read(float *temperature); 调用者可以判断结果: if (Sensor_Read(&temperature) != SENSOR_OK) { /* 故障处理 */ }