文章目录
概述
在整理 《QtCreator 下使用MSVC编译器》这篇文章时,回想起当年并没有彻底搞明白 “项目属性 -> 常规 -> 项目默认值 -> 字符集” 配置项的作用。且我坚信,搞明白此问题,那么 Qt + MSVC集成开发环境下的一些原有问题将会不攻自破。于是单独拎出来整理啦此篇文章。整理此篇文章时,我卡壳了,期间去整理发布了 《IDE /字符编码与文本文件(如cpp源代码文件)》,关于字符编码识别技术、BOM等基本内容可以参考其中的相关描述。
对配置项的基础测试
在此之前,我整理了《IDE /Qt Creator 文本编辑器之文件编码设置》,因此参考其中的,文件编码配置项的功能测试情况,对 VS项目属性 - 常规 - 字符集 配置项进行测试。
VS默认的字符集配置
当你在VS下新建不同类型的项目时,生成的默认配置值是不同的,如我们新建的Qt项目,
如我们新建的控制台应用程序,
如上,新建不同类型的项目,其默认的字符集配置值是不一样。新建控制台应用程序,默认选择的是使用Unicode字符集,我挺意外的。另外经过实际的测试,如果此处没有选择任何配置,或选择了 “未设置”,那么VS编译器又会作何处理? 这个问题压到后面的章节来解答。
Unicode字符集和多字节字符集
Unicode字符集是一个全面的字符集,能够表示几乎所有的字符,包括全世界各种语言的字符、符号、表情等,共有数十万个字符,具有较好的跨平台支持,但可能占用较大的存储空间。多字节字符集适用于特定语言或地区,通常只包含一部分字符,例如GB2312是一个多字节字符集,它只包含了中文字符和一些特殊符号,此类字符集存储空间较小,但在全球化应用中可能存在局限性。Unicode字符集的优势在于它提供了一种统一的字符表示方式,可以表示几乎所有语言的字符,有利于跨语言和跨平台的文本处理和交换。
多字节字符集(MBCS)通常用于处理特定语言的字符,如中文(GBK、GB18030)、日文(Shift-JIS)、韩文(EUC-KR)等。
GB2312 于1980年发布,最初用于表示包括6763个汉字在内的字符集。采用双字节编码方式,每个汉字用两个字节表示,其中第一个字节的范围是0xA1到0xF7,第二个字节的范围是0xA1到0xFE。由于GB2312只包含有限的字符,对于一些生僻字和少数民族语言的字符无法表示。GB18030 于2000年发布,是GB2312的扩展版本,用于表示更多的字符,包括汉字、日文假名、朝鲜文、拉丁字母、希腊字母等。其采用变字节编码方式,每个字符的编码字节可以是1个、2个、4个,甚至8个字节,根据字符的复杂程度进行编码。GB18030可以兼容GB2312编码,同时支持更广泛的字符范围,是一个全面的中文字符编码方式。
ANSI(American National Standards Institute)字符集通常指的是Windows操作系统下的本地字符集,它是根据地区和语言设置而确定的字符编码方式。对于中国地区的Windows操作系统,默认的ANSI字符集可能是GB2312、GBK、GB18030 等。
需要注意的是,
这里指定字符集设置,但是并没有指定字符集的具体的字符编码方式。因此如果这里选择了适用Unicode字符集,则代码文件的编码格式必须为Unicode字符集下的UTF-8、UTF-16、UTF-32等编码方案? 这是问题,后边会有解答。
是否影响文本编辑器
在 《IDE /Qt Creator 文本编辑器之文件编码设置》一文中,我们见证了 Qt Creator 文本编辑器-文件编码配置是如何影响代码文件的字符编码格式的。所以很想知道,VS下的这个字符集配置项,是否有类似功能。如下,经理了摸着石头过河的阶段,时间不充裕的,可以跳过。
— 20230701 --Begin–
在VS2017下测试新建Qt项目,
我们新建 名为 EncodeM 的 Qt Widget 应用项目,其默认的字符集配置是未进行任何选择的"空",前边已经有过截图。我们打开工程下的 encodem.h、encodem.cpp、encodem.ui、main.cpp文件,查看它们的编码方式均为 UTF-8 Without BOM 编码格式。至于其中的原理,这里不再赘述,请参考《IDE /字符编码与文本文件(如cpp源代码文件)》
在 encodem.h 文件中输入一行中文注释 “//文件竟然是UTF-8无BOM格式的”,然后保存文件,重新查看该文件的编码方式。哟,变成了GB2312编码格式。
将,字符集配置为 “使用多字节字符集”,
在 encodem.cpp 文件中输入一行中文注释 “//文件竟然是UTF-8无BOM格式的”,然后保存文件,变GB2312编码格式。
将,字符集配置为 “使用Unicode字符集”,
在 mian.cpp 文件中输入一行中文注释 “//文件竟然是UTF-8无BOM格式的”,然后保存文件。依然变GB2312编码格式。
添加,新建项,C++类,ClassX,
ClassX.h、ClassX.cpp 依然全部是 UTF-8无BOM 格式,在它们中添加中文字符后保存,现象同上。即使现在是选择了字符集配置为 “使用Unicode字符集”。相比QtCreator的文本编辑器-文件编码的效用,这里是毫无卵用的。至此,便有理由推测,VS项目属性 - 字符集设置,不对文件编辑器有影响。
在VS2017下测试新建Win控制台项目,
新建 名为 EncodeD 的 Visual C++ Windows 桌面 控制台应用程序项目,查看EncodeD.cpp文件的编码,如下,
此时的默认文件编码是 UTF-8 带BOM 编码格式的,该编码格式才是VS的原生啊。添加,新建项,C++类,ClassX,ClassX.h、ClassX.cpp 全部是 UTF-8无BOM 格式,在它们中添加中文字符后保存,会变成GB2312编码格式。即使该新项目时被IDE默认选择了 “使用Unicode字符集” 配置。
— 20230701 --End–
这期间,我在整理 《IDE /字符编码与文本文件(如cpp源代码文件)》,快完成它的时候,心里敞亮了许多。
–后期测试- 20230712 --Begin–
我又重新进行了一次实验。新建名为 EncodeZ 的 Visual C++ Windows 桌面 控制台应用程序项目,
与前期测试结果相同,EncodeZ.cpp 是UTF-8带BOM文件。然后我们还是添加新建项,C++类,ClassH,它们还会被识别为UTF-8,原因我们都在《IDE /字符编码与文本文件(如cpp源代码文件)》中说明了。接着,我们复制 ClassH.h 为 ClassH_bak_1.h 和 ClassH_bak_2.h,然后操作如下:
1、使用记事本在ClassH_bak_1.h中添加 “//中国汉字” 字符串
2、使用Notepad++在ClassH_bak_2.h中添加 “//中国汉字” 字符串
3、使用VS文本编辑器,在ClassH.h中添加 “//中国汉字” 字符串
全部保存后,重新打开加载它们,可得,ClassH_bak_1.h和ClassH_bak_2.h是UTF-8编码的,ClassH.h是ANSI编码的。在《IDE /字符编码与文本文件(如cpp源代码文件)》的一个结论是: “文本文件以什么编码格式打开,编辑后,便会以什么编码保存”,记事本、Notepad、QtCreator都是这样的,我想以上述例子来反正,VS文本编辑器是以本地编码格式GB2312或GB18030来加载显示的代码文件。
在上述测试基础之上,我将 ClassH_bak_1.h 和 ClassH_bak_2.h 直接拖入到VS文本编辑器中,都能正确打开加载和显示,不会有QtCreator文本编辑器那样的字符编码不匹配的告警。这说明,VS文本编辑器以本地编码加载文件不成功的时候,会自动的去索引尝试其他编码格式。这是文本编辑器的基本功能,就像记事本程序那样。
–后期测试- 20230712 --End–
最后的结论,
VS项目属性 - 常规 - 项目默认设置 - 字符集,该配置,其功能与VS文件编辑器采用何种编码来加载文本文件、保存文本文件并没有半毛钱的关系!
后来为重新审视 Qt Creator + MSVC 组成的集成开发环境时,也更加认识到这一点,Qt Creator 文本编辑器文件编码配置 和 MSVC编译器配置,是完全不同的方向。而 VS字符集配置就是针对MSVC编译器的配置之一。VS并没有提供如 Qt Creator 文本编辑器文件编码配置这样的功能。
结合 《IDE /字符编码与文本文件(如cpp源代码文件)》中的相关分析,往 “文件以什么编码格式打开加载,编辑操作后便会以什么编码保存” 这个结论上靠拢的话,我们可以认为VS总是默认首先以本地编码格式加载文本类型的代码文件。如此,当你使用VS编辑器打开那些只用ASCII字符的文件时,若有中文字符输入和保存操作,则相关文件必定 “变为” ANSI编码。
使VS像记事本那样显示文件编码
默认情况下,Visual Studio 是不会直接显示代码文件的编码格式的。然而可以通过一些插件或扩展来实现在 Visual Studio 中显示代码文件的编码格式。例如,“File Encoding Info” 插件。该插件可以在 Visual Studio 的状态栏中显示当前打开文件的编码格式。从VS工具菜单中打开扩展和更新窗口,搜索下载安装即可。
经过实际测试,当前版本的此插件,质量欠佳。
不仅显示格式不漂亮,而且分析出来的文件编码类型是不对。实测,无论代码文件是UTF-8带BOM还是GB2312,均被此插件识别为显示为 Unicode(UTF-8) 格式。因此,我果断卸载了,并幻想着以后能自己写一个插件。
VS下编译UTF-8无BOM的代码文件
如下是在好几年前记录下来的,那个时候还在用VS2013 集成开发环境,或者是在用没加装update3的VS2015集成开发环境。原本此记录还有些别的描述,因为已经无从追查,怕引起其他歧义,我删除了当时的文字描述,只保留了测试代码和测试步骤。
#include <iostream>
using namespace std;
int main()
{
std::cout << "我爱你中国!\r\n";
system("pause");
return 0;
}
依稀还记得,这个测试应该是在试图把QtCreator上的工程搬移到VS下时进行的。因为通常QtCreator下创建的代码文件都是UTF-8无BOM的编码格式,在将这些源代码直接拿到VS编译时,遇到了乱码的问题。
新测试过程如下,在VS2017下新建空项目,新建main.cpp文件,输入上述代码段,保存。VS给项目默认配置了"使用多字节字符集";main.cpp文件编码格式被存储为ANSI格式,这个我们也了然于心。
VS可以搞定ANSI和带BOM的源代码文件
测试1 基于 ANSI编码的 main.cpp 文件 + 使用多字节字符集
编译项目,测试运行,当然可以在控制台上正确输出中文字符。
测试2 基于 ANSI编码的 main.cpp 文件 + 配置使用Unicode字符集
编译项目,测试运行,依然可以在控制台上正确输出中文字符。可按照理论分析,此时应该乱码才对?先压制下求知欲,待会再说。
测试3 基于 UTF-8带BOM编码的 main.cpp 文件 + 配置使用Unicode字符集
转变 main.cpp文件编码类型,保存,重新编译测试。依然可以在控制台上正确输出中文字符。
测试4 基于 UTF-8带BOM编码的 main.cpp 文件 + 配置使用多字节字符集
重新编译测试。依然可以在控制台上正确输出中文字符。
测试5 基于 UTF-16-BE编码的 main.cpp 文件
分别测试,置使用Unicode字符集、配置使用多字节字符集,均可以在控制台正确输出中文字符。
VS搞不定UTF-8无BOM的源代码文件
测试6 基于 UTF-8无BOM编码的 main.cpp 文件
这正是当年我记录下来的测试配置,当时试图把一些QtCreator的源代码文件搬到VS中进行重新编译。我知道此时的运行会存在中文乱码。今天我们就彻底来分析到底是这个乱码是如何产生的。
置使用Unicode字符集,运行输出,
配置使用多字节字符集,运行输出,
测试分析至此。似乎觉得,项目属性字符集配置,并没有影响到编辑器对代码文件的解析过程,
1、如果源代码文件是ANSI编码的,无论你配置了VS使用何种字符集,中文字符串总能被正确解析。
2、如果源代码文件是带BOM的,在不同VS字符集配置下,代码文件中的中文字符串同样的地总能被正确的解析。
3、唯独UTF-8无BOM,这种在其他地方推荐使用的文件编码格式,VS竟然不认。且,且,且,改变VS字符集配置并没有影响输出的乱码。
如上三条现象均将矛头指向一个客观 “从表面上看,VS字符集配置似乎没有影响到MSVC编译器加载代码文件时使用的编码”,因为无论是输出正确的字符,还是输出错误的字符,都看上去与VS字符集配置无关。
乱码字符是怎么翻译出来的?
既然,UTF-8无BOM格式的main.cpp文件编译后的程序,不能正确输出,那么,就搞它。我们使用WinHex打开UTF-8编码的main.cpp文本文件,
“空格” 的 UTF-8 编码是 20 //16进制数
“<” 的 UTF-8 编码是 3C
“引号” 的 UTF-8 编码是 0x22
“我” 的 UTF-8 编码是 E6 88 91
“爱” 的 UTF-8 编码是 E7 88 B1
“你” 的 UTF-8 编码是 E4 BD A0
“中” 的 UTF-8 编码是 E4 B8 AD
“国” 的 UTF-8 编码是 E5 9B BD
如上,高亮的 [20 3C 3C 20 22] 编码值就是代码文件中,“我爱你中国” 字符串前的 [ << " ] 文本内容。我们也能使用WinHex看到 “我爱你中国” 的UTF-8编码值,都能对的上。(上图,WinHex软件配置使用ANSI字符编码)
因此可以断定,VS编译器,把UTF-8无BOM的源代码文件,当做ANSI编码的文件来加载了。我们进一步来分析验证下,以打破心结。“鎴戠埍浣犱腑鍥斤紒” 这些生僻字,都在汉字编码中有自己的位置吗?
“鎴” 字是存在的,
进入国家标准全文公开系统,在搜索框中输入" 字符集、信息交换用汉字编码字符集、GB18030 " 等关键字,可以找到相关标准,在线预览或者下载。在GB2312中找不到这个字。我下载了GB18030-2005版本(Win10在2015年发布,这是最相近的版本;GB18030-2022尚未正式启用)。在GB18030-2005版本中可以找到其对应的编码是,
但是93B4这个编码,并无法匹配上WinHex中显示的 E6 88 编码值 。头大啦,不想搞了。
在 Unicode官网的 Code Charts 中搜索 93B4,
打开文件U4E00.pdf,可以找到,
再尝试,
使用Windows接口,将Unicode码点转换成,UTF-8编码值,
#include <Windows.h>
int main() {
// Unicode 码点
wchar_t unicodeCodePoint = L'\u93B4'; //输出 E9 8E B4
// 转换为 UTF-8 编码
int bufferSize = WideCharToMultiByte(CP_UTF8, 0, &unicodeCodePoint, 1, NULL, 0, NULL, NULL);
char* utf8Buffer = new char[bufferSize];
WideCharToMultiByte(CP_UTF8, 0, &unicodeCodePoint, 1, utf8Buffer, bufferSize, NULL, NULL);
for (int i = 0; i < bufferSize; i++)
printf("UTF-8:0x%.2x\n", (char)utf8Buffer[i]);
delete[] utf8Buffer;
system("pause");
return 0;
}
码点 u+93B4 转换成的UTF-8编码值为 E9 8E B4,没有与WinHex中显示的编码值匹配。
通常,
当一个编码值在特定字符集的编码表中找不到对应的字符时,文本编辑器或其他输出设备通常会使用一些默认的替代字符或占位符来表示这些无法解析的编码值。常见的替代字符包括问号 “?”、方块 “□”、空格 " " 或其他特殊符号。具体的展示方式取决于编辑器或设备的实现,可能存在一些差异。
放弃,
“鎴戠埍浣犱腑鍥斤紒” 这些生僻字,到底是根据哪个字符编码翻译出来的啊!不搞了,留给以后得自己吧。剩下的,当下没有搞明白的,将放到 《字符串/多字节宽字节之间的转换与字符编码的关联》中继续奋战。
猜猜看
虽然我最终没有在GB1080-2005中成功匹配示例程序中打印出来生僻字,但是在控制台中的打印结果与在WinHex使用ANSI ASCII字符编码时显示的内容字符,是几乎完全一样的,都包含 “鎴戠埍浣犱腑鍥斤” 字符。因此有理由推测,VS将UTF-8无BOM的代码文件当做ANSI字符编码来读取了。
如果如上猜测是正确的,那么VS编译器读取代码文件时的步骤大概如下,
如果有BOM,则按照BOM指定的编码类型来加载,如果没有则一律安装ANSI来加载。UTF-8无BOM正中入坑。
再起航
虽然我还是没有明确地解决上述问题,但我选择了曲线救国。
新建一个1.txt文本文件(UTF-8无BOM编码),在其中输入 “鎴” 保存,使用WinHex打开,看见 E9 8E B4。
新建一个2.txt文本文件(ANSI编码),在其中输入 “鎴” 保存,使用WinHex打开,确实看见 EE B6。
新建一个3.txt文本文件(GB2312编码),在其中输入 “鎴” 保存。记事本打开它,显示编码格式为ANSI。使用WinHex打开,确实看见 EE B6.
新建一个4.txt文本文件(GB2312编码),在其中输入 “中国汉字” 保存。记事本打开它,显示编码格式为ANSI。
因此,可以完全确定 E6 88 就是 本地编码中的汉字 “鎴” 。VS的MSVC编译器就是把UTF-8无BOM的源代码文件当ANSI来搞了。
使VS/MSVC可以正确解析UTF-8代码文件
办法是有的,之前也做过尝试。在《IDE /C4819: 该文件包含不能在当前代码页(936)中表示的字符》文中也有提及。主要参考microsoft 设置源字符集、设置执行字符集、将源字符集和执行字符集设置为 UTF-8 等官方帮助文档。
在以前VS2015或更低版本的时候,我是用过如下指令的,将其放在cpp的开头,我记得测试是生效的,但是如今VS2017已经不识别此指令,可能是废弃了。
//原本是支持的
#pragma execution_charset_set("utf-8")
//打我记事就没有此指令
#pragma source_charset_set("utf-8")
按照上文中官方帮助提示,进行如下设置,/source-charset:utf-8
在上述配置下,确实是生效的。此时VS可以正确编译UTF-8无BOM的源代码文件,不会有乱码。且无论,VS字符集配置成什么类型,都不影响。
难不成 VS项目属性字符集配置是执行字符集
在起初我推测VS项目属性字符集配置是源字符集配置,这与思维定式有关,因为我一开始错误的想把这个配置往QtCreator文本编辑器配置功能上靠拢。
但是经过上文几个章节的测试和分析,它不像是源字符集配置,倒像是执行字符集配置。在我更细致的了解到执行字符集的含义后,也更加坚定了此想法,为此我做了如下测试。
将“我爱你中国”写入文本文件,分别保存为ANSI格式和UTF-8格式,使用WinHex查看其二进制编码值,
microsoft 设置执行字符集 文中提到,
在将源代码编译位可执行文件时,编译器会将源代码中的字符转换成可执行程序中的二进制数据。由于不同字符集采用不同的编码方式,因此在编译时必须确定所使用的编码方式,以便程序运行时可以正确读取和显示字符。
细细品味,上述过程与我们前边大篇幅讨论的“编译器词法分析前使用何种方式加载代码二进制文件为字符数据”的这个过程,它们使用字符集的目的是相反的。上述过程,是将字符转换为二进制数据,而之前讨论的是如何将二进制数据加载为字符。
将 main.cpp 存储为 UTF-8-BOM 编码, 选择字符集设置为 使用多字节字符集,使用WinHex查看生成的可执行文件,
注:使用菜单栏 - 搜索 - 查找16进制数值,“CED2” ,注意不要加空格。
结论1,
UTF-8-BOM 编码的代码文件,在使用使用多字节字符集配置的情况下,编出来的可执行程序中。原本代码文件中的UTF-8编码的字符串,现以其ANSI编码的二进制编码值存在于VsCode.exe这个可执行程序中。
继续测试,
在上文基础上,改变VS字符集设置为 使用Unicode字符集,使用WinHex查看生成的可执行文件,
我原本猜测的结果是,二进制的可执行文件中,“我爱你中国” 会以Unicode码点值或者UTF-16编码值存在。但结果却是,它们依然是ANSI编码值。将程序编译为Release版本来进行分析,依然如此。
继续测试,
在项目属性 C/C++命令行中 配置 /execution-charset:utf-8,重新编译,查看,
分析可知,上述对执行字符集的配置是生效的,可执行文件中的字符串,此刻已经是UTF-8存储的啦。同时,这也说明,VS项目属性中的字符集配置,不对编译器执行字符编码配置有影响。
其他,
在 QtCreator 下的pro工程文件中,理论上使用 QMAKE_CXXFLAGS += /source-charset:utf-8 /execution-charset:utf-8 后,可以向MSVC编译器传递编译参数,但是我当时没有测试成。这个有机会再搞。
至此,可得。VS项目属性中的字符集配置,不是源字符集设置,也不是执行字符集设置。那么,它到底是啥?
Windows API 之 A 和 W 接口
以CreateDirectory这个WinAPI为例子,我们都知道它有两个定义,
#ifdef UNICODE
#define CreateDirectory CreateDirectoryW
#else
#define CreateDirectory CreateDirectoryA
#endif // !UNICODE
看到 UNICODE 这个宏了没有,难不成 VS项目属性字符集配置,只是影响了该宏的定义?
#include <iostream>
#include <Windows.h>
using namespace std;
int main()
{
#ifdef UNICODE
std::cout << "我爱你Unicode!\r\n";
#else
std::cout << "我爱你ANSI!\r\n";
#endif // !UNICODE
LPCWSTR folderPath = L"D:\\path\\to\\your\\folder";
if (CreateDirectory(folderPath, NULL)) {
// ...
}
else {
// ....
}
system("pause"); return 0;
}
保持 main.cpp 源代码文件为 UTF-8-BOM 编码格式,先配置使用Unicode字符集,
可以看到此时 UNICODE 被定义,CreateDirectory 实为 CreateDirectoryW,程序可正常编译。
修改VS字符集配置为 使用多字节字符集,
可以看到此时 UNICODE 未被定义,CreateDirectory 实为 CreateDirectoryA,程序编译报错,
E0167 “LPCWSTR” 类型的实参与 “LPCSTR” 类型的形参不兼容 VsCodeT E:\TestEncode\VsCodeT\main.cpp 15
总结
至此,算是尘埃落定了吧。VS项目属性 - 常规 -项目默认配置 - 字符集配置,这鸟东西就是影响了 编译器预处理器宏 UNICODE 是否被定义。
如果你没有使用WinAPI的与字符串有关的那些接口,该宏的作用似乎不是很多。估计这也是新建Qt项目时,没有去指定它的原因吧。我们再新建个Qt项目,测试下其在不配置该VS字符集配置项时,UNICODE是否被定义了。结果是,VS新建的Qt项目,VS字符集配置为空,其实默认是 使用Unicode字符集,而不是 使用多字节 字符集,如下。
在即将结束此文整理的那天,我也在 设置执行字符集 这篇文章中找到了确切的VS如何加载代码文件的过程:
默认情况下,Visual Studio会检测字节顺序标记(Byte Order Mark,BOM)来确定源文件是否使用编码的Unicode格式,如UTF-16或UTF-8。如果没有找到字节顺序标记,源文件将被假定为使用当前用户代码页进行编码,除非通过 /source-charset 选项指定了字符集名称或代码页。
写在最后,
现在来看,标题中的问题仅仅是一个小问题,但是为了彻底搞明白它,前后跨越了3年,累计花费了10小时以上的时间。这个过程中扯出来很多其他关联的问题,收获还是蛮多的。文章中若有不合理的地方,欢迎批评指正。