5、头文件中的声明与定义:函数声明、全局变量声明(extern)、类型定义(typedef, struct)
好,咱们今天聊点实在的。头文件里到底该放什么?不该放什么?这个问题,我见过太多人栽跟头了。
刚入行那会儿,我也犯过傻。一个头文件里,又是函数实现,又是全局变量定义,结果编译的时候报了一堆重复定义的错误。当时我盯着屏幕看了半小时,愣是没想明白——明明只 include 了一次啊!后来才意识到,问题出在多个 .c 文件都包含了同一个头文件。
说白了,头文件就是一张「名片」。它告诉别人:我有这些功能,你可以用。但它不应该把「家底」全抖出来。
5.1 函数声明:只露脸,不现身
函数声明放在头文件里,这是最基础的操作。但要注意——只放声明,不放定义。
// 正确的做法:头文件里只放声明
#ifndef UTILS_H
#define UTILS_H
int calculate_sum(int a, int b); // 声明
void print_result(const char* msg); // 声明
#endif
// 错误的做法:头文件里放定义
#ifndef UTILS_H
#define UTILS_H
int calculate_sum(int a, int b) { // 定义!千万别这么干
return a + b;
}
#endif
为什么不能放定义?你想想看,如果两个 .c 文件都 #include 了这个头文件,链接器就会看到两个相同的函数定义,直接报重复定义错误。我当年就被这个坑过,改了一下午才排查出来。
5.2 全局变量声明:extern 的正确用法
全局变量这东西,能不用就不用。但有时候确实需要,比如整个系统只有一个配置结构体。
这时候,头文件里放的是 extern 声明,而不是定义。
// config.h
#ifndef CONFIG_H
#define CONFIG_H
extern int system_mode; // 声明:告诉编译器这个变量存在
extern float sampling_rate; // 声明
#endif
// config.c
#include "config.h"
int system_mode = 0; // 定义:实际分配内存
float sampling_rate = 100.0f; // 定义
这里有个关键点:extern 声明不分配内存,它只是告诉编译器「这个变量在别处定义了」。而真正分配内存的代码,只能出现在一个 .c 文件里。
int system_mode = 0; 而不是 extern int system_mode;。结果两个 .c 文件都包含了这个头文件,链接时报了重复定义。排查了整整两个小时,最后发现是少写了一个 extern。
嗯,这里要注意:extern 声明不要给初始值。给了初始值就变成定义了,又回到重复定义的老问题上。
5.3 类型定义:typedef 和 struct 的最佳实践
类型定义是头文件里最该放的东西。结构体、枚举、typedef 别名,这些都应该放在头文件里,因为多个 .c 文件可能都需要知道这些类型的布局。
// sensor.h
#ifndef SENSOR_H
#define SENSOR_H
#include <stdint.h>
// 传感器数据类型
typedef struct {
uint32_t timestamp;
float temperature;
float humidity;
uint8_t status;
} SensorData_t;
// 传感器状态枚举
typedef enum {
SENSOR_OK = 0,
SENSOR_ERROR = 1,
SENSOR_TIMEOUT = 2
} SensorStatus_t;
// 函数声明
SensorStatus_t sensor_read(SensorData_t* data);
void sensor_init(void);
#endif
我个人习惯在类型名后面加 _t 后缀,一眼就能看出这是个类型。当然,这也不是强制要求,但团队里统一风格很重要。
结构体定义放在头文件里有个好处:所有使用这个结构体的 .c 文件都能直接访问成员变量。但这也意味着,如果你修改了结构体的成员布局,所有相关的 .c 文件都需要重新编译。这就是所谓的「编译依赖」。
如果你想让结构体对外部透明,可以用「不透明指针」的技巧——在头文件里只放一个前向声明,结构体的完整定义放在 .c 文件里。不过这是进阶话题了,咱们后面再聊。
5.4 一张图理清头文件的内容结构
说了这么多,我画张图帮你理一理思路。头文件里到底该放什么,不该放什么,一目了然。
5.5 几个容易忽略的细节
聊几个我实际项目中遇到的细节问题,你写代码的时候留意一下。
| 场景 | 正确做法 | 错误做法 |
|---|---|---|
| 函数声明 | int func(int a); |
int func(int a) { return a; } |
| 全局变量 | extern int g_counter; |
int g_counter = 0; |
| 结构体定义 | typedef struct { ... } MyType; |
在 .c 里定义,头文件里不暴露 |
| 常量定义 | #define MAX_BUF 256 |
const int MAX_BUF = 256;(会占用内存) |
最后说一句:头文件设计得好,项目维护起来就轻松。头文件设计得乱,改一个结构体成员,整个项目都得重新编译,那滋味可不好受。
好了,这一章就聊到这儿。记住:头文件是接口,不是实现。把握好这个度,你的代码质量会上一个台阶。