编译进阶¶
约 1282 个字 145 行代码 预计阅读时间 6 分钟
C++ 构建系统与 VSCode 工作流¶
引言¶
为什么需要构建系统¶
在开发复杂的 C++ 项目时,手动编译多个源文件和外部库既耗时又容易出错。构建系统自动化了从源代码到可执行文件的转化过程,确保构建的一致性和可重现性。它能管理依赖关系、编译选项、库链接等复杂任务,提高开发效率,减少人为错误,并支持复杂项目的管理。
本教程的目标读者¶
本教程适合对 C/C++ 开发有一定了解的程序员,尤其是那些希望掌握现代构建工具(如 Make 和 CMake ),并在 VSCode 中进行配置和调试的开发者。我们将帮助你解决在 VSCode 中常见的问题,使你的开发环境更高效、更稳定。
Make 基础¶
参考链接¶
什么是 Make¶
Make 是一个自动化构建工具,使用 Makefile 文件来定义如何编译和链接程序。它通过检查文件的时间戳来决定哪些文件需要重新编译。
Makefile 的基本结构¶
Makefile 的基本结构由目标、依赖和命令组成,通常形式为:
target: dependencies
command
Makefile 示例¶
让我们考虑一个简单的 C 语言项目,该示例将展示如何使用 Makefile 来编译一个具有多个源文件和头文件的程序,并展示 Makefile 相比手动命令行编译的优势。
假设我们有一个简单的计算器程序,它包括以下文件:
main.c
:程序的主入口点。calculator.c
:包含计算逻辑的源文件。calculator.h
:头文件,声明calculator.c
中的函数。Makefile
:项目的构建脚本。
#include "calculator.h"
int main() {
// 使用 calculator 模块进行计算
int result = add(5, 3);
printf("5 + 3 = %d\n", result);
return 0;
}
#include "calculator.h"
int add(int a, int b) {
return a + b;
}
#ifndef CALCULATOR_H
#define CALCULATOR_H
int add(int a, int b);
#endif
#定义编译器
CC=gcc
#定义编译选项
CFLAGS=-Wall -g
#定义目标文件
TARGET=calculator
#定义对象文件
OBJS=main.o calculator.o
#默认目标
all: $(TARGET)
#从 .c 文件生成 .o 文件
$(TARGET): $(OBJS)
$(CC) $(CFLAGS) -o $@ $^
#编译单个 .c 文件到 .o 文件
%.o: %.c
$(CC) $(CFLAGS) -c $<
#清理编译生成的文件
clean:
rm -f $(TARGET) $(OBJS)
#伪目标,防止 make 将某个文件名当作默认目标
.PHONY: all clean
尝试进行下述操作以体会 Makefile
的优势:
- 在仅含这 4 个文件的文件目录下执行
make
命令,观察输出 - 再次执行
make
命令,观察输出 - 尝试修改
calculator.c
再次执行make
命令,观察输出 - 执行
make clean
命令,观察输出与当前目录文件的变化 - 现在我希望发布我的程序,因此我需要编译时不带调试信息,并使用
-O2
的优化进行编译。请通过修改Makefile
并进行make
实现这一步。 - 尝试使用上一节中的知识进行命令行编译
通过上述步骤的实验,尝试总结 Makefile
相比命令行的优势。
使用 Makefile 的优势
-
自动化编译:使用
make
命令即可编译整个项目,无需手动输入多个编译命令。 -
依赖管理:如果
calculator.c
或calculator.h
被修改,Makefile 将仅重新编译calculator.o
,而不是整个项目。 -
可配置性:通过修改 Makefile 中的
CC
和CFLAGS
变量,可以轻松更改编译器和编译选项。
Make 的常用命令¶
make
:执行默认目标,与make all
等效。make <target>
:执行定义的<target>
目标,如果没有这个目标将返回错误信息。make -j
:并行执行构建,使用本机的全部线程
CMake 入门¶
参考链接¶
- 逐级介绍了 make 和 cmake 的使用方法,推荐初学者观看
CMake 简介¶
CMake 是一个跨平台的构建工具,使用 CMakeLists.txt 文件来定义构建过程。它可以生成 Makefile 或其他 IDE 的项目文件。
CMakeLists.txt 文件结构¶
CMakeLists.txt 的基本结构包括:
cmake_minimum_required(VERSION 3.10)
project(MyProject)
add_executable(my_program main.cpp)
我们可以通过 set
来设置变量的值,从而调整编译参数和其他内容
set(<variable> <value>...)
#示例
set(CMAKE_CXX_STANDARD 11) # 设置C++标准
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3") # 启动O3优化
基本的 CMake 命令¶
cmake .
:在当前目录生成构建文件。cmake --build .
:构建项目。
构建简单的 C/C++ 项目¶
创建一个简单的 C++ 项目,使用以下 CMakeLists.txt:
cmake_minimum_required(VERSION 3.10)
project(MyApp)
add_executable(MyApp main.cpp)
CMake 项目嵌套¶
在大型项目中,合理的 CMake 嵌套结构是保持项目可维护性的关键,我们可以将一个大型项目分出多个不同的子目录,并给每个子目录创建一个 CMakeList.txt 文件,从而简化项目的复杂程度,使其更易维护。
我们可以使用 add_subdirectory
来添加一个子目录
add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])
# source_dir::包含 CMakeLists.txt 的子目录
# binary_dir:输出文件路径(默认值为 source_dir )(若填入相对路径,将相对于当前目录进行解析)
# EXCLUDE_FROM_ALL:若提供该项参数,则子目录中的目标默认不会包含在父目录的 ALL 目标中,并从 IDE 项目文件中排除,用户必须显式构建子目录中的目标。
CMake 嵌套示例¶
-
项目结构
simple_calc/ ├── CMakeLists.txt # 顶层 CMake ├── main.cpp # 主程序 └── add/ ├── CMakeLists.txt # 加法模块 CMake ├── add.h # 加法头文件 └── add.cpp # 加法实现(带 OpenMP)
-
以下是主文件的内容:
#include <iostream> #include <omp.h> #include "add/add.h" int main() { double a, b; std::cout << "输入两个数字 (用空格分隔): "; std::cin >> a >> b; // 打印当前使用的线程数 int threads = omp_get_max_threads(); std::cout << "使用OpenMP并行计算 (" << threads << " 线程)" << std::endl; double result = add(a, b); std::cout << a << " + " << b << " = " << result << std::endl; return 0; }
cmake_minimum_required(VERSION 3.5) project(SimpleCalc) # 设置C++标准 set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 使用GCC编译,编译选项加入OpenMP set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fopenmp") # 添加加法模块子目录 add_subdirectory(add) # 创建可执行文件 add_executable(simple_calc main.cpp) # 链接加法库 target_link_libraries(simple_calc PRIVATE add_lib)
-
以下是 add 文件夹的内容:
#include "add.h" #include <vector> #include <iostream> #include <omp.h> double add(double a, double b) { // 通过创建一个巨大的数组,我们实现了一个极其低效的加法 // 这是计划的一部分(划掉) // 其实是为了演示OpenMP const int N = 100000000; std::vector<double> vec(N, a); double sum = 0.0; // OpenMP并行求和 #pragma omp parallel for reduction(+:sum) for(int i = 0; i < N; i++) { sum += vec[i] + b; } return sum / N; }
#pragma once double add(double a, double b);
# 创建加法静态库 add_library(add_lib STATIC add.cpp) # 设置头文件位置(允许主程序包含"add/add.h") target_include_directories(add_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
-
编译方式
mkdir build cd build cmake .. cmake --build . ./simple_calc
Compile Database¶
什么是 Compile Database¶
Compile Database 是一个 JSON 格式的文件,包含了项目中每个编译单元的编译命令和参数。Compile Database 使得你的编辑器能够获取编译信息,启用代码跳转和高亮功能,从而提高开发效率。
生成 Compile Database¶
使用 CMake 生成¶
在 CMake 命令中添加参数:
cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON .
使用其他工具生成(如 Bear)¶
Bear 是一个工具,可以在构建时捕获编译命令并生成 compile_commands.json 文件。
使用方法:
bear -- make
在 VSCode 中使用 Compile Database¶
在 cpp plugin 中配置 compile commands
选项
"compileCommands": "${workspaceFolder}/build/compile_commands.json"