淺談VScode中多文件項目的編譯


淺談VScode中多文件項目的編譯

Author: Clivia Du

Completed Time:2021-9-20

First Review: 2021-11-28(美化、把圖片上傳了一下)

當我們想要實現一個大型的project,就會涉及多文件項目的編譯技術,在我們的機器人比賽中以及后續的碼農生涯中都是很必要的一環。下面就讓我們來看一看怎樣在VScode中實現多文件項目的編譯。

這里介紹三種實現方式:

1 逐條命令行

VScode與其他IDE不同的地方在於,VScode是一個可以用命令行控制程序的編輯器(事實上在配置VScode時安裝的各種插件就是一系列命令行功能的集合),對於熟悉ROS、Linux文件環境的各位來說,這種方式再親切不過。

我將以例子來具體說明。

首先,請在一個預置的文件夾下新建三個文件。

 1 // hello.h
 2 #ifndef HELLO_H
 3 #define HELLO_H
 4  5 #include <stdio.h>
 6  7 extern void greet(const char *name);
 8  9 #endif // HELLO_H
10 11 // hello.c
12 #include "hello.h"
13 14 void greet(const char *name)
15 {
16     printf("Hello, %s!\n", name);
17 }
18 19 // main.c
20 #include "hello.h"
21 22 int main(){
23     greet("各位大佬!");
24 }

這三個文件的邏輯是怎樣的呢?

首先我們在hello.h文件中聲明了greet函數,但是並沒有實現它。我們在hello.c文件中實現greet函數,在main.c函數中使用了該函數。

怎么編譯這個項目呢?

依次在界面下方的Terminal使用如下三個命令行。

1 gcc -c hello.c -o hello.o
2 gcc -c main.c -o main.o
3 gcc main.o hello.o -o main.exe

附注:像在matlab中一樣,clear可以幫助清除當前命令行中內容。

然后就可以運行程序main.exe了。

這三行命令都是什么意思呢?

這三行命令中,前兩行分別對hello.cmain.c文件做了預處理,編譯,匯編步驟,但是沒有做鏈接步驟(簡單起見,我們將前三個步驟統稱為編譯,於是我們稱稱這個過程為只進行編譯,而不鏈接)。最后一行,將mian.o, hello.o文件進行鏈接,最終得到可執行程序main.exe

關於編譯、鏈接等有興趣可以自行查閱程序在計算機中的實現過程,本文不做討論。

對於例子中的hello程序來說,使用多文件未免大材小用,但是我們不可避免要用到很多開源的項目,它們項目大多具有非常多的文件,但編譯的過程基本上和前面的過程是一致的。

將項目分成多個文件,首先可以方便組織文件結構。比如將實現同一個功能的代碼放在一個文件內,不同功能的代碼放在不同的文件內。並且,如果修改其中一個文件,只需要重新編譯這一個文件,最后再重新鏈接就好了,可以減少編譯時間。

ps:我的配置中將exe置於程序文件夾下,不便之處在於需要單獨打開exe文件才能看到結果,而不是像之前一樣在底框看到。

2 Makefile

這個我們比較熟悉,是考核過程中接觸到的一種編譯手段。

如果一個項目有很多的文件,每個文件都要一個一個輸入命令編譯太麻煩了。GNU環境下的make提供了一個自動化做這些事情的工具。

首先我們需要在剛才的文件夾下新建一個名字為Makefile(注意大小寫)的文件,寫入如下內容

1 main.exe : main.o hello.o
2         gcc main.o hello.o -o main.exe
3 4 main.o : main.c hello.h
5         gcc -c main.c -o main.o
6 7 hello.o: hello.c hello.h
8         gcc -c hello.c -o hello.o

然后在終端輸入

 make 

就會自動完成上述編譯工作。

如果提示如下錯誤:

無法將“make”項識別為cmdlet、函數、腳本文件或可運行程序的名稱…

原因:

windows本身時沒有make命令的,在安裝MinGW后才會有和Linux中make命令具有相同作用的mingw32-make。 為了使用方便也可以把mingw32-make的名稱改為make。 但是如果在其它的編譯軟件中使用了mingw32-make的名稱的時候,也同樣需要將該軟件中的名稱改成make。

關於配mingw32的教程很多,就不多說。

 

以后我們修改文件的內容,只需要再用make命令就可以自動編譯了。而且make軟件可以自動檢測文件修改時間,對於上次編譯后沒有修改的文件不再進行編譯,從而減少編譯的消耗。

這一定程度上就是我們sudo make的原型,因為linux也是受GNU支持的。

關於如何寫Makefile,可以看看這個教程https://link.zhihu.com/?target=https%3A//seisman.github.io/how-to-write-makefile/

3 Cmake

這也很有名了。

Makefile雖然已經集成化了命令,但是依舊很麻煩。現在有更現代的構建工具能夠幫助自動化生成Makefile文件,cmake是比較受歡迎的一種自動構建工具。

首先要安裝cmake,在ubuntu下一句話搞定:

 apt-get install cmake 

接下來就以一個實際的小工程為例,來講解怎么一步步編寫CMakeLists.txt文件,工程的目錄如下:

 1 bplustree:.
 2 │  CMakeLists.txt
 3 │  coverage_build.sh
 4 │  demo_build.sh
 5 │  LICENSE
 6 │  package.json
 7 │  README.md
 8  9 ├─cmake
10 │      CodeCoverage.cmake
11 12 ├─lib
13 │      bplustree.c
14 │      bplustree.h
15 │      CMakeLists.txt
16 17 └─tests
18         bplustree_coverage.c
19         bplustree_demo.c
20         CMakeLists.txt
21         testcase_generator.py

bplustree是一個主目錄,在子目錄lib和tests里含有源文件,我們需要在這3個目錄下分別編寫CMakeLists.txt文件。

下面先來看主目錄的CMakeLists.txt文件,開頭用project來定義項目名稱,一般和主目錄名保持一致。

 1 cmake_minimum_required (VERSION 2.6)
 2 #定義項目名稱
 3 project (bplustree)
 4 #設置debug模式,如果沒有這一行將不能調試設斷電
 5 set(CMAKE_BUILD_TYPE "Debug")
 6 #設置debug模式下的編譯參數
 7 set(CMAKE_CXX_FLAGS_DEBUG "$ENV{CXXFLAGS} -O0 -Wall -g -ggdb")
 8 set(CMAKE_CXX_FLAGS_RELEASE "$ENV{CXXFLAGS} -O3 -Wall")
 9 #配置自定義模塊路徑,這里是bplustree /cmake 
10 set(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake)
11 #配置庫名
12 set(LIB_BPLUSTREE_NAME bplustree)
13 #進入子目錄下執行 CMakeLists.txt文件
14 add_subdirectory(lib)
15 add_subdirectory(tests)

在lib目錄下,通過編譯bplustree.c文件生成動態庫和靜態庫,cmake代碼如下:

 1 # LIB_BPLUSTREE_SRC是當前目錄下的源代碼文件集合
 2 set(LIB_BPLUSTREE_SRC bplustree.c)
 3 #這是最后庫文件的輸出目錄,這里是bplustree /build/lib
 4 set(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib)
 5 #添加編譯選項的宏定義_BPLUS_TREE_DEBUG
 6 add_definitions(-D_BPLUS_TREE_DEBUG)
 7 #生成一個名為bplustree的動態庫,即libbplustree.so
 8 add_library(${LIB_BPLUSTREE_NAME} SHARED ${LIB_BPLUSTREE_SRC})
 9 #構建一個新的target,cmake會刪除其他同名的庫
10 #因此如果構建libbplustree.a,則會刪除libbplustree.so
11 # CLEAN_DIRECT_OUTPUT 1避免了出現這個問題
12 set_target_properties(${LIB_BPLUSTREE_NAME} PROPERTIES CLEAN_DIRECT_OUTPUT 1)
13 #構建動態庫版本號和api版本號libbplustree.so1.0和libbplustree.so1
14 set_target_properties(${LIB_BPLUSTREE_NAME} PROPERTIES VERSION 1.0 SOVERSION 1)
15 #把動態庫安裝到目錄bplustree /build/lib
16 install(TARGETS ${LIB_BPLUSTREE_NAME} LIBRARY DESTINATION ${LIBRARY_OUTPUT_PATH})
17 #靜態庫和動態庫的名字不能相同,靜態庫需要在末尾加上_static
18 add_library(${LIB_BPLUSTREE_NAME}_static STATIC ${LIB_BPLUSTREE_SRC})
19 #讓靜態庫輸出以libbplustree.a顯示而不是libbplustree_static.a
20 set_target_properties(${LIB_BPLUSTREE_NAME}_static PROPERTIES OUTPUT_NAME "${LIB_BPLUSTREE_NAME}")
21 set_target_properties(${LIB_BPLUSTREE_NAME}_static PROPERTIES CLEAN_DIRECT_OUTPUT 1)
22 #把靜態庫輸出到bplustree /build/lib
23 install(TARGETS ${LIB_BPLUSTREE_NAME}_static ARCHIVE DESTINATION ${LIBRARY_OUTPUT_PATH})

接下來構建目標

最后執行bplustree /test目錄下的CMakeLists.txt來構建最終的目標,構建的目標有2種,分別是bplustree_coverage和bplustree_demo,由CMAKE_BUILD_TYPE來決定具體使用哪一種。

 1 #設置構建目標的名字
 2 set(TEST_NAME ${PROJECT_NAME}_test)
 3 set(COVR_NAME ${PROJECT_NAME}_coverage)
 4 set(DEMO_NAME ${PROJECT_NAME}_demo)
 5 #設置目標輸出目錄,在bplustree /build/bin
 6 set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)
 7 #設置頭文件所在目錄bplustree / lib
 8 set(INC_DIR ${PROJECT_SOURCE_DIR}/lib)
 9 include_directories(${INC_DIR})
10 # 構建coverage目標
11 if(${CMAKE_BUILD_TYPE} MATCHES "Coverage")
12         set(LIB_DIR ../lib)
13         #設置構建目標所需的源文件
14         set(SRC_LIST ${LIB_DIR}/bplustree.c bplustree_coverage.c)
15         #構建可執行文件bplustree_coverage        
16         add_executable(${COVR_NAME} ${SRC_LIST})
17         #設置gcc的編譯選項
18         set(CMAKE_C_FLAGS "-O2 -Wall --coverage")
19 #使用自定義模塊CodeCoverage.cmake   
20         include(CodeCoverage)
21 #執行自定義模塊里的函數,生成新的目標coverage
22 #第一個參數是新的目標名,最后編譯時要生成coverage目標,
23 #使用make coverage,無參數的make命令不會生成該目標
24 #第2個參數是上面生成的可執行文件
25 #第3個參數是新目標coverage的輸出文件
26         setup_target_for_coverage(coverage ${COVR_NAME} coverage)
27 else()
28         set(SRC_LIST bplustree_demo.c)
29 #生成bplustree_demo可執行文件
30         add_executable(${DEMO_NAME} ${SRC_LIST})
31         set(CMAKE_C_FLAGS "-O2 -Wall -Werror -Wextra")
32 #將庫文件連接到目標,默認的是動態庫優先
33 #如果要鏈接靜態庫需要指明實際的文件
34 # target_link_libraries(${DEMO_NAME} libbplustree.a)
35         target_link_libraries(${DEMO_NAME} ${LIB_BPLUSTREE_NAME})
36 endif()
37

PS:在makefile中的目標(target),相當於一個個段落標簽,從當標簽開始執行,到下一個標簽結束,如

1 all:
2   ……
3 coverager:
4   …….
5 clean:
6   ……

上面的例子中,all、coverager和clean就是一個目標,默認make執行的是make all,需要執行其他目標時需要在make后聲明參數,如make coverager、make clean,此時將不會執行目標all里的語句。

果要更深程度的了解,可以看這個GitHub倉庫https://link.zhihu.com/?target=https%3A//github.com/ttroy50/cmake-examples

4 多語言混編(進階)

我們可能聽說過python是一種接口語言,可能也知道它對於其他語言有接口,到底是怎么回事呢?

鏈接技術給多語言混編提供了可能。而這種混編,也使得我們能夠對於編程中的不同情況進行不同的處理:如需要高性能能運算時交給C++去做,需要良好的人機交互體驗時則選擇python。

由於篇幅所限,我推薦大家閱讀這個https://blog.csdn.net/zhanghao3389/article/details/83069333

 

參考文章:

 【程序的運行(三)】編譯多文件項目 - 知乎 (zhihu.com)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM