• CMake的使用


    一、CMake概述

    CMake是一个项目构建工具,并且是跨平台的。关于项目构建我们所熟知的还有Makefile (通过make命令进行项目的构建),大多是IDE软件都集成了make,比如: VS的nmake、linux下的GNU make、Qt的qmake等,如果自己动手写makefile,会发现makefile 通常依赖于当前的编译平台,而且编写makefile的工作量比较大,解决依赖关系时也容易出错。

    而CMake恰好能解决上述问题,其允许开发者指定整个工程的编译流程,在根据编译平台,自动生成本地化的Makefile和工程文件,最后用户只需make 编译即可,所以可以把CMake看成一款自动生成Makefile的工具,其编译流程如下图:

    在这里插入图片描述

    蓝色虚线表示使用makefile构建项目的过程

    红色实线表示使用cmake构建项目的过程

    介绍完CMake的作用之后,再来总结一下它的优点:
    跨平台
    能够管理大型项目
    简化编译构建过程和编译过程
    可扩展:可以为cmake 编写特定功能的模块,扩充 cmake功能

    二、CMake的使用

    CMake支持大写、小写、混合大小写的命令。如果在编写CHakeLists.txt 文件时使用的工具有对应的命令提示,那么大小写随缘即可,不要太过在意。

    1.注释

    CMake使用#进行行注释,可以放在任何位置。

    #这是一个CMakeLists.txt 文件
    cmake_minmum_required(VERSION 3.0.0)
    

    CMake使用#[[]]形式进行块注释。

    #[[这是一个CMakeLists.txt 文件
    这是一个CMakeLists.txt 文件
    这是一个CMakeLists.txt 文件]]
    cmake_minmum_required(VERSION 3.0.0)
    

    2.简单编译程序

    1.测试源文件

    1.head.h

    #ifndef _HEAD_H
    #define _HEAD_H
    
    int add(int a,int b);
    int subtract(int a,int b);
    int multiply(int a,int b);
    int divide(int a,int b);
    
    #endif
    

    2.add.cc

    #include "head.h"
    
    int add(int a,int b)
    {
        return a + b;
    }
    

    3.subtract.cc

    #include "head.h"
    
    int subtract(int a,int b)
    {
        return a - b;
    }
    

    4.multiply.cc

    #include "head.h"
    
    int multiply(int a,int b)
    {
        return a * b;
    }
    

    5.divide.cc

    #include "head.h"
    
    int divide(int a,int b)
    {
        return (double)a / b;
    }
    

    6.main.cc

    #include "head.h"
    #include 
    
    int main()
    {
        int a = 20,b = 12;
        std::cout << "a = " << a << " b = " << b << std::endl;
        std::cout << "a + b = " << add(a,b) << std::endl;
        std::cout << "a - b = " << subtract(a,b) << std::endl;
        std::cout << "a * b = " << multiply(a,b) << std::endl;
        std::cout << "a / b = " << divide(a,b) << std::endl;
        return 0;
    }
    

    上述代码的目录结构树如下:

    -- v1
        |-- add.cc
        |-- divide.cc
        |-- head.h
        |-- main.cc
        |-- multiply.cc
        `-- subtract.cc
    

    我们可以使用如下指令进行编译:

    g++ *.cc -o main
    

    2…添加CMakeLists.txt

    cmake_minimum_required(VERSION 2.8)
    project(CALC)
    add_executable(main main.cc add.cc subtract.cc multiply.cc divide.cc)
    

    cmake_minimum_required:指定使用的cmake的最低版本(可选,非必选,如果不加可能会有警告)

    project:定义过程名称,并可指定工程的版本,工程描述,web主页地址,支持的语言(默认情况支持所有语言),如果不需要这些都是可以忽略的,只需要指定出过程的名字即可。

    project指令的语法格式如下:

    project( [...])
    project(
    		[VERSION [.[.[.ctweak>]]]]
    		[DESCRIPTION ]
    		[HONEPAGE_URL ]
    		[LANGUAGES  ...])
    

    add_executable :定义工程会生成一个可执行程序

    add_executable(可执行程序名  源文件名称)
    

    这里的可执行程序名和project中的项目名没有任何关系

    源文件名可以是一个也可以是多个,如有多个可以用空格或 ; 间隔

    #样式一
    add_executable(main main.cc add.cc subtract.cc multiply.cc divide.cc)
    #样式二
    add_executable(main main.cc;add.cc;subtract.cc;multiply.cc;divide.cc)
    

    3.执行cmake命令

    将CMakeLists.txt文件编辑好之后,就可以执行cmake命令了。

    cmake 命令原型
    cmake CMakeLists.txt文件所在路径
    

    在这里插入图片描述

    我们发现执行完毕cmake之后,当前目录就多了几个文件,其中包括Makefile,现在我们执行make指令即可完成编译,然后就可以完成程序的执行

    在这里插入图片描述

    这些生成的文件可能会与我们当前的文件进行混淆,那么我们就可以新建一个目录,在新建的目录下进行执行cmake

    mkdir build
    cd build
    cmake ..
    make
    

    在这里插入图片描述

    此时我们cmake生成的文件就在build目录下了

    在这里插入图片描述

    3.定义变量

    在上面的例子中一共提供了5个源文件,假设这五个源文件需要反复被使用,每次都直接将它们的名字写出来确实是很麻烦,此时我们就需要定义一个变量,将文件名对应的字符串存储起来,在cmake里定义变是需要使用set ,

    # SET指令的语法是:
    # [] 中的参数为可选项,入不需要可以不写
    SET(VAR [VALUE] [CACHE TYPE DOCSTRING [FORCE]])
    VAR:变量名
    VALUE:变量值
    

    对于我们上面的例子可以进行如下变量的定义

    # 方式1:各个源文件之间只有空格间隔
    # set(SRC_LIST main.cc add.cc subtract.cc multiply.cc divide.cc)
    
    #方式2:各个源文件之间使用 : 间隔
    set(SRC_LIST main.cc add.cc subtract.cc multiply.cc divide.cc)
    add_executable(main ${SRC_LIST})
    

    4.指定使用的C++标准

    在编写C++程序的时候,可能会用到C++11,C++14,C++17,C++20等新特性,那么就需要在编译的时候在编译的命令中指定出要使用哪个标准:

    g++ *.cc -o main -std=c++11
    

    上面的例子中通过参数-std=c++11指定出要使用C++11标准进行编译程序,C++标准对应有一些宏叫做DCMAKE_CXX_STANDARD。在CMake中要想指定C++标准有两种方式

    1.在CMakeLists.txt中通过set命令进行指定

    #增加-std=c++11
    set(CMAKE_CXX_STANDARD 11)
    #增加-std=c++14
    set(CMAKE_CXX_STANDARD 14)
    #增加-std=c++17
    set(CMAKE_CXX_STANDARD 17)
    

    2.在执行cmake命令的时候指定这个宏的值

    #增加-std=c++11
    cmake CMakeLists.txt文件路径 -DCMAKE_CXX_STANDARD=11
    #增加-std=c++14
    cmake CMakeLists.txt文件路径 -DCMAKE_CXX_STANDARD=14
    #增加-std=c++17
    cmake CMakeLists.txt文件路径 -DCMAKE_CXX_STANDARD=17
    

    在上面例子中CMake后的路径需要根据实际情况酌情修改。

    5.指定输出的路径

    在CMake 中指定可执行程序输出的路径,也对应一个宏,叫做EXECUTABLE_OUTPUT_PATH,它的值还是通过set命令进行设置:

    set(HOME /home/xxx/xxx)
    set(EXECUTABLE_OUT_PUT_PATH ${HOME}/bin)
    

    第一行:定义一个变量用于存储一个绝对路径

    第二行:将拼接好的路径值设置给EXECUTABLE_OUTPUT_PATH宏。如果这个路径中的子目录不存在,会自动生成,无需自己手动创建

    由于可执行程序是基于cmake命令生成的 makefile文件然后再执行make 命令得到的,所以如果此处指定可执行程序生成路径的时候使用的是相对路径./xxx/xxx,那么这个路径中的 ./ 对应的就是makefle文件所在的那个目录。

    CMakeLists.txt文件

    cmake_minimum_required(VERSION 2.8)
    project(CALC)
    set(EXECUTABLE_OUTPUT_PATH /home/hdp/cpp/cmake/v1/executable_dir)
    add_executable(main main.cc add.cc subtract.cc multiply.cc divide.cc)
    

    使用的指令入下:

    cd build
    rf * -rf
    cmake ..
    make
    cd ../executable_dir
    ./main
    

    在这里插入图片描述

    6.搜索文件

    如果一个项目里边的源文件很多,在编写CMakeLists.txt文件的时候不可能将项目目录的各个文件——罗列出来,这样太麻烦也不现实。所以,在CMake 中为我们提供了搜索文件的命令,可以使用 aux_source_directory命令或者file命令。

    1.方式1

    在CMake中使用aux_source_directory命令可以查找某个路径下的所有源文件,命令格式为

    aux_source_directory(< dir > < variable>)
    dir: 要搜索的目录
    variable: 将从dir目录下搜索到的源文件列表存储到变量中
    

    CMakeLists.txt文件

    2.方式2

    如果一个项目里边的源文件很多,在编写CMakeLists.txt文件的时候不可能将项目目录的各个文件——罗列出来,这样太麻烦了。所以,在CMake 中为我们提供了搜索文件的命令,他就是file(当然,除了搜索以外通过 file还可以做真他事情)。

    file(GLOB/GLOB_RECURSE 变量名 要搜索的文件路径和文件类型)
    GLOB:将指定目录下搜索到的满足条件的所有文件名生成一个列表,并将其存储到变量中。
    GLDB_RECURSE:递归搜索指定目录,将搜索到的满足条件的文件名生成一个列表,并将其存储到变量中。
    

    搜索当前目灵的src目录下所有的源文件,并存储到变量中

    file(GLOB MAIN_SRC ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)
    file(GLOB MAIN_HEAD ${CHANK_CURRENT_SOURCE_DIR}/include/*.h)
    

    CMAKE_CURRENT_SOURCE_DIR宏表示当前访问的CMakeLists.txt文件所在的路径

    PROJECT_SOURCE_DIR宏是指执行cmake命令后的路径,即还是CMakeLists.txt文件所在的路径

    关于要搜索的文件路径和类型可加双引号,也可不加

    file(GLOB MAIN_HEAD "${CMAKE_CURRENT_SOURCE_DIR}/src/*.h")
    

    如果需要搜索两个文件,搜索得到的文件放在变量a和b中,我们可以使用如下指令将a和b进行合并

    set(a a b)
    

    在我们的代码中,如果.cpp文件和.h文件在同一个目录中,则使用如下代码即可

    aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR} SRC_LIST)
    file(GLOB SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp)
    add_executable(main ${SRC_LIST})
    

    但是我们.cpp文件不在同一个目录中,即源码是如下的目录结构

    我们使用如下指令将.cc和.h分别放到src和include目录下

    mkdir src include
    mv head.h ./include/ // mv head.h include
    mv *.cc ./src/  // mv *.cc src
    

    文件结构如下:

    .
    |-- build
    |-- CMakeLists.txt
    |-- include
    |   `-- head.h
    `-- src
        |-- add.cc
        |-- divide.cc
        |-- main.cc
        |-- multiply.cc
        `-- subtract.cc
    

    tree 指令后面加 -L + 数字可以指定层级数

    [hdp@VM-12-6-centos v1]$ tree -L 1
    .
    |-- build
    |-- CMakeLists.txt
    |-- include
    `-- src
    

    此时我们的CMakeLists.txt的内容如下:

    cmake_minimum_required(VERSION 2.8)
    project(CALC)
    # aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR}/src SRC_LIST)
    file(GLOB SRC_LIST ${PROJECT_SOURCE_DIR}/src/*.cc)
    add_executable(main ${SRC_LIST})
    

    但是我们在编译的时候会出现以下的问题

    在这里插入图片描述

    它告诉我们源文件找不到,这是因为我们在.cc文件中包含点head.h文件是这样包含的

    #include "head.h"
    

    这样查找文件是在当前目录下,但是我们的head.h文件已经转移到了./include目录下

    这里解决方案有两种,一是修改.cc文件包含head.h的时候路径,即按照下面的方式进行包含

    #include "../include/head.h"
    

    但是如果有成千上万和源文件的话,那么都需要进行修改,此时我们可以在CMakeLists.txt文件中包含头文件

    7.包含头文件

    在编译项目源文件的时候,很多时候都需要将源文件对应的头文件路径指定出来,这样才能保证在编译过程中编译器能够找到这些头文件,并顺利通过编译。在CMake 中设置要包含的目录也很简单,通过一个命令就可以搞定了,他就是include_directories:

    include_directories(headpath)
    

    对于上面的问题,我们可以在CMakeLists.txt中加入如下代码:

    include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include)
    

    这样我们就可以完成编译

    需要注意的是,这里我们添加的是头文件所在的目录,不需要指定具体的头文件名称

    三、通过CMake 制作库文件

    有些时候我们编写的源代码并不需要将他们编译生成可执行程序,而是生成一些静态库或动态库提供给第三方使用,下面来讲解在cmake 中生成这两类库文件的方法。

    1.制作动静态库

    在cmake 中,如果要制作动/静态库,需要使用的命令如下:

    add_library(库名称 SHARED/STATIC 源文件1 [源文件2] ...)
    

    在Linux中,静态库名字分为三部分: lib +库名字 + a,此处只需要指定出库的名字就可以了,另外两部分在生成该文件的时候会自动填充。
    Windows中虽然库名和Linux格式不同,但也只需指定出名字即可。

    在Linux下动态库和静态库文件的后缀分别为:.so 和 .a

    在Windows下动态库和静态库文件的后缀分别为:.dll 和 .lib

    如果不通过第二个参数指定生成静态库还是动态库,默认生成的是静态库

    下面有一个目录,需要将src目录中的源文件编译成静态库,然后再使用:

    .
    |-- build
    |-- CMakeLists.txt
    |-- include
    |   `-- head.h
    |-- main.cc
    `-- src
        |-- add.cc
        |-- divide.cc
        |-- multiply.cc
        `-- subtract.c
    

    这里我们需要注意的是,main.cc不需要放在src目录下,因为制作库文件不需要执行的源文件

    根据上面的目录结构,可以这样编写CHakeLists.txt文件:

    cmake_minimum_required(VERSION 2.8)
    project(CALC)
    file(GLOB SRC_LIST ${PROJECT_SOURCE_DIR}/src/*.cc)
    include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include)
    add_library(calc SHARED ${SRC_LIST})
    

    然后再build目录下执行对应的指令

    在这里插入图片描述

    我们可以发现libclac.so文件的颜色是绿色的,这样因为动态库文件具有可执行权限,但是静态库没有可执行权限

    我们制作静态库只需要将上面的SHARED改成STATIC即可。这样我们就得到了动态库和静态库

    得到动态库之后,我们该如何进行使用呢

    第一步我们需要将库发布给使用者,我们需要发布两部分,一是对应的库文件,二是对应的头文件

    为什么我们还需要加上对应的头文件呢。这是因为无论是动态库还是静态库,他们的本质还是源代码,只不过他们是二进制的,而源文件是文本格式的,只有这个区别。但是对于计算机来说,无论是文本还是二进制的,最终都是以二进制的方式进行处理的,所以计算机也是能够识别的,因为他们的本质相同,所以二进制文件也是需要头文件的,因为在这些库文件中定义了函数,而这些函数的声明都在include对应的头文件中

    2.指定动静态库生成的路径

    方式1-适用于动态库

    对于生成的库文件来说和可执行程序一样都可以指定输出路径。由于在Linux下生成的动态库默认是有执行权限的,所以可以按照生成可执行程序的方式去指定它生成的目录

    cmake_minimum_required(VERSION 2.8)
    project(CALC)
    file(GLOB SRC_LIST ${PROJECT_SOURCE_DIR}/src/*.cc)
    include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include)
    set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)
    add_library(calc STATIC ${SRC_LIST})
    

    对于这种方式来说,其实就是通过set 命令给 EXECUTABLE_QUTPUT_PATH 宏设置了一个路径,这个路径就是可执行文件生成的路径。

    方式2-都适用

    由于在Linux下生成的静态库默认不具有可执行权限,所以在指定静态库生成的路径的时候就不能使用
    EXECUTABLE_OUTPUT_PATH 宏了,而应该使用LIBRARY_OUTPUT_PATH,这个宏对应静态库文件和动态库文件郡适用。

    cmake_minimum_required(VERSION 2.8)
    project(CALC)
    file(GLOB SRC_LIST ${PROJECT_SOURCE_DIR}/src/*.cc)
    include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include)
    set(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)
    # 生成静态库
    add_library(calc STATIC ${SRC_LIST})
    # 生成动态库
    add_library(calc SHARED ${SRC_LIST})
    

    3.在程序中链接静态库

    如果我们要使用我们制作的动静态库发布出去,在发布的时候我们需要提供库文件和头文件,因为头文件中有库文件中一系列函数的声明,如果没有函数的声明,那么别人给我们一个库,我们就不知道库中有什么,那么我们也就无法调用库中给我们提供的函数,其实库也是源文件,只不过是二进制的,对多个源文件进行了打包的操作,如果是源文件,那么就是文本格式,我们能够看懂文件中有哪些函数,即文件中的内容,但是现在文件中的内容是二进制的,我们就看不懂了,所以就需要提供对应的头文件,我们才能够知道库中有什么东西,才能对库中的函数进行调用,实现对应的功能。

    下面我们做一个测试,此时我们就不需要src目录下的文件了,可以由动静态来进行替代了,然后我们将静态库放到lib1中,动态库放到lib2中

    cp v2 v3 -r
    rm -rf src - r
    mkdir lib1 lib2
    mv ./lib/libcalc.a lib1
    mv ./lib/libcalc.so lib2
    

    我们查看文件的目录结构

    lib1
    `-- libcalc.a
    lib2
    `-- libcalc.so
    

    在cmake 中,链接静态库的命令如下:

    link_librarues( [...])
    

    参数一:指定要链接的静态库的名字,可以是全面 libxxx.a 也可以是掐头(lib)去尾(.a)之后的名字xxx

    参数2-N:要链接的其他静态库的名字

    如果该静态库不是系统提供的(自己制作的或者使用第三方提供的静态库),就会出现静态库找不到的问题,此时就需要将静态库的路径也指定出来

    link_directories()
    

    CMakeLists.txt的内容如下:

    cmake_minimum_required(VERSION 2.8)
    project(CALC)
    file(GLOB SRC_LIST ${PROJECT_SOURCE_DIR}/*.cc)
    # 包含头文件的路径
    include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include)
    # 链接静态库
    link_libraries(calc)
    # 包含静态库的路径
    link_directories(${CMAKE_CURRENT_SOURCE_DIR}/lib1)
    add_executable(main ${SRC_LIST})
    

    总结:静态库的使用包含两个命令:

    # 链接静态库
    link_libraries(calc)
    # 包含静态库的路径
    link_directories(${CMAKE_CURRENT_SOURCE_DIR}/lib1)
    

    在生成可执行程序的时候,不仅需要指定源文件,因为源文件中又调用了一些库文件,库文件中还是源代码,我们需要将库文件中的源代码追加到main中去,使用 link_libraries(calc) 进行链接,此时生成可执行程序的时候,除了会加载SRC_LIST中的源文件,还会加载 link_libraries 指定的库文件,然后把源文件和库文件放到一起链接,最终生成可执行程序 main

    如果我们使用的是静态库,那么在生成可执行程序的时候,静态库的库文件会被打包到可执行程序main中,而如果使用的是动态库,那么动态库中的库文件不会被打包到可执行程序中,是当可执行程序启动之后,动态库并不一样会被加载到内存中,当应用程序调用动态库中的函数之后才会将动态库加载到内存中,而静态库是可执行程序启动的时候就被加载到内存中。

    4.在程序中链接动态库

    在程序编写过程中,除了在项目中引入静态库,好多时候也会使用一些标准的或者第三方提供的一些动态库,关于动态库的制作、使用以及在内存中的加载方式和静态库都是不同的。

    在cmake中链接动态库的命令如下:

    target_link_libraries(
    	
    	...
    	[...]...)
    

    target:指定要加载动态库的文件的名字

    该文件可能是一个源文件

    该文件可能是一个动态库文件

    该文件可能是一个可执行文件

    PRIVATE|PUBLIC|INTERFACE:动态库的访问权限,默认为PUBLIC

    如果各个动态库之间没有依赖关系,无需做任何设置,三者没有没有区别,一般无需指定,使用默认的PUBLIC即可。

    动态帅的链接具有传递性,如果动态库A链接了动态库B、C,动态库D链接了动态库A,此时动态库D相当于也链接了动态库8、c,并可以使用动态库B、C中定义的方法。

    target_link_libraries(A B C)
    target_link_libraries(D A)
    

    PUBLIC : 在public后面的库会被Link到前面的 target 中,并且里面的符号(函数)也会被导出,提供给第三方使用。比如a链接了b和c,如果是public链接的方式链接了b和c,此时d链接了a,那么d也可以使用b和c中的函数

    PRIVATE:在private后面的库仅被link到前面的target中,并且终结掉,第三方不能感知你调了啥库。比如a链接了b和c,如果是private链接的方式链接了b和c,此时d链接了a,那么d无法使用b和c中的函数

    INTERFACE :在interface后面引入的库不会被链接到前面的 target中,只会导出符号。比如a链接了b和c。a可以使用b和c中的函数,但是a不知道这个函数是属于b还是c这个库,而采用private的方式进行链接,a是知道对应的函数是属于b还是c

    以上a,b,c,d都是指动态库

    链接系统动态库

    动态库的链接和静态库是完全不同的:

    静态库会在生成可执行程序的链接阶段被打包到可执行程序中,所以可执行程序启动,静态库就被加载到内存中了。
    动态库在生成可执行程序的链接阶段不会被打包到可执行程序中,当可执行程序被启动并且调用了动态库中的函数的时候,动态库才会被加载到内存。动态库只会在内存中加载一份就可以被多个进程使用。那么多个进程使用动态库,会不会导致数据不一致的问题呢?答案是不对的,因为每一个进程有对应的动态库加载区,当动态库被加载到物理内存了之后,它会被映射到进程地址空间中,每个进程通过mmu进行地址的映射,会映射到不同的物理内存中,进行不同的操作之后,数据会放到对应的地址中,不会影响原来物理地址的动态库

    因此,在 cmake中指定要链接的动态库的时候,应该将命令写到生成了可执行文件之后:

    cmake_minimum_required(VERSION 2.8)
    project(CALC)
    file(GLOB SRC_LIST ${PROJECT_SOURCE_DIR}/*.cc)
    include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include)
    link_directories(${CMAKE_CURRENT_SOURCE_DIR}/lib2)
    add_executable(main ${SRC_LIST})
    target_link_libraries(main calc)
    

    在 target_link_libraries(main calc) 中

    main:对应的是最终生成的可执行程序的名字

    calc:这是可执行程序要加悖的动态库,这个库是系统提供的线程库,全名为libcalc. so,在指定的时候一般会掐头(lib)去尾(.so)。

    温馨提示:使用 target_link_libraries命令既可以链接动态库也可以链接静态库。

    四、日志

    在CMake 中可以用用户显示—条消息,该命令的名字为message :

    message([STATUS | WARNING | AUTHOR_WARNING | SEND_ERROR | FATAL_ERROR ] "message to display" ...)
    

    (无):重要消息

    STATUS :非重要消息

    WARNING : CMake警告,会继续执行

    AUTHOR_WARNING : CMake警告(dev)。会继续执行

    SEND_ERROR : CMake潜误,继续执行,但是会跳过生成的步骤

    FATAL_ERROR :CMake错误,终止所有处理过程

    CMake的命令行工具会在stdout 上显示STATUS消息,在stderr 上显示其他所有消息。CMake的GUI会在它的log区域显示所有消息。

    CMake警告和错误消息的文本显示使用的是一种简单的标记语言。文本没有缩进,超过长度的行会回卷,段落之间以新行做为分隔符。

    # 输出一般日志信息
    message(STATUS "source path: ${PROJECT_SOURCE_OIR}“)
    # 输出警告信息
    message(WARNING "source path: ${PROJECT_soURce_DIR}")
    # 输出错误信息
    message(FATAL_ERROR "source path:${PROJECT_SDURCE_DIR}")
    

    打印变量的值的时候,需要通过${}将变量值取出

    五、变量操作

    1.追加

    有时候项目中的象文件并不一定都在同一个目录中,但是这些源文件最终却需要一起进行编译来生成最终的可执行文件或者库文件。如果我们通过 file 命令对各个目录下的源文件进行搜索,最后还需要做一个字符串拼接的操作,关于字符串拼接可以使用set命令也可以使用List命令。

    使用set拼接

    如果使用set进行字符串拼接,对应的命令格式如下:

    set(变量名1 ${变量名2} ${变量名2} ...)
    

    关于上面的命令其实就是将从第二个参数开始往后所有的字符出进行拼接,最后将结果存储到第一个参数中,如果第—个参数中原来有数据会对原数据就行覆盖。

    cmake_minimum_required(VERSION 3.0)
    project(TEST)
    set(TEMP "hello,world")
    file(GLOB SRC_1 ${PROJECT_SOURCE_DIR}/src1/*.cc)
    file(GLOB SRC_2 ${PROJECT_SOURCE_DIR}/src2/*.cc)
    # 追加(拼接)
    set(SRC_1 ${SRC_1} ${SRC_2} ${TEMP})
    message(STATUS "message: ${SRC_1}")
    

    使用list 拼接

    如果使用list进行字符串拼接,对应的命令格式如下:

    list(APPEND  [(

    list命令的功能比 set要强大,字符串拼接只是它的其中一个功能,所以需要在它第一个参数的位置指定出我们要做的操作,APPEND,表示进行数据追加,后边的参数和set就一样了。

    cmake_minimum_required(VERSION 3.0)
    project(TEST)
    set(TEMP "hello,world")
    file(GLOB SRC_1 ${PROJECT_SOURCE_DIR}/src1/*.cc)
    file(GLOB SRC_2 ${PROJECT_SOURCE_DIR}/src2/*.cc)
    # 追加(拼接)
    list(APPEND SRC_1 ${SRC_1} ${SRC_2} ${TEMP})
    message(STATUS "message: ${SRC_1}")
    

    list虽然是一个变量,但是在底层管理的时候会把若干个子字符串通过 ; 进行间隔,在message输出的时候不显示分号,为什么需要分号进行分割呢,这是因为 list 除了完成字符串的拼接,还能够完成字符串的删除,分号可以快速识别各个子字符串,这样就可以快速的完成删除操作

    在CMake中,使用 set 命令可以创建一个 list。一个在 list 内部是一个由分号;分割的一组字符串。例如,

    set(temp1 a;b;c;d;e)
    set(temp2 a b c d e)
    message(${temp1})
    message(${temp2})
    

    输出的结果为:

    abcde
    abcde
    

    2.字符串移除

    我们在通过 file 搜索某个目录就得到了该目录下所有的源文件,但是其中有些源文件并不是我们所需要的,比如:

    .
    |-- add.cc
    |-- divide.cc
    |-- main.cc
    |-- multiply.cc
    `-- subtract.cc
    

    在当前这么目录有五个源文件,其中 main. cpp是一个测试文件,并不需要参照动静态库的制作,如果我们想要把计算器相关的源文件生成一个动态库给别人便用,那么只需要add.cc,divide.cc,multiply.cc,subtract.cc这四个源文件就可以了。此时,就需要将main.cpp从搜索到的数掘中剔除出去,想要实现这个功能,也可以使用list

    list(REMOVE_ITEM   [() ...])
    

    通过上面的命令原型可以看到删除和追加数据类似,只不过是第一个参数变成了REMOVE_ITEM

    比如文件的目录结构如下:

    .
    |-- build
    |-- CMakeLists.txt
    |-- include
    |   `-- head.h
    |-- lib
    |   |-- libcalc.a
    |   `-- libcalc.so
    `-- src
        |-- add.cc
        |-- divide.cc
        |-- main.cc
        |-- multiply.cc
        `-- subtract.cc
    

    CMakeLists.txt文件的内容如下:

    cmake_minimum_required(VERSION 2.8)
    project(CALC)
    file(GLOB SRC_LIST ${PROJECT_SOURCE_DIR}/src/*.cc)
    include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include)
    set(tmp hello world)
    set(tmp1 ${tmp} ${SRC_LIST})
    message(${tmp})
    message(${tmp1})
    

    输出的内容如下:

    在这里插入图片描述

    我们向tmp中追加一些字符串

    list(APPEND tmp "xxx1" "yyy2" "zzz3" ${SRC_LIST})
    message(${tmp})
    

    运行的结果如下:

    在这里插入图片描述

    现在我们删除src中的main.cc文件所在的路径

    message("---------------------------")
    message(${SRC_LIST})
    list(REMOVE_ITEM SRC_LIST main.cc)
    message("---------------------------")
    message(${SRC_LIST})
    message("---------------------------")
    

    在这里插入图片描述

    将要移除的文件的名字指定给list 就可以了。但是一定要注意通过file 命令搜索源文件的时候得到的是文件的绝对路径(在list 中每个文件对应的路径都是一个item,并且都是绝对路径),那么在移除的时候也要将该文件的绝对路径指定出来才可以,否是移除操作不会成功。

    但是我们发现,main.cc文件的目录并没有被删除,这是因为如果我们把若干个字符串拼接在一起,在底层各个字符串之间维护了一个分隔符,即分号,通过分号能够识别出添加的若干个子字符串是什么,如果我们在查找,删除还是其他操作的时候,会根据分隔符找到对应的子字符串,但是这里我们删除main.cc时,它不属于其中的任意一个字符串,所以没有能够完成删除,这里我们需要添加main.cc的绝对路径

    所以正确的代码如下:

    list(REMOVE_ITEM SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/src/main.cc)
    

    此时我们就完成了删除操作

    在这里插入图片描述

    3.list的其他功能

    1.获取list的长度(子字符串的个数)

    list(LENGTH  )
    

    LENGTH:子命令 LENGTH 用于读取列表长度

    :当前操作的列表

    :新创建的变量,用于存储列表的长度

    2.读取列表中指定索引的的元素,可以指定多个索引

    list(GET   [ ...] )
    

    :当前操作的列表

    <element index>:列表元素的索引

    从0开始编号,索引0的元素为列表中的第一个元素;
    索引也可以是负数,-1表示列表的最后一个元素,-2表示列表倒数第二个元素,以此类推当索引.(不管是正还是负)超过列表的长度,运行会报错

    :新创建的变量,存储指定索引元素的返回结果,也是一个列表。

    3.将列表中的元素用连接符(字符串)连接起来组成一个字符串

    list(JOIN   )
    

    : 当前操作的列表

    :指定的连接符(字符串)

    :新创建的变量,存储返回的字符串

    <list> 123 456 789
    <glue>  xxx
    <output variable> 123xxx456xxx789xx
    

    4.查找列表是否存在指定的元素,若果未找到,返回-1

    list(FIND   )
    

    :当前操作的列表

    :需要再列表中搜索的元素 :新创建的变量

    如果列表中存在,那么返回在列表中的索引。如果未找到则返回–1。

    5.将元素追加到列表中

    list(APPEND  [ ...])
    

    6.在list中指定的位置插入若干元素

    list(INSERT    [ ...])
    

    7.将元素插入到列表的0索引位置

    list(PREPEND  [

    8.将列表中最后的元素移除

    list(POP_BACK  [ ...])
    

    9.将列表中第一个元素移除

    list(POP_FRONT  [...])
    

    10.将指定的元素从列表中移除

    list(REMOVE_ITEM   [ ...])
    

    11.将指定索引的元素从列表中移除

    list(REMOVE_AT   ...])
    

    12.移除列表中重复的元素

    list(REMOVE_DUPLICATES )
    

    13.列表翻转

    list(REVERSE )
    

    14.列表排序

    list(SORT  [COMPARE ] [CASE ] [ORDER ])
    

    COMPARE:指定排序方法。有如下几种值可选:

    ​ STRING:按照字母顺序进行排序,为默认的排序方法

    ​ FILE_BASENANE:如果是一系列路径名,会便用basename进行排序

    ​ NATURAL:使用自然数顺序排序

    CASE:指明是否大小写敏感。有如下几种值可选:

    ​ SENSITIVE:按照大小写敏感的方式进行排序,为默认值

    ​ INSENSITIVE:按照大小写不敏感方式进行排序

    ORDER:指明排序的顺序。有如下几种值可选

    ​ ASCENDING:按照升序排列,为默认值

    ​ DESCENDING:按照降序排列

    以上的内容我们在可以在官方文档进行查阅:https://cmake.org/cmake/help/v3.26/

    六、宏定义

    我们在编写程序的时候,可能会有很多日志进行输出,但是当程序发布了之后,就不需要进行日志输出了,需要把项目中的全部日志信息删掉,否则大量的日志信息输出会影响程序的运行效率。如果我们有大量的源文件,且源文件都有许多的日志打印,那么删除这些日志就非常浪费时间

    在进行程序测试的时候,我们可以在代码中添加一些宏定义,通过这些宏来控制这些代码是否生效,如下所示:

    #include 
    
    #define NUM 5
    int main()
    {
        int a = 10;
    #ifdef DEBUG
        std::cout << "这是一条 DEBUG 信息" << std::endl;
    #endif
        for(int i = 0;i < NUM; i++)
            std::cout << "hello,gcc" << std::endl;
        return 0;
    }
    

    在程序的第七行对DEEUG宏进行了判断,如果该宏被定义了,那么第八行就会进行日志输出,如果没有定义这个宏,第八行就相当于被注释掉了,因此最终无法看到日志输入出(上述代码中并没有定义这个宏)。
    为了让测试更灵活,我们可以不在代码中定义这个宏,而是在测试的时候去把它定义出来,其中一种方式就是在gcc/g++命令中去指定,如下:

    g++ test.cc -DDEBUG -o test
    

    在gcc/g++中命令通过参数-D指定出要定义宏的名字,这样就相当于在代码中定义了一个宏,其名字为 DEBUG

    在CMake中我们也可以做类似的事情,对应的命令叫做add_definitions:

    add_definitions(-D宏名称)
    

    针对上面的源文件编写一个CMakeLists.txt文件,内存如下:

    cmake_minmum_required(VERSION 3.0)
    project(TSET)
    add_definitions(-DDEBUG)
    add_executable(test ./test.cc)
    

    通过这种方式,上述代码中的第八行日志就能够被输出出来了。

    预定义宏

    下面的列表中为大家整理了一些CMake中常用的宏:

    PROJECT_SOURCE_DIR 使用cmake命令后紧跟的目录,一般是工程的目录

    PROJECT_BINARY_DIR 执行cmake命令的目录

    七、CMake嵌套

    1.嵌套的CMake

    如果项目很大,或者项目中有很多的源码目录,在通过CMake管理项目的时候如果只使用一个CNakeLists.txt ,那么这个文件相对会比较复杂,有一种化繁为简的方式就是给每个源码目录都添加一个CNakeLists.txt 文件(头文件目录不需要),这样每个文件都不会太复杂,而且更灵活,更容易维护。

    先来看—下下面的这个的目录结构:

    .
    |-- build
    |-- calc
    |   |-- add.cc
    |   |-- CMakeLists.txt
    |   |-- divide.cc
    |   |-- multiply.cc
    |   `-- subtract.cc
    |-- CMakeLists.txt
    |-- include
    |   |-- calc.h
    |   `-- sort.h
    |-- sort
    |   |-- CMakeLists.txt
    |   |-- insert.cc
    |   `-- quick.cc
    |-- test1
    |   |-- calc.cc
    |   `-- CMakeLists.txt
    `-- test2
        |-- CMakeLists.txt
        `-- sort.cc
    

    include目众:头文件目录

    calc目录:目录中的四个源文件对的加、减、乘、除算法。对应的头文件是incLude中的calc.h

    sort录:目录中的两个源文件对应的是插入排序和快速排序算法。对应的头文件是include中的sort.h

    test1目录: 测试目录,对加、减、乘,除算法进行测试

    test2目录:测试目录,对排序算法进行测试

    可以看到各个源文件目录所嵩要的CMakeLists.txt文件现在已经添加完毕了。接下来庖丁解牛,我们依次分析一下各个文件中需要添加的内容。

    众所周知,Linux的目录是树状结构,所以嵌套的CMake 也是一个树状结构,最顶层的CMakeLists.txt为根节点,其次都是子节点,因此,我们需要了解关于CMakeLists.txt文件变量作用域:

    根节点 CMakeLists.txt 中的变量是全局变量

    父节点 CMakeLists.txt 中的变量可以在子节点中使用

    子节点 CMakeLists.txt 中的变量只能在当前节点中使用

    添加子目录

    接下来我们还需要知道在CMake 中父子节点之间的关系是如何建立的,这里需要用到一个CMake命令

    add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])
    

    source_dir:指定了CMakeLists.txt 源文件和代码文件的位置,其实就是指定子目录

    binary_dir:指定了输出文件的路径,一般不需要指定,忽略即可。

    EXCLUDE FRON_ALL:在子路径下的目标默认不会被包含到父路径的 ALL目标里,并且也会被排除在IDE工程文件之外。用户必须显式构建在子路径下的目标。

    通过这种方式CMakeLists.txt文件之间的父子关系就被构建出来了。

    在上面的目录中我们要做如下事情:

    1.通过test1目录中的测试文件进行计算器相关的测试

    2.通过test2目录中的测试文件进行排序相关的测试

    现在相当于是要进行模块化测试,对于calc和 sort目录中的源文件来说,可以将它们先编译成库文件(可以是静态库也可以是动态库)然后在提供给测试文件使用即可。库文件的本质其实还是代码,只不过是从文本格式变成了二进制格式。

    对于calc 和 sort 目录的文件我们需要打包成动态库或者静态库,静态库会直接打包到可执行程序中,发布的时候发布一个可执行程序就可以了,动态库则需要需要提供动态库

    对于test1 和 test2 目录下的文件我们需要变成可执行程序

    下面我们编写根节点的 CMakeLists.txt 文件

    cmake_minimum_required(VERSION 2.8)
    project(test)
    # 定义变量
    # 静态库生成的路径
    set(LIBPATH ${PROJECT_SOURCE_DIR}/lib)
    # 可执行程序的存储目录
    set(EXECPATH ${PROJECT_SOURCE_DIR}/bin)
    # 头文件路径
    set(HEADPATH ${PROJECT_SOURCE_DIR}/include)
    
    # 库文件的名字
    set(CALCLIB calc)
    set(SORTLIB sort)
    # 可执行程序的名字
    set(APPNAME1 app1)
    set(APPNAME2 app2)
    
    # 给当前节点添加子目录
    add_subdirectory(calc)
    add_subdirectory(sort)
    add_subdirectory(test1)
    add_subdirectory(test2)
    

    calc目录下的 CMakeLists.txt 文件

    cmake_minimum_required(VERSION 2.8)
    project(calc)
    
    #搜索源文件
    aux_source_directory(./ SRC)
    # 包含头文件所在的目录
    include_directories(${HEADPATH})
    # 设置库文件生成的路径
    set(LIBRARY_OUTPUT_PATH ${LIBPATH})
    # 生成静态库
    add_library(${CALCLIB} STATIC ${SRC})
    

    sort目录下的 CMakeLists.txt 文件

    cmake_minimum_required(VERSION 2.8)
    project(sort)
    
    #搜索源文件
    aux_source_directory(./ SRC)
    # 包含头文件所在的目录
    include_directories(${HEADPATH})
    # 设置库文件生成的路径
    set(LIBRARY_OUTPUT_PATH ${LIBPATH})
    # 生成静态库
    add_library(${SORTLIB} STATIC ${SRC})
    

    test1目录下的 CMakeLists.txt 文件

    cmake_minimum_required(VERSION 2.8)
    project(test1)
    
    # 搜索源文件
    aux_source_directory(./ SRC)
    # 包含头文件所在的目录
    include_directories(${HEADPATH})
    # 指定静态库所在的目录
    link_directories(${LIBPATH})
    # 链接静态库
    link_libraries(${CALCLIB})
    # 设置可执行程序的路径
    set(EXECUTABLE_OUTPUT_PATH ${EXECPATH})
    # 生成可执行程序
    add_executable(${APPNAME1} ${SRC})
    

    test2目录下的 CMakeLists.txt 文件

    cmake_minimum_required(VERSION 2.8)
    project(test2)
    
    # 搜索源文件
    aux_source_directory(./ SRC)
    # 包含头文件所在的目录
    include_directories(${HEADPATH})
    # 指定静态库所在的目录
    link_directories(${LIBPATH})
    # 链接静态库
    link_libraries(${SORTLIB})
    # 设置可执行程序的路径
    set(EXECUTABLE_OUTPUT_PATH ${EXECPATH})
    # 生成可执行程序
    add_executable(${APPNAME2} ${SRC})
    

    我们可以在根节点 CMakeLists.txt 文件所在的目录执行 cmake,也可以构建一个build目录(构建目录),在build目录下执行 cmake …

    这样我们就完成了项目的编译:

    在这里插入图片描述

    2.在静态库中链接静态库

    下面们完成如下程序:sort中的静态库引用calc的静态库

    我们删除test1目录,在insert.cc中使用calc.h中的加法函数

    calc目录的 CMakeLists.txt 文件(和上文的一样)

    cmake_minimum_required(VERSION 2.8)
    project(calc)
    
    #搜索源文件
    aux_source_directory(./ SRC)
    # 包含头文件所在的目录
    include_directories(${HEADPATH})
    # 设置库文件生成的路径
    set(LIBRARY_OUTPUT_PATH ${LIBPATH})
    # 生成静态库
    add_library(${CALCLIB} STATIC ${SRC})
    

    sort目录的 CMakeLists.txt 文件(需要链接calc库)

    cmake_minimum_required(VERSION 2.8)
    project(sort)
    
    #搜索源文件
    aux_source_directory(./ SRC)
    # 包含头文件所在的目录
    include_directories(${HEADPATH})
    # 链接calc库
    link_libraries(${CALCLIB})
    link_directories(${LIBPATH})
    # 设置库文件生成的路径
    set(LIBRARY_OUTPUT_PATH ${LIBPATH})
    # 生成静态库
    add_library(${SORTLIB} STATIC ${SRC})
    

    test目录的 CMakeLists.txt 文件(因为sort中已经链接好了calc库,所以不需要链接calc库)

    cmake_minimum_required(VERSION 2.8)
    project(test2)
    
    # 搜索源文件
    aux_source_directory(./ SRC)
    # 包含头文件所在的目录
    include_directories(${HEADPATH})
    # 指定静态库所在的目录
    link_directories(${LIBPATH})
    # 链接静态库
    link_libraries(${SORTLIB})
    # 设置可执行程序的路径
    set(EXECUTABLE_OUTPUT_PATH ${EXECPATH})
    # 生成可执行程序
    add_executable(${APPNAME2} ${SRC})
    

    在这里插入图片描述

    3.在静态库中连接动态库

    下面我们将calc中的文件编译成动态库,然后再sort里面的静态库来进行链接,再给test进行使用

    calc目录的 CMakeLists.txt 文件(生成动态库)

    cmake_minimum_required(VERSION 2.8)
    project(calc)
    
    #搜索源文件
    aux_source_directory(./ SRC)
    # 包含头文件所在的目录
    include_directories(${HEADPATH})
    # 设置库文件生成的路径
    set(LIBRARY_OUTPUT_PATH ${LIBPATH})
    # 生成静态库
    add_library(${CALCLIB} SHARED ${SRC})
    

    sort目录的 CMakeLists.txt 文件(链接动态库)

    cmake_minimum_required(VERSION 2.8)
    project(sort)
    
    #搜索源文件
    aux_source_directory(./ SRC)
    # 包含头文件所在的目录
    include_directories(${HEADPATH})
    # 链接calc库
    link_directories(${LIBPATH})
    # 设置库文件生成的路径
    set(LIBRARY_OUTPUT_PATH ${LIBPATH})
    # 生成静态库
    add_library(${SORTLIB} STATIC ${SRC})
    # 链接动态库
    target_link_libraries(${SORTLIB} ${CALCLIB})
    

    在这里插入图片描述

    libsort.a加载到了app2中,但是libcalc.so没有加载到app2中,这是因为动态库是只有在调用其中的函数的时候才会加载到内存中。

  • 相关阅读:
    JVM常见面试题
    Redis学习(三)——事务、乐观锁、持久化、发布订阅
    2023年十款开源测试开发工具推荐(自动化、性能、造数据、流量复制)
    博客园众包平台:嵌入式开发任务悬赏5万,招募开发者接单
    SpringBoot 访问接口记录日志的保存和定时清理日志
    Vue3+element-plus日期选择器 el-date-picker 设置可选最小时间方法
    Glide:DecodeJob
    淘宝API商品详情测试工具,返回数据说明
    Jmeter---非GUI命令行的执行生成报告、使用ant插件执行接口测试脚本生成报告
    C++ map用法总结
  • 原文地址:https://blog.csdn.net/qq_67582098/article/details/139362793