当在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 并编译的流程如下:
- 编写 CMake 配置文件 CMakeLists.txt 。
- 执行命令
cmake PATH
或者ccmake PATH
生成 Makefile(ccmake
和cmake
的区别在于前者提供了一个交互式的界面)。其中,PATH
是 CMakeLists.txt 所在的目录。 - 使用
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
选项。前文有讲解。
简单的调试步骤示例
- 载入test可执行文件
gdb test --silent
。 - 运行
run
。 - 查看程序出错的地方
where
。 - 查看出错函数附近的代码
list
。 - 打开堆栈
backtrace
。 - 单步调节
next
或step
。 - 查看可疑表达式值
print var
。 - 在可疑行打断点
break 8
。 - 重新运行会在断点处停止。用
set variable
修改变量值。 - 继续运行
continue
。看结果是否正确。 - 退出gdb
quit
。
参考: