多文件工程

代码变多后,把接口、实现和程序入口放在同一个文件中会很难阅读。本章把 Hello, World! 示例拆成三个源文件,再由 CMake 构建成一个程序。

查看工程结构

工程目录
multi-file-project/
├── CMakeLists.txt    # 构建规则
├── build-and-run.sh  # 一键构建并运行
├── include/
│   └── greeting.h    # 接口声明
└── src/
    ├── greeting.cpp  # 接口实现
    └── main.cpp      # 程序入口

新增头文件放入 include/,新增源文件放入 src/,构建规则按这两个目录查找。

阅读源码

greeting.h 声明 MakeGreeting()。声明放在头文件里,供其他源文件在编译时核对调用写法。

include/greeting.h
// 问候接口:声明 MakeGreeting(),供其他源文件调用。
#pragma once

#include <string>

std::string MakeGreeting();

greeting.cpp 先包含自己的头文件,编译器据此检查函数定义与声明一致。

src/greeting.cpp
// 实现 greeting.h 中声明的 MakeGreeting()。
#include "greeting.h"

#include <string>

std::string MakeGreeting() {
  return "Hello, World!";
}

main.cpp 只包含 greeting.h,不需要知道函数实现在哪个文件,链接器会把两者接在一起。

src/main.cpp
// 程序入口:调用 MakeGreeting() 并输出结果。
#include <iostream>

#include "greeting.h"

int main() {
  std::cout << MakeGreeting() << "\n";
  return 0;
}

编译 main.cpp 时,编译器只需要看到函数声明。链接阶段再把 main.cpp 和 greeting.cpp 连成一个程序。

查看构建规则

CMakeLists.txt
# 构建 MultiFileProject 可执行目标。
cmake_minimum_required(VERSION 3.31)
project(MultiFileProject LANGUAGES CXX)

# 使用 C++20 作为语言标准。
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 可执行文件统一输出到 build/bin。
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)

# 编译数据库,记录每个源文件的实际编译参数。
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

# 收集 src/ 下的直属源文件,新增 .cpp 后重新构建即可进入目标。
file(GLOB APP_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp")

# 创建 app 可执行目标,源文件来自 src/ 的收集结果。
add_executable(app ${APP_SOURCES})

# 让 app 找到 include/ 中的头文件。
target_include_directories(app PRIVATE include)

# 编译时启用 libc++ 与常用警告。
target_compile_options(app PRIVATE -stdlib=libc++ -Wall -Wextra -Werror)
# 链接时也使用 libc++。
target_link_options(app PRIVATE -stdlib=libc++)

CONFIGURE_DEPENDS 让 Ninja 在构建前检查目录变化:新增 .cpp 后不必修改 CMakeLists.txt,直接重新构建,新文件就会进入目标。

这个收集规则有一个边界:只覆盖 src/ 直属的 .cpp。分层目录、多个目标或需要精确控制依赖时,改用显式列出源文件,让工程关系保持可见。

构建并运行

Ubuntu Bash(工程根目录)
# 配置工程,并生成 Ninja 使用的构建规则
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_COMPILER=clang++
# 编译并链接 app
cmake --build build
# 运行程序
./build/bin/app

输出如下:

输出
Hello, World!

也可以运行 bash build-and-run.sh。

常见错误

找不到 greeting.h

确认 CMakeLists.txt 包含 target_include_directories(app PRIVATE include),再重新配置并构建。

链接器找不到 MakeGreeting()

确认 src/greeting.cpp 存在,且文件后缀是 .cpp,因为收集规则只包含 src/ 直属的 .cpp 文件。修正后重新配置并构建:

Ubuntu Bash(工程根目录)
# 重新配置,让收集规则重新读取 src/ 下的源文件
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_COMPILER=clang++
# 编译并链接 app
cmake --build build

自测问题

先不要展开答案,试着自己把回答完整说出来,再展开对照。说不出来的部分,回到对应小节对照正文。

  1. target_include_directories(app PRIVATE include) 和源文件收集规则分别解决什么问题?

    查看答案

    target_include_directories 让目标知道到哪里查找头文件,解决声明的可见性。源文件收集规则决定哪些 .cpp 会进入目标并参与编译,解决实现是否进入构建和后续链接的问题。

本章回顾

你已经能把接口、实现和程序入口拆开。下一章会把可复用实现构建成静态库。