
用 DEV-C 写代码写到一定阶段几乎所有人都会撞上同一堵墙main.cpp 越来越长函数、结构体、宏定义全堆在一起翻到第 400 行想找个工具函数得靠搜索。真正的解法不是把文件拆成 main1.cpp、main2.cpp 乱放一气而是学会写自定义头文件——也就是扩展名为 .h 的那种文件。它的作用说白了很朴素把对外承诺和内部实现分开谁需要用就 include 一下代码立刻从一锅粥变成有层次的工程。这篇文章面向的是刚脱离单文件阶段的 DEV-C 使用者也会覆盖一些写了几年 C/C 但一直没把多文件编译搞明白的人。我会从 DEV-C 里怎么新建、保存、挂载 .h 文件讲起把编译与链接的分工、头文件保护的写法、变量和函数的边界、以及最烦人的 undefined reference 和 multiple definition 报错逐条拆开。全程贴着 DEV-C 5.11 和小熊猫 DEV-C 的实际菜单来写你能直接抄作业。1. 先把这件事说清楚头文件到底解决了什么问题1.1 从所有代码塞进一个文件说起单文件写程序在几百行以内是舒服的改一处、编一次、跑一次没有心智负担。问题出在两个地方一是复用二是协作。假设你写了一个判断素数的函数、一个把字符串转大写的函数、一个自定义的栈结构下一个项目还要用你只能复制粘贴。复制的那一刻起两份代码就开始各自演化第三次复制时你已经分不清哪份是最新的了。把头文件抽出来等于给这批工具建了一个目录其他文件只要引用这个目录就能拿来用维护点从 N 个变成 1 个。第二个问题是编译本身。C 和 C 都是分开编译、最后链接的语言模型。编译器每次只看一个源文件它根本不知道别的 .cpp 文件里定义了什么函数。你在 a.cpp 里调用了一个 b.cpp 里的函数编译器凭什么相信这个函数存在、参数对不对靠的就是你在 a.cpp 顶部 include 进来的那个 .h 文件。头文件对编译器说的话是这个函数长这样参数是这些类型返回值是这个类型你先把调用点编译通过真正的函数体在别处交给链接器去接。这句话是整个机制的钥匙理解了它后面所有的报错都好解释了。1.2 头文件的本质给编译器递一张目录我习惯把 .h 文件类比成餐厅的菜单。菜单上写着宫保鸡丁38 元你点单时服务员就知道你要什么但厨房里怎么炒、放多少辣椒跟菜单没关系。菜单是声明厨房是定义。头文件里放的就是菜单——函数声明、结构体定义、宏、类型别名实现放在 .cpp 里那是厨房。这个类比能解释很多现象。比如菜单上写了宫保鸡丁但厨房没这道菜会发生什么编译能过菜单有了链接会挂找不到做菜的报错就是undefined reference to xxx。反过来菜单上把同一道菜写了三遍服务员会以为有三道独立的菜这就是multiple definition或者redefinition。几乎所有关于头文件的问题都能用菜单 vs 厨房这套模型推出来。还有一个容易忽略的点头文件不参与独立的编译过程。你在 DEV-C 的工程树里看到 mathutils.h 挂在项目下面它不会被单独编译成 .o 文件。它只是一个被#include时原地展开的文本。#include这个指令本质上是预处理器的文本替换预处理器把 head.h 的全部内容一字不差地粘贴到 include 那一行所在的位置然后才交给编译器。所以头文件里写什么就等于直接写在每一个 include 它的 .cpp 文件开头。1.3 DEV-C 里 .h 文件的特殊待遇DEV-C 这套 IDE 有个很老派的设计它把源文件清单写在一个 .dev 工程文件里左侧的工程面板展示的就是这份清单。默认新建项目时它只给你一个 main.cpp附带一个可选的 main.h有时候不给你。你在工程上右键选择新建文件或者从文件 → 新建 → 源代码创建的空白文件默认扩展名都是 .cpp。想得到一个 .h 文件必须手动处理扩展名这是新手第一个卡点。DEV-C 对 .h 文件的态度是只显示、不编译。把 .h 加进工程好处是它会在左侧树里出现双击直接打开编辑还支持语法高亮和代码补全不加入工程也完全不影响使用只要#include 路径/文件名.h写对编译器照样能找到。所以有人问我的头文件没加到工程里能用吗答案是能加进去只是为了方便管理。真正必须加进工程的是 .cpp 文件因为只有它们会被编译成目标文件。顺带说一句版本差异。Orwell 的 DEV-C 5.11 停在 2015 年内置 TDM-GCC 4.9.2对 C 语言默认还是 gnu89 标准inline关键字属于扩展支持写的时候要多留个心眼。小熊猫 DEV-C 是近几年重新维护的分支编译器更新到 GCC 9 甚至更高支持 C99、C11、C17语法体验和报错提示都舒服很多。两个版本在新建头文件这个操作上几乎一致但编译选项和错误提示信息差别不小后面讲排查时会分别提到。2. DEV-C 中新建、保存与挂载 .h 文件的完整操作2.1 三种创建 .h 文件的真实做法第一种是新建工程时顺手加。文件 → 新建 → 项目弹出向导选Console Application语言选 C给个名字比如 MathDemo路径选一个干净的空目录。向导下一步会问是否创建 main.cpp勾上完成后你就有了一个最小可运行工程。接着在左侧工程面板的工程名上右键选新建文件New File得到空白标签页写内容然后按下面说的方式另存为 .h。第二种是纯手工新建再挂载。文件 → 新建 → 源代码CtrlN直接写头文件内容存盘时走文件 → 另存为文件名输入mathutils.h关键是右下角保存类型下拉框要选头文件 (*.h)或者干脆选所有文件否则 DEV-C 可能给你补成 mathutils.h.cpp 这种鬼东西。保存完回到工程面板右键选添加Add把刚才的 .h 和配套的 .cpp 一起加进工程。第三种是直接在文件系统里操作。用资源管理器在你工程目录下手动新建一个文本文件改成 mathutils.h再用 DEV-C 打开然后添加进工程。这个方法土但在批量整理已有代码时最快。我个人常年用第二种因为保存类型那一步虽然烦人但一次性想清楚文件定位比事后追着改扩展名省事。2.2 保存类型那一步扩展名是怎么定下来的这一步值得单独说因为它是最多人翻车的点。DEV-C 的保存对话框有一个下拉的保存类型还有一行文件名。如果你在文件名里只写mathutils那么最终扩展名由保存类型决定选 C 源文件就变成 mathutils.cpp选头文件才变成 mathutils.h。如果你在文件名里已经写全了mathutils.hDEV-C 一般会尊重你的输入但保险起见还是把保存类型调到头文件 (*.h)。另一个坑是 Windows 资源管理器默认隐藏已知扩展名。你在文件夹里看到文件叫mathutils以为它是 .h其实可能是 mathutils.h.cpp 或者 mathutils.txt。写编译不通过又找不到原因时第一件事就是打开资源管理器的查看 → 显示 → 文件扩展名把真实后缀看清楚。这个习惯救过我很多次。注意头文件的文件名不要带空格、不要用中文、不要起成stdio.h、string.h这类和标准库重名的名字。否则#include string.h很可能先命中你自己的文件导致标准库函数全部失踪报错信息还特别误导人。2.3 工程文件 .dev 与目录组织的实际做法一个能长期用下去的目录结构我推荐这样MathDemo/ ├── MathDemo.dev // DEV-C 工程文件 ├── main.cpp ├── src/ │ ├── mathutils.cpp │ └── stringutils.cpp ├── include/ │ ├── mathutils.h │ └── stringutils.h └── build/ // 存放 .o 和 .exe可选不过要注意DEV-C 5.11 对相对路径的处理比较弱如果你把头文件放进了 include/ 子目录引用时就得写#include include/mathutils.h或者去工程属性里配置包含目录。在项目 → 项目属性 → 目录里有一栏包含目录Include Directories把include加进去之后就能直接#include mathutils.h。这个配置写进 .dev 文件跟着工程走换台机器打开也不用重设。对于刚开始练手的人我建议先别搞子目录把所有 .h 和 .cpp 和 .dev 平铺在同一个文件夹里。#include mathutils.h因为用的是双引号编译器会先在当前源文件所在目录找一找就中零配置。等你有五个以上头文件、开始觉得乱的时候再上 include/ 目录结构那时候你对搜索顺序已经有直觉了改起来不会慌。3. 头文件里放什么、不放什么内容规范与保命写法3.1 声明与定义的分工头文件里的内容可以分成两类可以放的和绝对不能放的。可以放的包括函数声明、结构体/联合体/枚举定义、typedef 和 using 别名、宏定义、extern 变量声明、模板和 inline 函数。绝对不能放的是普通函数体、普通全局变量的定义、带初始化的非 const 全局变量。为什么普通函数体不能放假设 mathutils.h 里有int mu_add(int a,int b){return ab;}main.cpp 和 mathutils.cpp 都 include 了它。预处理之后两个 .cpp 各自都有了一份完整的 mu_add 函数体编译成两个 .o链接器同时看到两份同名的全局符号直接报multiple definition of mu_add(int, int)。解决方案有三条路把函数体挪到 .cpp 里、给函数加inline、给函数加static。三条路各有适用场景后面会讲怎么选。普通全局变量同理。int g_count 0;写在头文件里被两个 .cpp 包含就变成两份额外的定义。正确写法是在头文件里写extern int g_count;这只是个声明告诉编译器有这么个 int在别处定义然后在某一个.cpp 里写int g_count 0;完成实际的内存分配。这是 C/C 里声明不分配空间、定义才分配空间的最直接体现。3.2 头文件保护include guard 与 #pragma once重复包含是另一类经典问题。main.cpp include 了 mathutils.h又 include 了 stringutils.h而 stringutils.h 内部也 include 了 mathutils.h。预处理器是傻的它不管三七二十一把 mathutils.h 的内容粘了两次。如果里面有结构体定义第二次粘贴时编译器就喊redefinition of struct Point2D。标准解法是 include guard也就是条件编译包裹#ifndef MATHUTILS_H #define MATHUTILS_H // 头文件正文 #endif逻辑很简单第一次被包含时MATHUTILS_H还没定义#ifndef成立进入正文顺手把MATHUTILS_H定义掉。第二次被包含时MATHUTILS_H已经有定义了#ifndef不成立整段被跳过。宏名字要唯一惯例是文件名全大写、点换成下划线前面加上项目前缀比如MYAPP_MATHUTILS_H避免和别人的库撞名。另一个写法是#pragma once#pragma once // 头文件正文一行搞定可读性更好。它是编译器扩展不是标准但 GCC、Clang、MSVC 全都支持DEV-C 用的 GCC 自然没问题。我的习惯是两个都写先#pragma once让老编译器也走快速路径再包一层 include guard 保证跨编译器可移植。多打几行字的事儿换来的是永远不用再想这个问题。3.3 变量、宏、结构体、inline 函数的边界宏定义放头文件完全没问题它只是文本替换。但要注意宏不占符号空间重复定义只要内容一致编译器一般不报错会警告。所以别指望着 include guard 拦宏它的作用是拦整个文件。结构体、枚举、类定义放头文件也是安全的因为它们描述的是类型长什么样本身不产生链接期的符号多个编译单元各存一份类型信息链接器不关心。这就是为什么 STL 的 vector、string 都是纯头文件实现——模板和类型定义天生适合放头文件。inline 函数是头文件里的逃生通道。被声明为 inline 的函数允许在多个编译单元里有相同定义链接器会挑一份留下其余丢弃。所以短小的工具函数比如前面那个mu_distance_sq直接写成 inline 塞进头文件是完全没有问题的而且还能享受编译器的内联优化inline double mu_distance_sq(const Point2D p, const Point2D q) { double dx p.x - q.x; double dy p.y - q.y; return dx * dx dy * dy; }用 C 语言的朋友要注意C99 之前没有 inline 关键字GCC 提供__inline__作为扩展。DEV-C 5.11 默认 gnu89 标准下inline能被识别但语义有点绕最保险的做法是用static inline明确告诉编译器这个函数只在当前编译单元内部用每个 .cpp 各存一份副本牺牲一点体积换绝对不冲突。还有一个高级知识点值得记住C 里const修饰的全局变量默认是内部链接所以const int MAX_SIZE 100;写在头文件里是安全的每个包含它的文件各有一份不冲突。而 C 语言里 const 全局变量是外部链接的同样的写法会引发 multiple definition。这个差异很多人踩过写 C 的时候别照搬 C 的习惯。3.4 三条绝对不要写在头文件里的东西第一条using namespace std;。每次我看到有人在头文件里写这句都想寄一封信过去。这行代码会在每一个 include 它的源文件里生效把 std 命名空间里成百上千个名字全部倒进全局作用域。你项目里的 count、distance、begin、end 这些变量名全可能跟标准库撞车报错信息往往是有歧义的调用让人一头雾水。头文件和源文件里老老实实写 std::cout、std::string如果嫌长就用using std::string;这种精确引入。第二条带函数体的非 inline 函数。前面解释过原因了链接必挂。第三条#include一堆用不到的头。头文件里 include 的每个东西都会传染给所有包含它的文件。如果你在 mathutils.h 里 include 了整个iostream那么每个用到 mathutils.h 的 .cpp 都会被迫编译 iostream编译时间无谓地变长。原则是头文件里尽量用前置声明代替 include只有真正需要完整类型定义时才 include。比如函数参数是指针或引用时写struct Point2D;就够了不必把整个 Point2D 的定义拉进来。4. 完整实操从零手搓一个可复用工具头文件4.1 需求拆解与文件划分光说不练假把式这里做一个小而完整的东西一个数学工具模块提供加减乘除、最大公约数、两点距离平方外加一个记录调用次数的全局计数器再配一个 inline 的距离函数演示头文件内联写法。文件划分如下文件角色是否参与编译mathutils.h对外接口声明、结构体、宏、inline否mathutils.cpp实现函数体、全局变量定义是main.cpp使用者include 并调用是这个划分对应了最经典的头文件—实现—调用方三件套绝大部分 C/C 项目都是这个模式的变体。4.2 源码mathutils.h / mathutils.cpp / main.cpp先写 mathutils.h#pragma once #ifndef MYAPP_MATHUTILS_H #define MYAPP_MATHUTILS_H #define MU_VERSION 1.0.0 #define MU_MAX_DIM 64 // 全局计数器只声明不分配空间 extern int g_mu_call_count; // 类型定义放头文件是安全的 struct Point2D { double x; double y; }; // 函数声明 int mu_add(int a, int b); int mu_sub(int a, int b); long mu_mul(int a, int b); int mu_div(int a, int b, bool ok); int mu_gcd(int a, int b); // 短小函数直接内联在头文件里 inline double mu_distance_sq(const Point2D p, const Point2D q) { double dx p.x - q.x; double dy p.y - q.y; return dx * dx dy * dy; } #endif再写 mathutils.cpp#include mathutils.h // 全局变量在这里完成定义分配内存 int g_mu_call_count 0; int mu_add(int a, int b) { g_mu_call_count; return a b; } int mu_sub(int a, int b) { g_mu_call_count; return a - b; } long mu_mul(int a, int b) { g_mu_call_count; return (long)a * b; } int mu_div(int a, int b, bool ok) { g_mu_call_count; if (b 0) { ok false; return 0; } ok true; return a / b; } int mu_gcd(int a, int b) { g_mu_call_count; if (a 0) a -a; if (b 0) b -b; while (b ! 0) { int t a % b; a b; b t; } return a; }最后是 main.cpp#include iostream #include mathutils.h int main() { bool ok false; std::cout MU_VERSION MU_VERSION std::endl; std::cout 3 5 mu_add(3, 5) std::endl; std::cout 9 - 4 mu_sub(9, 4) std::endl; std::cout 6 * 7 mu_mul(6, 7) std::endl; std::cout 10 / 3 mu_div(10, 3, ok) std::endl; std::cout 48 与 36 的最大公约数 mu_gcd(48, 36) std::endl; Point2D p {0.0, 0.0}; Point2D q {3.0, 4.0}; std::cout 两点距离平方 mu_distance_sq(p, q) std::endl; std::cout 累计调用次数 g_mu_call_count std::endl; return 0; }注意mu_div(10, 3, ok)第三个参数是引用ok会被函数内部改写调用完可以拿它判断是否除零。这是 C 相对 C 的一个便利点C 里得传int *ok再解引用。4.3 DEV-C 中的编译运行与关键选项把三个文件放进同一个目录用 DEV-C 打开工程依次右键工程 → 添加把 mathutils.h 和 mathutils.cpp 都加进去。加 .h 纯粹是为了在左侧看着方便加 .cpp 才是关键——只把 main.cpp 加进工程、忘了加 mathutils.cpp编译能过、链接一定挂这是新手第一大坑报错就是undefined reference to mu_add(int, int)。工程里三个文件都到位之后按 CtrlF9 编译或者直接 F9 编译并运行。DEV-C 下方会弹出编译日志标签页正常情况下输出大致是Compiler: TDM-GCC 4.9.2 64-bit Building Makefile... g.exe -c main.cpp -o main.o g.exe -c mathutils.cpp -o mathutils.o g.exe main.o mathutils.o -o MathDemo.exe Compilation results... 0 errors, 0 warnings这三行就是整个 C/C 构建过程的缩影两个 .cpp 各自编译成 .o然后一个 g 命令把两个 .o 和运行时库链接成 exe。看懂了这三行你就理解了为什么头文件不出现——它根本没资格出现在编译命令行里它的内容早就被预处理粘贴到 main.cpp 和 mathutils.cpp 里面去了。如果你用的是 C 语言项目向导里语言选 C代码用 .c 后缀那 mathutils.h 里的inline和引用参数都要改函数改成int mu_div(int a, int b, int *ok);inline 函数改成static inline或者干脆搬到 .c 里。同时在项目 → 项目属性 → 编译器的编译时加入以下命令里补一个-stdc99让编译器按 C99 处理报错提示会准确很多。4.4 编译单元与链接过程现场还原想真正把这事刻进脑子可以做个实验故意把 mathutils.cpp 从头文件列表里移除只留下 mathutils.h 和 main.cpp编译一次。你会发现编译阶段完全不报错甚至编译器还会兴致勃勃地帮你检查函数签名对不对等到链接阶段才炸报出一条undefined reference to mu_add(int, int)。这个现象完美验证了前面说的编译只看签名、链接才找实现。反过来做第二个实验把 mathutils.cpp 里的任何一个函数体比如 mu_add复制一份到 mathutils.h 里保持声明不变。编译通过链接报multiple definition of mu_add(int, int)。因为 main.cpp 和 mathutils.cpp 各自展开了一份定义两份 .o 里都有这个符号。做第三个实验把 mu_add 的定义原样搬进头文件只是把函数签名改成inline int mu_add(int a, int b)。这次编译链接全过。inline 就是在告诉链接器我知道你会看到多份随便挑一份用别嚷嚷。三个实验做完头文件、定义、内联、链接的关系就彻底清楚了。这三个实验我建议每个初学者都亲手跑一遍比看十篇文章管用。5. 报错排查实录自定义头文件最常见的六类问题5.1 undefined reference定义没进编译这是出现频率最高的报错格式长这样[Linker error] undefined reference to mu_add(int, int)看到它先别慌按顺序查三件事。第一mathutils.cpp 在不在工程里左侧工程树看不到就是没加右键添加进来重新编译。第二函数名和参数类型有没有拼错C 有函数重载mu_add(int, int)和mu_add(double, double)是两个不同的符号报错信息里的签名和头文件里的声明逐字对比一遍。第三mathutils.cpp 顶部有没有#include mathutils.h没 include 的话编译器不知道你实现的是哪个函数可能按 C 的规则编出了一个同名但签名不同的符号链接照样找不到。还有一种情况是 C 和 C 混编导致的符号名不匹配。C 会把函数名做名称修饰name manglingmu_add编出来可能是_Z6mu_addiiC 不做修饰就是mu_add。如果 .cpp 是 C 编译的、另一个 .cpp 当 C 编译符号名对不上就报 undefined reference。解决办法是在头文件里加extern C包裹或者在项目里统一语言。DEV-C 里混编的情况不多但如果你以后把代码挪到别的工具链这个坑迟早会遇到。5.2 multiple definition定义写进了头文件报错长这样multiple definition of g_mu_call_count first defined here或者redefinition of struct Point2D第一种通常是变量在头文件里带了初始化被多个 .cpp 包含。解法是把定义挪到一个 .cpp头文件只留extern声明。第二种通常是少了 include guard同一个头文件被间接包含两次。解法就是加#pragma once和#ifndef包裹。还有一种隐蔽的情况你在头文件里写了一个 helper 函数忘了加 inline当前只有 main.cpp 一个使用者编译完全正常。等你后来加了第二个 .cpp 也 include 这个头文件突然就报 multiple definition。这种代码没改、加了文件就报错的现象八成就是这个原因。5.3 No such file or directory路径与搜索顺序报错长这样mathutils.h: No such file or directory双引号#include xxx.h和尖括号#include xxx.h的搜索顺序不同这条必须记牢写法搜索顺序#include mathutils.h当前源文件目录 → -I 指定的包含目录 → 系统目录#include mathutils.h-I 指定的包含目录 → 系统目录跳过当前目录自己写的头文件一律用双引号这是最省事的选择。如果你把 .h 放进了 include/ 子目录要么改 include 路径为include/mathutils.h要么去项目 → 项目属性 → 目录的包含目录里把 include 加进去之后保持mathutils.h的写法不变。我个人偏好后者因为代码里的 include 语句干净换目录结构时只改工程配置不改源码。5.4 重复包含引发的 redefinition前面提过这里补充一个真实场景。你有一个 utils.h 定义了 struct Config另一个 logger.h 里有#include utils.hmain.cpp 同时 include 了 utils.h 和 logger.h。预处理后 utils.h 展开两次struct Config 被定义两次报 redefinition。加 include guard 就是一瞬间的事但没加的人会盯着报错信息里的行号反复看以为是自己写错了结构体。还有一种是循环包含a.h include b.hb.h 又 include a.h。这种情况下 include guard 能救命——它保证每个头文件在单次预处理过程中只展开一次。但如果 a.h 和 b.h 互相需要对方的完整类型定义比如 a.h 里的结构体包含 b.h 里的结构体作为成员include guard 也救不了得动手重构要么合并成一个头文件要么改成指针成员加前置声明。循环包含是设计问题不是语法问题遇到了要顺着依赖关系理一遍。5.5 排查速查表把上面几类问题整理成一张表出问题时对着查现象可能原因快速验证undefined reference.cpp 没加入工程看左侧工程树有没有该 .cppundefined reference函数名/参数签名不一致头文件声明与实现逐字对比undefined reference缺#include xxx.h检查 .cpp 顶部multiple definition函数体写进头文件且非 inline搜索头文件里的{函数体multiple definition全局变量在头文件里带初始化检查是否写成 externredefinition缺 include guard检查#ifndef/#pragma onceNo such file路径写错或双引号/尖括号用错改用双引号并确认目录改了 .h 但行为没变没触发重新编译CtrlF9前先做一次全部重建编译过了但运行结果怪异头文件里有 using namespace std 造成名字冲突全局搜索该语句最后一行那个改了 .h 但没生效值得多说一句。DEV-C 的增量编译靠时间戳判断正常情况下改 .h 会触发依赖它的所有 .o 重新编译。但在网络盘、时间戳异常、或者从压缩包里解压出来的一堆同时间文件面前这个机制可能失灵导致你改了声明却还在用旧的 .o。遇到明明改了却没反应的情况直接运行 → 全部重新编译或者手动删掉目录下所有 .o 文件问题基本消失。提示把.o文件当成可丢弃的中间产物任何时候觉得构建结果可疑删掉全部 .o 重编一遍是成本最低的排查手段。6. 长期维护视角把 .h 用成真正的工程资产6.1 命名、目录与注释的约定头文件数量上到十几个以后命名规范的价值就显现了。我个人的约定是文件名全小写多个单词用下划线连比如string_utils.h宏全部大写加项目前缀比如MYAPP_STRING_UTILS_H每个头文件顶部写一段注释说明它提供什么、依赖什么、有没有线程安全要求。这段注释不是写给别人的是写给三个月后的自己的——那时候你早忘了 string_utils.h 里的 normalize 函数到底会不会修改原字符串。函数声明的注释我建议直接写在头文件里而不是实现文件里。因为使用者打开头文件就能看到接口文档不用翻到 .cpp 去看函数体。这才是头文件作为菜单的正确用法。DEV-C 有代码补全功能把参数名写完整int mu_div(int a, int b, bool ok);而不是int mu_div(int, int, bool);补全提示里就能看到参数含义。目录结构方面超过十个头文件就上 include/ 子目录超过三个模块就按模块再分子目录。但别一上来就搞三层嵌套那是给自己找罪受。DEV-C 对深目录的支持不算好路径一长工程属性里的包含目录配置容易出错新手阶段平铺最安全。6.2 从 .h 到静态库的扩展路径头文件用熟了之后自然会想更进一步把一批 .cpp 编译成静态库.a 文件以后新项目只带一个 .h 和一个 .a不用再拖一堆源码。这个思路在 DEV-C 里也能走通用 GCC 的 ar 命令就行g -c mathutils.cpp -o mathutils.o ar rcs libmathutils.a mathutils.o然后新项目里只需要libmathutils.a和mathutils.h编译时链接上这个库g main.cpp -L. -lmathutils -o app.exe工程属性里可以在链接器参数栏加上-L. -lmathutils。这条路的好处是源码不暴露、编译更快坏处是调试麻烦、改一行要重新打库。我的建议是模块还在频繁改动时用源码形式稳定了、要被多个项目引用了再固化成静态库。走到这一步你已经从会写 .h 文件进阶到会用构建系统组织代码了。回头看最开始那个把所有东西堆在 main.cpp 里的自己会发现真正的分水岭不是学会了某个语法而是脑子里建立了声明与实现分离的模型。这个模型在 C 里是头文件在 Java 里是接口在 Python 里是模块的__all__语言换了一茬又一茬思路从来没变过。我自己带过的新人里凡是能把.h用明白的后面学任何构建工具都上手很快因为底层逻辑早就打通了。