编译器对c++源文件编码的识别
注意,这里说的,是对于源文件编码的识别,而不是用c++来读写文件。实际上,不同编译器对于源文件编码的识别时有差异的,这是你需要特别注意的一点。如果你在 Code Blocks 里边在源文件中写了中文,用 sublime 打开却发现出现乱码,本文可以彻底解决这个问题。
基本知识和基本工具
本篇是整理时加上的一节,我觉得最好将一些基础知识提前说清楚,便于之后的陈述。
文件编码基础知识
前面说完了基本的编码知识,具体在文件中是这么实现的呢。
- 文本文件
通常意义上理解的文本文件,就是只保存字符的编码信息,而不保存字符的字体、大小、颜色等信息的文件。事实上,所有的编程源文件都是文本文件。
在windows下我们新建一个文本文件,一般默认是使用Notepad这个软件,默认编码是ANSI。不过如果你采用其他软件,例如Notepad++,vim,或者VS Code,结果可能是不尽相同。但是无论如何,字符在文件中保存,一定还是0101这样的编码(计算机上什么东西不是这样编码的呢)。
- ANSI
这个基本上没有什么好说的,windows下使用记事本(Notepad)新建一个文本文件,就是ANSI编码的。其在硬盘上保存的,就是这些字符的ANSI编码。
- utf-8
现在有一个问题,同样一个.txt文件,编辑器如何判断它是什么编码呢。所以很多文件都有一些打头的一些字节来表示自己的文件类型。一些utf-8文件就会有文件头,称为BOM,utf-8文件的BOM是三个字节:0xef
,0xbb
,0xbf
。不过一些由于utf-8编码的普及,现在无BOM的utf-8文件更多。
作为utf-8拥护者,我们一般推崇无BOM的utf-8文件
比如 sublime 识别文件默认就是utf-8,如果文件编码是utf-8,就会正确显示,如果文件实际上是ANSI编码,显然不会有BOM,sublime 还是把他当成utf-8文件来读,就会产生乱码。
- utf-16文件
前面说过,utf-16实际上有两种存储方式,大端序和小端序,这关系到一个utf-16字符两个字节哪个在硬盘中位置靠前。这个信息要告诉编辑器,就依靠文件头了:
- 大端序utf-16BE,文件头为0xfe,0xff。
- 小端序utf-16LE,文件头文0xff,0xfe。
- utf-32文件
目前我的工具无法查看utf-32文件,就不管了,一般来说utf-32文件不会用到。
工具
- Notepad++
我的主要工具就是Notepad++了,它会判断文件编码,可以很方便地转换编码,虽然我现在常用VS Code,不过写此文时,Notepad++帮了大忙,如果你碰到乱码文本文件,可以用他来查看。
- cmd(命令提示符)
要特别注意的是,命令提示符的输入输出默认编码是ANSI,所以任何输出字节都会按照ANSI解码,然后再打印出来。所以,通常只有ANSI编码字符串才能正确输出,如何输出其他编码字符,是后面的课题。
python3的str
的默认编码是utf-8,其可以正确输出是python自己实现的。
c++字符串的基本知识
- 字符初始化
类型名 | 单元 | 初始化(注意前缀) | 编码 |
---|---|---|---|
std::string | char | std::string s = "你好世界"; | 系统默认 |
std::string | char | std::string s = u8"你好世界"; | utf-8 |
std::u16string | char16_t | std::u16string s = u"你好世界"; | utf-16 |
std::u32string | char32_t | std::u32string s = U"你好世界"; | utf-32 |
std::wstring | wchar_t | std::wstring s = L"你好世界"; | 系统依赖 |
wchar_t
的长度
sizeof(wchar_t) == 2; //Winodws下
sizeof(wchar_t) == 4; //Unix/Linux下
编译器对源文件的识别
实验使用的编译器有visual studio 2017的cl.exe(x86_x64),clang 7.0.0(自己编译的release版本),以及gcc 8.1.0 (MinGW-W64),全部都在命令行下编译,实验时会具体说明。所有实验源文件编码也会标注。
#include <iostream>
#include <string>
// encoding = utf-8 (no BOM)
int main(){
std::string sa = "你好世界";
std::string sb = u8"你好世界";
std::cout << "len(sa)=" << sa.size() << ' ' << sa << std::endl;
std::cout << "len(sb)=" << sb.size() << ' ' << sb << std::endl;
}
输出:
- g++ or clang-cl
len(sa)=12 浣犲ソ涓栫晫
len(sb)=12 浣犲ソ涓栫晫
- cl.exe
len(sa)=12 浣犲ソ涓栫晫
len(sb)=18 娴g姴銈芥稉鏍櫕
如果上述源文件改成ANSI编码,结果是:
- g++
len(sa)=8 你好世界
len(sb)=8 你好世界
- clang-cl
test.cpp(6,23): warning: illegal character encoding in string literal [-Winvalid-source-encoding]
std::string sa = "<C4><E3><BA><C3><CA><C0><BD><E7>";
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
test.cpp(7,25): error: illegal character encoding in string literal
std::string sb = u8"<C4><E3><BA><C3><CA><C0><BD><E7>";
- cl.exe
len(sa)=8 你好世界
len(sb)=12 浣犲ソ涓栫晫
分析:从上诉结果可以看出,
- gcc和clang都是默认utf-8编码的,不过,g++没有编码检查,会把任何文件都当作utf-8文件来读取。
- gcc和clang的
u8
前缀是不起作用的,毕竟本来就默认是utf-8编码。 - cl 默认编码是ANSI,所以
u8
前缀是起作用的,但是对于无BOM的utf-8文件,它会当成ANSI来读取,所以它把utf-8的"你好世界"
含有的12字节当成了6个ANSI字符,并对其进行的转码,试图转为6个utf-8字符,也就是18字节,这显然是越搞越乱。
不过还是可以夸夸cl的,如果你用其他编码实验,对于cl,它都会给出:
len(sa)=8 你好世界
len(sb)=12 浣犲ソ涓栫晫
这是由于它会自动把他转换为ANSI编码,然后再编译,包括有BOM的utf-8文件也是这样的(显然,有BOM就是告诉了它这是一个utf-8文件,无BOM它就当是ANSI了)。
而对于gcc和clang,其中ANSI和utf-8都会按照utf-8文件来读取,但是对于clang,string中的非utf-8字符会报错。而其他编码,gcc和calng都不支持,不过,gcc会报出一系列很难看懂的错误,但是clang会贴心地告诉你:
fatal error: UTF-16 (LE) byte order mark detected in 'test.cpp', but encoding is not supported
1 error generated.
总结如下
- cl.exe默认编码是ANSI,它会将其他文件先转化成ANSI编码,再编译。但是对于无BOM的utf-8文件,会当成ANSI从而可能出错。
- gcc和clang都默认utf-8,clang拒绝ANSI的string(不过注释可以),gcc会把ANSI也当成utf-8,其他编码源文件都不支持。