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
具體目錄結構如下:
- 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} #目標:tcusubfunc # 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" //應用層頭文件2int main(int argc, char *argv[])
{func1(); //調用上層func1func2(); //調用上層func2return 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編譯過程中生成的各種中間文件、庫、最終目標文件等:
cd build
cmake ..
make
在cmake編譯的過程中發現了一個問題,就是一開始執行cmake . .時候,并沒有完成頭文件變量的傳遞,導致make編譯出錯。執行結果如下:
make,結果如下:
可以看到,由于SUB_SUB_INCLUDE_DIR變量并沒有傳遞到subfunc層的CMakeLists.txt,從而導致了在subfunc層并沒有包含全部發頭文件路徑,導致編譯的時候找不到subsubfunc.hpp。
解決方案:
經過反復測試發現,多執行幾次cmake…(三次以上)就可以解決這個問題,猜測是因為多次執行cmake的過程完成了SUB_SUB_INCLUDE_DIR和SUB_INCLUDE_DIR變量向上層的傳遞:
四、build目錄分析
- unit-test作為獨立的子目錄,生成了獨立的目標文件unit-test;
- subfunc下生成了libsubfunc.a的靜態庫文件,在subsubfunc下生成了libsubsubfunc.a的靜態庫文件,這兩個庫文件供最上層CMakeLists.txt調用,并最終生成一個目標文件cmaketest;
五、程序執行結果
六、總結
1. 一種是獨立的unit-test生成獨立的目標文件,與主CMakeLists.txt僅有一個調用與被調用的關系,并不存在任何的編譯依關系;2. 另一種多級CMakeLists.txt調用之間存在上下級的依賴關系,下層的源代碼給上層的調用提供支持(以生成靜態庫的方式),這里進行了分層設計。3. 下層的頭文件路徑僅傳遞給調用的上一層而不會傳遞給最上層,這種變量傳遞的方式提高了代碼的分層性,這里需要注意變量的設置(CACHE INTERNAL屬性的必須添加)以完成下層到上層的變量傳遞,上述的兩種分層CMakeLists.txt調用方式可覆蓋基本的全部開發場景。
七、未完待續
下章將繼續介紹C++相關的工程能力。歡迎關注知乎:北京不北歡迎關注douyin:near.X (北京不北)歡迎+V:beijing_bubei獲得免費答疑,長期技術交流。
八、參考文獻
,這里進行了分層設計。
3. 下層的頭文件路徑僅傳遞給調用的上一層而不會傳遞給最上層,這種變量傳遞的方式提高了代碼的分層性,這里需要注意變量的設置(CACHE INTERNAL屬性的必須添加)以完成下層到上層的變量傳遞,上述的兩種分層CMakeLists.txt調用方式可覆蓋基本的全部開發場景。
七、未完待續
下章將繼續介紹C++相關的工程能力。歡迎關注知乎:北京不北歡迎關注douyin:near.X (北京不北)歡迎+V:beijing_bubei獲得免費答疑,長期技術交流。
八、參考文獻
https://blog.csdn.net/weixin_42700740/article/details/126364574