zoukankan      html  css  js  c++  java
  • cmake教程(转载)

    转载:https://blog.csdn.net/whahu1989/article/details/82078563

    CMake是开源、跨平台的构建工具,可以让我们通过编写简单的配置文件去生成本地的Makefile,这个配置文件是独立于运行平台和编译器的,这样就不用亲自去编写Makefile了,而且配置文件可以直接拿到其它平台上使用,无需修改,非常方便。

    本文主要讲述在Linux下如何使用CMake来编译我们的程序。

    一 、安装CMake

    本文使用ubuntu18.04,安装cmake使用如下命令,
    sudo apt install cmake
    安装完成后,在终端下输入cmake -version查看cmake版本,

    这样cmake就安装好了。

    二 、简单样例

    首先让我们从最简单的代码入手,先来体验下cmake是如何操作的。编写main.c,如下,

    #include <stdio.h>
    
    int main(void)
    {
    printf("Hello World\n");
    
    return 0;
    }

    然后在main.c相同目录下编写CMakeLists.txt,内容如下,

    cmake_minimum_required (VERSION 2.8)
    
    project (demo)
    
    add_executable(main main.c)

    第一行意思是表示cmake的最低版本要求是2.8,我们安装的是3.10.2;第二行是表示本工程信息,也就是工程名叫demo;第三行比较关键,表示最终要生成的elf文件的名字叫main,使用的源文件是main.c
    在终端下切到main.c所在的目录下,然后输入以下命令运行cmake,
    cmake .
    会输出如下信息,

    再来看看目录下的文件,

    可以看到成功生成了Makefile,还有一些cmake运行时自动生成的文件。
    然后在终端下输入make并回车,

    可以看到执行cmake生成的Makefile可以显示进度,并带颜色。再看下目录下的文件,

    可以看到我们需要的elf文件main也成功生成了,然后运行main,

    运行成功!

    PS: 如果想重新生成main,输入make clean就可以删除main这个elf文件。

    三 、同一目录下多个源文件

    接下来进入稍微复杂的例子:在同一个目录下有多个源文件。
    在之前的目录下添加2个文件,testFunc.c和testFunc.h。添加完后整体文件结构如下,

    testFunc.c内容如下,

    /*
    ** testFunc.c
    */
    
    #include <stdio.h>
    #include "testFunc.h"
    
    void func(int data)
    {
    printf("data is %d\n", data);
    }

    testFunc.h内容如下,

    /*
    ** testFunc.h
    */
    
    #ifndef _TEST_FUNC_H_
    #define _TEST_FUNC_H_
    
    void func(int data);
    
    #endif

    修改main.c,调用testFunc.h里声明的函数func(),

    #include <stdio.h>
    
    #include "testFunc.h"
    
    int main(void)
    {
    func(100);
    
    return 0;
    }

    修改CMakeLists.txt,在add_executable的参数里把testFunc.c加进来

    cmake_minimum_required (VERSION 2.8)
    
    project (demo)
    
    add_executable(main main.c testFunc.c)

    然后重新执行cmake生成Makefile并运行make,

    然后运行重新生成的elf文件main,

    运行成功!

    可以类推,如果在同一目录下有多个源文件,那么只要在add_executable里把所有源文件都添加进去就可以了。但是如果有一百个源文件,再这样做就有点坑了,无法体现cmake的优越性,cmake提供了一个命令可以把指定目录下所有的源文件存储在一个变量中,这个命令就是 aux_source_directory(dir var)。
    第一个参数dir是指定目录,第二个参数var是用于存放源文件列表的变量。

    我们在main.c所在目录下再添加2个文件,testFunc1.c和testFunc1.h。添加完后整体文件结构如下,

    testFunc1.c如下,

    /*
    ** testFunc1.c
    */
    
    #include <stdio.h>
    #include "testFunc1.h"
    
    void func1(int data)
    {
    printf("data is %d\n", data);
    }
    
    
    
    /* testFunc1.h如下,
    ** testFunc1.h
    */
    
    #ifndef _TEST_FUNC1_H_
    #define _TEST_FUNC1_H_
    
    void func1(int data);
    
    #endif


    再修改main.c,调用testFunc1.h里声明的函数func1(),

    #include <stdio.h>
    
    #include "testFunc.h"
    #include "testFunc1.h"
    
    int main(void)
    {
    func(100);
    func1(200);
    
    return 0;
    }

    修改CMakeLists.txt,

    cmake_minimum_required (VERSION 2.8)
    
    project (demo)
    
    aux_source_directory(. SRC_LIST)
    
    add_executable(main ${SRC_LIST})

    使用aux_source_directory把当前目录下的源文件存列表存放到变量SRC_LIST里,然后在add_executable里调用SRC_LIST(注意调用变量时的写法)。
    再次执行cmake和make,并运行main,

    可以看到运行成功了。

    aux_source_directory()也存在弊端,它会把指定目录下的所有源文件都加进来,可能会加入一些我们不需要的文件,此时我们可以使用set命令去新建变量来存放需要的源文件,如下,

    cmake_minimum_required (VERSION 2.8)
    
    project (demo)
    
    set( SRC_LIST
    ./main.c
    ./testFunc1.c
    ./testFunc.c)
    
    add_executable(main ${SRC_LIST})

    四 、不同目录下多个源文件

    一般来说,当程序文件比较多时,我们会进行分类管理,把代码根据功能放在不同的目录下,这样方便查找。那么这种情况下如何编写CMakeLists.txt呢?
    我们把之前的源文件整理一下(新建2个目录test_func和test_func1),整理好后整体文件结构如下,

    把之前的testFunc.c和testFunc.h放到test_func目录下,testFunc1.c和testFunc1.h则放到test_func1目录下。

    其中,CMakeLists.txt和main.c在同一目录下,内容修改成如下所示,

    cmake_minimum_required (VERSION 2.8)
    
    project (demo)
    
    include_directories (test_func test_func1)
    
    aux_source_directory (test_func SRC_LIST)
    aux_source_directory (test_func1 SRC_LIST1)
    
    add_executable (main main.c ${SRC_LIST} ${SRC_LIST1})

    这里出现了一个新的命令:include_directories。该命令是用来向工程添加多个指定头文件的搜索路径,路径之间用空格分隔。
    因为main.c里include了testFunc.h和testFunc1.h,如果没有这个命令来指定头文件所在位置,就会无法编译。当然,也可以在main.c里使用include来指定路径,如下

    #include "test_func/testFunc.h"
    #include "test_func1/testFunc1.h"

    只是这种写法不好看。
    另外,我们使用了2次aux_source_directory,因为源文件分布在2个目录下,所以添加2次。

    五 、正规一点的组织结构

    正规一点来说,一般会把源文件放到src目录下,把头文件放入到include文件下,生成的对象文件放入到build目录下,最终输出的elf文件会放到bin目录下,这样整个结构更加清晰。让我们把前面的文件再次重新组织下,

    我们在最外层目录下新建一个CMakeLists.txt,内容如下,

    cmake_minimum_required (VERSION 2.8)
    
    project (demo)
    
    add_subdirectory (src)

    这里出现一个新的命令add_subdirectory(),这个命令可以向当前工程添加存放源文件的子目录,并可以指定中间二进制和目标二进制的存放位置,具体用法可以百度。
    这里指定src目录下存放了源文件,当执行cmake时,就会进入src目录下去找src目录下的CMakeLists.txt,所以在src目录下也建立一个CMakeLists.txt,内容如下,

    aux_source_directory (. SRC_LIST)
    
    include_directories (../include)
    
    add_executable (main ${SRC_LIST})
    
    set (EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)

    这里又出现一个新的命令set,是用于定义变量的,EXECUTABLE_OUT_PATH和PROJECT_SOURCE_DIR是CMake自带的预定义变量,其意义如下,

    EXECUTABLE_OUTPUT_PATH :目标二进制可执行文件的存放位置
    PROJECT_SOURCE_DIR:工程的根目录
    所以,这里set的意思是把存放elf文件的位置设置为工程根目录下的bin目录。(cmake有很多预定义变量,详细的可以网上搜索一下)

    添加好以上这2个CMakeLists.txt后,整体文件结构如下,

    下面来运行cmake,不过这次先让我们切到build目录下,然后输入以下命令,
    cmake ..
    Makefile会在build目录下生成,然后在build目录下运行make,

    运行ok,我们再切到bin目录下,发现main已经生成,并运行测试,

    测试OK!

    这里解释一下为什么在build目录下运行cmake?从前面几个case中可以看到,如果不这样做,cmake运行时生成的附带文件就会跟源码文件混在一起,这样会对程序的目录结构造成污染,而在build目录下运行cmake,生成的附带文件就只会待在build目录下,如果我们不想要这些文件了就可以直接清空build目录,非常方便。

    另外一种写法:
    前面的工程使用了2个CMakeLists.txt,最外层的CMakeLists.txt用于掌控全局,使用add_subdirectory来控制其它目录下的CMakeLists.txt的运行。

    上面的例子也可以只使用一个CMakeLists.txt,把最外层的CMakeLists.txt内容改成如下,

    cmake_minimum_required (VERSION 2.8)
    
    project (demo)
    
    set (EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)
    
    aux_source_directory (src SRC_LIST)
    
    include_directories (include)
    
    add_executable (main ${SRC_LIST})

    同时,还要把src目录下的CMakeLists.txt删除。

    六 、动态库和静态库的编译控制

    有时只需要编译出动态库和静态库,然后等着让其它程序去使用。让我们看下这种情况该如何使用cmake。首先按照如下重新组织文件,只留下testFunc.h和TestFunc.c,


    我们会在build目录下运行cmake,并把生成的库文件存放到lib目录下。
    CMakeLists.txt内容如下,

    cmake_minimum_required (VERSION 3.5)
    
    project (demo)
    
    set (SRC_LIST ${PROJECT_SOURCE_DIR}/testFunc/testFunc.c)
    
    add_library (testFunc_shared SHARED ${SRC_LIST})
    add_library (testFunc_static STATIC ${SRC_LIST})
    
    set_target_properties (testFunc_shared PROPERTIES OUTPUT_NAME "testFunc")
    set_target_properties (testFunc_static PROPERTIES OUTPUT_NAME "testFunc")
    
    set (LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)

    这里又出现了新的命令和预定义变量,

    add_library: 生成动态库或静态库(第1个参数指定库的名字;第2个参数决定是动态还是静态,如果没有就默认静态;第3个参数指定生成库的源文件)
    set_target_properties: 设置最终生成的库的名称,还有其它功能,如设置库的版本号等等
    LIBRARY_OUTPUT_PATH: 库文件的默认输出路径,这里设置为工程目录下的lib目录
    好了,让我们进入build目录下运行cmake ..,成功后再运行make,

    cd到lib目录下进行查看,发现已经成功生成了动态库和静态库,

    PS:前面使用set_target_properties重新定义了库的输出名称,如果不使用set_target_properties也可以,那么库的名称就是add_library里定义的名称,只是连续2次使用add_library指定库名称时(第一个参数),这个名称不能相同,而set_target_properties可以把名称设置为相同,只是最终生成的库文件后缀不同(一个是.so,一个是.a),这样相对来说会好看点。

    七 对库进行链接

    既然我们已经生成了库,那么就进行链接测试下。重新建一个工程目录,然后把上节生成的库拷贝过来,然后在在工程目录下新建src目录和bin目录,在src目录下添加一个main.c,整体结构如下,


    main.c内容如下,

    #include <stdio.h>
    
    #include "testFunc.h"
    
    int main(void)
    {
    func(100);
    
    return 0;
    }


    工程目录下的CMakeLists.txt内容如下,

    cmake_minimum_required (VERSION 3.5)
    
    project (demo)
    
    
    set (EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)
    
    set (SRC_LIST ${PROJECT_SOURCE_DIR}/src/main.c)
    
    # find testFunc.h
    include_directories (${PROJECT_SOURCE_DIR}/testFunc/inc)
    
    find_library(TESTFUNC_LIB testFunc HINTS ${PROJECT_SOURCE_DIR}/testFunc/lib)
    
    add_executable (main ${SRC_LIST})
    
    target_link_libraries (main ${TESTFUNC_LIB})

    这里出现2个新的命令,

    find_library: 在指定目录下查找指定库,并把库的绝对路径存放到变量里,其第一个参数是变量名称,第二个参数是库名称,第三个参数是HINTS,第4个参数是路径,其它用法可以参考cmake文档
    target_link_libraries: 把目标文件与库文件进行链接
    使用find_library的好处是在执行cmake ..时就会去查找库是否存在,这样可以提前发现错误,不用等到链接时。

    cd到build目录下,然后运行cmake .. && make,最后进入到bin目录下查看,发现main已经生成,运行之,

    运行成功!

    ps:在lib目录下有testFunc的静态库和动态库,find_library(TESTFUNC_LIB testFunc ...默认是查找动态库,如果想直接指定使用动态库还是静态库,可以写成find_library(TESTFUNC_LIB libtestFunc.so ...或者find_library(TESTFUNC_LIB libtestFunc.a ...

    ps: 查看elf文件使用了哪些库,可以使用readelf -d ./xx来查看

    之前本节教程使用的是库查找方法是link_directories,但是很多读者反映运行时有问题,本人去官方文档上查了下,发现不建议使用了,推荐使用find_library或者find_package


    八 、添加编译选项


    有时编译程序时想添加一些编译选项,如-Wall,-std=c++11等,就可以使用add_compile_options来进行操作。
    这里以一个简单程序来做演示,main.cpp如下

    #include <iostream>
    
    int main(void)
    {
    auto data = 100;
    std::cout << "data: " << data << "\n";
    return 0;
    }

    CMakeLists.txt内容如下,

    cmake_minimum_required (VERSION 2.8)
    
    project (demo)
    
    set (EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)
    
    add_compile_options(-std=c++11 -Wall)
    
    add_executable(main main.cpp)

    整体目录结构如下,

    然后cd到build目录下,执行cmake .. && make命令,就可以在bin目录下得到main的elf文件

    九、 添加控制选项


    有时希望在编译代码时只编译一些指定的源码,可以使用cmake的option命令,主要遇到的情况分为2种:

    本来要生成多个bin或库文件,现在只想生成部分指定的bin或库文件
    对于同一个bin文件,只想编译其中部分代码(使用宏来控制)
    第1种情况
    假设我们现在的工程会生成2个bin文件,main1和main2,现在整体结构体如下,

    外层的CMakeLists.txt内容如下,

    cmake_minimum_required(VERSION 3.5)
    
    project(demo)
    
    option(MYDEBUG "enable debug compilation" OFF)
    
    set (EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)
    
    add_subdirectory(src)

    这里使用了option命令,其第一个参数是这个option的名字,第二个参数是字符串,用来描述这个option是来干嘛的,第三个是option的值,ON或OFF,也可以不写,不写就是默认OFF。

    然后编写src目录下的CMakeLists.txt,如下

    cmake_minimum_required (VERSION 3.5)
    
    add_executable(main1 main1.c)
    
    if (MYDEBUG)
    add_executable(main2 main2.c)
    else()
    message(STATUS "Currently is not in debug mode") 
    endif()

    注意,这里使用了if-else来根据option来决定是否编译main2.c
    其中main1.c和main2.c的内容如下,

    // main1.c
    #include <stdio.h>
    
    int main(void)
    {
    printf("hello, this main1\n");
    
    return 0;
    }
    
    // main2.c
    #include <stdio.h>
    
    int main(void)
    {
    printf("hello, this main2\n");
    
    return 0;
    }

    然后cd到build目录下输入cmake .. && make就可以只编译出main1,如果想编译出main2,就把MYDEBUG设置为ON,再次输入cmake .. && make重新编译。

    每次想改变MYDEBUG时都需要去修改CMakeLists.txt,有点麻烦,其实可以通过cmake的命令行去操作,例如我们想把MYDEBUG设置为OFF,先cd到build目录,然后输入cmake .. -DMYDEBUG=ON,这样就可以编译出main1和main2 (在bin目录下)


    第2种情况
    假设我们有个main.c,其内容如下,

    #include <stdio.h>
    
    int main(void)
    {
    #ifdef WWW1
    printf("hello world1\n");
    #endif
    
    #ifdef WWW2 
    printf("hello world2\n");
    #endif
    
    return 0;
    }

    可以通过定义宏来控制打印的信息,我们CMakeLists.txt内容如下,

    cmake_minimum_required(VERSION 3.5)
    
    project(demo)
    
    set (EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)
    
    option(WWW1 "print one message" OFF)
    option(WWW2 "print another message" OFF)
    
    if (WWW1)
    add_definitions(-DWWW1)
    endif()
    
    if (WWW2)
    add_definitions(-DWWW2)
    endif()
    
    add_executable(main main.c)

    这里把option的名字保持和main.c里的宏名称一致,这样更加直观,也可以选择不同的名字。通过与add_definitions()的配合,就可以控制单个bin文件的打印输出了。

    整体工程结构如下,

    cd到build目录下执行cmake .. && make,然后到bin目录下执行./main,可以看到打印为空,
    接着分别按照下面指令去执行,然后查看打印效果,

    cmake .. -DWWW1=ON -DWWW2=OFF && make
    cmake .. -DWWW1=OFF -DWWW2=ON && make
    cmake .. -DWWW1=ON -DWWW2=ON && make


    这里有个小坑要注意下:假设有2个options叫A和B,先调用cmake设置了A,下次再调用cmake去设置B,如果没有删除上次执行cmake时产生的缓存文件,那么这次虽然没设置A,也会默认使用A上次的option值。

    所以如果option有变化,要么删除上次执行cmake时产生的缓存文件,要么把所有的option都显式的指定其值。

    十、总结


    以上是自己学习CMake的一点学习记录,通过简单的例子让大家入门CMake,学习的同时也阅读了很多网友的博客。CMake的知识点还有很多,具体详情可以在网上搜索。总之,CMake可以让我们不用去编写复杂的Makefile,并且跨平台,是个非常强大并值得一学的工具。

    如果有写的不对的地方,希望能留言指正,谢谢阅读。


    ————————————————
    版权声明:本文为CSDN博主「爱就是恒久忍耐」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/whahu1989/article/details/82078563

  • 相关阅读:
    系统维护相关问题
    Python环境维护
    哈希表解决字符串问题
    论文笔记二:《A Tutoral on Spectral Clustering》
    论文笔记之哈希学习比较--《Supervised Hashing with Kernels》《Towards Optimal Binary Code Learning via Ordinal Embedding》《Top Rank Supervised Binary Coding for Visual Search》
    Java中String、StringBuffer、StringBuilder的比较与源 代码分析
    浙大pat1040 Longest Symmetric String(25 分)
    浙大pat1039 Course List for Student(25 分)
    浙大pat---1036 Boys vs Girls (25)
    百炼oj-4151:电影节
  • 原文地址:https://www.cnblogs.com/wangbin2188/p/15769617.html
Copyright © 2011-2022 走看看