跳转至

编译进阶

约 1282 个字 145 行代码 预计阅读时间 6 分钟

C++ 构建系统与 VSCode 工作流

引言

为什么需要构建系统

在开发复杂的 C++ 项目时,手动编译多个源文件和外部库既耗时又容易出错。构建系统自动化了从源代码到可执行文件的转化过程,确保构建的一致性和可重现性。它能管理依赖关系、编译选项、库链接等复杂任务,提高开发效率,减少人为错误,并支持复杂项目的管理。

本教程的目标读者

本教程适合对 C/C++ 开发有一定了解的程序员,尤其是那些希望掌握现代构建工具(如 Make 和 CMake ),并在 VSCode 中进行配置和调试的开发者。我们将帮助你解决在 VSCode 中常见的问题,使你的开发环境更高效、更稳定。

Make 基础

参考链接

Makefile 教程


什么是 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 的优势
  1. 自动化编译:使用 make 命令即可编译整个项目,无需手动输入多个编译命令。

  2. 依赖管理:如果 calculator.ccalculator.h 被修改,Makefile 将仅重新编译 calculator.o,而不是整个项目。

  3. 可配置性:通过修改 Makefile 中的 CCCFLAGS 变量,可以轻松更改编译器和编译选项。

Make 的常用命令

  • make:执行默认目标,与 make all 等效。
  • make <target>:执行定义的 <target> 目标,如果没有这个目标将返回错误信息。
  • make -j:并行执行构建,使用本机的全部线程

CMake 入门

参考链接

CMake 教程博客

上海交通大学 IPADS-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"