3、源文件与头文件的分离:.c 和 .h 文件的分工、声明与定义分离、内部函数与外部函数的控制
好,咱们今天聊一个非常基础、但又特别容易被忽视的话题——源文件与头文件的分离。
我记得刚入行那会儿,写单片机程序,所有代码都塞在一个 main.c 里。几百行还好,写到两三千行的时候,光是找某个函数定义就得翻半天。后来带我的老工程师看了一眼我的代码,只说了一句:「你把 .c 和 .h 分开,声明和定义分开,内部函数用 static 锁住。」
嗯,从那以后,我的代码结构才真正开始「能看」了。
一、.c 和 .h 文件的分工
说白了,.c 文件负责「干活」,.h 文件负责「告诉别人我能干什么」。
- .c 文件:放函数的实现、全局变量的定义、静态函数的定义。这是代码真正执行的地方。
- .h 文件:放函数声明、外部变量声明、宏定义、类型定义。这是模块对外暴露的接口。
我个人的习惯是:每个模块至少有一个 .c 和一个同名的 .h。比如 uart.c 和 uart.h。这样别人一看就知道这个模块是干什么的。
核心原则:.h 文件里不要放函数定义,不要放变量定义。只放声明。
你想想看,如果 .h 里放了函数定义,然后两个 .c 文件都 #include 了这个 .h,链接的时候就会报「重复定义」的错误。我当年就踩过这个坑,查了半天才发现是 .h 里顺手写了个函数体。
二、声明与定义分离
这个其实很简单,但很多人一开始会搞混。
- 定义:分配存储空间,比如
int counter = 0;或者void uart_init(void) { ... } - 声明:告诉编译器「这个东西存在」,但不分配空间,比如
extern int counter;或者void uart_init(void);
举个例子:
// uart.c
#include "uart.h"
int uart_baud = 115200; // 定义
void uart_init(void) { // 定义
// 初始化代码
}
// uart.h
#ifndef UART_H
#define UART_H
extern int uart_baud; // 声明
void uart_init(void); // 声明
#endif
注意看,uart_baud 在 .c 里是定义,在 .h 里用 extern 声明。这样其他 .c 文件只要 #include "uart.h",就能访问这个变量了。
小技巧:我习惯在 .h 文件里把所有 extern 变量放在一起,用注释标明「外部变量声明区」。这样阅读代码的人一目了然。
三、内部函数与外部函数的控制
这里的关键字就是 static。
在 C 语言里,static 修饰函数时,表示这个函数只在当前 .c 文件内可见。其他文件即使声明了也无法调用。
为什么要用 static?说白了就是封装。模块内部的一些辅助函数,不应该暴露给外部。比如:
// uart.c
static void calc_baud_divisor(void) {
// 计算波特率分频系数,仅供内部使用
}
void uart_init(void) {
calc_baud_divisor(); // 内部调用
// 其他初始化
}
这样,calc_baud_divisor 就是内部函数,外部根本不知道它的存在。你想调用也调用不了。
我曾经犯过的错:在一个驱动模块里,把所有函数都写成了非 static 的。结果另一个同事在别的文件里直接调用了我的内部函数,后来我改了那个函数的参数,他的代码就崩了。从那以后,我坚持「能 static 就 static」。
对于变量也是一样。模块内部的全局变量,如果不想被外部访问,就用 static 修饰:
// uart.c
static int rx_buffer_full = 0; // 内部状态,外部不可见
这样,rx_buffer_full 就只属于 uart.c,其他文件无法直接读写。必须通过模块提供的接口函数来操作。
四、知识体系结构图
下面这张图,帮你理清 .c 和 .h 的关系:
五、实际项目中的组织方式
在一个稍微大一点的项目里,我通常这样组织文件:
| 模块 | .c 文件 | .h 文件 | 内部函数 (static) | 外部接口 |
|---|---|---|---|---|
| UART 驱动 | uart.c | uart.h | calc_baud_divisor() | uart_init(), uart_send() |
| I2C 驱动 | i2c.c | i2c.h | i2c_delay(), i2c_ack_check() | i2c_start(), i2c_write() |
| 定时器模块 | timer.c | timer.h | timer_isr_handler() | timer_init(), timer_start() |
你看,每个模块的 .h 文件只暴露必要的接口,内部实现细节全部用 static 隐藏。这样别人用你的模块时,只需要看 .h 文件就知道怎么用,不用关心 .c 里是怎么实现的。
我的一个习惯:写 .h 文件时,我会在注释里写明「这个函数是干什么的、参数是什么、返回值是什么」。这样别人(包括几个月后的我自己)一看就明白。
六、避坑指南
最后,分享几个我踩过的坑:
- 不要在 .h 里定义变量:哪怕你觉得「就一个变量,懒得分开写」,也请忍住。否则多文件包含时,链接器会报重复定义。
- 头文件保护一定要加:
#ifndef、#define、#endif这三行,缺一不可。我见过有人漏了,结果编译时出现奇怪的「重复声明」错误。 - static 函数不要出现在 .h 里:static 函数只在当前 .c 文件可见,你在 .h 里声明它没有任何意义,反而会让阅读代码的人困惑。
- extern 变量声明要加 extern:在 .h 里声明变量时,一定要写
extern。不写的话,编译器会把它当作定义,又会导致重复定义问题。
嗯,大概就是这些。其实 .c 和 .h 的分离,说白了就是接口与实现分离。你把这个原则想明白了,代码结构自然就清晰了。