模块化编程思想:.h与.c文件的分工,头文件卫士,全局变量与静态变量的作用域控制,接口设计原则。
好,咱们进入第二章。说实话,模块化编程这个概念,很多刚入行的朋友觉得不就是把代码拆成几个文件嘛,有什么好讲的?
但我见过太多项目,拆是拆了,结果拆完比不拆还乱。全局变量满天飞,头文件互相包含成死循环,改一个地方崩一片。嗯,这章咱们就把这些坑一个一个填上。
.h 与 .c 文件的分工
先问个问题:你写一个驱动,比如 I2C,代码应该放哪?
我的习惯很简单——.c 文件放实现,.h 文件放接口。
| 文件 | 放什么 | 不放什么 |
|---|---|---|
| .c | 函数定义、局部变量、静态变量、具体算法 | 外部可见的宏定义(除非必要)、函数声明 |
| .h | 函数声明、数据类型定义、宏定义、extern 变量声明 | 函数实现、变量定义(除非是 const) |
举个例子,一个简单的 LED 驱动:
// led.h
#ifndef __LED_H__
#define __LED_H__
#include "stdint.h"
void led_init(void);
void led_on(uint8_t pin);
void led_off(uint8_t pin);
#endif
// led.c
#include "led.h"
#include "gpio.h"
static uint8_t led_state = 0;
void led_init(void) {
gpio_set_mode(LED_PIN, OUTPUT);
led_state = 0;
}
void led_on(uint8_t pin) {
gpio_write(pin, HIGH);
led_state |= (1 << pin);
}
void led_off(uint8_t pin) {
gpio_write(pin, LOW);
led_state &= ~(1 << pin);
}
你看,.h 只告诉别人「我能干什么」,.c 才说「我怎么干」。调用者只需要看 .h,不需要关心 .c 里是怎么操作的。
头文件卫士
头文件卫士,说白了就是防止同一个头文件被重复包含。你想想看,如果 a.h 包含了 b.h,c.h 也包含了 b.h,然后某个文件又同时包含了 a.h 和 c.h,那 b.h 的内容就被编译了两次——轻则报重定义,重则编译直接崩。
标准写法有两种:
- #ifndef 方式(最常用,兼容性最好)
- #pragma once 方式(简洁,但部分老编译器不支持)
// 方式一:传统 #ifndef
#ifndef __MY_DRIVER_H__
#define __MY_DRIVER_H__
// 头文件内容
#endif
// 方式二:现代 #pragma once
#pragma once
// 头文件内容
命名规范上,我建议用 __模块名_H__ 的格式,比如 __I2C_DRIVER_H__。注意不要跟系统头文件冲突,别用 _STDIO_H 这种。
全局变量与静态变量的作用域控制
这是模块化编程里最容易翻车的地方。很多新手喜欢在 .h 里直接定义全局变量:
// 错误示范
// globals.h
int system_tick = 0; // 这是定义,不是声明!
然后每个包含这个头文件的 .c 文件都会生成一个 system_tick 的副本——链接阶段直接报 multiple definition。
正确的做法是:
- 在 .c 里定义变量,用
static限制作用域 - 在 .h 里用
extern声明,告诉编译器「这个变量在其他地方定义了」
// timer.h
#ifndef __TIMER_H__
#define __TIMER_H__
extern volatile uint32_t system_tick; // 声明,不是定义
void timer_init(void);
#endif
// timer.c
#include "timer.h"
volatile uint32_t system_tick = 0; // 真正的定义在这里
void timer_init(void) {
// 配置定时器
}
那 static 关键字怎么用?我总结了一个原则:
| 关键字 | 作用域 | 生命周期 | 典型用途 |
|---|---|---|---|
static(全局变量) |
仅限本 .c 文件 | 整个程序运行期 | 模块内部状态、缓存 |
static(局部变量) |
仅限本函数 | 整个程序运行期 | 保持函数调用间的状态 |
extern |
整个程序 | 整个程序运行期 | 跨模块共享变量 |
接口设计原则
接口设计,说白了就是「你给别人用的时候,怎么让人用得爽」。我总结了三条铁律:
- 最小暴露原则:.h 里只放别人需要的东西。内部函数、内部宏、内部类型,统统藏到 .c 里。
- 命名一致性:同一个模块的函数用统一前缀。比如
i2c_init、i2c_read、i2c_write,一看就知道是 I2C 模块的。 - 参数简洁:函数参数不要超过 4 个。超过 4 个,要么用结构体传参,要么说明你的设计有问题。
举个例子,一个 UART 驱动接口:
// uart_driver.h
#ifndef __UART_DRIVER_H__
#define __UART_DRIVER_H__
#include "stdint.h"
// 配置结构体,把多个参数打包
typedef struct {
uint32_t baudrate;
uint8_t data_bits;
uint8_t stop_bits;
uint8_t parity;
} uart_config_t;
// 对外接口——只有 4 个函数
void uart_init(uart_config_t *config);
void uart_send_byte(uint8_t data);
uint8_t uart_receive_byte(void);
uint8_t uart_is_data_ready(void);
#endif
你看,用户只需要知道这 4 个函数,不需要知道内部用了哪个定时器、哪个 DMA 通道。这就是接口设计。
知识结构图
下面这张图总结了本章的核心逻辑,你可以把它当作模块化编程的「地图」:
总结一下
模块化编程不是把代码拆开就完事了。你得想清楚:
- .h 是合同,.c 是履约
- 头文件卫士是保险,别省
- static 是你的好朋友,多用它
- 接口设计要克制,少即是多
嗯,把这些搞明白了,你的代码结构至少能打 80 分。剩下的 20 分,咱们后面几章慢慢补。