跳到主要内容
🔍
1.3.10已发布intermediate · 约 2 课时 · P0

API 封装与工程目录规范

学会模块化封装与标准工程目录,产出可复用的 API

  • 来源
  • 补充

标记说明:【来源】来自上传资料 · 【补充】课程新编 · 【纠错】按勘误表修正 · 【更新】过时内容已现代化 · 【待确认】无法可靠还原

完成标准(本章)

  • 📖 已阅读:滚动 ≥ 80% 且有效阅读 ≥ 120
  • ✏️ 已练习:小练习正确率 ≥ 60%
  • 📝 已通过测验:分数 ≥ 60
  • 🛠️ 已掌握还需完成实践任务
学习状态:未开始

1.3.10 API 封装与工程目录规范

本章来源:模块封装思路、.h/.c 分工与标准目录结构来自《第一阶段讲义》20.7/20.8 节【来源】,经重新组织表述;include guard 两种写法、static 隐藏内部函数、模块自测与命名规范为成体系补充(约 30%【补充】);两套多文件示例与诊断实验全部新写并本机实测【补充】。平台适用性 universal。

① 学习目标

  1. 说清 .h 与 .c 的分工:.h 放声明/类型/宏(接口契约),.c 放实现;
  2. 为头文件写 include guard(#ifndef 或 #pragma once),并说出二者的可移植性差异;
  3. 用 static 隐藏模块内部函数,让使用者只见公开 API;
  4. 独立完成"需求 → 接口声明 → 实现 → 使用方"的一次 API 封装闭环;
  5. 按 src/inc/bin/obj 标准目录组织多文件工程并编译链接(实践任务)。

② 前置知识

  • 必选:1.3.5 作用域与 static/extern(static 内部链接、extern 声明);
  • 建议:1.1.1 多文件编译链接、1.2.6 头文件与字符串。

③ 核心概念【来源】

概念说明
.h 的职责接口契约:类型定义、宏、函数原型、extern 变量声明;不放假定义、不放函数实现(inline/static inline 例外,如实标注)
.c 的职责实现 .h 声明的函数与全局变量;只放一处定义
include guard防止同一头文件被同一翻译单元重复包含:#ifndef 三行经典写法;#pragma once 简洁但非标准(主流编译器都支持,工程可移植性取舍见 ⑦)
static 隐藏内部函数模块内辅助函数加 static → 内部链接,别的 .c 看不到;公开接口不加 static(衔接 1.3.5)
标准目录src/(源码)、inc/ 或 include/(头文件)、bin/(可执行产物)、obj/(中间目标文件)、lib/(库);源码目录零产物
API 封装流程需求 → 接口声明(.h)→ 实现(.c)→ 使用方(应用层只 include 头文件)→ 模块自测
模块自测每个模块自带 self_test 函数(或独立 test 程序),编译运行即验证模块自洽;嵌入式上电自检同理
命名规范模块前缀统一(rb_push/calc_add)、宏全大写(RB_CAPACITY)、类型后缀 _t(ring_buffer 用结构体名)、内部函数 static

④ 通俗解释【补充】

  • .h 是菜单,.c 是后厨:菜单(声明)告诉客人有什么菜、怎么点;后厨(实现)怎么炒是秘密。客人只拿菜单,不关心后厨细节;
  • include guard 是"防重复入场券":一份菜单被重复递进来,检票员只看"来过没有"——没来过才放行,同一翻译单元不会读两遍;
  • static 是"后厨小门":内部助手只从后厨小门进出(本 .c 可见),客人永远见不到;
  • 标准目录是"五星酒店分区":大厅(bin)、备菜间(obj)、后厨(src)、菜单架(inc)——每个区域只放该放的东西,找东西不迷路。

⑤ 示例代码

代码示例与验证记录

  • examples/ex1-rb.cring buffer 模块 API 封装(.h 声明 + .c 实现 + 使用方,三文件真实编译链接)✓ 已实测(gcc 15.2.0 / MinGW-w64 x86_64 / Windows 11, 2026-08-15
    编译:gcc ex1-rb.c ex1-main.c -o ex1 -std=c11 -Wall -Wextra -Wpedantic
    适用环境:LinuxWindows(MinGW)
    展开预期输出(实测)
    count=3
    pop=1
    pop=2
    self_test=0
    

    差异说明:本机实测 0 警告。ex1-rb.h 用 #ifndef 守卫、只放声明;rb_tail_index 等内部函数 static 隐藏, 使用者(ex1-main.c)只见 rb_* 公开接口。真实工程中三文件分属 inc/ src/ app/ 目录(见正文树状图), 本示例为适配 verify:examples 的平铺布局,编译命令等价于 gcc -Iinc src/rb.c app/main.c

    完整源码见 /code 代码示例页

  • examples/ex2-calc.c标准目录构建(calc 模块:obj/ 目标文件 + bin/ 可执行文件)✓ 已实测(gcc 15.2.0 / MinGW-w64 x86_64 / Windows 11, 2026-08-15
    编译:mkdir obj bin 2>nul & gcc -c ex2-calc.c -o obj/calc.o -std=c11 -Wall -Wextra -Wpedantic && gcc -c ex2-main.c -o obj/main.o -std=c11 -Wall -Wextra -Wpedantic && gcc obj/calc.o obj/main.o -o bin/calc.exe -std=c11 -Wall -Wextra -Wpedantic
    适用环境:Windows(MinGW)
    展开预期输出(实测)
    add: 3+4 = 7
    sub: 10-6 = 4
    mul: 6*7 = 42
    div: 42/6 = 7
    div by zero -> 0, error=1
    

    差异说明:本机实测 0 警告。Linux 等价命令为 mkdir -p obj bin && gcc -c ...(本 compile_cmd 为 Windows/cmd 写法)。 踩坑记录:初版把 calc_div 与 calc_last_error 写进同一条 printf,本机实测 error=0——实参求值顺序未指定 (calc_last_error 先求值);改为先算后打印才得到确定结果,代码注释已说明

    完整源码见 /code 代码示例页

  • examples/ex3-multiple-definition.txt头文件放普通全局定义的后果(diagnostic:multiple definition 真实报错)诊断示例(预期报错/警告)✓ 已实测(gcc 15.2.0 / MinGW-w64 x86_64 / Windows 11, 2026-08-15
    编译:见文件内说明(实验:bad.h 放 int shared_counter = 0;,被 a.c 与 b.c 同时包含后编译链接)
    适用环境:LinuxWindows(MinGW)
    展开预期输出(实测)
    .../ld.exe: C:\...\Temp\ccJgGUkz.o:b.c:(.bss+0x0): multiple definition of `shared_counter'; C:\...\Temp\ccKx9gQA.o:a.c:(.bss+0x0): first defined here
    collect2.exe: error: ld returned 1 exit status
    

    差异说明:本机实测真实报错(临时对象文件名 ccXXXXXX.o 每次不同)。原因:bad.h 放了普通全局定义, 每个包含它的 .c 都生成一份定义,链接期冲突。正确做法:头文件放 extern 声明、定义只放一个 .c; 诊断文本随编译器/链接器可能不同,稳定核心是 multiple definition 与 first defined here 两行

    完整源码见 /code 代码示例页

⑥ 编译与运行方法

  • ex1(模块封装):gcc ex1-rb.c ex1-main.c -o ex1 -std=c11 -Wall -Wextra -Wpedantic——真实工程中对应 gcc -Iinc src/rb.c app/main.c -o bin/app(树状图见迁移训练);
  • ex2(标准目录):三条命令把目标文件放进 obj/、可执行文件放进 bin/(Windows 实测;Linux 用 mkdir -p obj bin./bin/calc);
  • 编译顺序规则:先 -c 编出 .o,再统一链接;改哪个 .c 只重编哪个 .o(增量编译,1.1.4 的 Makefile 会自动做)。

⑦ 常见错误

症状原因解决
multiple definition of 'xxx'; first defined here(链接期)头文件放了普通全局定义,被多个 .c 包含(本机实测诊断 ex3)头文件改 extern 声明,定义只放一个 .c
头文件被重复包含报 redefinition(编译期)没有 include guard加 #ifndef 守卫或 #pragma once(可移植性:前者标准、后者主流编译器都支持)
外部 .c 想用模块内部函数却链接不到内部函数没加 static 泄露了实现细节,或加了 static 却想跨文件调用公开接口进 .h;内部辅助一律 static
改了 .h 不生效依赖关系没声明(Makefile 依赖行漏头文件)或没重编依赖行写全(main.o: main.c rb.h)或 make clean 后重建
全局变量被其他模块随意读写变量进 .h 或没加 static变量 static 限定在 .c 内,跨模块访问走 API 函数(getter/setter)
编译命令里 -I 路径写错(fatal error: xxx.h: No such file)头文件目录与 -I 不一致按标准目录放 inc/,编译统一 -Iinc

⑧ 小练习

小练习

学习自测:提交后才显示答案与解析(前端判分,不作为正式考试)
  • ex-1-3-10-1.头文件(.h)里最适合放什么?(单选)

    ◌ 未作答
  • ex-1-3-10-2.模块内部辅助函数希望只在本 .c 可见,应该怎么声明?(单选)

    ◌ 未作答
  • ex-1-3-10-3.代码审查:某头文件写了 int shared_counter = 0; 且被两个 .c 包含,链接时会怎样?(单选)

    ◌ 未作答

⑨ 章节测验

章节测验

5 题题库 · 随机抽 5 题 · 及格线 60 分 · 前端判分(学习自测)
开始测验 →

⑩ 实战任务

实践任务

把多文件小项目重构为标准目录 + API 封装

把自己的多文件小项目(如成绩管理系统、1.2.5 的排序工具集,或 ex1 的 ring buffer 扩展版)重构为: 每个模块 .h/.c 分离(include guard + static 内部函数),按 src/inc/obj/bin 标准目录组织, 用多文件编译命令(或 Makefile/CMake)构建通过并运行。

输入与输出

无程序输入输出。交付物:① 标准目录工程(每模块 .h/.c 分离);② 构建与运行成功记录;③ 一句话说明 "本项目的公开 API 是什么"。

功能要求

  • 至少 2 个模块,每个模块 .h 声明 + .c 实现
  • 所有头文件有 include guard(#ifndef 或
  • 模块内部辅助函数一律 static
  • 按 src/inc/obj/bin(或等价 Makefile/CMake 目标目录)组织
  • 编译 0 警告(-std=c11 -Wall -Wextra -Wpedantic)

限制条件

  • 实验在专用目录进行,不污染课程仓库
  • 头文件不得放普通全局定义(extern 声明 + 单处定义)

验收步骤(自检清单 0/5

验收标准

  • 每个模块 .h/.c 分离且头文件只含声明(验收步骤 1)
  • 编译命令 0 警告且程序输出正确(验收步骤 4/5)
  • 能一句话说清公开 API 边界(交付物 ③)

常见失败原因

  • 头文件放了普通全局定义 → multiple definition
  • 忘了 include guard → redefinition
  • 内部函数没加 static,接口面积失控
  • -Iinc 路径写错 → No such file or directory

可选扩展

  • 给每个模块加 self_test 函数并在 main 里运行
  • 用 1.1.4 的 Makefile 或 1.1.5 的 CMake 描述该目录工程

完成必要清单后才能计入"已完成实践"(学习状态自动推导,不提供一键完成)

⑪ 面试问题

面试问题

  • 一个 C 模块的头文件应该放什么、不应该放什么?高频C/C++ · medium

    要点:放:类型定义、宏、函数原型、extern 变量声明;不放:普通全局定义、非 inline 函数实现。

    头文件是接口契约:使用者 include 它即可获得编译期所需的一切(类型、原型、宏)。普通全局定义 (int x = 0;)放进头文件后,每个包含它的 .c 都会生成一份定义,链接期 multiple definition; 函数实现同理。例外:static inline 函数与 static const 常量(内部链接、每翻译单元独立副本)可以在 头文件中出现,但要说清语义。判断标准一句话:同一头文件被多个 .c 包含后能否无冲突地链接。

    追问:
    • 追问:extern int x; 与 static int x; 放头文件各是什么语义?
    评分要点:
    • 接口契约定义
    • multiple definition 风险
    • inline/static 例外与语义
  • #ifndef 守卫和 #pragma once 有什么区别?工程里怎么选?高频C/C++ · medium

    要点:都是防重复包含:#ifndef 是标准三行写法;#pragma once 一行但非标准。工程默认

    #ifndef GUARD_H / #define GUARD_H / ... / #endif 由 C 标准保证,任何编译器都工作;缺点是需要 保证宏名唯一(模块前缀即可)。#pragma once 只需一行,主流编译器(gcc/clang/msvc)都支持且 行为一致,但它是预处理指令的编译器扩展,严格可移植代码用 #ifndef。嵌入式工程编译器集通常固定, 两种都能用;对外发布的库建议 #ifndef(对任何消费者都成立)。

    追问:
    • 追问:#ifndef 的宏名冲突会怎样?(守卫失效、重复包含报错;用模块前缀避免)
    评分要点:
    • 两者机制
    • 可移植性取舍
  • 模块内部函数为什么要加 static?不加会有什么工程问题?高频C/C++ · medium

    要点:static 给内部链接,函数只在定义它的 .c 内可见:隐藏实现细节、避免跨模块符号冲突。

    static 内部链接的三点收益:① 封装——使用者只见 .h 的公开 API,内部实现(缓冲区管理、表查找等) 不可见不可调用,改实现不影响外部;② 命名空间——两个模块都有同名内部函数(如 sort)互不冲突; ③ 优化机会——编译器可见完整调用图时可内联/死代码消除。不加 static 的全局函数会进符号表, 多模块工程里同名符号冲突、接口面积失控、重构成本上升。

    追问:
    • 追问:static 变量与 static 函数的作用域/链接属性分别是什么?
    评分要点:
    • 封装与符号冲突
    • 链接属性准确

⑫ 延伸阅读

  • 《C 程序设计语言(K&R)》4.5 节(头文件)——只引章节名;
  • 《C 语言程序设计:现代方法》第 15 章(编写大型程序)——只引章节名;
  • 嵌入式惯例:Linux 内核代码风格(Documentation/process/coding-style,static 函数与命名规范),只引名称;
  • 下一章预告:1.3.11 C11 嵌入式相关特性——用 stdint/_Static_assert/_Atomic 武装你的模块。

迁移训练(migration training)

标准目录树(跨平台通用,Windows/Linux/macOS 一致):

project/
├── inc/          # 头文件(接口)
│   └── rb.h
├── src/          # 源码(实现)
│   └── rb.c
├── app/          # 使用方/入口
│   └── main.c
├── obj/          # 目标文件(.o/.obj,构建产物,不入库)
├── bin/          # 可执行产物
└── lib/          # 静态/动态库(1.1.3)

要改的:产物后缀(.exe vs 无后缀)与删除命令(del vs rm);不变的:目录分区、-Iinc 用法、.h/.c 分工与全部 API 封装纪律。

内容来源映射

内容部分资料位置标记说明
模块封装思路、.h/.c 分工、标准目录结构第一阶段讲义20.7/20.8 节来源正文在原资料基础上重新组织表述,未大段复制原文
编码规范成体系内容(include guard 两种写法、static 隐藏内部函数、模块自测、命名规范)补充原资料仅零散提及;本章成体系补充,比例约 30%
ring buffer / calc 两套多文件示例与诊断实验、练习/测验/任务/面试题补充全部新编并本机实测(gcc 15.2.0)