2. 头文件基本结构:文件头注释、包含卫士、标准包含、类型定义、函数声明、宏定义

好,咱们直接进入正题。一个头文件该怎么组织,才能让同事看得懂、编译器不报错、自己半年后回来还能改?

我个人习惯把头文件分成六个部分,按顺序排好。就像盖房子要打地基、砌墙、封顶一样,顺序乱了,后面就容易出问题。

2.1 文件头注释:你是谁?

每个头文件的开头,都应该有一段注释。别小看这个,我在项目中遇到过好几次,一个文件用了三年,原作者离职了,新来的同事看着代码一脸懵——这文件到底干嘛的?

文件头注释应该包含什么?

  • 文件名
  • 简要描述(一句话说清楚用途)
  • 作者
  • 创建日期
  • 修改记录(谁、什么时候、改了啥)

举个例子,我常用的模板长这样:

/*
 * File:    led_driver.h
 * Brief:   LED驱动模块 - 控制板载指示灯
 * Author:  张三
 * Date:    2024-01-15
 *
 * Modify History:
 * 2024-03-20 李四 增加呼吸灯模式
 * 2024-06-10 张三 修复PWM占空比溢出问题
 */

嗯,这里要注意:修改记录别写「优化代码」「修复bug」这种废话。要写清楚改了哪个函数、解决了什么问题。不然以后查问题,你根本不知道这次修改到底动了什么。

2.2 包含卫士:别让编译器崩溃

包含卫士,说白了就是防止头文件被重复包含。你想想看,如果a.h包含了b.h,c.h又包含了a.h和b.h,编译器展开的时候b.h的内容出现两次,重定义错误就来了。

我建议用 #ifndef 的方式,简单可靠:

#ifndef __LED_DRIVER_H__
#define __LED_DRIVER_H__

// ... 头文件内容 ...

#endif /* __LED_DRIVER_H__ */

小技巧:宏名字的格式我推荐 __模块名_H__,前后各两个下划线。这样一眼就能看出是哪个文件,而且不容易跟其他宏冲突。

有人问我为什么不用 #pragma once?说实话,大部分编译器都支持,但我在一些老旧的ARM编译器上踩过坑——它不认这个。为了可移植性,我还是老老实实用 #ifndef

2.3 标准包含:先系统后用户

头文件里如果需要引用其他头文件,顺序有讲究。我的原则是:先系统头文件,后用户头文件

为什么?因为系统头文件稳定,不会因为你的项目改动而出问题。先包含它们,可以尽早暴露用户头文件里的依赖问题。

// 标准包含
#include <stdint.h>
#include <stdbool.h>
#include <string.h>

// 用户头文件
#include "system_config.h"
#include "gpio.h"

我曾经犯过的错:有一次我把用户头文件放在系统头文件前面,结果用户头文件里用到了 uint32_t,但 stdint.h 还没包含进来。编译器报了一堆错,排查了半天才发现是包含顺序的问题。从那以后,我再也不敢乱排顺序了。

2.4 类型定义:给数据穿上衣服

嵌入式开发里,类型定义太重要了。你总不能到处写 unsigned int 吧?换个平台,int的位数可能就变了。

我习惯在头文件里定义项目专用的类型别名:

typedef unsigned char  uint8_t;
typedef unsigned short uint16_t;
typedef unsigned int   uint32_t;

typedef enum {
    LED_OFF = 0,
    LED_ON,
    LED_BLINK_SLOW,
    LED_BLINK_FAST
} led_mode_t;

typedef struct {
    uint8_t      pin;
    uint8_t      port;
    led_mode_t   mode;
    uint16_t     period_ms;
} led_config_t;

你看,用枚举和结构体把数据组织起来,代码的可读性一下子就上来了。我在项目中见过有人用 #define 定义状态值,结果调试的时候根本分不清0、1、2分别代表什么——这就是典型的偷懒后患。

2.5 函数声明:接口要清晰

函数声明是头文件的核心。别人看你的头文件,主要就是看有哪些函数可以用。

我的建议是:每个函数都要有注释,说明功能、参数、返回值。别嫌麻烦,这是对自己和同事负责。

/**
 * @brief  初始化LED
 * @param  config LED配置参数
 * @return 0成功,-1失败
 */
int led_init(const led_config_t *config);

/**
 * @brief  设置LED状态
 * @param  mode LED模式
 */
void led_set_mode(led_mode_t mode);

/**
 * @brief  获取当前LED模式
 * @return 当前模式
 */
led_mode_t led_get_mode(void);

避坑指南:函数声明里的参数名不要省略。我见过有人写 int led_init(const led_config_t *),调用的时候根本不知道这个参数要传什么。参数名就是最好的文档。

2.6 宏定义:用得好是神器,用不好是灾难

宏定义是C语言的特色,但也是双刃剑。我个人的原则是:能用枚举和const变量代替的,就别用宏

但有些场景宏确实方便,比如:

// 硬件寄存器地址
#define LED_GPIO_BASE      0x40021000
#define LED_GPIO_MODER     (LED_GPIO_BASE + 0x00)
#define LED_GPIO_ODR       (LED_GPIO_BASE + 0x14)

// 位操作
#define BIT(n)             (1UL << (n))
#define SET_BIT(reg, bit)  ((reg) |= BIT(bit))
#define CLR_BIT(reg, bit)  ((reg) &= ~BIT(bit))

// 参数校验
#define IS_LED_MODE(mode)  (((mode) >= LED_OFF) && ((mode) <= LED_BLINK_FAST))

注意:宏定义一定要加括号!我见过有人写 #define MUL(a, b) a * b,然后调用 MUL(1+2, 3),结果展开成 1+2*3,等于7而不是9。这种bug查起来特别痛苦。

好了,这六个部分就是头文件的基本骨架。你按照这个顺序写,结构清晰、可维护性强。下一章咱们聊聊更进阶的话题——头文件之间的依赖关系怎么管理。