本文主要介绍微软 MSVC 编译器如何加载和处理 C++代码文件。
一. Visual Studio 字符集
使用 Visual Studio 创建的 C++工程可以在工程属性配置属性-->常规
中配置字符集:使用Unicode字符集
(默认)、使用多字节字符集
。
如图:
但这个设置项不会对编译器处理字符编码产生直接的影响(注意这里的“直接”二字,第 3 节会说到),只会在工程属性配置属性-->C/C++-->预处理器
加入相应的宏:
1 | 使用Unicode字符集 --> _UNICODE和UNICODE宏 |
这几个宏一般用来判断是使用 char 还是 wchar_t,在系统 API 中使用比较多,如 MessegeBox 通过是否定义了 UNICODE 宏来决定是使用 LPCSTR 还是 LPCWSTR(LPCSTR 即 const char*, LPCWSTR 即 const wchar_t*):
1 |
二. char 和 wchar_t
上面提到了,定义 API 时通过UNICODE 宏来决定是使用 char 还是 wchar_t类型,那么 char 和 wchar_t 有什么不同了?
char 和 wchar_t 是标准 C/C++字符类型,并不是 Windows 特有的。 char 固定占 1 个字节,wchar_t 固定占 2 个字节,从内存的角度来看,char、wchar_t 和其他数据类型一样,只是代表一段内存块,用来存储固定长度的二进制 0 或 1。 在编程时,我们一般习惯于将字符串储到 char 或 wchar_t 定义的内存空间中,将整形存储在 int 定义的内存空间中。
所以,用 char 还是 wchar_t 来存储字符,只是内存分配和数据存储上面的事情,它们本身也是与字符编码无直接关系的( 同样注意这里的“直接”二字,第 3 节会说到)。
三. 编译器如何处理硬编码字符
VC++编译器编译源代码的步骤中,涉及编码处理的步骤主要有 2 个:
第 1 步:预处理
1.1) 读取源文件,判断源文件采用的字符编码类型。(这一步不会改变文件内容)
1 | 编译器判断源文件编码类型的步骤为: |
1.2) 将源文件内容转成源字符集
(Source Character Set),默认为 UTF-8 编码。
第 2 步:链接
2.1) 将 1.2 中得到的 UTF-8 转为执行字符集
(Execution Character Set):
- 对于宽字符串(即 C/C++中以
L
标记的串,如L"abc"
,L'中'
),执行字符集
为 UTF-16 编码。 - 对于窄字符串(和宽字符串对应,即不以
L
标记的串),执行字符集
为系统当前的代码页。
现在我们就可以说清楚 Visual Studio 字符集设置、char、wchar*t 是如何***间接_**影响到编译器对字符编码的处理了:
1 | Visual Studio字符集设置 |
在 Visual Studio 2010(含)之后,支持使用
#pragma execution_character_set
来设置执行字符集。
四. 实例分析
- 已知汉字“中”的各种编码如下:
1 | GBK D6 D0 |
- 函数
DumpCharacterCode
用于按字节打印内存中的数据:
1 | void DumpCharacterCode(const char* pChar, int iSize) { |
设置系统代码页的方法:
“控制面板” –> “区域和语言” –> “管理” –> “非 Unicode 程序的语言” –> “更改系统区域设置”Visual Studio 保存文件到指定编码方法:
“文件” –> “高级保存选项”
4.1 测试编译器处理窄字符编码
测试代码如下:
1 | int _tmain(int argc, _TCHAR* argv[]) |
针对不同的系统代码页和源文件编码,打印出的汉字“中”的编码分别为:
测试用例 | 系统代码页 | 保存源文件编码 | 编译器判断文件采用的编码 | 源字符集(Source Character Set) | 执行字符集(Execution Character Set) | 打印输出 |
---|---|---|---|---|---|---|
用例 1 | 简体中文 CP936 | 简体中文 CP936 | 简体中文 CP936 | UTF-8 | 简体中文 CP936 | D6 D0 |
用例 2 | 简体中文 CP936 | UTF-8 BOM | UTF-8 BOM | UTF-8 | 简体中文 CP936 | D6 D0 |
用例 3 | 简体中文 CP936 | UTF-8 | 简体中文 CP936 | UTF-8 | 简体中文 CP936 | 编译错误(C2146) |
用例 4 | 西欧 CP1252 | 简体中文 CP936 | 西欧 CP1252 | UTF-8 | 西欧 CP1252 | D6 D0 |
用例 5 | 西欧 CP1252 | UTF-8 BOM | UTF-8 BOM | UTF-8 | 西欧 CP1252 | 3F 00 |
表格中列 4~6 依次对应编译处理源文件的几个步骤。3F
对应的 ASCII 字符为?
,编译器遇到不能识别的字符时,就会用?
来替代。 出现?
的情况会伴随着编译警告C4566
。
上面出现了 1 次3F
(用例 5),导致乱码的原因是UTF-8 --> 西欧 CP1252
. 西欧 CP1252
也就是 ASCII 的扩展,不支持汉字,所以用3F
替代。
用例 3 为什么会编译错误?
微软的编译器只能识别带 BOM 的 UTF-8,用例 3 的 UTF-8 没带 BOM,编译器会判定源文件编码为系统当前代码页 CP936。“中”的 UTF-8 编码为E4 B8 AD
,列 5 执行从 CP936 到 UTF-8 转换之后变成了E6 B6 93 3F
,列 6 再要将E6 B6 93 3F
转换为 CP936 肯定是转换不回去的,相当于 UTF-8(1) –> UTF-8 (2),再将 UTF-8(2)转换回 CP936,这时肯定得到的字符不是原来的字符了。
用例 4 为什么输出的D6 D0
,而不是3F
?
对着用例 4 的各个顺序来看,源文件通过 CP936 保存着,但编译器通过 CP1252 来读取的,CP1252 就是 ASCII 扩展,单字节的,虽然此时显示为乱码,但各字节仍然是 D6 D0;然后将读取到的文件内容从 CP1252 转成 UTF-8 编码,转码后为 C3 96 C3 90;然后再将 UTF-8 编码转回为 CP1251,转码就又变成了 D6 D0。 但这个D6 D0
在 CP1252 中是无法显示的,如果我们在用例 4 加入MessageBoxA(NULL, "中", "test", MB_OK);
会发现弹出的对话框中显示仍然是乱码。
可以使用下面的代码进行测试:
1 | int _tmain(int argc, _TCHAR* argv[]) |
4.2 测试编译器处理宽字符编码
测试代码如下:
1 | int _tmain(int argc, _TCHAR* argv[]) |
同样,针对不同的系统代码页和源文件编码,打印出的汉字“中”的编码分别为:
测试用例 | 系统代码页 | 保存源文件编码 | 编译器判断文件采用的编码 | 源字符集(Source Character Set) | 执行字符集(Execution Character Set) | 打印输出 |
---|---|---|---|---|---|---|
用例 1 | 简体中文 CP936 | 简体中文 CP936 | 简体中文 CP936 | UTF-8 | UTF-16 | 2D 4E 00 00 |
用例 2 | 简体中文 CP936 | UTF-8 BOM | UTF-8 BOM | UTF-8 | UTF-16 | 2D 4E 00 00 |
用例 3 | 简体中文 CP936 | UTF-8 | 简体中文 CP936 | UTF-8 | UTF-16 | 编译错误(C2146) |
用例 4 | 西欧 CP1252 | 简体中文 CP936 | 西欧 CP1252 | UTF-8 | UTF-16 | D6 00 D0 00 大小端 |
用例 5 | 西欧 CP1252 | UTF-8 BOM | UTF-8 BOM | UTF-8 | UTF-16 | 2D 4E 00 00 |
五. 彻底避免硬编码字符乱码
通过第 3 节的说明,很容易知道,要开发支持多语言,在任意语言(系统代码页)的 windows 环境下都正常编译,且运行起来没有乱码的程序,需要遵循如下原则:
- 代码文件采用 UTF-8 with BOM 编码。
- Visual Studio 字符集设置为 Unicode 字符集。
- 使用 wchar_t。
做到上面 3 步,你的代码被别人从 github 上 clone 下来编译,不会因为你代码中含有中文等字符,产生类似error C2015
这样的编译错误,更不会产生乱码。
本文介绍的方法只用来解决硬编码字符乱码的问题,至于数据传输中的乱码,需要统一字符编码来解决。
文章图片带有“CSDN”水印的说明:
由于该文章和图片最初发表在我的CSDN 博客中,因此图片被 CSDN 自动添加了水印。