0. 问题
上一节课我们新建了一个名为 “CS-CPP-Simple” 的C++简捷项目专用配置,简单是简单,但只能编译一个源文件。
刚开始学习 C/C++ 语言,确实会有一段不长不短的时间,我们可以把所有的功能写到同一个源文件里去;不过,随着学习的深入,我们需要将代码以某种形式划分,分成多个源文件中去,这时候,要如何继续在 vscode 里愉快地写 C/C++ 项目呢?
第一种方法,当然可以为 vscode 引入更强大,自然也更复杂的软件构建系统,比如下一节课我们要讲的 CMake,它也是软件行业中,C/C++ 项目事实上的 “工业标准”;但我们并不推荐学习者一步到位。
且不说,引入 CMake (或其它类似的构建工具)后,我们创建的每个项目日常所占用的磁盘空间,都会有近百倍的增长(哪怕你仍然只是在写学习上的一些小练习);更主要的是,创建一个 CMake项目,比较费时间——如果是工作,那倒无所谓,因为实际工作中,一年写的项目可能不会超过十个(很多人甚至好几年都在维护同一个项目);但对于学习者,一天开十个项目的情况都不少见,假设每个项目光准备就需要花我们十分钟……太低效了……
为此,我们决定在 “中杯” 和 “超大杯” 之间,推出一个 “大杯” 。方法很简单,修改 task.json 和 launch.json 中的个别字段即可,关键是:既要知其然,更要知其所以然。
1 课堂视频
请先花不到十分钟,看视频。友情提醒:不要边看视频边跟着操作。完整地,流畅地看完视频才能对完整操作产生感性认识,然后对整体操作建立基本概念。最后再跟着本课堂的图文内容操作,更高效,更不容易出错。
视频主要包含以下操作:
- 写一个包含三个源文件的项目;
- 如何基于 C/C++ 扩展,生成默认的tasks.json与launch.json,并做必要的修改(复习上一节课);
- 如何在第2步的基础上,继续修改前述两个文件,以实现多文件编译、运行、调试。
2. C/C++ 扩展为什么只肯编译一个源文件?
原因在于,微软官方的 C/C++ VSCode 扩展,使用时会默认生成用于编译的任务,这个任务配置写在 tasks.json 中,其中为 g++ 编译器指定的命令行参数如下:
"args": [
"-fdiagnostics-color=always",
"-g",
"${file}", // <-- 这个就是要编译的文件
"-o",
"${fileDirname}\\${fileBasenameNoExtension}.exe"
],
问题就在上面我加了注释的那一行:${file} 在 vscode 中,代表用户当前打开的文件——注意,不是用户已经打开的文件,而是 “活动文件”,也就是用户正在查看或编写的那个文件。所以它带给我们的麻烦,不仅是只能编译一个文件,而且是你必须正好打开你想要编译的那个文件,假设你打开的是 tasks.json,然后你按下 Ctrl + Shfit + B 以调用编译任务,就会在 vscode 底部的终端内,看到一句 “无法生成和调试,因为活动文件不是 C 或 C++ 源文件。”
3. 辅助知识 : VSCODE 内置变量
tasks.json 中,除了用到 ${file}
这个vscode内置变量(这里的变量和C/C++代码中的变量没有关系)外,还用到 ${fileDirname}
和 ${fileBasenameNoExtension}
。三者的解释如下:
${file}
当前打开的文件(也称活动文件)的完整路径与文件名称(含扩展名);${fileDirname}
上述文件的所在路径;${fileBasenameNoExtension}
上述文件的文件名,且不含扩展名。
这三者用在 “args” 数组中,最终就是让 g++.exe 编译 ${file}
,然后生成和源文件相同路径、相同文件名,但扩展名变成 .exe 的可执行文件。
假设你当前打开的源文件是 “c:\myProjects\Hello\main.cpp”,那么:
${file}
是: c:\myProjects\Hello\main.cpp;${fileDirname}
是: c:\myProjects\Hello;${fileBasenameNoExtension}
是 main;
再看 “args” 中的最后一行,显而易见,在编译成功后,你将得到 c:\myProjects\Hello\main.exe。
我们把一些常用的 vscode 变量列出来(包括上面三项):
${workspaceFolder}
:当前 vscode 打开的最顶级文件夹(称为工作文件夹)的完整路径(在Windows中也称为绝对路径);${workspaceFolderBasename}
:工作文件夹的名称;${file}
:当前打开的文件的完整路径与文件名称(含扩展名);${fileDirname}
:当前打开的文件的完整路径(不含文件名);${fileBasenameNoExtension}
:当前打开的文件的名称(不含扩展名);{fileExtname}
:当前打开文件的扩展名;${relativeFile}
:当前打开文件相对于工作目录(即${workspaceFolder}
)的相对路径;${relativeFileDirname}
:当前打开文件当前打开文件相对于工作目录(即${workspaceFolder}
)的路径部分;${cwd}
:当前任务启动时工作目录(即在哪个目录下运行该任务);${defaultBuildTask}
:上一节课 “5.3.2 只编译,不调试,不运行”小节里,我们演示的,当我们按下Ctrl + Shift + B 热键可直接执行的任务。
4. 解决方法
4.1 支持编译多个源文件
答案呼之欲出:想要一次编译多个C++源文件,只要指定编译器的编译源文件,是当前工作文件夹(${workspaceFolder}
)的所有 .cpp 文件即可,而 Windows 又允许(其实其它操作系统也允许)我们用 “*.cpp” 来代表所有扩展名为 .cpp 的文件……
听起来是如此的自然而然,但我记得,老的 GCC 编译器并不允许我们使用文件通配符……幸好,现在是肯定支持的。
因此,想要编译多个源文件,请修改 tasks.json 中,“args” 数组内,原来是:"${file}",
那一行,改为:${workspaceFolder}\\*.cpp
即可。
4.2 修改编译生成文件的名字
还没完,现在我们有多个源文件,如果仍然使用“活动文件”的名字作为生成的可执行文件名,岂不在打开 main.cpp 时会生成 main.exe,而一旦打开的是 another.cpp,这回生成的就是 another.exe 了,这可太乱了!
所以,我们还要把 "-o"
后面的那行参数:
"${fileDirname}\\${fileBasenameNoExtension}.exe"
,修改成
"${workspaceFolder}\\${workspaceFolderBasename}.exe"
这样,假设你打开的工作文件夹是 :c:\myProjects\Hello,编译成功后,你将得到:c:\myProjects\Hello\Hello.exe。
路径分隔符
上述设置中出现的 \ ,多半也可以写成 / ,此时不需要转义符,且通用于 Windows、Linux、iMac 等操作系统;不过,更正规的写法,是使用 vscode 的另一个变量 :
${/}
,这样,在Windows下,我们将得到 \ ,在Unix/Linux系的操作系统,则为 / 。
4.3 修改启动文件
生成的可执行文件名字被我们改了,这下当我们按下 F5 准备调试可执行文件,或者按下 Ctrl + F5 准备直接运行可执行文件时,vscode 就该报:找不到可执行文件 XXXX 了。
这回请打开 “launch.json” 文件,查看 “configurations” 数组中,“program” 字段的值,它原本是这样一行内容:
"program": "${fileDirname}\\${fileBasenameNoExtension}.exe",
把它改成我们刚刚在 tasks.json 中配置的目标文件:
"program": "${workspaceFolder}\\${workspaceFolderBasename}.exe",
4.4 处女座的坚持:修改任务和启动名称
这属于“处女座”的坚持——我不是,但我日常确实会这么做,只是一会儿演示时,为节省时间,我会跳过下面三个步骤。
- 默认生成的 tasks.json 的 label 字段是 “C/C++: g++.exe 生成活动文件”,显然,“生成活动文件”已经名不副实,所以建议将该字段值改为:“C/C++: g++.exe 生成多文件项目”;
- 默认生成的 launch.json 的name 字段是 “C/C++: g++.exe 生成和调试活动文件”,显然,“生成和调试活动文件”已经名不副实,所以建议将该字段值改为:“C/C++: g++.exe 生成和调试多文件项目”
- 在启动调试或运行前,启动任务会调用前置任务——也就是生成任务,二者之间的,竟然是通过任务的 label 字段来建立关联,而我们刚修改了任务的 label,所以,还是在 launch.json 里面,请找到 “preLaunchTask” 字段,然后将它精准地修改成:“C/C++: g++.exe 生成多文件项目”;
显然,你可以选择1、2、3都不做;但如果你做了 1,那你就一定要做 3。
附 相关文件
附1 main.cpp
#include <cstdlib> // std::system(...)
#include <iostream>
#include <string>
#include "input.hpp" // Input()
#include "hello.hpp" // Hello()
using namespace std;
int main()
{
std::system("chcp 65001 > nul"); // 修改所在控制台编码为 utf-8
std::string name = Input(); // 让用户输入姓名
std::cout << Hello(name) << std::endl; // 调用 Hello() 生成问候语,然后输出
std::system("pause"); // 让程序在退出前暂停
}
附2 input.hpp / input.cpp
- input.hpp
#ifndef _HELLO_INPUT_HPP_
#define _HELLO_INPUT_HPP_
#include <string>
// Input 函数,用于提示并接受用户输入的姓名,并返回
std::string Input();
#endif // _HELLO_INPUT_HPP_
- input.cpp
#include "input.hpp"
#include <iostream>
std::string Input()
{
std::string name;
std::cout << "请输入您的尊姓大名:";
std::getline(std::cin, name);
return name;
}
附3 hello.hpp / hellp.cpp
- hello.hpp
#ifndef _HELLO_HELLO_HPP_
#define _HELLO_HELLO_HPP_
#include <string>
// 依据姓名参数,生成问候语
std::string Hello(std::string const& name);
#endif // _HELLO_HELLO_HPP_
- hello.cpp
#include "hello.hpp"
std::string Hello(std::string const& name)
{
return "你好,亲爱的 " + name + "!"; // 因为有 name (类型是 std::string),
// 所以(才)可以前后通过 + 来拼接字符串
}
附4 tasks.json
{
"tasks": [
{
"type": "cppbuild",
"label": "C/C++: g++.exe 生成多文件项目",
"command": "C:\\msys64\\ucrt64\\bin\\g++.exe",
"args": [
"-fdiagnostics-color=always",
"-g",
// 待编译的文件,由当前活动文件改成项目下的 *.cpp
"${workspaceFolder}\\*.cpp",
"-o",
// 编译后生成的可执行文件,改用目录名
"${workspaceFolder}\\${workspaceFolderBasename}.exe"
],
"options": {
"cwd": "C:\\msys64\\ucrt64\\bin"
},
"problemMatcher": [
"$gcc"
],
"group": {
"kind": "build",
"isDefault": true // 本任务被设为默认生成(编译,构建)任务
},
"detail": "调试器生成的任务。"
}
],
"version": "2.0.0"
}
附5 launch.json
{
"configurations": [
{
"name": "C/C++: g++.exe 生成和调试多文件项目",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}\\${workspaceFolderBasename}.exe", // 待启动的程序
"args": [],
"stopAtEntry": false,
"cwd": "C:\\msys64\\ucrt64\\bin",
"environment": [],
"externalConsole": true, // 在外部独立控制台内运行程序
"MIMode": "gdb",
"miDebuggerPath": "C:\\msys64\\ucrt64\\bin\\gdb.exe",
"setupCommands": [
{
"description": "为 gdb 启用整齐打印",
"text": "-enable-pretty-printing",
"ignoreFailures": true
},
{
"description": "将反汇编风格设置为 Intel",
"text": "-gdb-set disassembly-flavor intel",
"ignoreFailures": true
},
{
"description": "让 GDB 使用 UTF-8 编码",
"text": "-gdb-set charset UTF-8",
"ignoreFailures": false
}
],
"preLaunchTask": "C/C++: g++.exe 生成多文件项目"
}
],
"version": "2.0.0"
}