探索 Windows 平台下 argv 和文件名编码
1. 状况
就像自带的记事本里有“ ANSI 编码”一样, Windows 非常喜欢创造一些对于三次元的我们来说很超前的东西,这几天写了一个 C++ 小工具,读取 argv[1] 里的文件名来完成一些工作,然后我发现这样的代码:
1 | int main(int argc, const char* argv[]) { |
我的 Windows 语言设置了简体中文
对于一些文件不起作用,比如包含希腊字符 Ελληνική γλώσσα
2. Windows C++ argv 的编码
我就比较好奇这个 argv 不是 UTF-8 编码的吗?
1 | int main(int argc, const char* argv[]) { |
输出:
1 | Charset: GB18030 |
??? Windows 你在干什么?我又去万能的 stackoverflow 翻了翻,发现 Windows 为了使
1 | int main(int argc, char* argv[]); |
支持 Unicode 创建了一个自己的扩展
1 | int wmain(int argc, wchar_t* argv[]) |
??????? wchar_t 是啥?我又去问咕咕噜, Windows 称其为 Unicode ,又起了个名叫“宽字符 (wide character) ”,今天也是跟 Windows 学专有名词的一天,看了看这个 wchar_t 发现它的 size 还不是一定的,我的 Windows 下是 2 字节,而 Linux 下是 4 字节(感觉不如 intxx_t ),这些先不说, UTF-8 的话 char* 就够了呀,我一查才发现 wmain 的 argv 是他妈的 UTF-16 编码的(而且 Windows 称 UTF-16 为“宽字符”,“ Unicode ”),我们 Windows 真是太超前 🌶!
3. 喷 Windows 和它钟爱的 UTF-16
Windows 又提出了以下等式:
1 | UTF-16 = Unicode = 宽字符 = wchar_t |
一想 Windows 是一个商业产品,又觉得这种误导大众的做法有一丝合理,但这么一查才发现 Windows API 到处都是 UTF-16 ,为啥要选这么一个不上不下的编码呢?哦,好像跟 Windows 不上不下一个道理。
我不明白 UTF-16 有什么好的,你说它比 UTF-8 优秀,可以根据字节数直接求出字符个数?它和定长编码 UTF-32 不一样,是变长编码,有可能 2 字节也有可能 4 字节,一个 emoji “😂” 就是 4 个字节,一个汉字“绷”就是两个字节;你说它比 UTF-32 优秀,可以节省空间?当今计算机体系下至少 50% 的内容都是英文的,使用 UTF-8 编码一个字节就可以解决(因为 UTF-8 兼容 ASCII ),而 UTF-16 要两个字节(也就不兼容 ASCII ),每 16 个 bit 有 6 个 bit 都用来标记字节位置( 1101 10xx xxxx xxxx 或 1101 11xx xxxx xxxx ),综合起来我觉得比 UTF-8 浪费了更多空间,又没有 UTF-32 的固定长度的优势,属于是综合了两边的缺点,而优点不值一提(一些 CJK 字符可以比 UTF-8 少用 1 个字节)。
你说的对,但是《 UTF-16 》是由 Unicode 联盟自主研发的一种字符编码标准。游戏发生在一个被称作「 wchar_t 」的幻想世界,在这里,被 Windows 选中的人将被授予「 DecodeError 」,导引解码之力。你将扮演一位名为「 wmain 」的神秘角色,在编码解码的过程中邂逅性格各异、能力独特的同伴们,和他们一起击败 Illegal byte sequence ,找回失散的亲人——同时,逐步发掘「 std::filesystem::filesystem_error 」的真相。
3. Windows Exclusive
我寻思再怎么为了程序能跑在 Windows 上也不能直接把 main 改成 wmain 吧,然后我又找到了这个:
1 | int argc = 0; |
包装一下应该可以用了
1 |
|
4. 嗑 Windows x UTF-16
修改成了这样
1 | int main(int argc, const char* argv[]) { |
还是不行,程序没有异常,但文件还是没有被修改,我又去看了看 Win API ,发现 Win 打开文件的 API 有两个,但都不支持 UTF-8 :
CreateFileA:使用 Windows 所谓的“ ANSI 编码”实际在我的电脑上是 GB18030 ,回到最初的起点了,闭环了属于是。CreateFileW:使用 Windows 所谓的“ Unicode ”实际是 UTF-16 ,就非逼着你用wchar_t*, Windows 和 UTF-16 太甜了,太好磕了。
也就是说 std::fstream 无论用哪个 constructor 最后到 Win API 都是不支持 UTF-8 的。
5. C++17 std::filesystem
好在 C++17 之后有了 std::filesystem ,可以这样:
1 | int main(int argc, const char* argv[]) { |
std::filesystem 会帮你把 UTF-8 的文件名转换成平台特定的编码, Cppreference 说它会在 Windows 上把文件名转成 UTF-16 ,使用 wchar_t* , POSIX 环境下直接使用 UTF-8 (char*) ,问题解决。