本文使用的环境是Windows平台
CMake 是一中高级编译配置工具,可以用来配置和编译我们的 C/C++ 程序,当然,不仅仅是支持这两种语言。熟悉编译工具的小伙伴肯定听说过 Automake 和 SCons。他们的作用都大同小异。
CMake 项目的始于1999年,当时开发公司 Kitware 被委托设计一套新的工具来简化研究人员的日常工作软件。目标很明确:提供一组工具,可以在不同平台上配置、构建、测试和部署项目。有关 CMake 更为精彩的叙述,请查阅官方网站www.cmake.org。
CMake 的所有操作都是通过编译 CMakeLists.txt
来完成的,当多个人用不同的语言或者编译器开发一个项目,最终要输出一个可执行文件或者共享库(dll,so等等)这时候就可以用到 CMake 这个神器,下边我们重点讲解如何根据项目需求,来配置CMakeLists.txt
[1]。
这里呢我们就暂时不详细讲解,因为笔者是使用的 Windows 和 VS 2022,安装时候直接选择 上C++桌面开发和C++跨平台开发的组件,CMake 会自动安装。
找到 VS 自动安装的 cmake.exe 将其添加到环境变量,方便我们直接在cmd窗口调用,一般是在C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin
,大家安装位置不一样,自己找一下。如果不会设置环境变量可以看《Windows 设置环境变量》。
这里我们演示一个最简的C++项目使用CMake构建的例子。不使用 VS 2022 ,步骤如下:
// main.cpp
#include
int main(){
std::cout << "hello word" << std::endl;
}
CMakeLists.txt
#CMakeLists.txt
# 最低需要版本 3.8
cmake_minimum_required(VERSION 3.8 FATAL_ERROR)
# 项目名称HELLO ,程序语言为C++
PROJECT (HELLO LANGUAGES CXX)
# 设置变量,将`main.cpp`用SRC变量替代
SET(SRC main.cpp)
# 将 SRC 指代的 cpp文件 编译为 hello
ADD_EXECUTABLE(hello ${SRC})
build
目录,切换进去,然后输入cmake ..
,进行项目构建(生成构建器)# 当前目录结构如下
├─ build
├─ CMakeLists.txt
└─ main.cpp
PS F:cmake01\build> cmake ..
-- Building for: Visual Studio 17 2022
-- Selecting Windows SDK version 10.0.22000.0 to target Windows 10.0.22621.
-- The CXX compiler identification is MSVC 19.35.32215.0
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler:
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: F:/cmake01/build
如果一切顺利,项目的配置已经在build
目录中生成
cmake --build .
编译可执行文件:PS F:\cmake01\build> cmake --build .
MSBuild version 17.5.0+6f08c67f3 for .NET Framework
Checking Build System
Building Custom Rule F:/cmake01/CMakeLists.txt
main.cpp
hello.vcxproj -> F:\cmake01\build\Debug\hello.exe
Building Custom Rule F:/BaiduSyncdisk/cmake01/CMakeLists.txt
生成的可执行文件就在 Debug 下。
如果你想更详细的去学习 CMake 的话,这里推荐您一本更精彩的书籍:《CMake Cookbook中文版》,如果是初学者的话,还是建议阅读完本教程,再去阅读这本书。
示例中,我们使用了一个简单的CMakeLists.txt
来构建“Hello world”可执行文件:
cmake_minimum_required(VERSION 3.8 FATAL_ERROR)
PROJECT (HELLO LANGUAGES CXX)
SET(SRC main.cpp)
ADD_EXECUTABLE(hello ${SRC})
CMake 中,C++是默认的编程语言。不过,还是建议使用LANGUAGES
选项在PROJECT
命令中显式地声明项目的语言[1]。
CMake 语言不区分大小写,但是参数区分大小写。
通过下列命令生成构建器:
$ cd build
$ cmake ..
这里,我们创建了一个目录 build (生成构建器的位置),进入 build 目录,并通过指定 CMakeLists.txt 的位置(本例中位于父目录中)来调用 CMake。
# 指定了工程的名字,并且支持所有语言
PROJECT (HELLO)
# 指定了工程的名字,并且支持语言是 C++
# PROJECT (HELLO CXX)
# 指定了工程的名字,并且支持语言是 C 和 C++
# PROJECT (HELLO C CXX)
SET(SRC_LIST main.cpp) # SRC_LIST 变量就包含了 main.cpp
还可以这样:SET(SRC_LIST main.cpp c1.cpp c2.cpp)
,含义是SRC_LIST
包含了 main.cpp c1.cpp c2.cpp
ADD_EXECUTABLE(hello ${SRC_LIST})
生成的可执行文件名是 hello,源文件读取变量 SRC_LIST 中的内容
${ }
方式取值SET(SRC_LIST main.cpp c1.cpp c2.cpp)
main 1.cpp
需要写成 SET(SRC_LIST "main 1.cpp")
简单的来讲,内部构建就是将 cmake 产生的临时文件都放在了项目源目录下,这样有一个很大的弊端就是产生的临时文件非常多,会破坏项目的结构,也可以讲对项目入侵严重;上边我们使用的其实就是所谓的外部构建,生成的临时文件都放在 build 目录下,有兴趣大家可以分别试一下,反正临时文件多到离谱。
本部分重点是如何分别构建静态库和动态库,以及如何配置 CMakeLists.txt 实现同时构建两种库,会捎带讲一下两个库的区别和优劣
区别 | 扩展名 | 说明 |
---|---|---|
静态库 | “.a”或“.lib” | 编译时会直接整合到目标程序中,编译成功的可执行文件可独立运行 |
动态库 | “.so”或“.dll” | 编译时不会放到连接的目标程序中,即可执行文件无法单独运行。 |
接下来我们试着从源码构建动态和静态库,然后再通过其他程序来实现库调用,来看CMake如何实现项目的配置
任务:
1,建立一个静态库和动态库,提供 HelloFunc 函数供其他程序编程使用,HelloFunc 向终端输出 Hello World 字符串。
2,新建一个项目,使用刚才我们构建的共享库。
首先我们新建一个构建库的项目,目录结构如下:
├── build
├── CMakeLists.txt
└── lib
├── CMakeLists.txt
├── hello.cpp
└── hello.h
接下来分别介绍每个文件中的内容:
hello.h 中的内容[2]:
#ifndef HELLO_H
#define Hello_H
void HelloFunc();
#endif
hello.cpp 中的内容[2]:
#include "hello.h"
#include
void HelloFunc(){
std::cout << "Hello World" << std::endl;
}
项目根目录下 CMakeLists.txt 的内容[2]:
PROJECT(HELLO)
ADD_SUBDIRECTORY(lib bin)
lib 文件夹中 CMakeLists.txt 的内容[2]:
SET(LIBHELLO_SRC hello.cpp)
ADD_LIBRARY(hello SHARED ${LIBHELLO_SRC})
SET(LIBHELLO_SRC hello.cpp)
:设置一个变量,名称为LIBHELLO_SRC
,指向hello.cpp
LIBHELLO_SRC
:我们定义的变量名称,可以任意定
hello.cpp
:文件hello.cpp
ADD_LIBRARY(hello SHARED ${LIBHELLO_SRC})
:构建一个名称为hello的动态库
hello
:dll 动态库名
SHARED
:表示是构建动态库
${LIBHELLO_SRC}
:我们定义的指向源文件的变量,上文就是指 hello.cpp
其实就是ADD_LIBRARY()
中关键字的区别,动态库就是SHARED
,静态库就是STATIC
。如果想要构建静态库,只需要替换该关键字即可
cd build
cmake ..
cmake --build .
cmake01
├─ build
│ ├─ bin
│ │ ├─ CMakeFiles
│ │ ├─ Debug
│ │ │ ├─ hello.dll # 这是我们最终要的动态库
│ │ │ └─ hello.pdb
│ │ └─ hello.dir
│ ├─ CMakeFiles
│ ├─ x64
│ └─ ...
├─ lib
│ ├─ CMakeLists.txt
│ ├─ hello.cpp
│ └─ hello.h
└─ CMakeLists.txt
cmake01
├─ build
│ ├─ bin
│ │ ├─ CMakeFiles
│ │ ├─ Debug
│ │ │ ├─ hello.lib # 这是我们最终要的静态库
│ │ │ └─ hello.pdb
│ │ ├─ hello.dir
│ ├─ CMakeFiles
│ ├─ x64
│ └─ ...
├─ lib
│ ├─ CMakeLists.txt
│ ├─ hello.cpp
│ └─ hello.h
└─ CMakeLists.txt
在构建的时候,一般我们会想要同时构建动态和静态库,所以我们直觉上首先想到的是这样:
# CMakeLists.txt
ADD_LIBRARY(hello SHARED ${LIBHELLO_SRC})
ADD_LIBRARY(hello STATIC ${LIBHELLO_SRC})
如果用这种方式,只会构建一个动态库,不会构建出静态库,这是因为 CMake 在构建的时候,如果已经有缓存文件的话,就不会再生成了。
如果要解决这个问题,我们可以曲线救国,先生成两个不一样的库名,再将后生成的库的名字修改成和前一个同样的名字,这里就要用到SET_TARGET_PROPERTIES
命令,这条指令可以用来设置输出的名称,那么我们的配置文件就应该改成如下内容:
# lib/CMakeLists.txt
SET(LIBHELLO_SRC hello.cpp)
ADD_LIBRARY(hello_static STATIC ${LIBHELLO_SRC})
//对hello_static的重名为hello
SET_TARGET_PROPERTIES(hello_static PROPERTIES OUTPUT_NAME "hello")
//cmake 在构建一个新的target 时,会尝试清理掉其他使用这个名字的库,因为,在构建 libhello.so 时, 就会清理掉 libhello.a
SET_TARGET_PROPERTIES(hello_static PROPERTIES CLEAN_DIRECT_OUTPUT 1)
ADD_LIBRARY(hello SHARED ${LIBHELLO_SRC})
SET_TARGET_PROPERTIES(hello PROPERTIES OUTPUT_NAME "hello")
SET_TARGET_PROPERTIES(hello PROPERTIES CLEAN_DIRECT_OUTPUT 1)
这样就可以在 build/bin/Debug/ 下同时生成 hello.dll
和 hello.lib
这里演示如何在我们的项目中使用共享库,首先新建一个项目,目录结构如下:
cmake02
├─ build
├─ include
│ └─ hello.h # 将之前使用的 hello.h 复制过来
├─ libs
│ ├─ hello.lib # 将生成的 hello.lib 复制进来
│ └─ hello.dll # 将生成的 hello.dll 复制进来
├─ CMakeLists.txt
└─ main.cpp
main.cpp 内容:
#include
int main(){
HelloFunc();
}
CMakeLists.txt 内容:
cmake_minimum_required(VERSION 3.8 FATAL_ERROR)
PROJECT(HELLO)
# 设置头文件路径:
INCLUDE_DIRECTORIES("${PROJECT_SOURCE_DIR}/include")
LINK_DIRECTORIES(${PROJECT_SOURCE_DIR}/lib)
# 生成可执行文件
ADD_EXECUTABLE(${PROJECT_NAME} main.cpp)
# 链接
TARGET_LINK_LIBRARIES(${PROJECT_NAME} ${PROJECT_SOURCE_DIR}/lib/hello.lib)
cd build
cmake ..
cmake --build .
cmake02
├─ build
│ ├─ CMakeFiles
│ ├─ Debug
│ │ ├─ HELLO.exe # 这就是生成的可执行文件,可以直接运行
│ │ └─ HELLO.pdb
│ ├─ HELLO.dir
│ └─ x64
├─ include
│ └─ hello.h
├─ lib
│ ├─ hello.lib
│ └─ hello.dll
├─ CMakeLists.txt
└─ main.cpp
这里其实使用的是hello.lib
,hello.dll
没有用到,因为我还不会,等我学会了补充
关于应用程序是选择调用dll(动态调用)还是调用lib文件(静态调用),这里简单讲一下我的理解:
应用场景 | 动态调用 | 静态调用 | 说明 |
---|---|---|---|
增量升级 | 方便 | 不方便 | 因为dll是和exe分开的,可以只更新dll |
链式调用 | 可以 | 不可以 | 可以在dll中再引用其他的dll,但是静态调用不支持 |