浅谈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.c
和main.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
,可以看看
3 Cmake
这也很有名了。
Makefile虽然已经集成化了命令,但是依旧很麻烦。现在有更现代的构建工具能够帮助自动化生成Makefile
文件,
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里的语句。
果要更深程度的了解,可以
4 多语言混编(进阶)
我们可能听说过python是一种接口语言,可能也知道它对于其他语言有接口,到底是怎么回事呢?
链接技术给多语言混编提供了可能。而这种混编,也使得我们能够对于编程中的不同情况进行不同的处理:如需要高性能能运算时交给C++去做,需要良好的人机交互体验时则选择python。
由于篇幅所限,我推荐大家阅读
参考文章: