字符集 – best practice

DinS          Written on 2017/11/14

经过了上一个专题的讲解,你会意识到一件事情:
for c++, character set is a landmine

为什么这么说呢?因为看起来似乎哪一条路都不好走。与Java等提供了统一的抽象字符串概念的语言不同,c++的字符串是面向专家级的。
如果使用string和ASCII字符,那么面对日益国际化的环境,你做出的软件会有很大局限性,不可取。
如果使用wstring和Unicode-16,看起来满足了国际化问题,但是会遇到许多其他问题,一个就是wchar_t在不同操作系统的实现不同,另一个就是存在大量使用string和char*的接口,很难兼容。所幸标准库提供了codecvt,但是不幸的消息是这个标准并没有得到很好的落实,比如大名鼎鼎的VS自2015之后就无法用codecvt了,而且没有修改这个bug的计划。
使用操作系统提供的API倒是可以实现字符集之间的转换,但是这又对程序的移植性提出了挑战。
为之奈何?
UTF-8是救赎之道
这是因为UTF-8可以解决上述面临的所有问题。
首先UTF-8属于unicode,因此可以解决国际化问题。
其次,UTF-8属于多字节字符集,其使用的是char,可以兼容string和char*接口。
然后,UTF-8使用起来几乎不涉及到字符集之间转换,规避掉这个问题。
最后,UTF-8不涉及具体操作系统,具有良好移植性。

虽然做出了上述评价,但是需要证据来支撑,下面将逐一讲解。

1.首先UTF-8属于unicode,因此可以解决国际化问题

这个应该不需要讲解,显而易见。

2.其次,UTF-8属于多字节字符集,其使用的是char,可以兼容string和char*接口

这里的关键是UTF-8可以使用string来装载。更好的说明是,把标准库的string在概念上看作UTF-8字符串。来看这样一个例子

这是一个以UTF-8编码的文本文件,我们要读入这个文件使用下面的代码

在return处打断点,看看结果

对于ASCII字符,utf-8完全可以用string来表示,这就是说,原来的string和char*接口都可以使用。

对于非ASCII,编译器显示不出内容,但是这并不意味着字符串里面没有东西。在后面我们可以看到,可以对其进行常规的字符串操作。

对于中英文混合的情况,看起来很糟糕。注意到line的l消失了,也许是被合并到前一个中文字符中显示了。

再来继续研究一下深入的情况,我们写了如下代码运行。

注意,strLine1里装载的是UTF-8编码格式的字符,但是可以跟””的字符通用。寻找结果是正确的。因此可以说对于UTF-8而言,ASCII字符就跟””的ASCII字符一样,这样保证了最大兼容性,所以可以毫不顾忌地在代码里混合起来使用。

再来看看unicode的情况。编译器是识别不了,但是这影响我们操作字符串吗?
我们准备两个utf-8编码的文件如下。(注:准备文件是为了提取字符对应的UTF-8编码,在c++17提供了UTF-8编码格式后也可以直接在代码里写)

然后我们的目的是把第一个文件中的“你好,世界”改成“你好,中国”。

第一部分获取“世界”的utf-8编码,第二部分获取“中国”的utf-8编码,第三部分搜索并替换,第四部分输出文件。

运行会如何呢?让我们一步一步来看

运行到这一步看起来很不妙啊,没有一个字符是正常的,而且strChina里面还混进了一个空格,之后能够执行正确吗?

寻找结果返回了一个9.虽然看起来有些奇怪,但是毕竟返回了一个数。
如果对utf-8稍有了解,可以知道中文在utf-8中都是占3个字节,也就是说这里的9可能是正确的

替换后字符依然显示不出来,并不意外,最后看看输出。

居然是正确的!这说明了什么?
其实对于计算机而言,并不存在什么乱码,所有的都是二进制。
因此编译器展示的乱码实际上不足为惧,之所以是乱码因为编译器试图用某种字符集来解释string里存放的二进制,但是没有找到对应的字符,所以乱码了。
但是实际上对string内容的操作与具体的字符集无关,find或者replace都是按字节对比。也就是说一旦我们提供了对应的utf-8字节,在string里是可以操作成功的。
也正是因为这一点,我们可以说utf-8兼容过去的字符串操作。只要提供了争取的字节内容,string操作就是有效的。

最后来看看strLine3的情况,写如下代码并运行。

在编译器中strLine3里看不到字符l,而我们又在找字符l,这能够成功吗?
实际上是可以的,返回结果是16,记住一个汉字在utf-8中占3字节,于是“another”占7字节,“另一行”占9字节,则l刚好是第16位。

通过以上几个试验,读者应该意识到:尽管编译器显示是乱码,但是string可以装填并操作utf-8字符。换句话说,string与utf-8字符相性极好,不需要额外的东西就可以达到国际化和兼容的目的。

3.然后,UTF-8使用起来几乎不涉及到字符集之间转换,规避掉这个问题

在程序中使用字符串主要出于两个目的:向用户显示内容和逻辑判断操作。
对于逻辑判断操作,因为utf-8完全可以用string来表示,所以不存在判断操作不了的情况。唯一需要额外注意的就是你必须先提供对应字段的utf-8字节,这在配置文件里写出来用utf-8保存即可。而且更为重要的是,真正用于逻辑判断操作的字符串一般都是ASCII字符,所以最好的实践方法是在程序中只出现ASCII字符串。如果避免不了unicode,就提供对应字符的utf-8编码,这个通过上面的试验已经证明可以正常运行。

对于用户显示,好消息是许多平台已经支持直接使用utf-8编码,这个在web世界最常见,比如各种浏览器。对于一些GUI框架,主流的都支持UTF-8,至少也是支持utf-16的,因此只需要在显示的时候把utf-8的string转换为utf-16即可。
这个转换可能需要使用操作系统API,但是因为在整个程序中只有这里才涉及字符集转换,因此还是很容易管理维护的,更何况utf-8本来就是utf-16变过来的
变化规则有规律可选,都可以自己写一个转换函数来实现跨平台。

4.最后,UTF-8不涉及具体操作系统,具有良好移植性

这个也是比较明显了,使用标准库string哪个操作系统都支持。

5.总结

最后总结一下c++中字符集的应对之道。
坚持使用标准库的string来表示字符串,在概念上把string里的内容视为utf-8编码字符。对于ASCII字符正常操作,对于unicode字符准备好对应的utf-8语言配置文件,从中读取字符串并正常操作。无视编译器的乱码。
避免在程序逻辑中出现unicode字符。
如果需要,在用户界面批量将utf-8字符串转换成utf-16。