彻底搞懂:字符编码、文本编码、u8、中文乱码、QT乱码问题

本文是作者自己体会和原创,限于水平有限,如有错误,还请赐教。

开篇一个带入问题:

比如如下两个:

char * first ="中国你好";

char * second=u8"中国你好";

这两种写法到底有啥区别?

网友回答:
GCC/Clang 控制字符串编码有两个开关,是 -finput-charset 和 -fexec-charset , 分别控制源代码读入编译器时的源文件的编码,以及编译器生成目标文件时普通字面量的(const) char[] 类型数组里面的编码。

VC 编译器(VS2015及以后),也有对应的两个开关,叫 /source-charset 和 /execution-charset。功能相同。

u8 前缀,功能与 -fexec-charset=utf-8 或 /execution-charset:utf-8 一样,就是用来限定这一个具体的const char[]数组类型的字面量在保存为字节流时,编码为utf-8的。

vc2015在一定程度上可以自动检查文件编码,源文件是GBK或者utf-16编码的,vs可以自动检查出来,不影响按UTF-8编码。

(以上内容摘抄至网上)

那么是不是说,只要在c++11以后,字符串前面加上u8,就再也不会乱码了呢?非也,实际情况因为操作系统的不同,或者版本不同,还有编译器版本的新旧,依旧会出现中文乱码。


下面说下我的理解:

(1)前文中-finput-charset 是控制源代码读入编译器时的源文件的编码。即在记事本或编辑器中写完代码后,保存时选择的文本编码。一般只有类似notepad或notepad--这样的软件才有这个编码选择功能。这个编码与当前文本编辑器保存时选择的编码,或与默认操作系统的编码相关。

比如中文操作系统win10下面,如果没有修改过默认编码,那么本地默认编码是GBK。如果代码写完后保存时也没选择编码,那么文件编码会按照中文GBK编码。除非程序员有这个良好意识,文件保存时手动选择utf8为编码(或者utf8-bom编码),才会按照utf8编码保存文本。

2)-fexec-charset 控制编译器生成目标文件时普通字面量的(const) char[] 类型数组里面的编码。这是什么意思呢?我们知道编译器编译代码后,会把里面的字符串,写到可执行文件的 .text段中去(编译原理必修课中有讲),以便程序运行后读取字符串在界面显示。即这个地方也是有编码格式的,如果指定为gbk,则写入.text段的就是gbk编码文本,同样如果是utf8则写入.text是utf8编码。

说了这么多,就是说u8会影响上面两个变量,而上面两个变量的组合,还有操作系统本身的默认编码等,混在一起会让人根据糊涂,还是不那么好搞明白,如何彻底避免编程时使用中文而运行不乱码?

作者提供一个简单的搭配方法,可以解决上面的所有问题。

(1)源文件的字符编码

在编写代码完毕后,不管你代码之中有没有使用中文,保存时一律使用utf-8 带BOM 的编码格式去保存文件。这个utf8-bom显示的告诉其它文本编辑器,我的编码是utf8的,你打开我时直接使用utf8打开我就行。这样避免了文本编辑器去动态识别文本编码的过程,也防止识别错误。据我所知,gbk和utf8存在个别编码重合的,同样一个代码,可能识别为gbk,也可能识别为utf8,比如"未知”这个特殊词,gbk和utf8编码都支持。文本保存指定为utf8-bom编码格式,vs或者gcc读入文件时,直接会使用utf8读取该代码文件,不仅省去编辑器的识别编码过程,也避免了编码识别错误问题。如果我们不这样做,会有什么后果?

后果如下:(1.1)不这样做,在中文操作系统下面,默认编码是gbk,保存中文字符文件很可能就是按照gbk保存的。如果我们同样在中文系统下面打开文件,是没有问题的。但是如果换一台其它系统语言的操作系统,比如英文操作系统(ascii),俄语操作系统(俄罗斯本地编码),它们都有自己专有的本地编码,打开gbk文件时会无法识别而直接乱码。如果保存为utf8-bom格式,则没有这个问题,因为现在所有的操作系统都能识别unicode,而utf8 utf16都是unicode的某种实现方式。现在任何操作系统打开utf8编码文件,都不会乱码。(说的有些绝对,应该说只要是支持unicode编码的系统,都能识别。现在系统应该都支持unicode编码)

(2)编写代码时字符串的字节编码。

凡是有中文的字符串,前面加前缀u8。比如char* t = u8"中国你好123"。这样就是显示告诉编译器,在生成exe文件字节码的时候,把中文编码为utf8格式,不要使用gbk或者其它编码。如果不这样做,会有什么后果?

后果如下:如果我们保证了前提(1),则打开文件时,t里面编码字符串是使用utf8编码的,那么编译器生成exe字节码时,它很可能会按照本地编码去生成字节码。具体来说不同的编译器可能有不同的处理方式,情况就比较复杂了。可能有的编译器按照本地编码(中文操作系统默认是GBK)去生成执行文件字节码,就会出现把原始编码utf8的中文,当作gbk编码去保存到exe的text段中,结果就是乱码。可能有的编译器做的比较智能,能够检查操作系统和当前文本编码,依旧按照utf8去生成字节码。总之结果是不确定的,大概率会错误。

如果没有保证前提(1),即打开文件时,t里面编码字符串压根不是utf8的,就是本地编码gbk(中文操作系统时gbk,其它比如俄罗斯操作系统就是俄罗斯的编码,(1.1)中已经说过),那么这里编辑器打开文件时,就可能已经乱码了。比如把这个源文件,放到英文操作系统中去打开,它识别不了gbk或者直接按照ascii去识别,打开时就可能乱码。如果还是在中文系统下面打开,比如中文版win10做的比较好,可以识别中文,不会有问题。但是某些版本的linux系统,对中文支持不好,打开依旧会是乱码。那么后续这里就更乱了,在生成可执行文件字节码时,可能是从乱码保存到本地码,结果还是乱码。也可能运气比较好,都是gbk,保存不是乱码。

根本解决办法:

所以为了避免一些鬼畜的问题。我们如果保证前提(1)文件保存一律使用utf8-bom编码格式保存,再保证条件(2)编码中有中文则加前缀u8。就解决了所有这些鬼畜问题。代码中编程读取文件解析的时候,同样全部使用utf8格式来解析。这样就万无一失了。

3)QT中出现中文乱码的情况

注意QT的本地编码,是可以在程序中动态变化的。

QTextCodec *QTextCodec::codecForLocale() 这个函数可以获取当前QT运行环境中,使用的本地编码。不出意外的话,如果是中文操作系统,没有修改本地编码的情况下,这个返回就是GBK。

同样调用void QTextCodec::setCodecForLocale(QTextCodec *c),可以编程动态去修改QT运行环境中的本地编码。

许多QT使用者喜欢不分青红皂白直接调用QString QString::fromLocal8Bit来处理中文,这样不是每次都能凑效的。因为这个Local是可以动态变化的。再啰嗦一次,如果是中文操作系统,这个local就是gbk,恰好不换操作系统环境,恰好写中文恰好可以显示。

但是如果换个操作系统,比如用户运行环境是英文操作系统;或者编程中动态使用上面的SetCodecForLocale修改了编码,则下面的FromLocal8Bit处理中文就又稀里糊涂的会出现乱码。

实际上,我们压根就不应该随意使用QString QString::fromLocal8Bit,不要依赖本地编码,本地编码在不同系统上而不同。如果我们保证了前面说的条件(1)和条件(2),我们完全可以只使用如下这个接口:QString fromUtf8(const char *str, int size = -1) 。当然str中的中文需要使用u8来标识前缀的,或者本身就是读取文件中的utf8格式的字符串。代码中只按照utf8格式去解析处理字符串就行。即保证输入文件是utf8格式,编程也按照utf8来解析,则一切复杂问题都可以简化,甚至没有问题了。

4解决QT中读取文件文件,加载文件后显示到界面,出现乱码。

也是一样的问题。
读取的文件,编码本身是未知的,或者千奇百怪的。统一在读取之前就使用文本编辑器切换到utf8 编码。或者你不做切换,手动去识别文件的编码,比如识别gbk等,但是何必在编程中去做这个吃亏不讨好的事情呢?如果是国际版,几十种本地编码都要识别,这可是个苦差!

原文件手动切换为utf8编码后,编程读取文件时,再按照utf8编码去获取字符,即直接使用前面的QString fromUtf8(const char *str, int size = -1)。这样保证源输入文件是utf8编码的,编程处理也简化直接按照utf8来读取。大家都简单省事,省去了鬼畜的动态识别编码。动态识别本地编码,然后识别后匹配去解析文件,一句话,吃力不讨好,还可能出错。

当然如果那天避免不了了,该识别本地码也得按照原理去识别:只要保证输入文件的编码,和编程读取解析时的编码一致,就能避免乱码,这是不二法则。

(4)u8能不能支持,是取决于编译器版本。

在C++11之前,通常是使用L""来指定宽字符串,但是并没有要求编译器规定宽字符串的存储,对此windows使用的是2个字节,即UCS-2(早期),UTF-16(最新)。

C++11引入了u8"utf-8字符串",u"utf-16字符串",U"utf-32字符串"。以及R"(非转义字符串)"可直接写不用转义的字符串,和对应的utf版本u8R"(utf-8非转义字符串)",uR"(utf-16非转义字符串)",UR"(utf-32字符串)"。

简而言之,只要你编译器支持C++11,就是可以支持的了。目前大部分主流编译器都是支持的。

结束语:编码问题,看似一个小问题,但如果没有彻底搞明白,则时常像幽灵一样搅乱程序显示。一开始就把它搞明白,避免以后一直糊涂下去,不失为一个好的处理办法。

猜你喜欢

转载自blog.csdn.net/peterbig/article/details/124728388