当我们的项目文件很少的时候,我们可以直接使用 GNU gcc
命令来编译生成可执行程序,命令也并不会太长。但随着项目复杂度日益增加,源文件不断增多,使用单行命令变得不再能胜任我们的工作,这个时候我们就需要考虑一些替代工具了。这里将要简单介绍一下 C/C++ 中经常用到两种编译工具:Make 和 CMake。
在开始之前我们需要准备一些示例代码,方便随后的编译测试。
准备实例代码
首先我们在主目录下创建一个项目文件夹取名:car-project
,然后依次创建 main.cpp
、Car 类
、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 编译有这样几个好处:
- 使编译更简单,不用每次都在命令行中输入所有的源文件;
- 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-project
的 CMakeLists.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_DIR
或PROJECT_SOURCE_DIR
表示工程的根目录CMAKE_BINARY_DIR
或PROJECT_BINARY_DIR
表示编译目录,如果是内部构建,则编译目录与工程根目录相同,如果是外部构建,则表示外部构建创建的编译目录,如上例中的build目录CMAKE_CURRENT_SOURCE_DIR
表示当前处理的 CMakeLists.txt 所在文件夹的路径CMAKE_CURRENT_LIST_FILE
当前 CMakeLists.txt 文件的完整路径CMAKE_C_COMPILER
和CMAKE_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])
查找指定的库,并将库文件路径保存到一个变量,用法比较多,这里只展示其中一种。