Cmake学习
1 语法特性介绍
基本语法格式:指令(参数 1 参数 2...)
- 参数使用括弧括起
- 参数之间使用空格或分号分开
指令是大小写无关的,参数和变量是大小写相关的
set(HELLO hello.cpp) add_executable(hello main.cpp hello.cpp) ADD_EXECUTABLE(hello main.cpp ${HELLO})
变量使用${}方式取值,但是在 IF 控制语句中是直接使用变量名 if (HELLO)
2重要指令和CMake常用变量
2.1 重要指令
-
cmake_minimum_required ,指定CMake的最小版本要求
# CMake最小版本要求为2.8.3 cmake_minimum_required(VERSION 2.8.3)
- 语法: cmake_minimum_required(VERSION versionNumber [FATAL_ERROR])
-
project 定义工程名称,并可指定工程支持的语言
# 指定工程名为HELLOWORLD project(HELLOWORLD)
- 语法: project(projectname [CXX] [C] [Java])
-
set 显式的定义变量
# 定义SRC变量,其值为sayhello.cpp hello.cpp set(SRC sayhello.cpp hello.cpp)
- 语法:set(VAR [VALUE] [CACHE TYPE DOCSTRING [FORCE]])
-
include_directories 向工程添加多个特定的头文件搜索路径 --->相当于指定g++编译器的 -I 参数。语法: include_directories([AFTER|BEFORE] [SYSTEM] dir1 dir2 ...)
直接写 include,不写绝对路径,即相对于cmakelists文件所在的路径
-
link_directories 向工程添加多个特定的库文件搜索路径 --->相当于指定g++编译器的-L参数
# 将/usr/lib/mylibfolder 和 ./lib 添加到库文件搜索路径 link_directories(/usr/lib/mylibfolder ./lib)
语法: link_directories(dir1 dir2 ...)
-
add_library 生成库文件
# 通过变量 SRC 生成 libhello.so 共享库 add_library(hello SHARED ${SRC})
- 语法: add_library(libname [SHARED|STATIC|MODULE] [EXCLUDE_FROM_ALL] source1 source2 ... sourceN)
-
add_compile_options 添加编译参数
# 添加编译参数 -Wall -std=c++11 -O2 add_compile_options(-Wall -std=c++11 -O2)
-
add_executable 生成可执行文件
# 编译main.cpp生成可执行文件main add_executable(main main.cpp)
- 语法:add_executable(exename source1 source2 ... sourceN)
-
target_link_libraries 为 target 添加需要链接的共享库 -->相同于指定g++编译器-l参数
# 将hello动态库文件链接到可执行文件main target_link_libraries(main hello)
- 语法: target_link_libraries(target library1<debug | optimized> library2...)
-
add_subdirectory 向当前工程添加存放源文件的子目录,并可以指定中间二进制和目标二进制存放的位置
# 添加src子目录,src中需有一个CMakeLists.txt add_subdirectory(src)
语法: add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])
-
aux_source_directory 发现一个目录下所有的源代码文件并将列表存储在一个变量中,这个指令临时被用来自动构建源文件列表
- 语法: aux_source_directory(dir VARIABLE)
# 定义SRC变量,其值为当前目录下, 所有的源代码文件 aux_source_directory(. SRC) # 编译SRC变量所代表的源代码文件,生成main可执行文件 add_executable(main ${SRC})
2.2 CMake常用变量
2.2.1常用变量的使用方式
2.2.2常用变量
-
CMAKE_C_FLAGS gcc编译选项
-
CMAKE_CXX_FLAGS g++编译选项
# 在CMAKE_CXX_FLAGS编译选项后追加-std=c++11 set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")
-
CMAKE_BUILD_TYPE 编译类型(Debug, Release)
# 设定编译类型为debug,调试时需要选择debug set(CMAKE_BUILD_TYPE Debug) # 设定编译类型为release,发布时需要选择release set(CMAKE_BUILD_TYPE Release)
-
CMAKE_BINARY_DIR
PROJECT_BINARY_DIR
<projectname>_BINARY_DIR
这三个变量指代的内容是一致的。如果是 in source build,指的就是工程顶层目录。如果是 out-of-source 编译,指的是工程编译发生的目录。PROJECT_BINARY_DIR 跟其他指令稍有区别,不过现在,你可以理解为他们是一致的。
-
CMAKE_SOURCE_DIR
PROJECT_SOURCE_DIR
<projectname>_SOURCE_DIR
这三个变量指代的内容是一致的,不论采用何种编译方式,都是指工程顶层目录的绝对你路径。
例如: include_directories(${CMAKE_SOURCE_DIR}/include)
当我们想指明我们头文件所在的位置时,可以借用CMAKE_SOURCE_DIR宏,先导航到你的代码工程文件的绝对路径。
- CMAKE_C_COMPILER:指定C编译器
- CMAKE_CXX_COMPILER:指定C++编译器
- EXECUTABLE_OUTPUT_PATH:可执行文件输出的存放路径
- LIBRARY_OUTPUT_PATH:库文件输出的存放路径
3 CMake编译工程
CMake目录结构:项目主目录存在一个CMakeLists.txt文件
两种方式设置编译规则:
- 包含源文件的那个子文件夹包含CMakeLists.txt文件,主目录的CMakeLists.txt通过add_subdirectory添加子目录即可;
- 包含源文件的子文件夹未包含CMakeLists.txt文件,子目录编译规则体现在主目录的CMakeLists.txt中;
3.1 编译流程
在 linux 平台下使用 CMake 构建C/C++工程的流程如下:
- 手动编写 CMakeLists.txt。
- 执行命令
cmake PATH
生成 Makefile ( PATH 是顶层CMakeLists.txt 所在的目录 )。 - 执行命令
make
进行编译。
# important tips
. # 表示当前目录
./ # 表示当前目录.. # 表示上级目录
../ # 表示上级目录
3.2 两种构建方式
-
内部构建(in-source build):不推荐使用
内部构建会在同级目录下产生一大堆中间文件,这些中间文件并不是我们最终所需要的,和工程源文件放在一起会显得杂乱无章。
## 内部构建# 在当前目录下,编译本目录的CMakeLists.txt,生成Makefile和其他文件 cmake . # 执行make命令,生成target make
-
外部构建(out-of-source build):推荐使用
将编译输出文件与源文件放到不同目录中,(源cmakelist放在一级项目文件夹里面,再在一级项目文件夹里新建一个 build文件夹,存放cmakelist执行输出的makefile等生成文件,不使得项目结构杂乱)
## 外部构建# 1. 在当前目录下,创建build文件夹 mkdir build # 2. 进入到build文件夹 cd build # 3. 编译上级目录的CMakeLists.txt,生成Makefile和其他文件 cmake .. # 4. 执行make命令,生成target make
4【实战】CMake代码实践
4.1 最小CMake工程
4.1.1 项目代码
项目文件只有一个,就是一个helloworld.cpp
4.1.2 cmake工程
1)在helloworld.cpp文件夹下,新建一个CMakeList.txt,一个build文件夹
2)CMakeLists.txt文件内写入如下内容(注意写指令借助tab)
3)在build 目录下执行 cmake . .;会发现 build文件夹 下生成许多cmake工程文件,包括makefile
4)在 build目录下执行makefile;该目录下会生成 最终可执行的目标二进制文件。
CMakeLists.txt:
# Set the minimum version of CMake that can be used
cmake_minimum_required(VERSION 3.0)# Set the project name
project (HELLO)# Add an executable
add_executable(hello_cmake main.cpp)
4.2 多目录工程
//mian.cpp
#include "swap.h"int main() {swap obj_ab(2,3);obj_ab.print_info();obj_ab.run();obj_ab.print_info();
}
//swap.h
#pragma once
#include <iostream>class swap
{
private:int a, b;
public:swap(int a, int b){this->a = a;this->b = b;}void run();void print_info();
};
//swap.cpp
#include "swap.h"void swap::run() {int temp;temp = a;a = b;b = temp;
}void swap::print_info() {std::cout << "a = " << a << std::endl;std::cout << "b = " << b << std::endl;
}
4.2.1 多目录工程-直接编译
这个案例直接编译的cmakelists的写法和上面的helloworld案例的套路基本上一样,区别就是多了一个指定头文件、练习使用下set。
CMakeLists.txt:
cmake_minimum_required(VERSION 3.0)project(SWAP)include_directories(include)set(SOURCES main.cpp src/swap.cpp)add_executable(swap_cmake ${SOURCES})
编译后的工程结构:
.
├── build
│ ├── CMakeCache.txt
│ ├── CMakeFiles
│ │ ├── 3.22.1
│ │ │ ├── CMakeCCompiler.cmake
│ │ │ ├── CMakeCXXCompiler.cmake
│ │ │ ├── CMakeDetermineCompilerABI_C.bin
│ │ │ ├── CMakeDetermineCompilerABI_CXX.bin
│ │ │ ├── CMakeSystem.cmake
│ │ │ ├── CompilerIdC
│ │ │ │ ├── a.out
│ │ │ │ ├── CMakeCCompilerId.c
│ │ │ │ └── tmp
│ │ │ └── CompilerIdCXX
│ │ │ ├── a.out
│ │ │ ├── CMakeCXXCompilerId.cpp
│ │ │ └── tmp
│ │ ├── cmake.check_cache
│ │ ├── CMakeDirectoryInformation.cmake
│ │ ├── CMakeOutput.log
│ │ ├── CMakeTmp
│ │ ├── main_cmake.dir
│ │ │ ├── build.make
│ │ │ ├── cmake_clean.cmake
│ │ │ ├── compiler_depend.make
│ │ │ ├── compiler_depend.ts
│ │ │ ├── DependInfo.cmake
│ │ │ ├── depend.make
│ │ │ ├── flags.make
│ │ │ ├── link.txt
│ │ │ ├── main.cpp.o
│ │ │ ├── main.cpp.o.d
│ │ │ ├── progress.make
│ │ │ └── src
│ │ │ ├── swap.cpp.o
│ │ │ └── swap.cpp.o.d
│ │ ├── Makefile2
│ │ ├── Makefile.cmake
│ │ ├── progress.marks
│ │ ├── swap_cmake.dir
│ │ │ ├── build.make
│ │ │ ├── cmake_clean.cmake
│ │ │ ├── compiler_depend.make
│ │ │ ├── compiler_depend.ts
│ │ │ ├── DependInfo.cmake
│ │ │ ├── depend.make
│ │ │ ├── flags.make
│ │ │ ├── link.txt
│ │ │ ├── main.cpp.o
│ │ │ ├── main.cpp.o.d
│ │ │ ├── progress.make
│ │ │ └── src
│ │ │ ├── swap.cpp.o
│ │ │ └── swap.cpp.o.d
│ │ └── TargetDirectories.txt
│ ├── cmake_install.cmake
│ ├── Makefile
│ └── swap_cmake
├── CMakeLists.txt
├── include
│ └── swap.h
├── main.cpp
└── src└── swap.cpp
4.2.2 多目录工程-src做成(静态/动态)库,间接编译
4.2.2.1 src分别做成 动态库(.so)/ 静态库(.a)
cmake_minimum_required(VERSION 3.0)project(SWAP)include_directories(include)set(SOURCES src/swap.cpp)add_library(SwapTest SHARED ${SOURCES})
add_library(SwapTest STATIC ${SOURCES})set (LIBRARY_OUTPUT_PATH /usr/lib)
4.2.2.2 使用静态库或动态库
4.3 更完整的项目c++开发过程
案例名称:士兵突击
需求列表:
- 士兵 许三多 有一把枪,叫做 AK47
- 士兵 可以 开火
- 士兵 可以 给枪装填子弹
- 枪 能够 发射 子弹
- 枪 能够 装填子弹 —— 增加子弹数量
开发过程:
- 开发枪类
- 开发士兵类
4.3.1 合理设置项目目录
//Gun.h
#pragma once
#include <string>class Gun
{
public:Gun(std::string type){this->_bullet_count = 0;this->_type = type;}void addBullet(int bullet_num);bool shoot();private:int _bullet_count;std::string _type;};
//Soldier.h
#pragma once#include <string>
#include "Gun.h"class Solider
{
public:Solider(std::string name);~Solider();void addGun(Gun *ptr_gun);void addBulletToGun(int num);bool fire();private:std::string _name;Gun *_ptr_gun;
};
4.3.2 编写项目源文件
//Gun.cpp
#include "Gun.h"
#include "iostream"
using namespace std;void Gun::addBullet(int bullet_num)
{this->_bullet_count += bullet_num;
}bool Gun::shoot()
{if (this->_bullet_count<=0){cout << "There is no bullet!" << endl;return false;}this->_bullet_count -= 1;cout << "shoot successfully!" << endl;return true;}
//Soldier.cpp
#include "Soldier.h"Solider::Solider(std::string name)
{this->_name = name;this->_ptr_gun = nullptr;
}void Solider::addGun(Gun *ptr_gun)
{this->_ptr_gun = ptr_gun;
}void Solider::addBulletToGun(int num)
{this->_ptr_gun->addBullet(num);
}bool Solider::fire()
{return(this->_ptr_gun->shoot());
}Solider::~Solider()
{if (this->_ptr_gun==nullptr){return;}delete this->_ptr_gun;this->_ptr_gun = nullptr;}
#include "Gun.h"
#include "Soldier.h"
#include <iostream>void test()
{Solider sanduo("xusanduo");sanduo.addGun(new Gun("AK47"));sanduo.addBulletToGun(20);sanduo.fire();
}int main()
{std::cout << "This is a test string..." << std::endl;std::cout << "This is a test string..." << std::endl;test();return 0;
}
4.3.3 编写CMakeLists.txt
cmake_minimum_required(VERSION 3.0)project(SOLIDERFIRE)set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall")set(CMAKE_BUILD_TYPE Debug)include_directories(${CMAKE_SOURCE_DIR}/include)add_executable(my_cmake_exe main.cpp src/Gun.cpp src/Solider.cpp)
4.3.4 编译CMake项目
操作和4.1完全相同。
这里强调一点,当我们随着代码的迭代开发,要修改项目中的文件,重新编译获得可执行文件时,我们只需要重新make一下。注意:这个时候只会重新编译你修改的文件。这一点非常重要,对于大型项目的编译时间。
makefile和CMakeLists使用区别与联系
CMake主要是编写CMakeLists.txt文件,然后用cmake命令将CMakeLists.txt文件转化为make所需要的makefile文件,最后用make命令编译源码生成可执行程序或共享库。
简单来说:
makefile里面封装的是最底层的gcc/g++编译命令、编译参数、文件之间的依赖关系等;
CMakeLists.txt 则在makefile基础上更抽象了一层,使得项目编译工程的书写更简洁。
二者关系如下图:
6.1Cross-platform development 6.2 语法特性介绍_哔哩哔哩_bilibili
更详细的:
CMake是一个跨平台的自动化构建系统,可以用于管理和构建项目的源代码,比make更为高级。而Makefile是一种构建工具,用于自动化构建可执行文件和库。CMake可以生成Makefile,但是CMake不仅仅可以生成Makefile,还可以生成Visual Studio项目、Xcode项目等。因此,CMake可以完全代替Makefile使用。
CMake的使用场景包括但不限于以下几种:
- 跨平台构建:CMake可以在不同的平台上生成相应的构建文件,使得项目可以在不同的平台上构建。在 Linux/Unix 平台,生成 makefile,在苹果平台,可以生成 xcode,在 Windows 平台,可以生成 MSVC 的工程文件。想像如果平台的构建工程使用 makefile构建,换平台将会很麻烦。
- 管理大型项目:CMake可以管理大型项目的构建,包括多个子目录和库的构建。
- 自定义构建:CMake可以根据用户的需求自定义构建过程,包括编译选项、链接选项等。
而Makefile的使用场景包括但不限于以下几种:
- 简单项目构建:对于简单的项目,使用Makefile可以快速构建可执行文件和库。
- 熟悉Unix/Linux环境:Makefile是Unix/Linux环境下的标准构建工具,熟悉Makefile可以更好地理解Unix/Linux环境下的构建过程。
- 自定义构建:Makefile可以根据用户的需求自定义构建过程,包括编译选项、链接选项等。
因此,CMake适用于大型、跨平台的项目,而Makefile适用于简单的项目和熟悉Unix/Linux环境的用户。
学习参考:
6.1Cross-platform development 6.2 语法特性介绍_哔哩哔哩_bilibiliNotion – The all-in-one workspace for your notes, tasks, wikis, and databases.
认识Make、Makefile、CMake和CMakeLists_cmakelists makefile-CSDN博客
CMake入门教程_cmake -dgcov=on -dcmake_build_type=debug-CSDN博客
Linux下CMake简明教程_linux cmake-CSDN博客