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 一张图理清头文件的内容结构

说了这么多,我画张图帮你理一理思路。头文件里到底该放什么,不该放什么,一目了然。

头文件内容结构图 头文件 (.h) ✅ 可以放 函数声明(原型) extern 全局变量声明 typedef / struct / enum 定义 宏定义 (#define) ❌ 不可以放 函数定义(实现体) 全局变量定义(无 extern) static 函数/变量定义 #include "xxx.c"

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;(会占用内存)
一个小技巧:如果你不确定某个东西该放头文件还是 .c 文件,问自己一个问题——「其他 .c 文件需要知道这个东西吗?」如果不需要,就放 .c 文件里。少暴露,少耦合,少出 bug。

最后说一句:头文件设计得好,项目维护起来就轻松。头文件设计得乱,改一个结构体成员,整个项目都得重新编译,那滋味可不好受。

好了,这一章就聊到这儿。记住:头文件是接口,不是实现。把握好这个度,你的代码质量会上一个台阶。