3、源文件与头文件的分离:.c 和 .h 文件的分工、声明与定义分离、内部函数与外部函数的控制

好,咱们今天聊一个非常基础、但又特别容易被忽视的话题——源文件与头文件的分离

我记得刚入行那会儿,写单片机程序,所有代码都塞在一个 main.c 里。几百行还好,写到两三千行的时候,光是找某个函数定义就得翻半天。后来带我的老工程师看了一眼我的代码,只说了一句:「你把 .c 和 .h 分开,声明和定义分开,内部函数用 static 锁住。」

嗯,从那以后,我的代码结构才真正开始「能看」了。

一、.c 和 .h 文件的分工

说白了,.c 文件负责「干活」.h 文件负责「告诉别人我能干什么」

  • .c 文件:放函数的实现、全局变量的定义、静态函数的定义。这是代码真正执行的地方。
  • .h 文件:放函数声明、外部变量声明、宏定义、类型定义。这是模块对外暴露的接口。

我个人的习惯是:每个模块至少有一个 .c 和一个同名的 .h。比如 uart.cuart.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 源文件 「干活的地方」 函数定义 void uart_init(void) { ... } 全局变量定义 int uart_baud = 115200; 内部函数 (static) static void calc_div(void) { } 内部变量 (static) static int rx_flag = 0; .h 头文件 「告诉别人我能干什么」 函数声明 void uart_init(void); 外部变量声明 extern int uart_baud; 宏定义 / 类型定义 #define UART_BUFSZ 64 头文件保护 #ifndef UART_H ... #endif .c 负责实现,.h 负责接口声明,static 控制内部可见性

五、实际项目中的组织方式

在一个稍微大一点的项目里,我通常这样组织文件:

模块 .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 的分离,说白了就是接口与实现分离。你把这个原则想明白了,代码结构自然就清晰了。


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