加载中...
让C/C++简捷项目支持多文件编译
第1节:MSYS2+GCC 安装与应用
第2节:安装配置 VSCODE,理解「配置」和「设置」的区别
第3节:C/C++简捷项目专用配置
第4节:让C/C++简捷项目支持多文件编译
第5节:备战大项目:CMake专项配置
课文封面

当我们学习C/C++一段时间后,就会迎来需要将代码拆分到多个源文件的时候。如何在 vscode 中编译多个源文件的简单项目呢?

上节课我们创建的 “CS-CPP-Simple” 配置,默认仅支持编译一个C++源文件。这节课我们学习如何通过简单修改该配置下的 “任务设置”(tasks.json) 和 “启动设置” (launch.json),实现编译多个C++源文件的目标。

0. 问题

上一节课我们新建了一个名为 “CS-CPP-Simple” 的C++简捷项目专用配置,简单是简单,但只能编译一个源文件。

刚开始学习 C/C++ 语言,确实会有一段不长不短的时间,我们可以把所有的功能写到同一个源文件里去;不过,随着学习的深入,我们需要将代码以某种形式划分,分成多个源文件中去,这时候,要如何继续在 vscode 里愉快地写 C/C++ 项目呢?

第一种方法,当然可以为 vscode 引入更强大,自然也更复杂的软件构建系统,比如下一节课我们要讲的 CMake,它也是软件行业中,C/C++ 项目事实上的 “工业标准”;但我们并不推荐学习者一步到位。

且不说,引入 CMake (或其它类似的构建工具)后,我们创建的每个项目日常所占用的磁盘空间,都会有近百倍的增长(哪怕你仍然只是在写学习上的一些小练习);更主要的是,创建一个 CMake项目,比较费时间——如果是工作,那倒无所谓,因为实际工作中,一年写的项目可能不会超过十个(很多人甚至好几年都在维护同一个项目);但对于学习者,一天开十个项目的情况都不少见,假设每个项目光准备就需要花我们十分钟……太低效了……

为此,我们决定在 “中杯” 和 “超大杯” 之间,推出一个 “大杯” 。方法很简单,修改 task.json 和 launch.json 中的个别字段即可,关键是:既要知其然,更要知其所以然。

1 课堂视频

请先花不到十分钟,看视频。友情提醒:不要边看视频边跟着操作。完整地,流畅地看完视频才能对完整操作产生感性认识,然后对整体操作建立基本概念。最后再跟着本课堂的图文内容操作,更高效,更不容易出错。

视频主要包含以下操作:

  1. 写一个包含三个源文件的项目;
  2. 如何基于 C/C++ 扩展,生成默认的tasks.json与launch.json,并做必要的修改(复习上一节课);
  3. 如何在第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 处女座的坚持:修改任务和启动名称

这属于“处女座”的坚持——我不是,但我日常确实会这么做,只是一会儿演示时,为节省时间,我会跳过下面三个步骤。

  1. 默认生成的 tasks.json 的 label 字段是 “C/C++: g++.exe 生成活动文件”,显然,“生成活动文件”已经名不副实,所以建议将该字段值改为:“C/C++: g++.exe 生成多文件项目”
  2. 默认生成的 launch.json 的name 字段是 “C/C++: g++.exe 生成和调试活动文件”,显然,“生成和调试活动文件”已经名不副实,所以建议将该字段值改为:“C/C++: g++.exe 生成和调试多文件项目”
  3. 在启动调试或运行前,启动任务会调用前置任务——也就是生成任务,二者之间的,竟然是通过任务的 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" }