我遇到了一个很离谱的问题:U 盘里的 G:\MyProject 文件夹删不掉。

它已经没剩多少东西了,里面只有一个 build 目录。我在 PowerShell 里用 ls 看了一眼,发现一个长得非常不对劲的文件:

G:\MyProject\build\D:\1.bin

这里的 D:\1.bin 不是 D 盘上的文件路径,而是一个完整的文件名。

更奇怪的是:列目录时能看到它,按完整路径访问却报“找不到”。连整个 MyProject 项目文件夹都被它卡住,无法正常删除。

一个看得见、摸不着的“幽灵文件”,就这么出现了。

起因:一段忘记修改的硬编码路径

后来我想起,这个项目之前被我复制到 Linux 上运行过。代码里还有一段忘记修改的 Windows 路径,写法类似这样:

std::ofstream file("D:\\1.bin");

在 C++ 字符串里,\\ 会被转义成一个实际的反斜杠。因此,程序传给文件系统的字符串是:

D:\1.bin

在 Windows 上,这表示 D 盘根目录里的 1.bin。

但 Linux 不认识 Windows 的盘符,也不把反斜杠当作路径分隔符。Linux 使用的路径分隔符是正斜杠 /,所以冒号和反斜杠在这里都只是文件名中的普通字符。

于是,D:\1.bin 被当成了一个相对文件名,直接创建在程序当时的工作目录下。

假设工作目录是 /media/user/U盘/MyProject/build,最终产生的文件就是:

/media/user/U盘/MyProject/build/D:\1.bin

我本来想指定一个 Windows 绝对路径,结果在 Linux 上给文件起了一个叫“Windows 绝对路径”的名字。

为什么回到 Windows 就变成了“幽灵”?

这个 U 盘使用的是 NTFS。磁盘上的文件名存储规则,与 Windows 普通文件操作接口接受的路径规则,并不完全相同。Linux 下的 NTFS 驱动在某些配置下可以创建 Windows 不接受的文件名。

所以,这个文件确实被写到了 U 盘上;但把 U 盘插回 Windows 后,名字里的 : 和 \ 又与 Windows 的路径语法冲突了。

这也解释了为什么它能被 ls 列出来,却无法正常访问:

  • 列目录是在读取目录记录,可以返回磁盘里已经存在的名字。
  • 按路径打开或删除需要解析路径,而这个名字无法按普通 Windows 路径规则正常处理。

排查时,目录权限显示 Everyone 拥有完全控制权限。真正明显的异常,是这个包含保留字符的文件名。

甚至连查文件 ID,都需要换个思路:打开名字正常的父目录,再通过目录枚举接口取出文件名和 ID,避开对异常文件完整路径的解析。

不过,查到 ID 并不代表就能直接按 ID 删除。微软文档还专门记录了按 ID 打开的句柄在执行普通删除操作时会被拒绝的限制。

教训:跨平台项目的路径里,禁止用反斜杠

以后准备跨平台的项目,给自己定一条规则:

路径字符串中禁止使用反斜杠 \ 作为分隔符,统一使用正斜杠 /;需要拼接路径时,使用 std::filesystem::path。

例如,相对路径直接写成:

std::ofstream file("test/1.bin");

Linux 本来就使用正斜杠,Windows 的常规文件路径处理也接受正斜杠。这种写法在两边都能正确表达 test 目录下的 1.bin,前提是目录已经存在。

更完整的写法,是先创建目录,再使用路径对象拼接:

#include <filesystem>
#include <fstream>

int main() {
    const auto dir = std::filesystem::path("test");
    std::filesystem::create_directories(dir);

    std::ofstream file(dir / "1.bin", std::ios::binary);
    if (!file) return 1;
}

这里的 / 是路径拼接运算符,std::filesystem 会按当前系统处理路径。

另外,把反斜杠换成正斜杠,并不能让硬编码的盘符变得跨平台。D:/1.bin 在 Linux 上仍然不会表示 Windows 的 D 盘。输出目录应该来自配置、用户选择,或者约定好的相对路径。

这次最让我意外的地方,是程序居然没有在创建文件时直接失败。它在 Linux 上成功执行了一个完全不同的含义,然后把问题留到了下一次插回 Windows 的时候。

一个忘记修改的硬编码路径,最后真的变成了一个删不掉的文件名。

参考资料