C++最佳实践之工程编译

C++最佳实践之工程编译

在大型c/c++工程开发中,往往会涉及多级CMakeLists.txt的调用,并且调用方式错综复杂,主要有以下两种方式:

1. 子目录中的CMakeList.txt独立生成目标,不作为主目标生成过程的依赖关系(比如dev_tool、driver、ut_test等),与主目标并无任何关系。
 
2. 子目录中的CMakeList.txt作为主目标的依赖源文件,不单独生成目标,作为主目标生成过程主的部分源文件,通常以生成.a源文件的方式提供给主CMakeList.txt使用。

一、工程目录结构

下面给出了测试工程目录,进行了两项测试:

  • unit—test目录作为独立生成目标,其CMakeLists.txt在主CMakeList.txt主被调用;

  • subfunc和subsubfunc作为主CMakeList.txt向下两级的依赖,为主CMakeList.txt提供源文件支持,其CMakeLists.txt为逐级调用的方式:
    CMakeList.txt->subfunc/CMakeList.txt->subfunc/subfuncfunc/CMakeLists.txt

具体目录结构如下:


1708667424747.png
  • build: 编译目录,生成的目标执行文件、静态库、中间缓存文件都在此处
  • inc:主头文件目录
  • src:主源文件目录
  • subfunc:依赖的一级子目录
  • uinit-test:单元测试目录,独立生成的目标文件
  • CMakeList.txt:最上层,主CMakeList.txt

二、工程源代码

2.1、工程源代码

cmake_minimum_required(VERSION 3.8)       # cmake 版本
PROJECT(cmaketest)                        # 工程名

#set project name
set(PROJECT_NAME cmaketest)               # 设置工程名字

set(CMAKE_CXX_STANDARD 17)                # 设置C++标准为C++17
set(CMAKE_CXX_STANDARD_REQUIRED ON)       

# 设置本地头文件路径,注意:子目录的头文件是通过target_include_directories添加到${PROJECT_NAME}中
INCLUDE_DIRECTORIES(
    inc                                   #上层头文件
    ${SUB_INCLUDE_DIR}                    #下级头文件
)

#将源文件路径添加到变量src_list中
AUX_SOURCE_DIRECTORY(.          SRC_LIST)
AUX_SOURCE_DIRECTORY(src        SRC_LIST)

# 7.生成目标(可执行文件):cmaketest
ADD_EXECUTABLE(${PROJECT_NAME} ${SRC_LIST})

# 8.设置编译时依赖的subfunc静态库
target_link_libraries(${PROJECT_NAME}    #目标:tcu
    subfunc        # sub子目录下的静态库文件
    subsubfunc     # subsub子目录下的静态库文件
)

# 9.添加子目录,这样子目录中的CMakeLists.txt才会被调用

# 调用subfunc子目录中的CMakeLists.txt,生成静态库而不生成新目标,目标与主CMakeLists.txt中设定的一致
add_subdirectory(subfunc) 
# 调用unit-test子目录中的CMakeLists.txt,生成新目标,目标与主CMakeLists.txt中设定的无关,仅仅是调用
add_subdirectory(unit-test)

注意

include_directories包含的头文件路径可以被各级子目录中的目标所引用;

target_include_directories包含的头文件只能被特定目标使用;

采用变量传递的方式(${sub_include_dir}引入子路径)引入子目录的头文件路径

cmakettest/main.cpp:

#include <iostream>
#include <string>
#include "func1.hpp"   //应用层头文件1
#include "func2.hpp"   //应用层头文件2

int main(int argc, char *argv[])
{
    func1();          //调用上层func1
    func2();          //调用上层func2
    return 0;
}

cmakettest/inc/func1.hpp:

#ifndef __FUNC1_HPP__
#define __FUNC1_HPP__

int func1(void);

#endif

cmakettest/inc/func2.hpp:

#ifndef __FUNC2_HPP__
#define __FUNC2_HPP__

int func2(void);

#endif

cmakettest/src/func1.cpp:

#include "subfunc.hpp"   //subfunc头文件
#include "func1.hpp"     //应用层头文件1
#include <iostream>
#include <string>

int func1(void)
{
    std::cout<<"------------func1函数调用开始----------"<<std::endl;
    subfunc1();
    std::cout<<"------------func1函数调用结束----------"<<std::endl<<std::endl;
    return 0;
}

cmakettest/src/func2.cpp:

#include "subfunc.hpp"   //subfunc头文件
#include "func2.hpp"     //应用层头文件1
#include <iostream>
#include <string>

int func2(void)
{
    std::cout<<"------------func2函数调用开始----------"<<std::endl;
    subfunc2();
    std::cout<<"------------func2函数调用结束----------"<<std::endl;
    return 0;
}

2.2、subfunc以及subsubfunc子目录

cmaketest/subfunc/CMakeLists.txt

# 1.将本目录下的所有.c 文件添加到SUB_DIR_LIB_SRCS变量
AUX_SOURCE_DIRECTORY(. SUB_DIR_SRC_LIST)

# 2.设置当前的头文件路径
set(SUB_INCLUDE_DIR 
    ${CMAKE_CURRENT_SOURCE_DIR}          # 当前源文件路径
    ${SUB_SUB_INCLUDE_DIR}               # 由下层subsubfunc目录传递的头文件路径
    CACHE INTERNAL "subfunc include dir" # 这个字符串相当于对变量SUB_INCLUDE_DIR的描述说明,不能省略,但可以自己随便定义,只有添加了这个描述SUB_INCLUDE_DIR变量才能被上层CMakeLists.txt调用!!!
)

MESSAGE(STATUS "subfunc层头文件路径 :${SUB_INCLUDE_DIR}")

# 3.生成静态库
add_library(subfunc ${SUB_DIR_SRC_LIST})

# 4.添加subsubfunc子目录,这样子目录中的CMakeLists.txt才会被调用
add_subdirectory(subsubfunc)

cmaketest/subfunc/subfunc.hpp:

#ifndef __SUB_FUNC_HPP__
#define __SUB_FUNC_HPP__

int subfunc1(void);
int subfunc2(void);

#endif

cmaketest/subfunc/subfunc.cpp:

#include "subfunc.hpp"
#include "subsubfunc.hpp"
#include <iostream>
#include <string>

int subfunc1(void)
{
    std::cout<<"------subfunc1函数调用开始------"<<std::endl;
    /* 中间调用subsubfunc1函数 */
subsubfunc1();
    std::cout<<"------subfunc1函数调用结束------"<<std::endl;
    return 0;
}
int subfunc2(void)
{
    std::cout<<"------subfunc2函数调用开始------"<<std::endl;
subsubfunc2();

    /* 中间调用subsubfunc2函数 */

    std::cout<<"------subfunc2函数调用结束------"<<std::endl;
    return 0;
}

cmaketest/subfunc/subsubfunc/CMakeLists.txt

# 1.将本目录下的所有.c 文件添加到SUB_DIR_LIB_SRCS变量
AUX_SOURCE_DIRECTORY(. SUB_SUB_DIR_SRC_LIST)

# 2.设置当前的头文件路径
set(SUB_SUB_INCLUDE_DIR 
    ${CMAKE_CURRENT_SOURCE_DIR}              # 当前源文件路径
    CACHE INTERNAL "subsubfunc include dir"  # 这个字符串相当于对变量SUB_SUB_INCLUDE_DIR的描述说明,不能省略,但可以自己随便定义,只有添加了这个描述SUB_SUB_INCLUDE_DIR变量才能被上层CMakeLists.txt调用!!!
)

MESSAGE(STATUS "subsubfunc层头文件路径 :${SUB_SUB_INCLUDE_DIR}")

# 3.生成静态库
add_library(subsubfunc ${SUB_SUB_DIR_SRC_LIST})

分析

1. 头文件目录变量的逐级向上传递,通过设置sub_sub_include_dir变量(必须包含cache internel “subsubfunc include dir”描述),将subsubfunc下的头文件路径传递给了上级subfunc的CMakeList.txt;

2. 通过设置SUB_INCLUDE_DIR变量(必须添加CACHE INTERNAL "subfunc include dir"描述),将subfunc文件下的头文件路径(包含之前获得的${SUB_SUB_INCLUDE_DIR})传递给了最上层文件下的CMakeLists.txt。

2.3、UT子目录

cmaketest/unit-test/CMakeLists.txt

add_executable(unit-test unit-test.cpp)
target_link_libraries(unit-test boost_system pthread)

cmaketest/unit-test/unit-test.cpp

#include <boost/asio.hpp> 
#include <iostream>

int main(int argc,char* argv[])
{
    std::cout<<"unit-test代码调用!!!"<<std::endl;
    return 0;
}

三、编译与实践

在项目路径下创建/build文件夹,用于存放cmake编译过程中生成的各种中间文件、库、最终目标文件等:


1708673843226.png
cd build
cmake ..
make

在cmake编译的过程中发现了一个问题,就是一开始执行cmake . .时候,并没有完成头文件变量的传递,导致make编译出错。执行结果如下:

1708673932571.png

make,结果如下:

1708674003451.png

可以看到,由于SUB_SUB_INCLUDE_DIR变量并没有传递到subfunc层的CMakeLists.txt,从而导致了在subfunc层并没有包含全部发头文件路径,导致编译的时候找不到subsubfunc.hpp。

解决方案:

经过反复测试发现,多执行几次cmake..(三次以上)就可以解决这个问题,猜测是因为多次执行cmake的过程完成了SUB_SUB_INCLUDE_DIR和SUB_INCLUDE_DIR变量向上层的传递:


1708674140937.png

四、build目录分析

1708674302060.png
  • unit-test作为独立的子目录,生成了独立的目标文件unit-test;
  • subfunc下生成了libsubfunc.a的静态库文件,在subsubfunc下生成了libsubsubfunc.a的静态库文件,这两个库文件供最上层CMakeLists.txt调用,并最终生成一个目标文件cmaketest;

五、程序执行结果

1708674379390.png

六、总结

1. 一种是独立的unit-test生成独立的目标文件,与主CMakeLists.txt仅有一个调用与被调用的关系,并不存在任何的编译依关系;

2. 另一种多级CMakeLists.txt调用之间存在上下级的依赖关系,下层的源代码给上层的调用提供支持(以生成静态库的方式),这里进行了分层设计。

3. 下层的头文件路径仅传递给调用的上一层而不会传递给最上层,这种变量传递的方式提高了代码的分层性,这里需要注意变量的设置(CACHE INTERNAL属性的必须添加)以完成下层到上层的变量传递,上述的两种分层CMakeLists.txt调用方式可覆盖基本的全部开发场景。

七、未完待续

下章将继续介绍C++相关的工程能力。

欢迎关注知乎:北京不北

欢迎关注douyin:near.X (北京不北)

欢迎+V:beijing_bubei

获得免费答疑,长期技术交流。

八、参考文献

https://blog.csdn.net/weixin_42700740/article/details/126364574

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,509评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,806评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,875评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,441评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,488评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,365评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,190评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,062评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,500评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,706评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,834评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,559评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,167评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,779评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,912评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,958评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,779评论 2 354

推荐阅读更多精彩内容