一天,你在码代码。说不定你会写出这么一句话:

auto message = QString("( *・ω・)✄╰ひ╯");
// do something...

请先忽略为什么会输出这么一个奇怪的颜文字,当你编译的时候,如果你用的是MSVC的编译器,它非常神奇的开始报错:

warning: C4566: 由通用字符名称“”表示的字符不能在当前代码页(936)中表示出来

然后警告的几个字符就成了乱码。

引号里面是这个字符的字符名称。你一开始以为和你碰到下面这个错误一样:

warning:C4819:该文件包含不能在当前代码页中表示的字符。请将该文件保存为Unicode格式以防数据丢失

一般说来,遇到C4819这种情况,你查到的解决方案都会叫你需要把源文件保存为UTF-8,并且加上BOM就好。在使用GCC的Qt环境下,这种方法着实可行。

但是当时我用的是MSVC……

所以当我看向编辑器的编码显示栏,显示的东西让我吃了一惊:

UTF-8 with BOM

Why?

在遇到这个问题后,我重新用GCC进行了编译。结果是正常的……

太奇怪了。

之后便开始百度,而大部分解决方案都粗暴且看起来没有可移植性:直接改系统代码页、改内存存储的字符的也有,还有一个劲咬定是文件编码错误的。看起来没有一个令人满意。

说起来GCC通过了编译,同时工作良好。那么直接使用GCC不就好了?

然而用过的人应该知道,在Windows下,MSVC编译器体验极佳,性能相比GCC还是有一定提升。这些优势不是那么容易抛弃的,看来只能找找MSVC和GCC的不同了。

之后我仍在百度,最后发现了一个解决方案:

使用UTF-8字符串。

也就是在字符串常量的引号之前加一个u8,变成这样:

auto message = QString(u8"( *・ω・)✄╰ひ╯");
// do something...

通过编译,没有警告,工作良好。

(此处宽字符串(L修饰)也可行,但是类型会变成wchar_t *,对Qt编程来说不大友好)

奇了怪,为什么GCC编译时就不需要加个修饰符呢?

在《C++ Primer》中有这么一段描述(第5版 中文版 第30页):

C++提供了几种字符类型,其中多数支持国际化。基本的字符类型是char,一个char的空间应确保存放机器基本字符集中任意字符对应的数字值。也就是说,一个char的大小和一个机器字节一样。

其他字符类型用于扩展字符集,如wchar_tchar16_tchar32_twchar_t类型用于确保可以存放机器最大扩展字符集中的任意一个字符,类型char16_tchar32_t则为Unicode字符集服务(Unicode是用于表示所有自然语言中字符的标准)。

我们知道,我们直接写出的字符串字面值的类型是const char *。在引号前加个L则会产生宽字符型字符串字面值,即wchar_t *。二者的区别非常有趣。C++ 标准并没有强制编译器为这些类型分配写在标准文档里的固定字长,而是如前文所说给予了极大的自由。

Microsoft的文档中,我们可以知道,MSVC为char分配了一个字节,而w_char是两个。于是乎,我们便可以理解MSVC给出的错误了:我明白你要输入的字符是什么(这便是为什么它不是报C4819的原因),但是我不能把它塞进(我的)char里,所以我便开始报错。

等一下……明明我在使用中文时你也没有报错啊,它在UTF-8里也是两个字节的啊?为什么你就能让它通过编译呢?

MSVC的报错已经给了我们提示了:

warning: C4566: 由通用字符名称“”表示的字符不能在当前代码页(936)中表示出来

Microsoft的另一篇文档中可以看到:

字符集映射
所有源字符集成员和转义序列将转换为执行字符集中的等效项。 对于 Microsoft C 和 C++,源和执行字符集都为 ASCII。

代码页便是字符集编码的别名。MSVC在编译时从你的UTF-8编码的文件里读取了字符,不代表着它编译时就会往程序里写入UTF-8的字符。实际上,它会在当前代码页的字符集里(代码页936对应GBK)寻找对应字符并塞进实际编译出的char里(即字符集映射)。显而易见,颜文字里的字符可不都是能在GBK里找到的,于是乎MSVC开始抱怨,并且把这个字符截断。可以在这个链接看到Microsoft的官方说明。

而当我们使用u8修饰时,情况便变得不同。MSVC不会尝试去字符集映射,而是保留UTF-8表示,于是乎那几个在char塞不下的字符便能塞进去了,问题便迎刃而解。

在进一步搜寻和尝试中,我发现其实GCC的策略与MSVC差不多。一般说来char是一个字节,而wchar_t是两个字节。

那么为什么在Qt下使用时GCC和MSVC的效果不同呢?

在翻找的时候,我发现了一句话,就在QStringLiteral宏的介绍博客中:

In Qt5, we changed the default decoding for the char* strings to UTF-8.

在Qt5中,我们把char* 字符串的默认编码 改成了UTF-8。

看来有些眉目了。

在Qt5中,GCC应当并没有“字符集映射”这一操作,于是QString得到的是一个存储了UTF-8的char*字符串,而MSVC的“字符串映射”操作使这个字符串中的宽字符在传递给Qt前就被截断。

所以,在处理有宽字符的文本时的时候,要注意使用u8字符串(或者宽字符串)。

不过,如果要使用Qt的翻译功能,u8就不起作用了,因为lupdate不认u8字符串……这种时候可以使用MSVC的这个宏来强制它认定所有字符串为u8字符串:

#ifdef _MSC_VER
#pragma execution_character_set("utf-8")
#endif

而GCC则不用担心。

发表评论