最近在 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 版本
- export BUILD_VER := RELEASE
- export CXXFLAGS := -Wall -std=c++11
- export RM := rm -f
- .PHONY:build_all clean_all clean_all_cache
- build_all:
- cd ./lib_so && make
- cd ./lib_a && make
- cd ./exe_test && make
- clean_all:
- cd ./lib_so && make clean
- cd ./lib_a && make clean
- cd ./exe_test && make clean
- clean_all_cache:
- cd ./lib_so && make clean_cache
- cd ./lib_a && make clean_cache
- cd ./exe_test && make clean_cache
该 makefile 首先会覆写 3 个变量, 并将变量导出成为全局变量, 其中 BUILD_VER 用于控制生成程序的版本, 紧随其后的是 3 个伪目标, 分别用于生成每个工程, 清理所有生成文件以及清理生成过程中的产生的. o 和. d
接下来再来看 project/debug.makefile, 这个 makefile 用于生成所有工程的 debug 版本
- include ./makefile
- BUILD_VER := DEBUG
该 makefile 引入 release 版的 makefile, 并修改 BUILD_VER 为 DEBUG, 该 makefile 名称不是 make 能够自动识别的名称, 使用需要加上 -f 参数, 如 : make -f debug.makefile
通过上面两个 makefile, 我们基本完成了对代码生成的版本控制和整个项目的生成流程, 下面只需要针对每一个工程, 编写对应的 makefile 即可
下面是 3 个工程的 makefile :
首先是静态库工程 lib_a
- vpath %.cpp ./src
- vpath %.h ./header
- .PHONY: all clean clean_cache
- all : # 默认目标
- CXXINCLUDES = -I ./header
- ARFLAGS = -rcs
- SRCS_WITH_PATH = $(wildcard ./src/*.cpp)
- SRCS = $(SRCS_WITH_PATH:./src/%.cpp=%.cpp)
- DEPS = $(SRCS:.cpp=.d)
- DEPS_WITH_PATH = $(SRCS:%.cpp=./.d/%.d)
- OBJS = $(SRCS:.cpp=.o)
- OBJS_WITH_PATH = $(SRCS:%.cpp=./compile/%.o)
- TARGET_NAME = tsi.a
- OUTDIR = ../../lib/
- ifeq ($(BUILD_VER), DEBUG)
- CXXFLAGS += -g3
- TARGET_NAME := tsiD.a
- endif
- ifeq ($(BUILD_VER), RELEASE)
- CXXFLAGS += -O2
- endif
- #生成依赖关系, 保证修改. h 时也会重新编译相关. cpp
- -include $(DEPS)
- %.d:$(SRCS)
- @set -e;\
- $(RM) $@;\
- $(CXX) $(CXXINCLUDES) -MM $<> .d/$@;
- %.o:%.cpp
- $(CXX) $(CXXFLAGS) $(CXXINCLUDES) -c $<-o ./compile/$@
- all:$(TARGET_NAME)
- $(TARGET_NAME):$(OBJS)
- $(AR) $(ARFLAGS) $(OUTDIR)$(TARGET_NAME) $(OBJS_WITH_PATH)
- clean:
- $(RM) $(OUTDIR)$(TARGET_NAME) $(OBJS_WITH_PATH) $(DEPS_WITH_PATH)
- clean_cache:
- $(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
- vpath %.cpp ./src
- vpath %.h ./header
- .PHONY: all clean clean_cache
- all : # 默认目标
- CXXFLAGS += -fPIC
- CXXINCLUDES = -I ./header
- SRCS_WITH_PATH = $(wildcard ./src/*.cpp)
- SRCS = $(SRCS_WITH_PATH:./src/%.cpp=%.cpp)
- DEPS = $(SRCS:.cpp=.d)
- DEPS_WITH_PATH = $(SRCS:%.cpp=./.d/%.d)
- OBJS = $(SRCS:.cpp=.o)
- OBJS_WITH_PATH = $(SRCS:%.cpp=./compile/%.o)
- TARGET_NAME = libtest.so
- OUTDIR = ../../lib/
- ifeq ($(BUILD_VER), DEBUG)
- CXXFLAGS += -g3
- TARGET_NAME := libtestD.so
- endif
- ifeq ($(BUILD_VER), RELEASE)
- CXXFLAGS += -O2
- endif
- #生成依赖关系, 保证修改. h 时也会重新编译相关. cpp
- -include $(DEPS)
- %.d:$(SRCS)
- @set -e;\
- $(RM) $@;\
- $(CXX) $(CXXINCLUDES) -MM $<> .d/$@;
- %.o:%.cpp
- $(CXX) $(CXXFLAGS) $(CXXINCLUDES) -c $<-o ./compile/$@
- all:$(TARGET_NAME)
- $(TARGET_NAME):$(OBJS)
- $(CXX) -shared -o $(OUTDIR)$(TARGET_NAME) $(OBJS_WITH_PATH)
- clean:
- $(RM) $(OUTDIR)$(TARGET_NAME) $(OBJS_WITH_PATH) $(DEPS_WITH_PATH)
- clean_cache:
- $(RM) $(OBJS_WITH_PATH) $(DEPS_WITH_PATH)
可执行程序 makefile
- vpath %.cpp ./src
- vpath %.h ./header
- .PHONY: all clean clean_cache
- all : # 默认目标
- CXXINCLUDES = -I ./header -I ../lib_a/header -I ../lib_so/header
- SRCS_WITH_PATH = $(wildcard ./src/*.cpp)
- SRCS = $(SRCS_WITH_PATH:./src/%.cpp=%.cpp)
- DEPS = $(SRCS:.cpp=.d)
- DEPS_WITH_PATH = $(SRCS:%.cpp=./.d/%.d)
- OBJS = $(SRCS:.cpp=.o)
- OBJS_WITH_PATH = $(SRCS:%.cpp=./compile/%.o)
- LINK_LIB = ../../lib/tsi.a
- LINK_USR_SO = -L ../../lib -Wl,-rpath=../lib -ltest
- TARGET_NAME = test
- OUTDIR = ../../release/
- ifeq ($(BUILD_VER), DEBUG)
- CXXFLAGS += -g3
- LINK_LIB := ../../lib/tsiD.a
- LINK_USR_SO := -L ../../lib -Wl,-rpath=../lib -ltestD
- TARGET_NAME := testD
- OUTDIR := ../../debug/
- endif
- ifeq ($(BUILD_VER), RELEASE)
- CXXFLAGS += -O2
- endif
- #生成依赖关系, 保证修改. h 时也会重新编译相关. cpp
- -include $(DEPS)
- %.d:$(SRCS)
- @set -e;\
- $(RM) $@;\
- $(CXX) $(CXXINCLUDES) -MM $<> .d/$@;
- %.o:%.cpp
- $(CXX) $(CXXFLAGS) $(CXXINCLUDES) -c $<-o ./compile/$@
- all:$(TARGET_NAME)
- $(TARGET_NAME):$(OBJS)
- $(CXX) -o $(OUTDIR)$(TARGET_NAME) $(OBJS_WITH_PATH) $(LINK_LIB) $(LINK_USR_SO)
- clean:
- $(RM) $(OUTDIR)$(TARGET_NAME) $(OBJS_WITH_PATH) $(DEPS_WITH_PATH)
- clean_cache:
- $(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, 再进行详细说明
- {
- "configurations": [
- {
- "name": "run release",
- "type": "cppdbg",
- "request": "launch",
- "program": "${workspaceFolder}/release/test",
- "args": ["-r", "-debug"],
- "stopAtEntry": false,
- "cwd": "${workspaceFolder}/release",
- "environment": [],
- "externalConsole": false,
- "MIMode": "gdb",
- "setupCommands": [
- {
- "description": "为 gdb 启用整齐打印",
- "text": "-enable-pretty-printing",
- "ignoreFailures": true
- }
- ],
- "preLaunchTask": "make release"
- },
- {
- "name": "run debug",
- "type": "cppdbg",
- "request": "launch",
- "program": "${workspaceFolder}/debug/testD",
- "args": ["-r", "-debug"],
- "stopAtEntry": true,
- "cwd": "${workspaceFolder}/debug",
- "environment": [],
- "externalConsole": false,
- "MIMode": "gdb",
- "setupCommands": [
- {
- "description": "为 gdb 启用整齐打印",
- "text": "-enable-pretty-printing",
- "ignoreFailures": true
- }
- ],
- "preLaunchTask": "make debug"
- }
- ]
- }
这里我配置了两个启动选项, 一个直接运行 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, 进行说明
- {
- "version": "2.0.0",
- "tasks": [
- {
- "type": "shell",
- "label": "make release",
- "command": "make",
- "args": [],
- "options": {
- "cwd": "${workspaceFolder}/project"
- },
- "group": "build"
- },
- {
- "type": "shell",
- "label": "make debug",
- "command": "make -f debug.makefile",
- "args": [],
- "options": {
- "cwd": "${workspaceFolder}/project"
- },
- "group": "build"
- },
- {
- "type": "shell",
- "label": "make clean",
- "command": "make",
- "args": ["clean_all"],
- "options": {
- "cwd": "${workspaceFolder}/project"
- },
- "group": "build"
- },
- {
- "type": "shell",
- "label": "make clean debug",
- "command": "make -f debug.makefile",
- "args": ["clean_all"],
- "options": {
- "cwd": "${workspaceFolder}/project"
- },
- "group": "build"
- },
- {
- "type": "shell",
- "label": "make clean cache",
- "command": "make",
- "args": ["clean_all_cache"],
- "options": {
- "cwd": "${workspaceFolder}/project"
- },
- "group": "build"
- }
- ]
- }
在这个文件中我配置了 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 断点调试运行截图
- #include "test.h"
- int main(int argc, char* argv[])
- {
- if (argc> 0)
- {
- std::cout <<"input param :";
- for (int idx = 0; idx < argc; ++idx)
- {
- std::cout << argv[idx] << " ";
- }
- std::cout << std::endl;
- }
- std::cout << std::endl << "using a" << std::endl;
- std::cout << tsi::a1st::lib_name() << std::endl
- << tsi::a2nd::lib_author() << std::endl
- << tsi::a3rd::lib_version() << std::endl;
- std::cout << std::endl << "using so" << std::endl;
- std::cout << tsi::so1st::lib_name() << std::endl
- << tsi::so2nd::lib_author() << std::endl
- << tsi::so3rd::lib_version() << std::endl;
- return 0;
- }
- #ifndef _TSI_TEST_
- #define _TSI_TEST_
- #include <iostream>
- #include "a_1st.h"
- #include "a_2nd.h"
- #include "a_3rd.h"
- #include "so_1st.h"
- #include "so_2nd.h"
- #include "so_3rd.h"
- #endif
这样, 一个简单的项目工程的开发环境就搭建成功了. PS: 在调试时, 我遇到了一个小问题, 这里也贴一下
这里一开始我无法进行 gdb 调试, 提示无法读取文件云云, 点击创建后, 有了上述提示, 在网上检索了一下, 只有解决方案, 没有详细解释其中机理, 这里我先贴出解决办法
在 / 目录下建立 build 目录, 在该目录中建立错误提示中对应的目录, 并下载提示对应版本 glibc 到目录中并解压即可解决问题
关于该错误我认为是 gdb 调试加载路径错误导致, 如果有了解详细原因的朋友, 请务必留言指点, 在此谢过
来源: https://www.cnblogs.com/TssiNG-Z/p/13411627.html