一. Visual Studio字符集

使用Visual Studio创建的C++工程可以在工程属性配置属性-->常规中配置字符集:使用Unicode字符集(默认)、使用多字节字符集
如图:

这个设置项不会对编译器处理字符编码产生直接的影响(注意这里的“直接”二字,第3节会说到),只会在工程属性配置属性-->C/C++-->预处理器加入相应的宏:

使用Unicode字符集 --> _UNICODE和UNICODE宏
使用多字节字符集   --> _MBCS宏

这几个宏一般用来判断是使用char还是wchar_t,在系统API中使用比较多,如MessegeBox通过是否定义了UNICODE宏来决定是使用LPCSTR还是LPCWSTR(LPCSTR即const char*, LPCWSTR即const wchar_t*):

#ifdef UNICODE
#define MessageBox  MessageBoxW
#else
#define MessageBox  MessageBoxA
#endif // !UNICODE

二. 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. 若文件开始处有BOM(EF BB BF),则判定为UTF-8编码;
2. 若没有BOM,则试图从文件的前8个字节来判断文件是否像UTF-16编码,如果像,则就判断为UTF-16编码。
3. 如果既没BOM,也不是UTF-16编码,则使用系统当前的代码页(简体中文操作系统为CP936)。

不了解字符编码的朋友可以参考前一篇博客拨开字符编码的迷雾–字符编码概述

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是如何间接影响到编译器对字符编码的处理了:

Visual Studio字符集设置|
决定声明哪一个宏(UNICODE还是_MBCS宏)|
宏又决定了API参数使用char还是wchar_t|
编译器在进行【执行字符集】编码时对char和wchar_采用不同的处理方式,从而对字符编码产生了影响。

在Visual Studio 2010(含)之后,支持使用# pragma execution_character_set来设置执行字符集。

四. 实例分析

  • 已知汉字“中”的各种编码如下:
GBK        D6 D0
Unicode    2D 4E
UTF-8      E4 B8 AD
  • 函数DumpCharacterCode用于按字节打印内存中的数据:
void DumpCharacterCode(const char* pChar, int iSize) {for(int i = 0; i < iSize; i++) {char a = *pChar++;printf("%02X ", a & 0xff);}printf("\n");
}
  • 设置系统代码页的方法:
    “控制面板” –> “区域和语言” –> “管理” –> “非Unicode程序的语言” –> “更改系统区域设置”

  • Visual Studio保存文件到指定编码方法:
    “文件” –> “高级保存选项”

4.1 测试编译器处理窄字符编码

测试代码如下:

int _tmain(int argc, _TCHAR* argv[])
{char buf[100] = {"中"};   // charDumpCharacterCode(buf, 2);  // 也可以打印4个字节return 0;
}

针对不同的系统代码页和源文件编码,打印出的汉字“中”的编码分别为:

测试用例 系统代码页 保存源文件编码 编译器判断文件采用的编码 源字符集(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); 会发现弹出的对话框中显示仍然是乱码。
可以使用下面的代码进行测试(ANSIToUTF8、UTF8ToANSI函数见拨开字符编码的迷雾–字符编码转换):

int _tmain(int argc, _TCHAR* argv[])
{char buf[3] = { 0 };    // 模拟CP936编码的“中”buf[0] = 0xD6;buf[1] = 0xD0;std::string strUTF8 = ANSIToUTF8(buf, 1252);char *p = (char*)strUTF8.c_str();  // 通过visual studio查看指针p处内存为: C3 96 C3 90std::string str = UTF8ToANSI(strUTF8, 1252);p = (char*)str.c_str();   // 通过visual studio查看指针p处内存为: D6 D0return 0;
}

4.2 测试编译器处理宽字符编码

测试代码如下:

int _tmain(int argc, _TCHAR* argv[])
{wchar_t buf[100] = {L"中"};   // wchar_tDumpCharacterCode((char*)buf, 4); // 打印4个字节return 0;
}

同样,针对不同的系统代码页和源文件编码,打印出的汉字“中”的编码分别为:

测试用例 系统代码页 保存源文件编码 编译器判断文件采用的编码 源字符集(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环境下都正常编译,且运行起来没有乱码的程序,需要遵循如下原则:
1. 代码文件采用UTF-8 with BOM编码。
2. Visual Studio字符集设置为Unicode字符集。
3. 使用wchar_t。

做到上面3步,你的代码被别人从github上clone下来编译,不会因为你代码中含有中文等字符,产生类似error C2015这样的编译错误,更不会产生乱码。

本文介绍的方法只用来解决硬编码字符乱码的问题,至于数据传输中的乱码,需要统一字符编码来解决。

参考: https://blogs.msdn.microsoft.com/vcblog/2016/02/22/new-options-for-managing-character-sets-in-the-microsoft-cc-compiler

拨开字符编码的迷雾--编译器如何处理文件编码相关推荐

  1. HM-16.0编码过程:将YUV文件编码成HEVC格式的码流

    HM-16.0编码:将YUV文件编码成HEVC格式的码流 注: 1   为了快速优化运行(不调程序的时候),可以将程序的版本设为"release",否则还是设为"debu ...

  2. linux ubuntu编码转换,Ubuntu中的文件编码转换

    find default -type d -exec mkdir -p utf/{} \; find default -type f -exec iconv -f GBK -t UTF-8 {} -o ...

  3. python文件编码与解码_Python读取文件编码解码问题

    原博文 2018-04-19 12:51 − #### 用chardet检测编码 ``` import chardet raw = open("model.json", 'rb') ...

  4. linux下查看文件编码及修改编码

    linux下查看文件编码及修改编码 查看文件编码 在Linux中查看文件编码可以通过以下几种方式: 1.在Vim中可以直接查看文件编码 :set fileencoding 即可显示文件编码格式. 如果 ...

  5. linux检测文件名编码,Linux下查看文件编码,文件或文件名编码格式转换 | 缥缈的云...

    如果你需要在Linux中操作windows下的文件,那么你可能会经常遇到文件编码转换的问题.Windows中默认的文件格式是GBK(gb2312),而Linux一般都是UTF-8.下面介绍一下,在Li ...

  6. 使用vim转换文件编码

    使用命令:set fileencoding=utf-8设置目标文件编码,然后保存 附:vim编码原理 Vim 有四个跟字符编码方式有关的选项,encoding.fileencoding.fileenc ...

  7. linux windows文件 编码_一站式解读彻底搞懂Python编码

    " Python的编码问题,有一个完整的体系.如果不从整体上进行讨论,总是云里雾里的." 从敲代码到屏幕看到字符,涉及好几个关键的环节.每一个环节对于字符的正常输出都有很大的影响. ...

  8. linux 编码文件,linux文件编码

    linux下新建一个文件,或采用fopen新建,那么文件的编码是什么? 怎么查看文件编码格式: 查看文件编码file命令 file ip.txt ip.txt: UTF-8 Unicode text, ...

  9. linux 文件格式latin1,Linux下查看文件编码,文件编码格式转换和文件名编码

    如果你需要在中操作windows下的文件,那么你可能会经常遇到文件编码转换的问题.Windows中默认的文件格式是GBK(gb2312),而Linux一般都是UTF-8.下面介绍一下,在Linux中如 ...

最新文章

  1. 25~50K|云视科技SLAM算法工程师/机器人算法软件工程师招聘(社招+实习)
  2. 《LeetCode力扣练习》第141题 环形链表 Java
  3. [Leedcode][JAVA][第820题][字典树][Set]
  4. vmware linux版本_vmware无法在kali下打开问题
  5. SPOJ COT Count on a tree(主席树+倍增lca)
  6. django 安装/部署过程
  7. GPS 的物理数学原理
  8. Lucene(.net)学习
  9. 计算机设备资产台帐,小学固定资产台账表
  10. 谈谈编程(2) 软件开发中的方法论
  11. 嵌入式实操----基于RT1170 首板硬件之CAN BUS TJA1043显示调试(十八)
  12. IT十年人生过客-二十九-结婚
  13. JavaScript函数——输入某年某月某日,判断这一天是一年中的第几天
  14. html整体框架的大小,html如何动态改变框架的大小
  15. 现实迷途 第二十章 峰回路转
  16. 泰坦尼克号沉船生还预测
  17. 国开网计算机应用基础模块4,2020春国开《-计算机应用基础》模块4 PowerPoint2010客观题...
  18. 跟sky学数字IC/FPGA设计学习培训课程:全集已出
  19. 玩转华为数据中心交换机系列 | 配置VLAN内协议报文透传示例
  20. SQLServer Job 邮件发送

热门文章

  1. 隔壁老王的iptables防火墙
  2. 浅谈屏蔽搜索引擎爬虫(蜘蛛)抓取/索引/收录网页的几种思路
  3. 摩斯密码php,普及一下LOL中的摩斯密码 绝对的干货
  4. windows mobile数据同步方案
  5. subscript on non-array or too many dimensions
  6. ASO优化常见的问题精选之ASO指数是什么?
  7. GIT创建版本库及版本的迭代
  8. python中的matplotlib绘图
  9. java获取百度实时天气(无限制)
  10. 戴尔910服务器系统安装教程,图解戴尔dell工作站T3600/T5600/T7600/T7910安装windows 7系统...