坑爹的 GBK:大家都应该去用 UTF-8

124 天前
 mikewang

最近在把我用 C 写的一批 Linux 工具移植到 Windows 上,在字符编码上遇到了大坑。


举个简单的例子:数文件层级。

在 Linux 上,我们数斜杠数量就好。

在 Windows 上,再加上反斜杠,应该就好了。——我是这样想的。

#include <stdio.h>

int main(int argc, char *argv[]) {
    int level;
    const char *p;

    if (argc < 2) {
        return 1;
    }

    for (level = 0, p = argv[1]; *p; p++) {
        if (*p == '/' || *p == '\\') {
            level++;
        }
    }

    printf("%d\n", level);

    return 0;
}

用 MinGW 的 GCC 编译一下,然后跑几个用例:

gcc -o getlevel.exe getlevel.c
C:\>getlevel C:\浙江省\宁波市\北仑区\小港街道.txt
4

C:\>getlevel C:\浙江省\宁波市\北仑区\大碶街道.txt
5

天塌了,这么简单的代码竟然出了 bug 。


原来 的 编码是 {0xb4, 0x5c},其中 0x5c 和反斜杠的 ASCII 编码一模一样。

GBK 的第一字节兼容 ASCII ,但第二字节的范围是 0x40 ~ 0xfe,与 ASCII 的 0x00 ~ 0x7f 重叠。BUG 就这么诞生了。

UTF-8 没有这个问题的原因是:只要字节范围在 0x00 ~ 0x7f,那么就一定是 ASCII ,因为后续字节都避开了这个范围。虽然中文编码比 GB 系列长了,但是这个设计确实省了很多事。包括 strstr() strcmp() 之类的都不会出现奇奇怪怪的 bug 。


或许我应该使用 wmain() 然后获取 wchar_t,但是 wmain() 是 Windows 特有的东西,这样做就没法和 Linux 公用同一套代码了。目前加上了 mbtowc() 作为修复。原本简洁的代码变得十分复杂:(

说到这又不得不吐槽下 Windows 的各种奇怪 API 了,不知道它是如何存活到现在的...

12250 次点击
所在节点    C
99 条回复
Thymolblue
124 天前
2025 年了 Visual Studio 中文语言的默认编码还是 GB2312 。同事改完代码一推到仓库全是乱码
dearmymy
124 天前
当年给公司写 mfc 程序,新手的我被 win 的各种字符串整的心理阴影。
yolee599
124 天前
用 #if 宏来实现不同平台的条件编译就可以了啊
henix
124 天前
我的处理方式是边界处全部转换成 UTF-8 ,这样内部的处理逻辑就可以保持一致了
参考 https://utf8everywhere.org/#windows
geelaw
124 天前
UTF-8 是自同步的,所以任何合法的 UTF-8 序列是另一个合法的 UTF-8 序列的子串时,必然是 Unicode 码位意义下的子串。

无论如何 Windows 和 Linux 都没法共用一套代码,因为 Linux 上反斜线可用于文件名,因此 /a\b 在 Windows 上层数是 2 ,在 Linux 上层数是 1 。

另外计算斜线和反斜线并不能正确得出层数,主流操作系统里 . 是本目录,.. 是上层目录(但对于根目录来说是本目录),这两个名称存在于所有目录里,需要特别处理。
geelaw
124 天前
另外楼主似乎以为 Linux 上文件名是 UTF-8 编码的,这是错的。Linux 文件名是不含 '/' 也不含 '\0' 的任何 uint8_t 串,操作系统并不关心 U 不 UTF 的。这一点和 Win32 无甚差别:Win32 规定文件名是任何不含一些选定不可用字符的 uint16_t 串,路径分割符是 '\\' 和 '/'。

楼主的代码在 Linux 上可用(排除上面 . 和 .. 的考虑的话),仅仅是因为 C 标准的传递参数的方式和 Linux 原生路径表示是一样的。
wtks1
124 天前
就算写 shell 脚本现在也用 utf-8 ,gbk 保存的默认打开中文全是乱码
hwdq0012
124 天前
utf8 显示到命令提示符上错误了,还不能直接用 utf82gbk 转换,因为系统会把一些乱码替换为 ”方块问号“
跨平台必知必会的编码问题
hwdq0012
124 天前
@geelaw #5 windows 的 unicode 前面可能有 bom ,不过系统默认设置是使用 local 编码,ide 也是

高版本的 windows 才有预览版本的 utf8 功能,但很多 bug , 而且市场上已经形成 windows 上使用 local 编码程序生态了,你切了 utf8 那些软件都显示乱码
DOLLOR
124 天前
windows 自带的控制台也是一个坑,哪怕你 chcp 65001 之后 printf 的 utf-8 编码里的中文能正常显示了,但 scanf 接收你输入的文字,还依然是 gbk 编码。
tool2dx
124 天前
GBK 编码挺好的,中文汉字必定是 2 字节,第一个字节必定大于 128 (0~255),我都是单独把中文和英文先筛选出来,再做处理的。
w568w
124 天前
> for (level = 0, p = argv[1]; *p; p++)

这个处理方法是不对的,一个 char 代表「 UTF-8 编码序列中的一个字节」,不存在任何和文本相关的含义。尽管 UTF-8 有一些和 ASCII 兼容的假设,但存在很多 corner case (就像主帖提到的),所以不可靠。

如果是高级语言,要枚举字符应当先枚举 Unicode 码点( runes )。

用 mbtowc 转换其实也有问题。wc 指的是「空终止宽字符串」,它不等于 runes 。例如,Windows 上它代指的是经过 UTF-16LE [1] 编码的字符串,对高位字符也需要用多字节的 surrogate pairs 来占位。Linux 上可能是 UTF-32 ,但也不一定。总之,一般建议避免使用 wchar_t 。

言而总之,如果你想枚举 UTF-8 字符串中的字符,最合规的做法是要么依赖 ICU 、utf-8 这样的字符处理库,要么用 C11 里的 mbrtoc32 ( mb -> UTF-32 )。

[1] https://learn.microsoft.com/en-us/cpp/cpp/char-wchar-t-char16-t-char32-t
bbao
124 天前
我这两天好像突然穿越回到了 2010 年前后,有讨论 GBK 的,有讨论 跨域的,又看到了上古神兽 JQUERY 、TOMCAT 。
CHTuring
124 天前
@bbao #13 哈哈哈哈哈,同感
ysc3839
124 天前
不应该用 mbtowc ,这么做会产生更多问题。
如果只需要支持 Windows 10 ,那可以在 manifest 中声明使用 UTF-8 编码,然后一律使用 UTF-8 。否则需要在调用系统 API 时手动把 UTF-8 转换成 UTF-16 ,然后调用 UTF-16 版本的 API 。
rekulas
124 天前
12 楼说的对 虽然我不怎么写 c 但你这个判断一看就不正确
ShadowPower
124 天前
fairytale
124 天前
Windows ?难道不应该用 CreateFileW ?
atuocn
124 天前
#5

```
UTF-8 是自同步的,所以任何合法的 UTF-8 序列是另一个合法的 UTF-8 序列的子串时,必然是 Unicode 码位意义下的子串。
```

这个说法是以前不了解的。感谢
fairytale
124 天前
Windows 如果用 ucrt140 的话,就可以全套 utf8 了(别混用 win api )

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1133223

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX