浅谈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)

posted @ 2021-09-21 00:45  climerecho  阅读(7113)  评论(1编辑  收藏  举报