第3章:多文件项目——把源文件拆成 .cpp 和 .h
说实话,我刚开始学 C++ 的时候,也喜欢把所有代码塞到一个 main.cpp 里。那时候觉得「多文件?麻烦!一个文件搞定多爽」。直到有一次,我接手了一个同事留下的项目——好家伙,一个文件 8000 行,找函数定义全靠 Ctrl+F……嗯,从那以后,我再也不敢这么干了。
这一章,我们就来聊聊怎么把代码拆成多个 .cpp 和 .h 文件,然后用 CMake 把它们组织起来。说白了,就是让项目结构更清晰,让编译器知道该编译哪些文件。
为什么要把代码拆开?
你想想看,一个项目如果只有单个文件,会有什么问题?
- 编译慢:改一行代码,整个文件重新编译。项目大了以后,每次改代码都要等几分钟。
- 难维护:几千行代码挤在一起,找 bug 像大海捞针。
- 没法复用:写好的工具函数想在其他项目里用?得复制粘贴,改一处还得改多处。
拆成多文件之后,每个 .cpp 文件单独编译成 .o 目标文件,最后再链接到一起。改了一个文件,只需要重新编译那一个文件,其他文件直接用缓存。编译速度能快不少。
核心原则:.h 文件放声明,.cpp 文件放实现。头文件是「接口」,源文件是「实现细节」。
一个简单的多文件项目长什么样?
假设我们要写一个计算器程序,支持加法和减法。我习惯这样组织文件:
project/
├── CMakeLists.txt
├── main.cpp
├── add.h
├── add.cpp
├── sub.h
└── sub.cpp
每个模块一对文件:头文件声明函数,源文件实现函数。main.cpp 负责调用它们。
add.h
#ifndef ADD_H
#define ADD_H
int add(int a, int b);
#endif
add.cpp
#include "add.h"
int add(int a, int b) {
return a + b;
}
sub.h
#ifndef SUB_H
#define SUB_H
int sub(int a, int b);
#endif
sub.cpp
#include "sub.h"
int sub(int a, int b) {
return a - b;
}
main.cpp
#include <iostream>
#include "add.h"
#include "sub.h"
int main() {
int x = 10, y = 5;
std::cout << "add: " << add(x, y) << std::endl;
std::cout << "sub: " << sub(x, y) << std::endl;
return 0;
}
小提示:头文件里的 #ifndef / #define / #endif 是「头文件保护符」,防止同一个头文件被多次包含导致重复定义。我建议每个头文件都加上,养成习惯。
CMake 怎么写?
多文件项目的 CMakeLists.txt 其实很简单。你只需要把所有的 .cpp 文件列在 add_executable 里就行:
cmake_minimum_required(VERSION 3.10)
project(MultiFileDemo)
add_executable(main
main.cpp
add.cpp
sub.cpp
)
注意,这里只列了 .cpp 文件,没有列 .h 文件。为什么?因为 CMake 在编译时会自动扫描 #include 指令,头文件会被自动依赖进去。你不需要手动添加 .h 文件到 add_executable 里。
不过,我个人习惯还是把 .h 文件也列上,虽然不强制,但这样在 IDE 里能看到头文件,方便编辑:
add_executable(main
main.cpp
add.cpp
add.h
sub.cpp
sub.h
)
注意:如果你把 .h 文件列在 add_executable 里,CMake 会把它标记为「头文件」,不会参与编译,但会出现在 IDE 的项目视图中。这纯粹是为了开发体验,不影响编译结果。
编译和运行
和单文件项目一样,三步走:
mkdir build
cd build
cmake ..
make
或者用 cmake --build . 代替 make:
cmake --build .
运行:
./main
输出:
add: 15
sub: 5
知识结构图
下面这张图展示了多文件项目的核心逻辑:
避坑指南
我曾经在一个项目里犯过一个低级错误:写好了 add.cpp 和 sub.cpp,但在 CMakeLists.txt 里只写了 main.cpp。结果编译通过,链接时报了一堆「undefined reference」错误。当时我还纳闷:明明函数声明都写了啊?后来才发现,源文件根本没参与编译。
所以,记住一条铁律:每个 .cpp 文件都要出现在 add_executable 里。漏一个,链接就报错。
另外,头文件里的函数声明要和源文件里的函数签名完全一致。我见过有人把参数类型写错了,比如头文件写 int add(int a, int b),源文件写 int add(int x, int y, int z)……这种错误编译器不会报错,但链接时会说「找不到函数定义」。嗯,排查起来挺头疼的。
总结
多文件项目其实不复杂,核心就三点:
- 每个模块一对 .h 和 .cpp 文件
- CMakeLists.txt 里列出所有 .cpp 文件
- 头文件用 #ifndef 保护,防止重复包含
掌握了这些,你就可以把任何单文件项目拆成多文件了。下一章我们会聊更高级的话题——但先别急,先把多文件玩熟练再说。