Microsoft C / C ++编译器已经与DOS,16位Windows和32/64位Windows一起发展。 在此期间,它对不同字符集,代码页和Unicode的支持也发生了变化。 本文将解释我们的编译器在过去的工作方式,还介绍了 C / C ++编译器提供的一些新开关,特别支持BOM-less UTF-8文件和控制执行字符集。 请下载并尝试一下 。
网上有很多很棒的资源,详细描述Unicode,DBCS,MBCS,代码页和其他内容。 我不会在这里重现这一点,并会很快涵盖基本概念。Unicode联盟网站是了解更多关于Unicode的好地方。
理解编译器如何处理不同的字符集有两个主要方面。 首先是它如何解释源文件(源字符集)中的字节,其次是它写入二进制文件(执行字符集)的字节数。 了解源代码本身如何编码并存储在磁盘上很重要。
显式指示Unicode编码
有一种使用BOM(字节顺序标记)表示Unicode文件的标准方法。 此BOM可以指示UTF-32,UTF-16和UTF-8,以及它是大端还是小端。这些由通过将U + FEFF字符编码为正在使用的任何编码而产生的字节序列来表示。 UTF-8编码为字节流,因此不需要指示字节的实际“顺序”,但UTF-8的指示符通常仍称为“BOM”。
隐含的编码指示
在支持Unicode之前的Windows(和DOS)的早期,存储了文本文件,但没有指出文件正在使用什么编码。 这是应用程序如何解释这一点。在DOS中,ASCII范围以外的任何字符都将使用内置于视频卡的内容输出。 在Windows中,这被称为OEM(437)代码页。这包括一些非英文字符以及一些用于围绕文本绘制框的线条绘制字符。
Windows最终添加了对DBCS(双字节字符集)和MBCS(多字节字符集)的支持。 目前还没有标准的方式来指示文本文件的编码是什么,并且通常使用系统的当前代码页设置的字节来解释字节。 当32位Windows到达时,它具有用于UTF-16的单独API和用于所谓的“ANSI”API的另一组API。 这些API采用8位字符,使用系统的当前代码页进行解释。
注意:在Windows中,您无法将代码页设置为Unicode代码页(UTF-16或UTF-8),因此在很多情况下,没有简单的方法可以让旧版应用程序理解没有代码的Unicode编码文件BOM。
现在在不使用BOM的情况下以UTF-8编码文件也很常见。 这是大多数Linux环境中的默认设置。 虽然许多Linux工具可以处理BOM,但大多数工具不会生成一个。 没有BOM实际上使许多事情变得更简单,例如连接文件或附加到文件,而不必担心谁将写入BOM。
Microsoft C / C ++编译器如何从文件中读取文本
在过去的某个时候,Microsoft编译器被更改为在内部使用UTF-8。 所以,当从磁盘读取文件时,它们将被即时转换为UTF-8。如果文件具有BOM,我们使用它并使用指定的任何编码读取文件并将其转换为UTF-8。 如果文件没有BOM,我们通过查看前8个字节来尝试检测UTF-16编码的小端和大端形式。 如果文件看起来像UTF-16,我们会将它看作是文件上有一个UTF-16 BOM。
如果没有BOM并且看起来不像UTF-16,那么我们使用当前代码页(对GetACP的调用结果)将磁盘上的字节转换为UTF-8。这可能会也可能不会是正确的,这取决于文件如何实际编码以及它包含哪些字符。 如果文件实际上编码为UTF-8,则系统代码页不能设置为CP_UTF8,因此这将永远不会正确。
执行字符集
理解“执行字符集”也很重要。 基于执行字符集,编译器将不同地解释字符串。 我们来看一个简单的例子来开始。
const char ch ='h';
const char u8ch = u8'h';
const wchar_t wch = L'h';
const char b [] =“h”;
const char u8b [] = u8“h”;
const wchar_t wb [] = L“h”;
上面的代码将被解释为您输入了该内容。
const char ch = 0x68;
const char u8ch = 0x68;
const wchar_t wch = 0x68;
const char b [] = {0x68,0};
const char u8b [] = {0x68,0};
const wchar_t wb [] = {0x68,0};
无论文件编码还是当前代码页,这都应该是非常有意义的并且是真实的。 现在,让我们看看下面的代码。
const char ch ='屰';
const char u8ch ='屰';
const wchar_t wch = L'屰';
const char b [] =“屰”;
const char u8b [] = u8“屰”;
const wchar_t wbuffer [] = L“屰”;
注意:我随机选取了这个字符,但它似乎是汉字,意思是“不服从”,这似乎适合我的目的。 它是Unicode U + 5C70字符。
我们有几个因素需要考虑。 如何编码包含此代码的文件? 那么我们正在编译的系统的当前代码页是什么? 在UTF-16中,编码是0x5C70,在UTF-8中它是序列0xE5,0xB1,0xB0。 在936代码页中,它是0x8C,0xDB。 它不能在代码页1252(Latin-1)中表示,这正是我正在运行的。1252代码页通常在Windows上以英文和其他许多西方语言使用。 表1显示了在使用代码页1252的系统上运行时各种文件编码的结果。
表1 - 编译各种编码的代码时的结果示例。
文件编码 | UTF-8 w / BOM | UTF-16LE不带BOM | UTF-8不含BOM | DBCS(936) |
源文件中的字节代表屰 | 0xE5,0xB1,0xB0 | 0x70,0x5C | 0xE5,0xB1,0xB0 | 0x8C,0xDB |
来源转换 | UTF8 - > UTF8 | UTF16-LE - > UTF-8 | 1252 - > UTF8 | 1252 - > UTF-8 |
内部(UTF-8)表示 | 0xE5,0xB1,0xB0 | 0xE5,0xB1,0xB0 | 0xC3,0xA5,0xC2,0xB1,0xC2,0xB0 | 0xC5,0x92,0xC3,0x9B |
转换为执行字符集 | ||||
char ch ='屰'; UTF-8 - > CP1252 | 0x3F的* | 0x3F的* | 0XB0 | 位于0xDB |
char u8ch = u8'屰'; UTF-8 - > UTF-8 | 错误C2015 | 错误C2015 | 错误C2015 | 错误C2015 |
wchar_t wch = L'屰'; UTF-8 - > UTF-16LE | 0x5C70 | 0x5C70 | 0x00E5 | 0x0152 |
char b [] =“屰”; UTF-8 - > CP1252 | 0x3F,0 * | 0x3F,0 * | 0xE5,0xB1,0xB0,0 | 0x8C,0xDB,0 |
char u8b [] = u8“屰”; UTF-8-> UTF-8 | 0xE5,0xB1,0xB0,0 | 0xE5,0xB1,0xB0,0 | 0xC3,0xA5,0xC2,0xB1,0xC2,0xB0,0 | 0xC5,0x92,0xC3,0x9B,0 |
wchar_t wb [] = L“屰”; UTF-8 - > UTF-16LE | 0x5C70,0 | 0x5C70,0 | 0x00E5,0x00B1,0x00B0,0 | 0x0152,0x00DB,0 |
星号(*)表示警告C4566是为这些产生的。 在这些情况下,警告是“由通用字符名称代表的字符'\ u5C70'不能在当前代码页中表示(1252)”
错误C2015是“太多字符不变”
这些结果可能不会像字母'h'这个简单的例子那么有意义,但是我会在每种情况下看看发生了什么。
在第一列和第二列中,我们知道文件的编码是什么,所以转换为UTF-8的内部表示正确的是0xE5,0xB1,0xB0。 但是,执行字符集是Windows代码页1252,并且当我们尝试将Unicode字符U + 5C70转换为该代码页时,它将失败并使用0x3F(这是问号)的默认替换字符。 我们发出警告C4566,但使用转换后的0x3F字符。 对于u8字符文字,我们已经是UTF-8形式,不需要转换,但是我们不能在一个字节中存储三个字节,因此会发出错误C2015。 对于宽文字,“宽执行字符集”始终是UTF-16,所以宽字符和宽字符串都能正确转换。 对于u8字符串文字,我们在内部已经处于UTF-8格式,并且没有转换完成。
在第三列(没有BOM的UTF-8)中,磁盘上的字符是0xe5,0xb1和0xb0。 使用当前代码页1252解释每个字符并将其转换为UTF-8,从而生成三个两字节UTF-8字符(0xC3,0xA5),(0xC2,0xB1)和(0xC2,0xB0)的内部序列)。 对于简单的字符分配,字符被转换回代码页1252,给出0xE5,0xB1,0xB0。 这导致多字符文字,结果与编译器遇到'abcd'时的结果相同。多字符文本的值是实现定义的,在VC中是一个int,其中每个字节来自一个字符。 当分配给一个字符时,你得到转换,并看到低字节。对于u8字符文字,当使用多个字节时,我们会生成错误C2015。 注意:对于窄字符和宽字符,编译器对多字符文字的处理是非常不同的。对于宽字符,我们只是取多字符文字的第一个字符,在本例中为0x00E5。 在窄字符串文字中,使用当前代码页将序列转换回来,并产生四个字节:0xe5,0xb1,0xb0,0。u8字符串字面值使用与内部表示法相同的字符集,并且是0xC3,0xA5,0xC2 ,0xB1,0xC2,0xB0,0。对于宽字符串文字,我们使用UTF-16作为执行字符集,结果为0x00E5,0x00B1,0x00B2,0。
最后,在第四列中,我们使用代码页936保存文件,其中字符以0x8C,0xDB的形式存储在磁盘上。 我们使用1252的当前代码页对其进行转换,并获得两个两字节的UTF-8字符:(0xC5,0x92),(0xC3,0x9B)。 对于窄字符文字,字符被转换回0x8C,0xDB并且char获得0xDB的值。对于u8字符文字,字符不会被转换,但它是一个错误。 对于宽字符文字,字符会转换为UTF-16,从而产生0x0152,0x00DB。使用第一个值并且0x0152是值。 对于字符串文字,类似的转换完成。
更改系统代码页
如果使用不同于1252的代码页,则第二列和第三列的结果也会不同。 从上面的描述中,您应该能够预测在这些情况下会发生什么。由于这些差异,许多开发人员只会建立在设置为代码页1252的系统上。对于其他代码页,您可以得到不同的结果,不会有任何警告或错误。
编译器指令
还有两个编译器指令可以影响这个过程。 这些是“#pragma setlocale”和“#pragma execution_character_set”。
setlocale编译指示在这里有所记录https://msdn.microsoft.com/en-us/library/3e22ty2t.aspx 。该编译指示试图允许用户在解析文件时更改文件的源字符集。 它似乎已被添加到允许使用非Unicode文件指定宽字体。 但是,这里有一些错误,只能有效地使用单字节字符集。 如果你试图像上面这样在上面的例子中添加一个编译指示语言环境。
#pragma setlocale(“。936”)
const char buffer [] =“屰”;
const wchar_t wbuffer [] = L“屰”;
const char ch ='屰';
const wchar_t wch = L'屰';
结果在表2中,差异在红色中突出显示。 它所做的只是让更多的案例无法转换并导致0x3F(?)字符。该附注实际上并不改变源文件的读取方式,而是仅在宽字符或宽字符串文字被使用时才使用。 当看到宽文字时,编译器将单个内部UTF-8字符转换回1252,试图“撤消”读取文件时发生的转换。 然后将它们从原始表单转换为由“setlocale”编译指示设置的代码页。 但是,在这种特殊情况下,第3列和第4列中的UTF-8的初始转换分别导致3或2个UTF-8字符。 例如,在第4列中,(0xC5,0x92)的内部UTF-8字符被转换回CP1252,结果为字符0x8C。 编译器然后尝试将其转换为CP936。 然而,0x8C只是一个前导字节,而不是一个完整的字符,所以转换失败时产生0x3F,这是默认的替换字符。 第二个字符的转换也失败,导致另一个0x3F。 所以,第三列最后有三个宽字符串文字的0x3F字符,第四列有两个0x3F字符。
对于具有BOM的Unicode文件,结果与以前相同,这是合理的,因为通过BOM强烈指定了文件的编码。
表2 - 编译各种编码的代码时的结果示例。 表1与红色的区别。
文件编码 | UTF-8 w / BOM | UTF-16LE不带BOM | UTF-8不含BOM | DBCS(936) |
源文件中的字节代表屰 | 0xE5,0xB1,0xB0 | 0x70,0x5C | 0xE5,0xB1,0xB0 | 0x8C,0xDB |
来源转换 | UTF8 - > UTF8 | UTF16-LE - > UTF-8 | 1252 - > UTF8 | 1252 - > UTF-8 |
内部(UTF-8)表示 | 0xE5,0xB1,0xB0 | 0xE5,0xB1,0xB0 | 0xC3,0xA5,0xC2,0xB1,0xC2,0xB0 | 0xC5,0x92,0xC3,0x9B |
转换为执行字符集 | ||||
char ch ='屰'; UTF-8 - > CP1252 | 0x3F的* | 0x3F的* | 0XB0 | 位于0xDB |
char u8ch = u8'屰'; UTF-8 - > UTF-8 | 错误C2015 | 错误C2015 | 错误C2015 | 错误C2015 |
wchar_t wch = L'屰'; UTF-8 - > UTF-16LE | 0x5C70 | 0x5C70 | 0x003F | 0x003F |
char b [] =“屰”; UTF-8 - > CP1252 | 0x3F,0 * | 0x3F,0 * | 0xE5,0xB1,0xB0,0 | 0x8C,0xDB,0 |
char u8b [] = u8“屰”; UTF-8-> UTF-8 | 0xE5,0xB1,0xB0,0 | 0xE5,0xB1,0xB0,0 | 0xC3,0xA5,0xC2,0xB1,0xC2,0xB0,0 | 0xC5,0x92,0xC3,0x9B,0 |
wchar_t wb [] = L“屰”; UTF-8 - > UTF-16LE | 0x5C70,0 | 0x5C70,0 | 0x003F,0x003F,0x003F,0 | 0x003F,0x003F,0 |
影响所有这一切的另一个杂注是#pragma execution_character_set。 它采用目标执行字符集,但只支持一个值,即“utf-8”。它是为了让用户指定一个utf-8执行字符集而引入的,并且在VS2008和VS2010出货后实现。 这是在u8文字前缀被支持之前完成的,并且不再需要。 在这一点上,我们真的鼓励用户使用新的前缀而不是#pragma execution_character_set。
当前问题摘要
#pragma setlocale有许多问题。
- 它不能设置为UTF-8,这是一个主要限制。
- 它只影响字符串和字符文字。
- 它实际上不能正确使用DBCS字符集。
execution_character_set编译指示允许您将窄字符串编码为UTF-8,但不支持任何其他字符集。 此外,全局设置的唯一方法是使用包含此附注的标头的/ FI(强制包含)。
试图以跨平台的方式编译包含非ASCII字符串的代码是非常困难的。
VS2015更新2中的新选项
为了解决这些问题,有几个新的编译器命令行选项允许您指定源字符集和执行字符集。 / source-charset:选项可以采用IANA字符集名称或Windows代码页标识符(前缀为点)。
/source-charset:<iana-name>|.NNNN
如果传递了IANA名称,则该名称使用IMultiLanguage2 :: GetCharsetInfo映射到Windows代码页。代码页用于将编译器遇到的所有无BOM文件转换为其内部UTF-8格式。 如果将UTF-8指定为源字符集,则由于编译器在内部使用UTF-8,因此根本不执行任何翻译。 如果指定名称未知或者在代码页上检索信息时发生其他错误,则会发出错误。 一个限制是无法使用UTF-7,UTF-16或使用两个以上字节编码字符的任何DBCS字符集。 此外,不是ASCII超集的代码页可能会被编译器接受,但可能会导致许多有关意外字符的错误。
/ source-charset选项会影响翻译单元中未自动识别的所有文件。 (请记住,我们会自动识别带有BOM的文件以及无BOM的UTF-16文件。)因此,在同一翻译单元中不可能有UTF-8编码文件和DBCS编码文件。
/execution-charset:<iana-name>|.NNNN选项使用与/ source-charset相同的查找机制来获取代码页。 它控制如何生成窄字符和字符串文字。
还有一个/ utf-8选项是设置“/ source-charset:utf-8”和“/ execution-charset:utf-8”的同义词。
请注意,如果使用这些新选项中的任何一个,则现在使用#pragma setlocale或#pragma execution-character-set时会出错。在新的选项和使用明确的u8文字之间,不再需要使用这些旧的编译指示,特别是考虑到错误。 但是,如果不使用新选项,现有的编译指示将继续像以前一样工作。
最后,还有一个新的/ validate-charset选项,它可以通过上述任何选项自动打开。 可以使用/ validate-charset来关闭此功能,尽管这不被推荐。之前,我们会在转换为内部UTF-8格式时对某些字符集进行验证,但是,我们不会检查UTF-8源文件并直接读取它们,这可能会在稍后导致出现细微问题。 无论是否有BOM,该开关都可以验证UTF-8文件。
重访的例子
通过在需要的地方正确指定源字符集,无论源文件的编码如何,结果现在都是相同的。 另外,我们可以指定一个独立于源字符集的特定执行字符集,对于特定的执行字符集,结果应该是相同的。 在表3中,您可以看到,无论源文件的编码如何,我们现在都可以得到完全相同的结果。 绿色数据表示与表1中原始示例的变化。
表4显示了使用UTF-8执行字符集的结果,表5使用GB2312作为执行字符集。
表3 - 为每个源文件使用正确的源字符集的示例(当前代码第1252页)。 绿色显示与表1不同。
文件编码 | UTF-8 w / BOM | UTF-16LE不带BOM | UTF-8不含BOM | DBCS(936) |
源文件中的字节代表屰 | 0xE5,0xB1,0xB0 | 0x70,0x5C | 0xE5,0xB1,0xB0 | 0x8C,0xDB |
来源转换 | UTF8 - > UTF8 | UTF16-LE - > UTF-8 | UTF8 - > UTF8 | CP936 - > UTF-8 |
内部(UTF-8)表示 | 0xE5,0xB1,0xB0 | 0xE5,0xB1,0xB0 | 0xE5,0xB1,0xB0 | 0xE5,0xB1,0xB0 |
转换为执行字符集 | ||||
char ch ='屰'; UTF-8 - > CP1252 | 0x3F的* | 0x3F的* | 0x3F的* | 0x3F的* |
char u8ch = u8'屰'; UTF-8 - > UTF-8 | 错误C2015 | 错误C2015 | 错误C2015 | 错误C2015 |
wchar_t wch = L'屰'; UTF-8 - > UTF-16LE | 0x5C70 | 0x5C70 | 0x5C70 | 0x5C70 |
char b [] =“屰”; UTF-8 - > CP1252 | 0x3F,0 * | 0x3F,0 * | 0x3F,0 * | 0x3F,0 * |
char u8b [] = u8“屰”; UTF-8-> UTF-8 | 0xE5,0xB1,0xB0,0 | 0xE5,0xB1,0xB0,0 | 0xE5,0xB1,0xB0,0 | 0xE5,0xB1,0xB0,0 |
wchar_t wb [] = L“屰”; UTF-8 - > UTF-16LE | 0x5C70,0 | 0x5C70,0 | 0x5C70,0 | 0x5C70,0 |
表4 - 使用UTF-8(代码页65001)的执行字符集正确/源字符集进行文件编码
文件编码 | UTF-8 w / BOM | UTF-16LE不带BOM | UTF-8不含BOM | DBCS(936) |
源文件中的字节代表屰 | 0xE5,0xB1,0xB0 | 0x70,0x5C | 0xE5,0xB1,0xB0 | 0x8C,0xDB |
来源转换 | UTF8 - > UTF8 | UTF16-LE - > UTF-8 | UTF8 - > UTF8 | CP936 - > UTF-8 |
内部(UTF-8)表示 | 0xE5,0xB1,0xB0 | 0xE5,0xB1,0xB0 | 0xE5,0xB1,0xB0 | 0xE5,0xB1,0xB0 |
转换为执行字符集 | ||||
char ch ='屰'; UTF-8 - > UTF-8 | 0XB0 | 0XB0 | 0XB0 | 0XB0 |
char u8ch = u8'屰'; UTF-8 - > UTF-8 | 错误C2015 | 错误C2015 | 错误C2015 | 错误C2015 |
wchar_t wch = L'屰'; UTF-8 - > UTF-16LE | 0x5C70 | 0x5C70 | 0x5C70 | 0x5C70 |
char b [] =“屰”; UTF-8 - > UTF-8 | 0xE5,0xB1,0xB0,0 | 0xE5,0xB1,0xB0,0 | 0xE5,0xB1,0xB0,0 | 0xE5,0xB1,0xB0,0 |
char u8b [] = u8“屰”; UTF-8-> UTF-8 | 0xE5,0xB1,0xB0,0 | 0xE5,0xB1,0xB0,0 | 0xE5,0xB1,0xB0,0 | 0xE5,0xB1,0xB0,0 |
wchar_t wb [] = L“屰”; UTF-8 - > UTF-16LE | 0x5C70,0 | 0x5C70,0 | 0x5C70,0 | 0x5C70,0 |
表5 - 使用GB2312的执行字符集(代码页936)
文件编码 | UTF-8 w / BOM | UTF-16LE不带BOM | UTF-8不含BOM | DBCS(936) |
源文件中的字节代表屰 | 0xE5,0xB1,0xB0 | 0x70,0x5C | 0xE5,0xB1,0xB0 | 0x8C,0xDB |
来源转换 | UTF8 - > UTF8 | UTF16-LE - > UTF-8 | UTF8 - > UTF8 | CP936 - > UTF-8 |
内部(UTF-8)表示 | 0xE5,0xB1,0xB0 | 0xE5,0xB1,0xB0 | 0xE5,0xB1,0xB0 | 0xE5,0xB1,0xB0 |
转换为执行字符集 | ||||
char ch ='屰'; UTF-8 - > CP936 | 位于0xDB | 位于0xDB | 位于0xDB | 位于0xDB |
char u8ch = u8'屰'; UTF-8 - > UTF-8 | 错误C2015 | 错误C2015 | 错误C2015 | 错误C2015 |
wchar_t wch = L'屰'; UTF-8 - > UTF-16LE | 0x5C70 | 0x5C70 | 0x5C70 | 0x5C70 |
char b [] =“屰”; UTF-8 - > CP936 | 0x8C,0xDB,0 | 0x8C,0xDB,0 | 0x8C,0xDB,0 | 0x8C,0xDB,0 |
char u8b [] = u8“屰”; UTF-8-> UTF-8 | 0xE5,0xB1,0xB0,0 | 0xE5,0xB1,0xB0,0 | 0xE5,0xB1,0xB0,0 | 0xE5,0xB1,0xB0,0 |
wchar_t wb [] = L“屰”; UTF-8 - > UTF-16LE | 0x5C70,0 | 0x5C70,0 | 0x5C70,0 | 0x5C70,0 |
做,不该做,未来
在Windows上,尽可能将文件保存为带有BOM的Unicode。 这样可以避免许多情况下的问题,大多数工具都支持使用BOM读取文件。
在那些不含BOM的UTF-8文件已存在的情况下,或者在更改为BOM时出现问题的情况下,请使用/ source-charset:utf-8选项正确读取这些文件。
除非不存在其他选项,否则请不要将utf-8以外的其他项使用/ source-charset。 将文件保存为Unicode(甚至是无BOM的UTF8)比使用DBCS编码更好。
由于Linux通常使用无BOM的UTF-8文件和UTF-8执行字符集,在/ Linux / Windows之间定位代码时,使用/ execution-charset或/ utf-8可以提供帮助。
不要使用#pragma execution_character_set。 相反,在需要的地方使用u8文字。
请勿使用#pragma setlocale。 相反,将该文件另存为Unicode,使用明确的字节序列或使用通用字符名称,而不是在同一文件中使用多个字符集。
注意:许多Windows和CRT API目前不支持UTF-8编码的字符串,Windows代码页和CRT语言环境都不能设置为UTF-8。我们目前正在研究如何在运行时改进我们的UTF-8支持。 但是,即使有这种限制,Windows平台上的许多应用程序也会在内部使用UTF-8编码,并在Windows上根据需要转换为UTF-16。
在未来编译器的主要版本中,我们希望将缺省BOM文件的默认处理更改为UTF-8,但在更新中更改该文件可能导致太多沉默的重大更改。验证UTF-8文件应该能够捕捉到几乎所有那些不正确的假设,所以我希望它会发生。