3. 源文件的组织:.c 和 .h 文件的职责划分、一个模块一个文件夹的实践、内部函数与外部函数的声明
好,咱们今天聊点实在的。你写 C 语言,肯定遇到过这种场景:一个 main.c 文件写了三千行,全局变量满天飞,函数声明堆在文件头,改一个 bug 得翻三遍滚动条。嗯,我当年刚入行时也这么干过。直到有一次,一个同事接手我的代码,看了半天,默默说了一句:“这代码,我不敢动。”
从那天起,我开始认真琢磨一件事——源文件到底该怎么组织?
说白了,就是 .c 和 .h 文件各管各的,别混在一起。一个模块一个文件夹,内部函数别暴露出去。今天我就把这几年的经验掰开揉碎讲给你听。
3.1 .c 和 .h 文件的职责划分
很多人觉得 .h 文件就是放函数声明的,.c 文件就是放实现的。这话对,但不全对。我见过不少项目,.h 文件里塞满了全局变量定义、静态函数声明,甚至还有业务逻辑。这其实是个坑。
我个人习惯,把职责分得清清楚楚:
| 文件类型 | 核心职责 | 应该放什么 | 不应该放什么 |
|---|---|---|---|
| .h 文件 | 接口声明 | 函数原型、宏定义、类型定义、extern 变量声明 | 函数实现、变量定义、静态函数声明 |
| .c 文件 | 实现细节 | 函数实现、静态函数、模块内部变量 | 对外暴露的实现细节 |
你想想看,.h 文件是给外部模块看的。它就像一张菜单,告诉别人“我这模块能提供什么服务”。至于后厨怎么炒菜,那是 .c 文件的事,别人不需要知道。
核心原则:.h 文件只暴露接口,不暴露实现。.c 文件只实现功能,不暴露细节。
3.2 一个模块一个文件夹的实践
我记得第一次接手一个大型嵌入式项目时,打开工程目录,好家伙,所有 .c 和 .h 文件全堆在一个文件夹里。光 timer 相关的文件就有 timer.c、timer.h、timer_config.c、timer_utils.h……找文件全靠猜。
后来我学乖了。每个模块一个文件夹,文件夹名就是模块名。比如:
project/
├── main.c
├── uart/
│ ├── uart.c
│ ├── uart.h
│ └── uart_config.h
├── timer/
│ ├── timer.c
│ ├── timer.h
│ └── timer_private.h
├── led/
│ ├── led.c
│ └── led.h
└── common/
├── types.h
└── error.h
这样做的好处很明显:
- 查找方便:想改 UART 驱动,直接进 uart 文件夹,不用翻几十个文件。
- 模块独立:每个模块可以独立编译、测试,甚至独立移植到其他项目。
- 命名清晰:函数名可以短一点,比如 uart_send() 而不是 uart_uart_send()。
小技巧:我习惯在每个模块文件夹里放一个 README.md,简单描述这个模块的用途、依赖关系和初始化顺序。半年后你再看自己的代码,会感谢这个习惯的。
3.3 内部函数与外部函数的声明
这是很多人容易搞混的地方。什么叫内部函数?就是只在当前 .c 文件内部使用的函数。什么叫外部函数?就是其他模块需要调用的函数。
内部函数用 static 关键字修饰。外部函数不加 static,并且在对应的 .h 文件中声明。
举个例子:
// timer.c
#include "timer.h"
// 内部函数:只在本文件使用
static void timer_hardware_init(void) {
// 配置寄存器
}
static uint32_t timer_calculate_prescaler(uint32_t freq) {
// 计算分频系数
return freq / 1000;
}
// 外部函数:在 timer.h 中声明
void timer_init(uint32_t freq) {
timer_hardware_init();
uint32_t prescaler = timer_calculate_prescaler(freq);
// 设置定时器
}
void timer_start(void) {
// 启动定时器
}
// timer.h
#ifndef TIMER_H
#define TIMER_H
#include <stdint.h>
// 外部函数声明
void timer_init(uint32_t freq);
void timer_start(void);
#endif
你看,timer_hardware_init() 和 timer_calculate_prescaler() 都是内部函数,加了 static。外部根本看不到它们。这样有什么好处?
- 封装性:外部模块只能通过 timer_init() 和 timer_start() 来操作定时器,内部实现细节被隐藏了。
- 安全性:别人不会误调用你的内部函数,也不会依赖你的内部实现。
- 可维护性:你想重构内部实现,只要接口不变,外部模块完全不受影响。
注意:千万不要在 .h 文件中声明 static 函数。static 函数只在定义它的 .c 文件中可见,在 .h 中声明毫无意义,反而会让阅读代码的人困惑。
3.4 知识体系结构图
下面这张图,是我自己总结的源文件组织逻辑。你看一遍,基本就清楚了。
3.5 避坑指南
最后,分享几个我踩过的坑:
- 别在 .h 文件里定义变量:比如
int g_counter;写在 .h 里,多个 .c 文件包含它,链接时会报重复定义。正确做法是在 .c 里定义,在 .h 里用extern声明。 - 别把内部函数声明在 .h 里:我曾经见过一个项目,.h 文件里声明了十几个 static 函数。这完全没用,还误导别人以为这些函数是公开的。
- 别一个 .c 文件写几千行:如果一个模块的 .c 文件超过 500 行,我建议你考虑拆分成子模块。比如 timer.c 可以拆成 timer_core.c、timer_config.c、timer_irq.c。
- 别忽略头文件保护:每个 .h 文件都要加
#ifndef/#define/#endif,防止重复包含。这是基本功,但很多人会忘。
我的习惯:每个模块文件夹里,除了 .c 和 .h,我还会放一个 module_private.h。这个文件放模块内部多个 .c 文件之间共享的声明,但对外不可见。比如 timer_private.h 里放一些内部结构体定义。这样既保持了封装,又方便内部协作。
好了,关于源文件组织,今天就聊这么多。记住一句话:.h 是承诺,.c 是兑现。内部函数藏起来,外部函数亮出来。 下次写代码时,试试这个思路,你会发现代码清爽很多。