搜索

C++ 教程

C++基础

第1章 搭建开发环境与第一个程序 第2章 基本数据类型与变量 第3章 运算符与表达式 第4章 流程控制语句 第5章 数组与字符串 第6章 函数基础 第7章 指针基础 第8章 引用与const限定 第9章 动态内存管理 第10章 结构体与联合体 第11章 函数进阶

面向对象

第12章 面向对象编程入门 第13章 封装与访问控制 第14章 运算符重载 第15章 继承与派生 第16章 多态与虚函数

STL与进阶

第17章 模板编程基础 第18章 标准模板库(STL)容器 第19章 STL算法与迭代器 第20章 异常处理机制 第21章 文件输入输出 第22章 字符串类string 第23章 命名空间与作用域 第24章 类型转换操作符

现代C++

第25章 智能指针 第26章 移动语义与右值引用 第27章 Lambda表达式 第28章 多线程编程基础 第29章 模板元编程入门 第30章 设计模式基础 第31章 C++11/14新特性 第32章 C++17/20核心特性

实战进阶

第33章 性能优化技巧 第34章 调试与测试 第35章 项目结构与构建 第36章 网络编程入门 第37章 数据库连接操作 第38章 图形界面开发基础 第39章 综合项目实战 第40章 进阶学习路线

第35章 项目结构与构建

第35章 项目结构与构建

高级 👤 已掌握C++核心语法,准备从单文件练习转向真实项目开发的开发者 🔧 组织多人协作的中大型C++项目,实现自动化编译、测试与部署
本章你将学到
  • 如何规划一个清晰、可维护的C++项目目录结构
  • 使用CMake编写跨平台的构建脚本
  • 集成第三方库与自动化测试到构建流程中
核心概念项目布局构建系统

当你从学习单个`.cpp`文件转向开发一个包含数十个模块、依赖多个外部库的真实项目时,第一个拦路虎往往不是算法,而是“项目应该怎么放”和“代码怎么编译”。我见过不少团队,初期随意堆放文件,导致后期编译命令复杂得像天书,新人一周都搭不好环境。

项目目录结构:代码的家

一个清晰的项目结构就像图书馆的索引系统,能极大提升开发和维护效率。没有统一标准,但经过多年实践,社区形成了一些最佳实践。

项目根目录/
↓
include/
→
src/
→
tests/
→
third_party/
↓
lib/
→
bin/
→
build/

上图展示了一个典型的中型C++项目布局。关键在于分离:头文件与源文件分离,产品代码与测试代码分离,源代码与生成物分离。

📁 关键目录解析

  • include/:存放所有公开的头文件(.h/.hpp)。通常按模块创建子目录,如include/myproject/core/。这是项目对外的“接口目录”。
  • src/:存放所有源文件(.cpp)和内部头文件。目录结构应与include/镜像,便于管理。
  • tests/:所有单元测试、集成测试代码。使用如Google Test框架,目录结构模仿src/,每个src/foo.cpp对应一个tests/test_foo.cpp。
  • third_party/:项目依赖的第三方库源代码(如jsoncpp, spdlog)。使用Git子模块(git submodule)管理是推荐做法。
  • build/:编译输出目录(可被.gitignore)。所有中间文件(.o)、最终可执行文件、库文件都应生成在此,保持源码目录清洁。

实际开发中,我推荐在src/内为每个主要模块创建子目录,并放置一个CMakeLists.txt。例如一个网络库项目:

MyNetworkLib/
├── CMakeLists.txt          # 根构建脚本
├── include/
│   └── mynet/
│       ├── tcp_client.h
│       └── utils.h
├── src/
│   ├── core/               # 核心模块
│   │   ├── CMakeLists.txt
│   │   └── tcp_client.cpp
│   └── utils/              # 工具模块
│       ├── CMakeLists.txt
│       └── logger.cpp
├── tests/
│   ├── CMakeLists.txt
│   └── test_tcp_client.cpp
└── third_party/
    └── googletest/         # Git子模块引入

构建系统:从Makefile到CMake

直接写Makefile管理多目录、多平台的项目是痛苦的。GCC版本差异、库路径不同、依赖关系复杂,都会让Makefile迅速膨胀。CMake是一个元构建系统,它生成平台相关的构建文件(如Unix的Makefile或Windows的Visual Studio项目)。

主流C++构建系统对比
工具定位优点缺点适用场景
CMake元构建系统,生成器跨平台支持极佳,生态强大,FindPackage机制完善语法略显古怪,学习曲线陡峭中大型跨平台项目,需集成多种第三方库
GNU Make直接构建工具Unix/Linux原生,极度灵活跨平台差,依赖管理复杂Linux/Unix环境下的中小型项目或脚本
Bazel一体化构建与测试工具构建速度快,可重现性强生态偏Google内部,配置复杂超大型单体仓库(Monorepo)项目
MSBuild微软构建平台与Visual Studio深度集成基本局限于Windows生态纯Windows平台开发

对于绝大多数C++项目,CMake是当前事实上的标准选择。从3.0版本开始,其现代语法(如target_*命令)让配置更加清晰。

🔥 核心原则:面向Target的现代CMake

旧式CMake使用全局变量(如CMAKE_CXX_FLAGS)配置所有内容,容易造成标志污染。现代CMake(3.0+)的核心思想是定义目标(Target)(可执行文件或库),并将属性(包含目录、编译选项、链接库)直接附加到目标上。

让我们看一个根目录CMakeLists.txt的现代写法示例:

# CMakeLists.txt (项目根目录)
cmake_minimum_required(VERSION 3.15) # 指定最低版本,我推荐至少3.15
project(MyNetworkLib VERSION 1.0.0 LANGUAGES CXX) # 定义项目名、版本和语言

set(CMAKE_CXX_STANDARD 17) # 设置C++标准为C++17
set(CMAKE_CXX_STANDARD_REQUIRED ON) # 要求编译器必须支持该标准

# 添加子目录,每个子目录有自己的CMakeLists.txt
add_subdirectory(src/core)
add_subdirectory(src/utils)
add_subdirectory(third_party/googletest) # 通常第三方库也通过CMake管理
add_subdirectory(tests)

然后,在src/core/CMakeLists.txt中定义库目标:

# src/core/CMakeLists.txt
add_library(mynet_core STATIC # 创建静态库目标‘mynet_core’
    tcp_client.cpp
)

target_include_directories(mynet_core PUBLIC
    "${CMAKE_CURRENT_SOURCE_DIR}/../../include" # 公开头文件目录
)

target_compile_features(mynet_core PUBLIC cxx_std_17) # 公开C++17要求
# 链接其他依赖,如线程库
target_link_libraries(mynet_core PUBLIC Threads::Threads)

⚠️ 常见误区

  • 滥用全局变量:避免使用include_directories()和link_libraries()等全局命令。应使用target_include_directories()和target_link_libraries()将属性精确绑定到特定目标。
  • 硬编码路径:使用${CMAKE_CURRENT_SOURCE_DIR}、${CMAKE_BINARY_DIR}等CMake变量,而不是绝对路径“/home/user/project/include”,以保证跨平台性。
  • 忽略构建类型:CMake有Debug、Release、RelWithDebInfo、MinSizeRel等构建类型。通过cmake -DCMAKE_BUILD_TYPE=Debug ..指定,否则可能没有调试符号。

依赖管理:引入第三方库

现代C++项目几乎必然依赖外部库。手动下载、编译、配置包含路径和库路径是繁琐且易错的。CMake提供了find_package()和FetchContent等机制来管理依赖。

系统已安装的库

  • 使用 find_package(Boost REQUIRED COMPONENTS filesystem system)
  • 通过 target_link_libraries(myapp PUBLIC Boost::filesystem) 链接
  • 要求开发者在系统路径(如/usr/local)提前安装好库

源码集成(推荐)

  • 使用 FetchContent (CMake 3.11+) 或 ExternalProject
  • 直接从Git仓库下载、编译特定版本
  • 保证构建可重现,适合团队协作和CI/CD

包管理器

  • 使用vcpkg、Conan等C++包管理器
  • 自动处理依赖关系和平台差异
  • 生态仍在发展中,但已是大型项目趋势

我推荐对关键依赖使用FetchContent,它能将第三方库的构建直接集成到你的CMake流程中。以下是如何集成jsoncpp库的例子:

# 在CMakeLists.txt中使用FetchContent
include(FetchContent)
FetchContent_Declare(
    jsoncpp
    GIT_REPOSITORY https://github.com/open-source-parsers/jsoncpp.git
    GIT_TAG 1.9.5 # 指定明确的版本标签!
)
FetchContent_MakeAvailable(jsoncpp)

# 之后就可以像使用普通目标一样链接它
target_link_libraries(myapp PUBLIC jsoncpp_lib)

编者提示:使用第三方库时,常涉及头文件包含和命名冲突。合理运用命名空间(第23章内容)是避免符号污染的关键。为你自己的库代码定义唯一的顶层命名空间,如namespace mycompany::mynet { ... }。

自动化测试集成

将测试作为构建的一部分,是保证代码质量的核心实践。Google Test (gtest) 是C++领域最流行的单元测试框架。通过CMake集成它非常方便。

1. 引入GTest源码
→
2. 编译GTest库
→
3. 创建测试可执行文件
→
4. 链接被测代码与GTest
→
5. 添加`ctest`测试目标

在tests/CMakeLists.txt中:

# 添加gtest为子目录(已通过FetchContent或子模块引入)
add_subdirectory("${PROJECT_SOURCE_DIR}/third_party/googletest")

# 创建测试可执行文件
add_executable(test_tcp_client test_tcp_client.cpp)
target_link_libraries(test_tcp_client PRIVATE
    mynet_core          # 链接我们自己的核心库
    GTest::gtest_main   # 链接gtest主库
)

# 告诉CTest这个可执行文件是一个测试
include(GoogleTest)
gtest_discover_tests(test_tcp_client)

配置完成后,在构建目录下执行make test(或ninja test)或直接运行ctest命令,即可自动运行所有测试并报告结果。在CI/CD流水线中,这通常是编译后的必做步骤。

实战:一个完整的构建流程

假设你的项目目录已按上述结构组织好,并且写好了CMakeLists.txt。以下是命令行下的标准构建流程:

# 1. 进入项目根目录,创建一个独立的构建目录(推荐)
$ cd /path/to/MyNetworkLib
$ mkdir build && cd build

# 2. 运行cmake生成构建文件。`..`指向包含顶层CMakeLists.txt的目录
#    使用Ninja生成器(比make更快)
$ cmake -G "Ninja" -DCMAKE_BUILD_TYPE=Debug ..

# 3. 执行编译。Ninja使用`ninja`命令,Makefile使用`make -j8`
$ ninja

# 4. 运行测试
$ ninja test
# 或
$ ctest --output-on-failure

# 5. 安装(如果需要)到系统目录或自定义目录
$ ninja install

踩坑提醒:如果你在Windows上使用Visual Studio生成器,步骤2可能是cmake -G "Visual Studio 16 2019" -A x64 ..,然后使用cmake --build . --config Debug来编译。务必查阅CMake生成器文档。

动手试一试

1. 创建你的第一个结构化项目:在本地新建一个目录,模仿本章的“MyNetworkLib”结构,创建include/, src/, tests/等目录。
2. 编写CMakeLists.txt:在根目录和src/子目录下创建CMakeLists.txt。尝试定义一个库目标,包含至少两个.cpp文件和一个.h文件。
3. 集成Google Test:使用git submodule add https://github.com/google/googletest.git third_party/googletest引入gtest。修改tests/CMakeLists.txt,为一个简单的函数(例如一个加法函数)编写并运行一个单元测试。
4. 跨平台尝试:如果你主要在Windows开发,尝试在WSL或Linux虚拟机中,使用相同的CMakeLists.txt文件构建你的项目,观察其跨平台能力。

检验你的理解

  1. 为什么要把头文件放在include/目录,而源文件放在src/目录?如果混在一起有什么弊端?
  2. 现代CMake的“面向目标”与旧式CMake的“全局变量”方式主要区别是什么?target_include_directories(my_lib PUBLIC ...)中的PUBLIC关键字是什么含义?
  3. 使用FetchContent管理第三方库依赖,相比于要求开发者手动在系统安装该库,有哪些优势?
  4. 在构建目录下执行ninja命令后,最终的可执行文件或库文件通常会生成在哪个目录?为什么我们不建议在源码目录内直接编译?

本章小结

  • 项目结构是工程的基石:采用include/、src/、tests/分离的目录结构,能极大提升代码的可读性、可维护性和可测试性。
  • CMake是现代C++构建的事实标准:掌握现代CMake(3.0+)面向目标的语法,使用target_*命令精确管理编译属性、依赖和链接关系,避免全局变量污染。
  • 依赖管理应追求可重现性:优先使用FetchContent或包管理器来管理第三方库,确保任何开发者都能以相同的方式获取和构建依赖。
  • 构建必须包含自动化测试:将单元测试框架(如Google Test)集成到CMake构建流程中,并通过ctest统一运行,这是持续集成和保证代码质量的关键环节。
  • 构建目录与源码目录分离:始终坚持在独立的build/目录中进行配置和编译,这是保持源码清洁、支持多配置并行构建的最佳实践。

意见反馈

微信二维码 微信扫码关注 抖音二维码 抖音扫码关注