cmake小记

当在Linux系统下编写程序时候,如果没有类似于visual studio、vs code等IDE(集成开发环境)时,如何编译、运行程序呢?

 

如果有十几个文件需要编译,总不能在terminal上一个文件一个文件的g++。对于较大的工程,如果还使用g++写命令行就太痛苦了。而使用makefile可以管理整个工程的编译规则,之后用一个make命令就可自动编译,相对方便很多,d用makefile文件管理程序脚本之间的相互依赖关系,其语法相对比较复杂。

另一种有效的方法就是利用cmake工具,自动生成makefile文件。

 

cmake允许开发者编写一种平台无关的 CMakeLists.txt 文件来定制整个编译流程,然后再根据目标用户的平台进一步生成所需的本地化 Makefile 和工程文件,如 Unix 的 Makefile 或 Windows 的 Visual Studio 工程。从而做到“Write once, run everywhere”。显然,CMake 是一个比上述几种 make 更高级的编译配置工具。

 

 

在 linux 平台下使用 CMake 生成 Makefile 并编译的流程如下:

  1. 编写 CMake 配置文件 CMakeLists.txt 。
  2. 执行命令 cmake PATH 或者 ccmake PATH 生成 Makefile(ccmake 和 cmake 的区别在于前者提供了一个交互式的界面)。其中, PATH 是 CMakeLists.txt 所在的目录。
  3. 使用 make 命令进行编译。

 

1,编写 CMakeLists.txt

CMakeLists.txt 的语法比较简单,由命令、注释和空格组成,其中命令是不区分大小写的,符号"#"后面的内容被认为是注释。命令由命令名称、小括号和参数组成,参数之间使用空格或者分号进行间隔。 变量使用${}方式取值,但是在 IF 控制语句中是直接使用变量名。

假设源代码为main.cpp,内容如下:

#main.cpp
#include <iostream>
     
int main() {
    std::cout << "Hello, world!" << std::endl;
    return 0;
}

 

编写 CMakeLists.txt 文件,并保存在与 main.cpp 源文件同个目录(比如Demo\)下:

#CMakeLists.txt
CMAKE_MINIMUM_REQUIRED(VERSION 2.6)
PROJECT(hello_world)
ADD_EXECUTABLE(hello main.cpp)

第一句是指定运行此配置文件所需的 CMake 的最低版本

第二句是生成一个名为hello_world的工程;

第三句是将main.cpp编译成名为hello的可执行文件。

注意cmake的命令不区分大小写,但一般建议使用大写的,cmake的注释为#。

 

完成了CMakeLists.txt文件的编写后,可以执行cmake命令生成Makefile文件了。此时我们由两种方法可以执行cmake、编译和安装:

 

在terminal中,cd到CMakeLists.txt和cpp存放的文件夹Demo\下,运行

$cmake .
$make

或者

mkdir build
cd build
cmake ..
make

两种方法最大的不同在于执行cmake和make的工作路径不同。

第一种方法中,cmake生成的所有中间文件和可执行文件都会存放在项目目录Demo/中;

而第二种方法中,中间文件和可执行文件都将存放在build目录中。

第二种方法的优点显而易见,它最大限度的保持了代码目录的整洁。同时由于第二种方法的生成、编译和安装是发生在不同于项目目录的其他目录中,所以第二种方法就叫做“外部构建”(out-of-source build)。

在外部构建的情况下,PROJECT_SOURCE_DIR指向的目录同内部构建相同,即Demo/目录;而PROJECT_BINARY_DIR则有所不同,指向Demo/build目录。

 

同一目录,多个源文件

上面的例子只有单个源文件。假如Demo目录下有main.cpp,test1.cpp两个源文件

工程目录如下

./Demo2
    |
    +--- main.cpp
    |
    +--- test1.cpp
    |
    +--- test1.h

这个时候,CMakeLists.txt 可以改成如下的形式:

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

# 项目信息
project (Demo2)

# 指定生成目标
add_executable(Demo main.cpp test1.cpp)

唯一的改动只是在 add_executable 命令中增加了test1.cpp源文件。这样写当然没什么问题,但是如果源文件很多,把所有源文件的名字都加进去将是一件烦人的工作。更省事的方法是使用 aux_source_directory命令,该命令会查找指定目录下的所有源文件,然后将结果存进指定变量名。其语法如下:

aux_source_directory(<dir> <variable>)

因此,可以修改 CMakeLists.txt 如下:

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

# 项目信息
project (Demo2)

# 查找当前目录下的所有源文件
# 并将名称保存到 DIR_SRCS 变量
aux_source_directory(. DIR_SRCS)

# 指定生成目标
add_executable(Demo ${DIR_SRCS})

这样,CMake 会将当前目录所有源文件的文件名赋值给变量DIR_SRCS,再指示变量DIR_SRCS中的源文件需要编译成一个名称为 Demo 的可执行文件。

 

多个目录,多个源文件

现在进一步将 test1.h 和test1.cpp文件移动到 src 目录下。

./Demo3
    |
    +--- main.cc
    |
    +--- src/
          |
          +--- test1.cpp
          |
          +--- test1.h

对于这种情况,需要分别在项目根目录 Demo3 和 src 目录里各编写一个 CMakeLists.txt 文件。为了方便,我们可以先将 src 目录里的文件编译成静态库再由 main 函数调用。

根目录中的 CMakeLists.txt :

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

# 项目信息
project (Demo3)

# 查找当前目录下的所有源文件
# 并将名称保存到 DIR_SRCS 变量
aux_source_directory(. DIR_SRCS)

# 添加 src 子目录
add_subdirectory(src bin)

# 指定生成目标 
add_executable(Demo main.cpp)

# 添加链接库
target_link_libraries(Demo test1)

该文件添加了下面的内容:

第3行,使用命令 add_subdirectory 指明本项目包含一个子目录 src,这样 src 目录下的 CMakeLists.txt 文件和源代码也会被处理 。

( ADD_SUBDIRECTORY(source_dir [binary_dir] [EXCLUDE_FROM_ALL])这个指令用于向当前工程添加存放源文件的子目录,并可以指定中间二进制和目标二进制存放的位置, EXCLUDE_FROM_ALL 参数的含义是将这个目录从编译过程中排除。比如,工程的 example,可能就需要工程构建完成后,再进入 example 目录单独进行构建。在我们的项目中,我们添加了src目录到项目中,而把对应于src目录生成的中间文件和目标文件存放到bin目录下 )

第6行,使用命令 target_link_libraries 指明可执行文件 main 需要连接一个名为 test1 的链接库 。

子目录中的 CMakeLists.txt:

# 查找当前目录下的所有源文件
# 并将名称保存到 DIR_LIB_SRCS 变量
aux_source_directory(. DIR_LIB_SRCS)

# 生成链接库
add_library (test1 ${DIR_LIB_SRCS})

在该文件中使用命令add_library将 src 目录中的源文件编译为共享库。

 

此时文件中生成了hello可执行文件,hello文件是编译之后的输出文件。

$./hello

可以看到输出结果:

Hello, world!

 

cmake的其他命令:

SET命令

SET(CMAKE_INSTALL_PREFIX /usr/local)    

现阶段,只需要了解SET命令可以用来显式的定义变量即可。在以上的例子中,我们显式的将CMAKE_INSTALL_PREFIX的值定义为/usr/local,如此在外部构建情况下执行make install命令时,make会将生成的可执行文件拷贝到/usr/local/bin目录下。
当然,可执行文件的安装路径CMAKE_INSTALL_PREFIX也可以在执行cmake命令的时候指定,cmake参数如下:

cmake -DCMAKE_INSTALL_PREFIX=/usr ..

如果cmake参数和CMakeLists.txt文件中都不指定该值的话,则该值为默认的/usr/local。

 

INCLUDE_DIRECTORIES命令

INCLUDE_DIRECTORIES(/usr/include/thrift)    

INCLUDE_DIRECTORIES类似gcc中的编译参数“-I”,指定编译过程中编译器搜索头文件的路径。当项目需要的头文件不在系统默认的搜索路径时,需要指定该路径。

 

EXECUTABLE_OUTPUT_PATH和LIBRARY_OUTPUT_PATH
我们可以通过 SET 指令重新定义 EXECUTABLE_OUTPUT_PATH 和 LIBRARY_OUTPUT_PATH 变量来指定最终的目标二进制的位置(指最终生成的CRNode可执行文件或者最终的共享库,而不包含编译生成的中间文件)。命令如下:

SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin) 
SET(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib)

 

TARGET_LINK_LIBRARIES命令

TARGET_LINK_LIBRARIES(CRNode log4cpp thrift) 

这句话指定在链接目标文件的时候需要链接的外部库,其效果类似gcc的编译参数“-l”,可以解决外部库的依赖问题。

 

INSTALL命令
在执行INSTALL命令的时候需要注意CMAKE_INSTALL_PREFIX参数的值。其命令形式如下:

INSTALL(TARGETS targets...
 	[[ARCHIVE|LIBRARY|RUNTIME]
 	[DESTINATION < dir >]
 	[PERMISSIONS permissions...]
 	[CONFIGURATIONS 	[Debug|Release|...]]
 	[COMPONENT < component >]
 	[OPTIONAL] 	] [...])

参数中的 TARGETS 后面跟的就是我们通过 ADD_EXECUTABLE 或者 ADD_LIBRARY 定义的目标文件,可能是可执行二进制、动态库、静态库。DESTINATION 定义了安装的路径,如果路径以/开头,那么指的是绝对路径,这时候CMAKE_INSTALL_PREFIX 其实就无效了。如果你希望使用 CMAKE_INSTALL_PREFIX 来定义安装路径,就要写成相对路径,即不要以/开头,那么安装后的路径就是${CMAKE_INSTALL_PREFIX} /< destination 定义的路径>
你不需要关心 TARGETS 具体生成的路径,只需要写上 TARGETS 名称就可以了。
非目标文件的可执行程序安装(比如脚本之类):

INSTALL(PROGRAMS files... DESTINATION < dir >
 	[PERMISSIONS permissions...]
 	[CONFIGURATIONS [Debug|Release|...]]
 	[COMPONENT < component >]
 	[RENAME < name >] [OPTIONAL])

跟上面的 FILES 指令使用方法一样,唯一的不同是安装后权限为OWNER_EXECUTE, GROUP_EXECUTE, 和 WORLD_EXECUTE,即 755 权限目录的安装。
安装一个目录的命令如下:

INSTALL(DIRECTORY dirs... DESTINATION < dir >
 	[FILE_PERMISSIONS permissions...]
 	[DIRECTORY_PERMISSIONS permissions...]
 	[USE_SOURCE_PERMISSIONS]
 	[CONFIGURATIONS [Debug|Release|...]]
 	[COMPONENT < component >]
 	[[PATTERN < pattern > | REGEX < regex >]
 	[EXCLUDE] [PERMISSIONS permissions...]] [...])

DIRECTORY 后面连接的是所在 Source 目录的相对路径,但务必注意:abc 和 abc/有很大的区别。如果目录名不以/结尾,那么这个目录将被安装为目标路径下的 abc,如果目录名以/结尾,代表将这个目录中的内容安装到目标路径,但不包括这个目录本身。

 

支持 gdb

让 CMake 支持 gdb 的设置也很容易,只需要指定 Debug 模式下开启 -g 选项:

set(CMAKE_BUILD_TYPE "Debug")
set(CMAKE_CXX_FLAGS_DEBUG "$ENV{CXXFLAGS} -O0 -Wall -g -ggdb")
set(CMAKE_CXX_FLAGS_RELEASE "$ENV{CXXFLAGS} -O3 -Wall")

之后可以直接对生成的程序使用 gdb 来调试。

在 Visual Studio 中我们可以生成 debug 版和 release 版的程序,使用 CMake 我们也可以达到上述效果。debug 版的项目生成的可执行文件需要有调试信息并且不需要进行优化,而 release 版的不需要调试信息但需要优化。这些特性在 gcc/g++ 中是通过编译时的参数来决定的,如果将优化程度调到最高需要设置参数-O3,最低是 -O0 即不做优化;添加调试信息的参数是 -g -ggdb ,如果不添加这个参数,调试信息就不会被包含在生成的二进制文件中。

CMake 中有一个变量 CMAKE_BUILD_TYPE ,可以的取值是 Debug Release RelWithDebInfo 和 MinSizeRel。当这个变量值为 Debug 的时候,CMake 会使用变量 CMAKE_CXX_FLAGS_DEBUG 和 CMAKE_C_FLAGS_DEBUG 中的字符串作为编译选项生成 Makefile ,当这个变量值为 Release 的时候,工程会使用变量 CMAKE_CXX_FLAGS_RELEASE 和 CMAKE_C_FLAGS_RELEASE 选项生成 Makefile。

 

设置编译选项的讲究--add_compile_options和CMAKE_CXX_FLAGS的区别

在cmake脚本中,设置编译选项可以通过add_compile_options命令,也可以通过set命令修改CMAKE_CXX_FLAGS或CMAKE_C_FLAGS。

使用这两种方式在有的情况下效果是一样的,但请注意它们还是有区别的:

add_compile_options命令添加的编译选项是针对所有编译器的(包括c和c++编译器),add_definitions这个命令也是同样针对所有编译器,一样注意这个区别。

而set命令设置CMAKE_C_FLAGS或CMAKE_CXX_FLAGS变量则是分别只针对c和c++编译器的。

例如下面的代码

#判断编译器类型,如果是gcc编译器,则在编译选项中加入c++11支持
if(CMAKE_COMPILER_IS_GNUCXX)
    add_compile_options(-std=c++11)
    message(STATUS "optional:-std=c++11")   
endif(CMAKE_COMPILER_IS_GNUCXX)

使用add_compile_options添加-std=c++11选项,是想在编译c++代码时加上c++11支持选项。但是因为add_compile_options是针对所有类型编译器的,所以在编译c代码时,就会产生warning. 然并不影响编译,但看着的确是不爽啊,要消除这个warning,就不能使用add_compile_options,而是只针对c++编译器添加这个option。

#判断编译器类型,如果是gcc编译器,则在编译选项中加入c++11支持 
if(CMAKE_COMPILER_IS_GNUCXX)
    set(CMAKE_CXX_FLAGS "-std=c++11 ${CMAKE_CXX_FLAGS}")
    message(STATUS "optional:-std=c++11")
endif(CMAKE_COMPILER_IS_GNUCXX)

所以如下修改代码,则警告消除。

 

让CMake找到我的头文件

include_directories(./include)

作用:把当前目录(CMakeLists.txt所在目录)下的include文件夹加入到包含路径

我习惯这样写:

include_directories(${CMAKE_CURRENT_LIST_DIR}/include)

CMAKE_CURRENT_LIST_DIR这个变量,它表示当前CMakeLists所在的路径.

 

让CMake找到我的源文件

aux_source_directory(./src ${hello_src})

作用: 把当前路径下src目录下的所有源文件路径放到变量hello_src中

解释命令:aux_source_directory(<dir> <variable>)
作用:查找dir路径下的所有源文件,保存到variable变量中.

上面的例子中,hello_src是一个自定义变量,在执行了aux_source_directory(./src ${hello_src})之后,我就可以像这样来添加一个可执行文件:add_executable(hello ${hello_src}), 意思是用hello_src里面的所有源文件来构建hello可执行程序, 不用手动列出src目录下的所有源文件了。

注意:
aux_source_directory 不会递归包含子目录,仅包含指定的dir目录

CMake官方不推荐使用aux_source_directory及其类似命令(file(GLOB_RECURSE …))来搜索源文件,原因是这样包含的话,如果我再在被搜索的路径下添加源文件,我不需要修改CMakeLists脚本,也就是说,源文件多了,而CMakeLists并不需要(没有)变化,也就使得构建系统不能察觉到新加的文件,除非手动重新运行cmake,否则新添加的文件就不会被编译到项目结果中。

类似include_directories()中CMAKE_CURRENT_LIST_DIR的用法,

也可以写成:aux_source_directory(${CMAKE_CURRENT_LIST_DIR}/src ${hello_src})

传递FLAGS给C++编译器

如果我的main.cpp里面用到了C++11,那么我需要告诉CMake在生成的Makefile里告诉编译器启用C++11。与此类似,我可能也要传递其他FLAGS给编译器,怎么办?

答案是:设置CMAKE_CXX_FLAGS变量

set(CMAKE_CXX_COMPILER "clang++" ) # 显示指定使用的C++编译器 
set(CMAKE_CXX_FLAGS "-std=c++11") # c++11 
set(CMAKE_CXX_FLAGS "-g") # 调试信息 
set(CMAKE_CXX_FLAGS "-Wall") # 开启所有警告 
set(CMAKE_CXX_FLAGS_DEBUG "-O0" ) # 调试包不优化 
set(CMAKE_CXX_FLAGS_RELEASE "-O2 -DNDEBUG " ) # release包优化

解释

CMAKE_CXX_FLAGS 是CMake传给C++编译器的编译选项,等同于 g++ -std=c++11 -g -Wall
CMAKE_CXX_FLAGS_DEBUG 是除了CMAKE_CXX_FLAGS外,在Debug配置下,额外的参数
CMAKE_CXX_FLAGS_RELEASE 同理,是除了CMAKE_CXX_FLAGS外,在Release配置下,额外的参数

 

g++常用命令

gdb调试

gdb是一个用来调试C和C++程序的功能强大的调试器,能在程序运行时观察程序的内部结构和内存使用情况。
gdb主要提供以下功能:

  • 监视程序中变量的值的变化。
  • 设置断点,使程序在指定的代码行上暂停执行,便于观察。
  • 单步执行代码。
  • 分析崩溃程序产生的core文件。

推荐书籍《Debugging with GDB》[在线][下载pdf]。通过在gdb下输入help或在命令行上输入gdb h查看关于gdb选项说明的简单列表。键入help后跟命令的分类名。可以获得该类命令的详细清单。搜索和word相关的命令可用apropos word

为使gdb能正常工作,必须在程序编译时包含调试信息。即-g选项。前文有讲解。

简单的调试步骤示例

  1. 载入test可执行文件gdb test --silent
  2. 运行run
  3. 查看程序出错的地方where
  4. 查看出错函数附近的代码list
  5. 打开堆栈backtrace
  6. 单步调节nextstep
  7. 查看可疑表达式值print var
  8. 在可疑行打断点break 8
  9. 重新运行会在断点处停止。用set variable修改变量值。
  10. 继续运行continue。看结果是否正确。
  11. 退出gdbquit

 

参考:

CMake 入门实战

CMake快速入门教程:实战 - IT由零开始 - 博客园

在Ubuntu下搭建C/C++编程环境 - Shine_zy的博客 - CSDN博客

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值