PAYATHON 2026

VS Code 无法识别 ${fileDirname}\\**.cpp 并链接全部文件

排障赵师傅

结论

${fileDirname} 是由 VS Code 替换的变量,*.cpp 和 **/*.cpp 则是需要 shell 展开的通配符。任务直接启动 g++.exe 时,通配符可能会原样传给编译器,其他 .cpp 文件也就不会参与编译和链接。

在 Windows + MSYS2/UCRT64 环境中,可以这样处理:

  1. 直接在 args 中逐个列出源文件。这种方式简单、稳定。
  2. 显式调用 MSYS2 Bash,由 Bash 展开通配符后运行 g++。
  3. 使用 PowerShell 枚举 .cpp 文件,再把文件列表传给 g++。
  4. 项目规模扩大后,改用 Make、CMake 等构建工具。

修改斜杠方向解决不了这个问题。路径分隔符不会负责展开通配符,这项工作需要由 shell 完成。

${fileDirname}\\**.cpp 为什么没有按预期工作

这段配置包含三个处理环节:

  • VS Code 替换 ${fileDirname}、${workspaceFolder} 等变量。
  • shell 展开 *.cpp 一类的通配符。
  • g++ 接收确定的源文件参数,然后完成编译和链接。

正常情况下,shell 会把:

./*.cpp

展开为:

./main.cpp ./add.cpp

实际执行的命令相当于:

g++ -g ./main.cpp ./add.cpp -o ./app.exe

如果任务直接启动 g++.exe,没有经过合适的 shell,编译器收到的可能仍是下面这个字面参数:

C:\project\*.cpp

Windows 版 g++ 不一定会代替 shell 展开它。终端、任务类型和工具链不同,具体行为也可能不同,所以不宜依赖这种隐式处理。

几种写法的含义也不一样:

  • *.cpp:匹配当前目录中的 .cpp 文件。
  • **.cpp:仍然只是一个路径段,不表示递归查找。
  • **/*.cpp:在支持 globstar 的 Bash 中递归匹配。
  • //:既不是递归通配符,也不会启用通配符展开。

方案一:显式列出源文件

只有几个源文件的练习项目,可以直接写出每个文件名:

{
  "version": "2.0.0",
  "tasks": [
    {
      "type": "cppbuild",
      "label": "Build project",
      "command": "C:\\msys64\\ucrt64\\bin\\g++.exe",
      "args": [
        "-fdiagnostics-color=always",
        "-g",
        "${workspaceFolder}\\main.cpp",
        "${workspaceFolder}\\add.cpp",
        "-o",
        "${workspaceFolder}\\app.exe"
      ],
      "options": {
        "cwd": "${workspaceFolder}",
        "env": {
          "MSYSTEM": "UCRT64",
          "PATH": "C:\\msys64\\ucrt64\\bin;${env:PATH}"
        }
      },
      "problemMatcher": [
        "$gcc"
      ],
      "group": {
        "kind": "build",
        "isDefault": true
      }
    }
  ]
}

配置中没有通配符,因此不需要 shell 参与。每次新增源文件后,都要同步修改 tasks.json。

这里通常更适合使用 ${workspaceFolder}。${fileDirname} 取决于当前打开或激活的文件,如果该文件位于子目录,任务可能会从错误的目录开始构建。

方案二:通过 MSYS2 Bash 展开通配符

如果只需编译项目根目录中的 .cpp 文件,可以让 VS Code 启动 Bash,再由 Bash 调用 UCRT64 的 g++:

{
  "version": "2.0.0",
  "tasks": [
    {
      "type": "process",
      "label": "Build all C++ files with MSYS2 Bash",
      "command": "C:\\msys64\\usr\\bin\\bash.exe",
      "args": [
        "-lc",
        "exec /ucrt64/bin/g++ -fdiagnostics-color=always -g ./*.cpp -o ./app.exe"
      ],
      "options": {
        "cwd": "${workspaceFolder}",
        "env": {
          "MSYSTEM": "UCRT64"
        }
      },
      "problemMatcher": [
        "$gcc"
      ],
      "group": {
        "kind": "build",
        "isDefault": true
      }
    }
  ]
}

执行过程如下:

  1. VS Code 把工作目录设为 ${workspaceFolder}。
  2. bash.exe 解析命令字符串。
  3. Bash 将 ./*.cpp 展开为多个文件名。
  4. /ucrt64/bin/g++ 接收到完整的源文件列表。
  5. g++ 编译各个翻译单元,并将它们链接成 app.exe。

不要给通配符本身加引号,否则 Bash 不会展开它:

g++ "./*.cpp" -o app.exe

需要递归编译子目录中的 .cpp 文件时,可以启用 Bash 的 globstar:

{
  "type": "process",
  "label": "Build C++ files recursively",
  "command": "C:\\msys64\\usr\\bin\\bash.exe",
  "args": [
    "-lc",
    "shopt -s globstar nullglob; set -- ./**/*.cpp; (($#)) || { echo 'No .cpp files found' >&2; exit 1; }; exec /ucrt64/bin/g++ -fdiagnostics-color=always -g \"$@\" -o ./app.exe"
  ],
  "options": {
    "cwd": "${workspaceFolder}",
    "env": {
      "MSYSTEM": "UCRT64"
    }
  },
  "problemMatcher": [
    "$gcc"
  ]
}

globstar 允许 ** 跨越目录层级,nullglob 可以避免在没有匹配文件时将通配符原样传给编译器。"$@" 会把展开后的每个文件名分别作为参数传入,即使文件名中有空格也能正确处理。

方案三:由 PowerShell 枚举源文件

也可以让 PowerShell 查找文件,再调用 UCRT64 的 g++.exe:

{
  "version": "2.0.0",
  "tasks": [
    {
      "type": "process",
      "label": "Build all C++ files with PowerShell",
      "command": "powershell.exe",
      "args": [
        "-NoProfile",
        "-Command",
        "$src = @(Get-ChildItem -LiteralPath . -Filter '*.cpp' -File | ForEach-Object FullName); if ($src.Count -eq 0) { throw 'No .cpp files found' }; & 'C:\\msys64\\ucrt64\\bin\\g++.exe' '-fdiagnostics-color=always' '-g' @src '-o' '.\\app.exe'; exit $LASTEXITCODE"
      ],
      "options": {
        "cwd": "${workspaceFolder}",
        "env": {
          "MSYSTEM": "UCRT64",
          "PATH": "C:\\msys64\\ucrt64\\bin;${env:PATH}"
        }
      },
      "problemMatcher": [
        "$gcc"
      ],
      "group": {
        "kind": "build",
        "isDefault": true
      }
    }
  ]
}

如果还要查找子目录,可以为 Get-ChildItem 加上 -Recurse:

Get-ChildItem -LiteralPath . -Filter '*.cpp' -File -Recurse

这个方案由 PowerShell 处理通配符,g++.exe 接收到的仍然是已经枚举好的文件路径。

“identifier is undefined” 不一定是链接错误

如果看到下面的提示:

identifier "add" is undefined

通常表示编译 main.cpp 时找不到 add 的声明。即使 add.cpp 已经参与编译,另一个翻译单元也不会自动获得其中的声明。应当把声明放进头文件:

// add.h
#pragma once

int add(int x, int y);
// add.cpp
#include "add.h"

int add(int x, int y)
{
    return x + y;
}
// main.cpp
#include <iostream>
#include "add.h"

int main()
{
    std::cout << add(2, 3) << '\n';
}

如果 main.cpp 可以正常编译,但链接阶段出现类似错误:

undefined reference to `add(int, int)'

这时更可能是 add.cpp 没有参与链接,或者声明与定义在参数、命名空间或限定符上不一致。

编辑器中的 IntelliSense 提示也要与实际构建输出区分开。判断任务是否成功,应查看 VS Code 的 Terminal 或 Problems 面板,确认 $gcc 匹配器记录的真实编译器信息。

项目较大时使用构建工具

将目录中的所有 .cpp 文件一次性交给 g++,适合小型练习项目。项目中出现子目录、测试程序、第三方代码或多个可执行文件后,这种做法可能会把多个 main() 或不属于当前目标的源文件一起链接。

此时可以用 Make、CMake 等构建工具管理源文件、目标文件和增量编译,让 tasks.json 只负责调用构建命令。这样能够明确每个可执行文件包含哪些源文件,也不必再让 VS Code 处理源文件通配符。

备注:内容仅供参考。