模块化编程思想:.h与.c文件的分工,头文件卫士,全局变量与静态变量的作用域控制,接口设计原则。

好,咱们进入第二章。说实话,模块化编程这个概念,很多刚入行的朋友觉得不就是把代码拆成几个文件嘛,有什么好讲的?

但我见过太多项目,拆是拆了,结果拆完比不拆还乱。全局变量满天飞,头文件互相包含成死循环,改一个地方崩一片。嗯,这章咱们就把这些坑一个一个填上。

.h 与 .c 文件的分工

先问个问题:你写一个驱动,比如 I2C,代码应该放哪?

我的习惯很简单——.c 文件放实现,.h 文件放接口。

文件 放什么 不放什么
.c 函数定义、局部变量、静态变量、具体算法 外部可见的宏定义(除非必要)、函数声明
.h 函数声明、数据类型定义、宏定义、extern 变量声明 函数实现、变量定义(除非是 const)

举个例子,一个简单的 LED 驱动:

// led.h
#ifndef __LED_H__
#define __LED_H__

#include "stdint.h"

void led_init(void);
void led_on(uint8_t pin);
void led_off(uint8_t pin);

#endif
// led.c
#include "led.h"
#include "gpio.h"

static uint8_t led_state = 0;

void led_init(void) {
    gpio_set_mode(LED_PIN, OUTPUT);
    led_state = 0;
}

void led_on(uint8_t pin) {
    gpio_write(pin, HIGH);
    led_state |= (1 << pin);
}

void led_off(uint8_t pin) {
    gpio_write(pin, LOW);
    led_state &= ~(1 << pin);
}

你看,.h 只告诉别人「我能干什么」,.c 才说「我怎么干」。调用者只需要看 .h,不需要关心 .c 里是怎么操作的。

我的经验: 我在项目中见过有人把函数实现直接写在 .h 里,结果被多个 .c 包含后,链接阶段疯狂报重复定义。记住,.h 不是用来写代码的,是用来签合同的。

头文件卫士

头文件卫士,说白了就是防止同一个头文件被重复包含。你想想看,如果 a.h 包含了 b.h,c.h 也包含了 b.h,然后某个文件又同时包含了 a.h 和 c.h,那 b.h 的内容就被编译了两次——轻则报重定义,重则编译直接崩。

标准写法有两种:

  1. #ifndef 方式(最常用,兼容性最好)
  2. #pragma once 方式(简洁,但部分老编译器不支持)
// 方式一:传统 #ifndef
#ifndef __MY_DRIVER_H__
#define __MY_DRIVER_H__

// 头文件内容

#endif

// 方式二:现代 #pragma once
#pragma once

// 头文件内容
注意: 我曾经在一个项目里用了 #pragma once,结果客户用的编译器是十年前的版本,不支持这个指令。从那以后,我统一用 #ifndef,虽然多写两行,但稳。

命名规范上,我建议用 __模块名_H__ 的格式,比如 __I2C_DRIVER_H__。注意不要跟系统头文件冲突,别用 _STDIO_H 这种。

全局变量与静态变量的作用域控制

这是模块化编程里最容易翻车的地方。很多新手喜欢在 .h 里直接定义全局变量:

// 错误示范
// globals.h
int system_tick = 0;  // 这是定义,不是声明!

然后每个包含这个头文件的 .c 文件都会生成一个 system_tick 的副本——链接阶段直接报 multiple definition。

正确的做法是:

  1. 在 .c 里定义变量,用 static 限制作用域
  2. 在 .h 里用 extern 声明,告诉编译器「这个变量在其他地方定义了」
// timer.h
#ifndef __TIMER_H__
#define __TIMER_H__

extern volatile uint32_t system_tick;  // 声明,不是定义

void timer_init(void);

#endif
// timer.c
#include "timer.h"

volatile uint32_t system_tick = 0;  // 真正的定义在这里

void timer_init(void) {
    // 配置定时器
}

那 static 关键字怎么用?我总结了一个原则:

关键字 作用域 生命周期 典型用途
static(全局变量) 仅限本 .c 文件 整个程序运行期 模块内部状态、缓存
static(局部变量) 仅限本函数 整个程序运行期 保持函数调用间的状态
extern 整个程序 整个程序运行期 跨模块共享变量
核心原则: 能用 static 就别用 extern。每个模块的内部状态应该对外部不可见,这才是模块化的精髓。

接口设计原则

接口设计,说白了就是「你给别人用的时候,怎么让人用得爽」。我总结了三条铁律:

  1. 最小暴露原则:.h 里只放别人需要的东西。内部函数、内部宏、内部类型,统统藏到 .c 里。
  2. 命名一致性:同一个模块的函数用统一前缀。比如 i2c_init、i2c_read、i2c_write,一看就知道是 I2C 模块的。
  3. 参数简洁:函数参数不要超过 4 个。超过 4 个,要么用结构体传参,要么说明你的设计有问题。

举个例子,一个 UART 驱动接口:

// uart_driver.h
#ifndef __UART_DRIVER_H__
#define __UART_DRIVER_H__

#include "stdint.h"

// 配置结构体,把多个参数打包
typedef struct {
    uint32_t baudrate;
    uint8_t  data_bits;
    uint8_t  stop_bits;
    uint8_t  parity;
} uart_config_t;

// 对外接口——只有 4 个函数
void    uart_init(uart_config_t *config);
void    uart_send_byte(uint8_t data);
uint8_t uart_receive_byte(void);
uint8_t uart_is_data_ready(void);

#endif

你看,用户只需要知道这 4 个函数,不需要知道内部用了哪个定时器、哪个 DMA 通道。这就是接口设计。

避坑指南: 我曾经设计过一个驱动,.h 里暴露了 20 多个函数,结果团队里其他同事根本不知道哪些该用、哪些不该用。后来我重构,只留了 5 个对外接口,内部函数全部加 static,世界清净了。

知识结构图

下面这张图总结了本章的核心逻辑,你可以把它当作模块化编程的「地图」:

模块化编程核心知识结构 模块化编程 .h 与 .c 分工 作用域控制 接口设计原则 .h:接口声明 / 类型 / 宏 .c:实现 / 局部变量 / 算法 static:文件内可见 extern:跨文件共享 最小暴露原则 命名一致性 参数简洁(≤4个) 核心目标:高内聚、低耦合 每个模块只暴露必要的接口,内部实现对外部完全隐藏

总结一下

模块化编程不是把代码拆开就完事了。你得想清楚:

  • .h 是合同,.c 是履约
  • 头文件卫士是保险,别省
  • static 是你的好朋友,多用它
  • 接口设计要克制,少即是多

嗯,把这些搞明白了,你的代码结构至少能打 80 分。剩下的 20 分,咱们后面几章慢慢补。