第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

知识结构图

下面这张图展示了多文件项目的核心逻辑:

多文件项目核心逻辑 .cpp 源文件 .h 头文件 CMakeLists.txt add_executable(main ...) 编译 → .o 目标文件 链接 → 可执行文件 CMake 根据 add_executable 中的 .cpp 列表生成编译规则 头文件通过 #include 自动被依赖,无需手动添加 ⚠ 常见坑点 漏掉 .cpp 文件 → 链接错误(undefined reference)

避坑指南

我曾经在一个项目里犯过一个低级错误:写好了 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 保护,防止重复包含

掌握了这些,你就可以把任何单文件项目拆成多文件了。下一章我们会聊更高级的话题——但先别急,先把多文件玩熟练再说。


公众号:蓝海资料掘金营,微信deep3321