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 知识体系结构图

下面这张图,是我自己总结的源文件组织逻辑。你看一遍,基本就清楚了。

源文件组织核心逻辑 模块文件夹 .h 文件(接口) .c 文件(实现) 函数原型、宏、类型定义 函数实现、static 函数 函数分类:内部函数(static) vs 外部函数(非 static) 内部函数:static 修饰 仅本 .c 文件可见 外部函数:非 static 在 .h 中声明,全局可见

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 是兑现。内部函数藏起来,外部函数亮出来。 下次写代码时,试试这个思路,你会发现代码清爽很多。

公众号:蓝海资料掘金营,微信 deep3321