linux下使用vscode搭建C++开发环境
最近在linux上跑一些开源库做学习用, 顺手就搭了一下vscode的c++开发环境, 这里分享一下vscode进行C++开发的基本环境结构.
1. 首先是编辑器, vscode直接官网下载的, 后期可以用 apt 直接更新, 个人觉得还是挺方便的, 有喜欢折腾的小伙伴可以去github上拉开源版本的下来自己编译, 这里不过多赘述
2. 其次是编译器, 我使用的是GNU编译器g++, 生成脚本我选择了makefile
以上是基础工具, 如果把vscode换成vim + shell脚本, 调试直接gdb的话, 就基本上是原生环境开发了
接下来就是开发环境的搭建了, 这里我先整理一下一个工程量稍微大一些的项目所应该包含的项目种类, 再根据整理的结果给出一个我写的例子, 之后再对该例进行不断完善
对于一个大型工程来说, 可能至少会包含以下几种不同的工程:
1. 可执行程序 : 即项目主要的目标
2. 静态库 : 集成一些基础的工具函数和一些基础功能的封装
3. 动态库 : 作为插件, 非核心功能之类的东西
4. 资源文件 : 各种图片, 文件, 音频, xml等等
以上是我认为的一个工程量稍大的程序可能会包含的项目种类, 根据上面这四类, 我构建了如下的文件结构 :
.
├── debug
├── lib
├── project
│ ├── debug.makefile
│ ├── exe_test
│ │ ├── compile
│ │ ├── .d
│ │ ├── header
│ │ │ └── test.h
│ │ ├── makefile
│ │ └── src
│ │ └── test.cpp
│ ├── lib_a
│ │ ├── compile
│ │ ├── .d
│ │ ├── header
│ │ │ ├── a_1st.h
│ │ │ ├── a_2nd.h
│ │ │ └── a_3rd.h
│ │ ├── makefile
│ │ └── src
│ │ ├── a_1st.cpp
│ │ ├── a_2nd.cpp
│ │ └── a_3rd.cpp
│ ├── lib_so
│ │ ├── compile
│ │ ├── .d
│ │ ├── header
│ │ │ ├── so_1st.h
│ │ │ ├── so_2nd.h
│ │ │ └── so_3rd.h
│ │ ├── makefile
│ │ └── src
│ │ ├── so_1st.cpp
│ │ ├── so_2nd.cpp
│ │ └── so_3rd.cpp
│ └── makefile
├── release
└── .vscode
├── c_cpp_properties.json
├── launch.json
├── settings.json
└── tasks.json
20 directories, 23 files
在当前项目目录下共有4个子目录和一个vscode专用的隐藏目录 :
1. debug : 所有我们生成的debug版本的可执行程序以及debug版本程序所需的资源都会生成在这个目录中
2. release : 同上, 但可执行程序和资源文件都是release版的
3. lib : 所有动态库, 静态库会生成在这个目录中, debug版和release版用文件名结尾是否带 D 来区分
4. project : 所有当前项目相关的工程都在这个目录中
5. .vscode : vscode专用目录, 其中包含了当前项目相关的vscode配置信息
下面再看一下project目录, 该目录下共有3个项目目录和两个makefile :
1. lib_a : 该项目最终会生成一个静态库供程序使用
2. lib_so : 该项目最终会生成一个动态库供程序使用
3. exe_test : 该项目最终会生成一个可执行程序, 该程序会使用到上述静态库和动态库
4. 两个makefile用于控制所有项目的debug版, release版生成
最后再解析一下每一个项目目录, 每个项目都包含了4个子目录和一个makefile :
1. src : 所有的源文件放置在该目录中
2. header : 所有的头文件放置在该目录中
3. compile : 编译后的.o文件会在这个目录中生成
4. .d : 该目录用于存放每个源文件的依赖关系
5. makefile : 该makefile控制当前项目的生成
以上是例子文件结构的大概说明, 下面我们就这个例子进行完善, 针对每一个工程和整个项目, 编写makefile, 完成代码的编译生成
首先针对整个项目, 我们要生成每一个工程, 并保证工程的生成顺序符合每个工程间的依赖关系
这里先看一下project/makefile, 这个makefile用于生成所有工程release版本
1 export BUILD_VER := RELEASE 2 export CXXFLAGS := -Wall -std=c++11 3 export RM := rm -f 4 5 .PHONY:build_all clean_all clean_all_cache 6 7 build_all: 8 cd ./lib_so && make 9 cd ./lib_a && make 10 cd ./exe_test && make 11 12 clean_all: 13 cd ./lib_so && make clean 14 cd ./lib_a && make clean 15 cd ./exe_test && make clean 16 17 clean_all_cache: 18 cd ./lib_so && make clean_cache 19 cd ./lib_a && make clean_cache 20 cd ./exe_test && make clean_cache
该makefile首先会覆写3个变量, 并将变量导出成为全局变量, 其中BUILD_VER用于控制生成程序的版本, 紧随其后的是3个伪目标, 分别用于生成每个工程, 清理所有生成文件以及清理生成过程中的产生的.o和.d
接下来再来看project/debug.makefile, 这个makefile用于生成所有工程的debug版本
1 include ./makefile 2 3 BUILD_VER := DEBUG
该makefile引入release版的makefile, 并修改BUILD_VER为DEBUG, 该makefile名称不是make能够自动识别的名称, 使用需要加上 -f 参数, 如 : make -f debug.makefile
通过上面两个makefile, 我们基本完成了对代码生成的版本控制和整个项目的生成流程, 下面只需要针对每一个工程, 编写对应的makefile即可
下面是3个工程的makefile :
首先是静态库工程lib_a
1 vpath %.cpp ./src 2 vpath %.h ./header 3 4 .PHONY: all clean clean_cache 5 all : # 默认目标 6 7 CXXINCLUDES = -I ./header 8 ARFLAGS = -rcs 9 SRCS_WITH_PATH = $(wildcard ./src/*.cpp) 10 SRCS = $(SRCS_WITH_PATH:./src/%.cpp=%.cpp) 11 DEPS = $(SRCS:.cpp=.d) 12 DEPS_WITH_PATH = $(SRCS:%.cpp=./.d/%.d) 13 OBJS = $(SRCS:.cpp=.o) 14 OBJS_WITH_PATH = $(SRCS:%.cpp=./compile/%.o) 15 TARGET_NAME = tsi.a 16 OUTDIR = ../../lib/ 17 18 ifeq ($(BUILD_VER), DEBUG) 19 CXXFLAGS += -g3 20 TARGET_NAME := tsiD.a 21 endif 22 23 ifeq ($(BUILD_VER), RELEASE) 24 CXXFLAGS += -O2 25 endif 26 27 #生成依赖关系,保证修改.h时也会重新编译相关.cpp 28 -include $(DEPS) 29 30 %.d:$(SRCS) 31 @set -e;\ 32 $(RM) $@;\ 33 $(CXX) $(CXXINCLUDES) -MM $< > .d/$@; 34 35 %.o:%.cpp 36 $(CXX) $(CXXFLAGS) $(CXXINCLUDES) -c $< -o ./compile/$@ 37 38 all:$(TARGET_NAME) 39 40 $(TARGET_NAME):$(OBJS) 41 $(AR) $(ARFLAGS) $(OUTDIR)$(TARGET_NAME) $(OBJS_WITH_PATH) 42 43 clean: 44 $(RM) $(OUTDIR)$(TARGET_NAME) $(OBJS_WITH_PATH) $(DEPS_WITH_PATH) 45 46 clean_cache: 47 $(RM) $(OBJS_WITH_PATH) $(DEPS_WITH_PATH)
makefile中首先读取了当前工程下的两个目录, 保证正确搜索.h和.cpp之后声明三个伪目标, 并以all为终极目标, 之后声明了一系列变量, 这里详细解释一下每一个变量, 跟大家解释一下我的思路
CXXINCLUDES : 该变量包含了生成时c++的包含目录
ARFLAGS : 静态库打包标志
SRCS_WITH_PATH : 包含路径的所有源文件, 该写法可以自动匹配指定目录下的所有.cpp, 大型工程中可能会有很多源文件, 每次更新删除都要修改makefile的话会很不方便
SRCS : 剔除所有源文件的前缀路径
DEPS : 对每一个源文件, 生成一个对应的写有依赖关系的.d文件
DEPS_WITH_PATH : 包含前缀路径的全部.d文件
OBJS : 源文件编译生成的全部.o文件
OBJS_WITH_PATH : 包含前缀路径的全部.o文件
TARGET_NAME : 生成目标的名称
OUTDIR : 输出目录
在声明了以上这些变量之后, 通过对全局变量BUILD_VER的值的判断, 在CXXFLAGS里添加不同的参数以控制版本, 并对文件名等信息做修改
接下来我用-include让当前makefile读取所有.d依赖关系, 当前文件由于没有找到这些.d文件, 会在文件中搜索有无生成的静态目标, 这时, make会搜索到下方的%.d:$(SRCS)
根据该静态目标, .d文件便会被生成出来并被加载
假设我们当前指明生成的是伪目标all
all所依赖的目标是我们指定的文件名$(TARGET_NAME), 该变量所指向的目标又依赖于所有的.o文件, 由于.o文件没有被生成, make又会搜索并调用静态目标%.o:%.cpp进行.o文件的生成
在生成完所有的.o文件之后, 目标$(TARGET_NAME)才会被执行, 最终在../../lib目录中生成tsi.a或tsiD.a
理解了上面的内容之后, 接下来两个工程 : 动态库以及可执行文件的makefile基本也可以套用上面的内容再进行修改得到, 这里我贴出我的写法供大家参考
动态库makefile
1 vpath %.cpp ./src 2 vpath %.h ./header 3 4 .PHONY: all clean clean_cache 5 all : # 默认目标 6 7 CXXFLAGS += -fPIC 8 CXXINCLUDES = -I ./header 9 SRCS_WITH_PATH = $(wildcard ./src/*.cpp) 10 SRCS = $(SRCS_WITH_PATH:./src/%.cpp=%.cpp) 11 DEPS = $(SRCS:.cpp=.d) 12 DEPS_WITH_PATH = $(SRCS:%.cpp=./.d/%.d) 13 OBJS = $(SRCS:.cpp=.o) 14 OBJS_WITH_PATH = $(SRCS:%.cpp=./compile/%.o) 15 TARGET_NAME = libtest.so 16 OUTDIR = ../../lib/ 17 18 ifeq ($(BUILD_VER), DEBUG) 19 CXXFLAGS += -g3 20 TARGET_NAME := libtestD.so 21 endif 22 23 ifeq ($(BUILD_VER), RELEASE) 24 CXXFLAGS += -O2 25 endif 26 27 #生成依赖关系,保证修改.h时也会重新编译相关.cpp 28 -include $(DEPS) 29 30 %.d:$(SRCS) 31 @set -e;\ 32 $(RM) $@;\ 33 $(CXX) $(CXXINCLUDES) -MM $< > .d/$@; 34 35 %.o:%.cpp 36 $(CXX) $(CXXFLAGS) $(CXXINCLUDES) -c $< -o ./compile/$@ 37 38 all:$(TARGET_NAME) 39 40 $(TARGET_NAME):$(OBJS) 41 $(CXX) -shared -o $(OUTDIR)$(TARGET_NAME) $(OBJS_WITH_PATH) 42 43 clean: 44 $(RM) $(OUTDIR)$(TARGET_NAME) $(OBJS_WITH_PATH) $(DEPS_WITH_PATH) 45 46 clean_cache: 47 $(RM) $(OBJS_WITH_PATH) $(DEPS_WITH_PATH)
可执行程序makefile
1 vpath %.cpp ./src 2 vpath %.h ./header 3 4 .PHONY: all clean clean_cache 5 all : # 默认目标 6 7 CXXINCLUDES = -I ./header -I ../lib_a/header -I ../lib_so/header 8 SRCS_WITH_PATH = $(wildcard ./src/*.cpp) 9 SRCS = $(SRCS_WITH_PATH:./src/%.cpp=%.cpp) 10 DEPS = $(SRCS:.cpp=.d) 11 DEPS_WITH_PATH = $(SRCS:%.cpp=./.d/%.d) 12 OBJS = $(SRCS:.cpp=.o) 13 OBJS_WITH_PATH = $(SRCS:%.cpp=./compile/%.o) 14 LINK_LIB = ../../lib/tsi.a 15 LINK_USR_SO = -L ../../lib -Wl,-rpath=../lib -ltest 16 TARGET_NAME = test 17 OUTDIR = ../../release/ 18 19 ifeq ($(BUILD_VER), DEBUG) 20 CXXFLAGS += -g3 21 LINK_LIB := ../../lib/tsiD.a 22 LINK_USR_SO := -L ../../lib -Wl,-rpath=../lib -ltestD 23 TARGET_NAME := testD 24 OUTDIR := ../../debug/ 25 endif 26 27 ifeq ($(BUILD_VER), RELEASE) 28 CXXFLAGS += -O2 29 endif 30 31 #生成依赖关系,保证修改.h时也会重新编译相关.cpp 32 -include $(DEPS) 33 34 %.d:$(SRCS) 35 @set -e;\ 36 $(RM) $@;\ 37 $(CXX) $(CXXINCLUDES) -MM $< > .d/$@; 38 39 %.o:%.cpp 40 $(CXX) $(CXXFLAGS) $(CXXINCLUDES) -c $< -o ./compile/$@ 41 42 all:$(TARGET_NAME) 43 44 $(TARGET_NAME):$(OBJS) 45 $(CXX) -o $(OUTDIR)$(TARGET_NAME) $(OBJS_WITH_PATH) $(LINK_LIB) $(LINK_USR_SO) 46 47 clean: 48 $(RM) $(OUTDIR)$(TARGET_NAME) $(OBJS_WITH_PATH) $(DEPS_WITH_PATH) 49 50 clean_cache: 51 $(RM) $(OBJS_WITH_PATH) $(DEPS_WITH_PATH)
这里有几点需要注意的是, 在可执行程序链接时, 我用-Wl,-rpath指定了程序执行时回去何处寻找libtest.so这个动态库, 如果不想这样写, 需要指定动态库生成到系统默认加载的路径中去, 比如/usr/lib, 同样程序在其他机器上部署时也需要做同样的操作
另外就是关于.d依赖生成我使用的参数是-MM, 因为GNU编译器如果使用-M参数会自动加入一些其它的依赖关系, 具体内容可以用g++ -M xxx.cpp做简单验证, 如下图:
-MM:
-M(后面还有....):
在完成了上述步骤之后, 我们的项目其实已经可以正常编译生成执行了, 只是跟vscode没什么联系, 这里我们先在project目录中运行make, make clean_all, make, make clean_all_cache来看一下辛苦编写makefile的成果
很成功, 舒服了
接下来, 为了做到一键运行(F5)或者一键debug调试, 我们要对vscode进行项目配置, 这里我们要修改.vscode目录下的三个文件:launch.json task.json c_cpp_properties.json
在此之前先贴一下我在vscode中安装的插件, 这些插件能让开发环境更美观, 代码编写更顺畅
其中C/C++提供了只能高亮头文件查找等功能, Chinese是一些选项的汉化, Font Switcher可以快速更换字体(这个无所谓...), One Dark Pro是一款比较美观的主题配色, Python(个人需求, 写一些简单的脚本还是很方便的), TabOut可以针对各种括号按tab快速跳出, vscode-icons美化各种图标
下面到了vscode的启动配置, 在vscode的运行选项卡中, 我们可以选择当前项目启动的配置, 该配置集由launch.json来控制, 这里我先贴出我的launch.json, 再进行详细说明
1 { 2 "configurations": [ 3 { 4 "name": "run release", 5 "type": "cppdbg", 6 "request": "launch", 7 "program": "${workspaceFolder}/release/test", 8 "args": ["-r", "-debug"], 9 "stopAtEntry": false, 10 "cwd": "${workspaceFolder}/release", 11 "environment": [], 12 "externalConsole": false, 13 "MIMode": "gdb", 14 "setupCommands": [ 15 { 16 "description": "为 gdb 启用整齐打印", 17 "text": "-enable-pretty-printing", 18 "ignoreFailures": true 19 } 20 ], 21 "preLaunchTask": "make release" 22 }, 23 { 24 "name": "run debug", 25 "type": "cppdbg", 26 "request": "launch", 27 "program": "${workspaceFolder}/debug/testD", 28 "args": ["-r", "-debug"], 29 "stopAtEntry": true, 30 "cwd": "${workspaceFolder}/debug", 31 "environment": [], 32 "externalConsole": false, 33 "MIMode": "gdb", 34 "setupCommands": [ 35 { 36 "description": "为 gdb 启用整齐打印", 37 "text": "-enable-pretty-printing", 38 "ignoreFailures": true 39 } 40 ], 41 "preLaunchTask": "make debug" 42 } 43 ] 44 }
这里我配置了两个启动选项, 一个直接运行release程序, 另一个运行debug程序, 这里针对debug启动项进行解释说明
name : 我们在启动选项卡里看到的启动项名称
type : cppdbg就可以, 具体可以查阅vscode官方说明
request : 启动项类型, 一种是附加程序一种是直接启动, 这里是直接启动
program : 启动程序路径, 在vscode里打开的根目录即为${workspaceFolder}, 后面加上release路径
args : 传入程序的参数
stopAtEntry : 程序是否自动在入口暂停, debug版才有用哦
cwd : 程序运行时的目录
environment :要添加到程序环境中的环境变量, 具体可以查阅vscode官方说明, 这里我直接没填
externalConsole : 选择程序是在新的控制台中启动还是在集成控制台启动
MIMode : 调试器选择
setupCommands : vscode官方文档查, 这里我是直接用默认配置的
preLaunchTask : 这个是最重要的选项了, 该选项指明了在运行当前选项卡之前要运行的task任务, 这个task任务配置在同目录下的tasks.json中, 这里填的内容是task的label
为了解释preLaunchTask这个选项, 我们引入tasks.json, 这里贴出我的tasks.json, 进行说明
1 { 2 "version": "2.0.0", 3 "tasks": [ 4 { 5 "type": "shell", 6 "label": "make release", 7 "command": "make", 8 "args": [], 9 "options": { 10 "cwd": "${workspaceFolder}/project" 11 }, 12 "group": "build" 13 }, 14 { 15 "type": "shell", 16 "label": "make debug", 17 "command": "make -f debug.makefile", 18 "args": [], 19 "options": { 20 "cwd": "${workspaceFolder}/project" 21 }, 22 "group": "build" 23 }, 24 { 25 "type": "shell", 26 "label": "make clean", 27 "command": "make", 28 "args": ["clean_all"], 29 "options": { 30 "cwd": "${workspaceFolder}/project" 31 }, 32 "group": "build" 33 }, 34 { 35 "type": "shell", 36 "label": "make clean debug", 37 "command": "make -f debug.makefile", 38 "args": ["clean_all"], 39 "options": { 40 "cwd": "${workspaceFolder}/project" 41 }, 42 "group": "build" 43 }, 44 { 45 "type": "shell", 46 "label": "make clean cache", 47 "command": "make", 48 "args": ["clean_all_cache"], 49 "options": { 50 "cwd": "${workspaceFolder}/project" 51 }, 52 "group": "build" 53 } 54 ] 55 }
在这个文件中我配置了5个task, 其中前2个task : make release 和 make debug用于执行不同的makefile
这里我针对make debug做个简单说明
type : task的类型, 这里填shell相当于执行shell命令
label : task的名字
command : 要执行的指令, 这里要注意 make -f xxx.file这种命令, -f xxx这个内容要直接写到命令内容中, 而不能写到下面的args里, 会无法识别, 这里大家可以自行验证一下
args : command要附加的参数
options : 其他选项
cwd : task执行的目录
group : task的分组, 可以查一下vscode官方说明
经过以上配置, vscode就和我们的makefile联系在一起了, 选好启动项f5就完事了, 这里我贴出我的test.cpp, test.h, 和vscode断点调试运行截图
1 #include "test.h" 2 3 int main(int argc, char* argv[]) 4 { 5 if (argc > 0) 6 { 7 std::cout << "input param : "; 8 for (int idx = 0; idx < argc; ++idx) 9 { 10 std::cout << argv[idx] << " "; 11 } 12 std::cout << std::endl; 13 } 14 15 std::cout << std::endl << "using a" << std::endl; 16 std::cout << tsi::a1st::lib_name() << std::endl 17 << tsi::a2nd::lib_author() << std::endl 18 << tsi::a3rd::lib_version() << std::endl; 19 20 std::cout << std::endl << "using so" << std::endl; 21 std::cout << tsi::so1st::lib_name() << std::endl 22 << tsi::so2nd::lib_author() << std::endl 23 << tsi::so3rd::lib_version() << std::endl; 24 return 0; 25 }
1 #ifndef _TSI_TEST_ 2 #define _TSI_TEST_ 3 4 #include <iostream> 5 6 #include "a_1st.h" 7 #include "a_2nd.h" 8 #include "a_3rd.h" 9 #include "so_1st.h" 10 #include "so_2nd.h" 11 #include "so_3rd.h" 12 13 #endif
这样, 一个简单的项目工程的开发环境就搭建成功了. PS: 在调试时, 我遇到了一个小问题, 这里也贴一下
这里一开始我无法进行gdb调试, 提示无法读取文件云云, 点击创建后, 有了上述提示, 在网上检索了一下, 只有解决方案, 没有详细解释其中机理, 这里我先贴出解决办法
在/目录下建立build目录,在该目录中建立错误提示中对应的目录, 并下载提示对应版本glibc到目录中并解压即可解决问题
关于该错误我认为是gdb调试加载路径错误导致, 如果有了解详细原因的朋友, 请务必留言指点, 在此谢过
以上, 上方示例只是一个简单的项目结构, 其中一定还有很多不足之处, 本文仅起到一个抛砖引玉的作用, 如有错误疏漏, 请务必指出, 有问题欢迎讨论, 转载注明出处, 感谢