C/C++ 项目编译实践

当我们的项目文件很少的时候,我们可以直接使用 GNU gcc 命令来编译生成可执行程序,命令也并不会太长。但随着项目复杂度日益增加,源文件不断增多,使用单行命令变得不再能胜任我们的工作,这个时候我们就需要考虑一些替代工具了。这里将要简单介绍一下 C/C++ 中经常用到两种编译工具:Make 和 CMake。

在开始之前我们需要准备一些示例代码,方便随后的编译测试。

准备实例代码

首先我们在主目录下创建一个项目文件夹取名:car-project,然后依次创建 main.cppCar 类Engine 类,创建好后文件的目录结构如下:

~/car-project/
  ├─ src/
  │  ├─ Car.h
  │  ├─ Car.cpp
  │  ├─ Engine.h
  │  └─ Engine.cpp
  └─ main.cpp

main.cpp 文件:

#include <iostream>
#include "src/Car.h"

int main() {
    Car *car = new Car("Model 3");
    car->engine = new Engine("BMW");
    std::cout << car->info() << std::endl;
    delete car;
    return 0;
}

src/Car.h 文件:

#include <string>
#include "Engine.h"

class Car {
public:
    std::string name;
    Engine *engine;

    Car(std::string name);
    ~Car();
    std::string info();
};

src/Car.cpp 文件:

#include "Car.h"

Car::Car(std::string name) {
    this->name = name;
}
Car::~Car() {
    delete engine;
}
std::string Car::info() {
    return "I'm a car named: " + name + "\n" +
        "I have an engine maked by: " + engine->vendor;
}

src/Engine.h 文件:

#include <string>

class Engine {
public:
    std::string vendor;
    Engine(std::string vendor);
    std::string info();
};

src/Engine.cpp 文件:

#include "Engine.h"

Engine::Engine(std::string vendor) {
    this->vendor = vendor;
}
std::string Engine::info() {
    return "I'm an engine by vendor: " + vendor;
}

先用 gcc 命令编译测试一下代码是否正确:

~/car-project$ g++ main.cpp ./src/Car.cpp ./src/Engine.cpp -o main
~/car-project$ ./main
I'm a car named: Model 3
I have an engine maked by: BMW

嗯,代码是没问题的,接下来既可以使用 Make 和 CMake 来编译测试了。

使用 Make

Make 的配置文件是 Makefile,使用 Make 编译有这样几个好处:

  1. 使编译更简单,不用每次都在命令行中输入所有的源文件;
  2. make 命令在编译的时候会先检查依赖的文件是否更改,每次只重新编译修改的文件,加快了编译过程。

先来看一下 Makefile 的语法结构,比较简单:

target1 target2 target3...: prerequisite1 prerequisite2 prerequisite3...
    command1
    command2
    command3
  • target 是目标,通常有三种情况:
    • 可以是一个目标文件( .o 文件),如:Car.o
    • 可以是一个可执行文件,如:main
    • 可以是一个标签,标签被称为伪目标,如:clean
  • prerequisite 是条件,指定依赖关系,即要生成前面的 target,所需要依赖的文件或是另一个目标。
  • command 需要执行的命令,即要生成这个目标,对应执行的命令。

需要注意,在冒号的左边,可以是一个或多个目标,而在冒号的右边,则可以是零个或多个依赖条件。目标顶格写,而 command 前面则必须有一个制表符(即 Tab 键)。

我们在 car-project 项目的根目录下创建一个 Makefile 文件,文件内容如下:

# 编译 main 可执行程序
main: main.o Car.o Engine.o
    g++ main.o Car.o Engine.o -o main

main.o: main.cpp
    g++ -c main.cpp -o main.o

Car.o: src/Car.h src/Car.cpp
    g++ -c ./src/Car.cpp -o Car.o

Engine.o: src/Engine.h src/Engine.cpp
    g++ -c ./src/Engine.cpp -o Engine.o

# 伪目标, 删除所有目标文件
clean:
    rm *.o main

有了这个 Makefile,我们便可以使用 make 命令去生成可执行程序。

~/car-project$ make
g++ -c main.cpp -o main.o
g++ -c ./src/Car.cpp -o Car.o
g++ -c ./src/Engine.cpp -o Engine.o
g++ main.o Car.o Engine.o -o main

~/car-project$ ./main
I'm a car named: Model 3
I have an engine maked by: BMW

可见,通过 make 命令去生成可执行程序确实简单了许多。

接着我们只简单修改一下 src/Car.cpp 文件,比如新增加一个空的成员函数 void test() {} 然后再执行 make 命令试一下。

~/car-project$ make
g++ -c ./src/Car.cpp -o Car.o
g++ main.o Car.o Engine.o -o main

Make 发现我们只修改了 Car.o 目标的依赖项 src/Car.cpp 于是只重新编译生成 Car.o,而 Car.o 又是 main 目标的依赖项,所以还要重新生成一下 main,其他没有修改的文件则不需要重新编译,这使得编译过程变快了。

当然关于 make 命令和 Makefile 还有很多用法,感兴趣可以阅读这篇文章,里面有更多相关的介绍:阮一峰的 Make 命令教程

使用 CMake

前面所用的 Make 工具通常是在 Linux 环境下使用,其中的 Makefile 文件中大多是 Linux 系统下的命令,如果项目要在 Windows 或者其他平台编译,则不得不对 Makefile 进行大量的改造,这就是 CMake 出现的原因。

CMake 允许开发者编写一种平台无关的 CMakeLists.txt 文件来定制整个编译流程,然后再根据目标用户的平台进一步生成所需的本地化 Makefile 或工程文件,如Linux 下的 Makefile文件 或 Windows 的 Visual Studio 工程文件。

在 Ubuntu 中默认不自带 CMake,可使用 apt 来安装,Windows、Mac 等其他系统可在 CMake 官网下载安装包 安装。

~/car-project$ sudo apt install cmake

我们还是通过编写 car-projectCMakeLists.txt 来学习 CMake,同样也是在 car-project 的根目录下创建一个 CMakeLists.txt 的文件,内容如下:

# CMake最低版本号要求
cmake_minimum_required(VERSION 3.0)

# 配置项目名
project(car-project)

# 递归遍历获取项目的所有源文件列表
file(GLOB_RECURSE SRC_LIST FOLLOW_SYMLINKS main.cpp src/*.cpp)

# 打印消息:把获取到的源文件列表打印出来
message(DEBUG, ${SRC_LIST})

# 生成可执行文件 main,后面是源码列表
add_executable(main ${SRC_LIST})

配置比较简单,最后一行 add_executable(main ${SRC_LIST}) 是用所有的源文件来生成一个可执行程序 main

我们可以直接使用 cmake . 在项目根目录生成 Makefile 和相关文件,但生成的文件会和项目文件混淆在一起,因此最好通过 cmake -B <path-to-build> 指定生成到某一个目录中。

~/car-project$ cmake -B build
-- The C compiler identification is GNU 9.3.0
-- The CXX compiler identification is GNU 9.3.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
DEBUG,/home/test/car-project/main.cpp /home/test/car-project/src/Car.cpp /home/test/car-project/src/Engine.cpp
-- Configuring done
-- Generating done
-- Build files have been written to: /home/test/car-project/build

~/car-project$ ls --file-type ./build
CMakeCache.txt  CMakeFiles/  Makefile  cmake_install.cmake

./build 目录下已经生成了 Makefile 文件,我们可以到 ./build 目录执行 make 命令来生成可执行程序,当然更简单的方式是通过 cmake --build <dir> 运行 ./build/Makefile 来编译生成可执行程序。

~/car-project$ cmake --build build
Scanning dependencies of target main
[ 25%] Building CXX object CMakeFiles/main.dir/main.cpp.o
[ 50%] Building CXX object CMakeFiles/main.dir/src/Car.cpp.o
[ 75%] Building CXX object CMakeFiles/main.dir/src/Engine.cpp.o
[100%] Linking CXX executable main
[100%] Built target main

~/car-project$ ./build/main
I'm a car named: Model 3
I have an engine maked by: BMW

基本的 CMake 操作就是上面这些,当然 CMake 的功能非常丰富,有很多参数和配置可以设置,在文章的最后我会介绍一些常用的,更详细内容可以参阅:CMake 官方文档

CMake 常用参数

-S <path-to-source> 指定项目目录,cmake -S . 等价于 cmake .
-G <generator-name> 指定生成目标,如:cmake -G 'Visual Studio 17 2022' 生成 Visual Studio 2022 的项目文件,其他支持的生成目标可以通过 cmake --help 来查看,下面截取了 Windows 下 cmake --help 的部分打印(带 * 号的是默认生成目标):

  • . . .
  • Visual Studio 17 2022
  • Visual Studio 16 2019
  • * NMake Makefiles
  • NMake Makefiles JOM
  • MSYS Makefiles
  • MinGW Makefiles
  • Green Hills MULTI
  • Unix Makefiles
  • Ninja
  • . . .

--install-prefix <directory> 指定安装目录前缀

--install <dir> 安装编译目录中的内容

CMakeLists.txt 常见配置

部分配置参考了这篇博客:
arcticfox《程序员C语言快速上手——工程篇(十三)》

CMake 中的自带变量,当然也可以用 set() 去修改

  • PROJECT_NAME 该变量可获取project命令配置的项目名
  • CMAKE_SOURCE_DIRPROJECT_SOURCE_DIR 表示工程的根目录
  • CMAKE_BINARY_DIRPROJECT_BINARY_DIR 表示编译目录,如果是内部构建,则编译目录与工程根目录相同,如果是外部构建,则表示外部构建创建的编译目录,如上例中的build目录
  • CMAKE_CURRENT_SOURCE_DIR 表示当前处理的 CMakeLists.txt 所在文件夹的路径
  • CMAKE_CURRENT_LIST_FILE 当前 CMakeLists.txt 文件的完整路径
  • CMAKE_C_COMPILERCMAKE_CXX_COMPILER 分别表示 C 和 C++ 编译器的路径

set(<variable> <value>... [PARENT_SCOPE]) 设置变量,如:set(SRC_LIST main.cpp src/Car.cpp src/Engine.cpp) 设置了一个源文件列表变量 SRC_LIST,后续可以通过 ${SRC_LIST} 来读取这个变量的值。

  • EXECUTABLE_OUTPUT_PATH 设置该变量可修改可执行程序的生成路径,set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin) 将可执行程序生成到 ./build/bin/ 中。
  • LIBRARY_OUTPUT_PATH 设置该变量可修改库文件生成路径,set(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib) 将静态库和动态库生成到 ./build/lib/ 中。

message([<mode>] "message text" ...) 打印消息,模式 <mode> 可以指定也可以不填写,常用的有下面这些:

  • 没有 或 NOTICE 提示信息
  • DEBUG 调试信息
  • WARNING CMake警告,继续处理
  • SEND_ERROR CMake错误,继续处理,但会跳过生成
  • FATAL_ERROR CMake错误,停止处理和生成

add_executable(<name> [WIN32] [MACOSX_BUNDLE] [EXCLUDE_FROM_ALL] [source1] [source2 ...]) 使用给定的源文件,生成可执行程序。

add_library(<name> [STATIC | SHARED | MODULE] [EXCLUDE_FROM_ALL] [<source>...]) 使用给定的源文件,生成一个库(静态库或共享库),如:add_library(car STATIC src/Car.cpp src/Engine.cpp) 会在 ./build 目录下生成 libcar.a 的静态库。

link_directories([AFTER|BEFORE] directory1 [directory2 ...]) 添加库的搜索目录。

find_library(<VAR> name [path]) 查找指定的库,并将库文件路径保存到一个变量,用法比较多,这里只展示其中一种。

本作品采用《CC 协议》,转载必须注明作者和本文链接
xiaer
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!