微软C/C++编译器中字符集管理的新方法

微软C/C++编译器已经与DOS、16位窗口和32/64位Windows一起演进。  它支持不同的字符集、代码页和Unicode 在这期间也发生了变化。 这篇文章将解释我们的编译器在过去是如何工作的,还将介绍一些新的开关 提供单位 C/C++编译器在 Visual Studio 2015更新2 CTP ,特别支持无BOM的UTF-8文件和控制执行字符集。  请下载并试用。  有关更新2中其他编译器更改的信息,请查看 邮递 .网上有一些很好的资源,详细地描述了Unicode、DBCS、MBCS、代码页和其他内容。我不想在这里重复这一点,我将很快介绍基本概念。 Unicode联盟 这个网站是一个了解Unicode的好地方。理解编译器如何处理不同的字符集主要有两个方面。第一个是它如何解释源文件(源字符集)中的字节,第二个是它写入二进制文件(执行字符集)中的字节。  理解源代码本身是如何编码和存储在磁盘上的是很重要的。

null

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采用8位字符,这些字符是使用系统的当前代码页解释的。注意:在Windows中,您不能将代码页设置为Unicode代码页(UTF-16或UTF-8),因此在许多情况下,要让旧的应用程序理解没有BOM的Unicode编码文件并不容易。现在用UTF-8编码文件而不使用BOM也是很常见的。这是大多数Linux环境中的默认设置。尽管许多Linux工具可以处理BOM,但大多数工具不会生成BOM。没有BOM实际上使许多事情变得更简单,比如连接文件或附加到文件,而不必担心谁将编写BOM。

微软C/C++编译器如何从文件读取文本

在过去的某个时候,微软的编译器被修改为在内部使用UTF-8。因此,当从磁盘读取文件时,它们会被动态转换为UTF-8。如果一个文件有一个BOM,我们就使用它,并使用指定的编码读取文件,然后将其转换为UTF-8。如果文件没有BOM,我们就通过查看前8个字节来检测UTF-16编码的小端和大端形式。如果文件看起来像UTF-16,我们会将其视为文件上有UTF-16 BOM。如果没有BOM并且看起来不像UTF-16,那么我们使用当前代码页(调用GetACP的结果)将磁盘上的字节转换为UTF-8。这可能是正确的,也可能是不正确的,这取决于文件的实际编码方式和包含的字符。如果文件实际编码为UTF-8,这将永远不会正确,因为系统代码页不能设置为CPu UTF8。

执行字符集

理解“执行字符集”也很重要。基于执行字符集,编译器将以不同的方式解释字符串。让我们从一个简单的例子开始。

const char ch=’h’;const char u8ch=u8’h’;const wchar_t wch=L’h’;const char b[]=“h”;常量字符u8b[]=u8〃h“;const wchar_t wb[]=L“h”;

上面的代码将被解释为您键入了这个。

const char ch=0x68;const char u8ch=0x68;常量wcharu t wch=0x68;const char b[]={0x68,0};const char u8b[]={0x68,0};const wcharu t wb[]={0x68,0};

这应该是非常有意义的,无论文件编码或当前代码页如何,都是正确的。现在,让我们看看下面的代码。

const char ch=’屰’;const char u8ch=’屰’;常量wcharu t wch=L’屰’;常量字符b[]=“屰”;常量字符u8b[]=u8〃屰”;常量wcharu t wbuffer[]=L”屰”;

注:我随机挑选了这个字,但它似乎是汉字的意思是“不服从”,这似乎适合我的目的。它是Unicode U+5C70字符。

在这方面我们要考虑几个因素。包含此代码的文件是如何编码的?我们正在编译的系统的当前代码页是什么?在UTF-16中,编码是0x5C70,在UTF-8中,编码是序列0xE5、0xB1、0xB0。在936代码页中,它是0x8C,0xDB。它不能在代码页1252(拉丁语-1)中表示,这是我当前正在运行的代码页。1252代码页通常在Windows上使用英语和许多其他西方语言。表1显示了在使用代码页1252的系统上运行时各种文件编码的结果。表1—今天使用各种编码编译代码时的结果示例。

文件编码 带物料清单的UTF-8 UTF-16LE带/或不带物料清单 UTF-8,无物料清单 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
转换为执行字符集
字符ch=’屰’;UTF-8->CP1252 0x3F型* 0x3F型* 0xB0型 0xDB
char u8ch=u8’屰’;UTF-8->UTF-8 错误C2015 错误C2015 错误C2015 错误C2015
wcharu t wch=L’屰’;UTF-8->UTF-16LE 0x5C70型 0x5C70型 0x00E5型 0x0152号
字符b[]=“屰”;UTF-8->CP1252 0x3F,0* 0x3F,0* 0xE5、0xB1、0xB0、0 0x8C、0xDB、0
字符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”那么有意义,但我将详细介绍每种情况下的情况。在第1列和第2列中,我们知道文件的编码是什么,因此到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,字符得到0xDB的值。对于u8字符文本,字符不会被转换,但这是一个错误。对于宽字符文本,字符被转换为UTF-16,结果是0x0152、0x00DB。使用第一个值,0x0152是值。对于字符串文字,将进行类似的转换。

更改系统代码页

如果使用的代码页不同于1252,则第二列和第三列的结果也将不同。根据上面的描述,您应该能够预测在这些情况下会发生什么。由于这些差异,许多开发人员只会在设置为代码页1252的系统上进行构建。对于其他代码页,您可以得到不同的结果,而不会出现警告或错误。

编译制导

还有两个编译器指令可以影响这个过程。它们是“#pragma setlocale”和“#pragma execution_character_set”。setlocale pragma在这里有所记录 https://msdn.microsoft.com/en-us/library/3e22ty2t.aspx . 此pragma尝试允许用户在解析文件时更改文件的源字符集。添加它似乎是为了允许使用非Unicode文件指定宽文本。但是,这里面有一些bug,只允许它与单字节字符集一起使用。如果您尝试像这样向上面的示例中添加pragma set locale。

#pragma setlocale(“.936”)常量字符缓冲区[]=“屰”;常量wcharu t wbuffer[]=L”屰”;const char ch=’屰’;常量wcharu t wch=L’屰’;

结果见表2,差异以红色突出显示。它所做的只是使更多的案例无法转换并导致0x3F(?)字符。pragma实际上并不改变读取源文件的方式,而是仅在使用宽字符或宽字符串时使用。当看到宽文本时,编译器将单个内部UTF-8字符转换回1252,尝试“撤消”读取文件时发生的转换。然后将它们从原始表单转换为“setlocale”pragma设置的代码页。然而,在这种特殊情况下,在第3列和第4列中对UTF-8的初始转换将分别产生3个或2个UTF-8字符。例如,在第4列中,(0xC5,0x92)的内部UTF-8字符被转换回CP1252,从而生成字符0x8C。然后编译器尝试将其转换为CP936。然而,0x8C只是一个前导字节,而不是一个完整的字符,因此转换失败,产生0x3F,默认的替换字符。第二个字符的转换也失败,导致另一个0x3F。因此,第三列的宽字符串文本以三个0x3F字符结束,第4列的文本中有两个0x3F字符。对于带有BOM表的Unicode文件,结果与之前相同,这是有意义的,因为文件的编码是通过BOM表强烈指定的。表2–今天使用各种编码编译代码时的结果示例。与表1的差异用红色表示。

文件编码 带物料清单的UTF-8 UTF-16LE带/或不带物料清单 UTF-8,无物料清单 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
转换为执行字符集
字符ch=’屰’;UTF-8->CP1252 0x3F型* 0x3F型* 0xB0型 0xDB
char u8ch=u8’屰’;UTF-8->UTF-8 错误C2015 错误C2015 错误C2015 错误C2015
wcharu t wch=L’屰’;UTF-8->UTF-16LE 0x5C70型 0x5C70型 0x003F 0x003F
字符b[]=“屰”;UTF-8->CP1252 0x3F,0* 0x3F,0* 0xE5、0xB1、0xB0、0 0x8C、0xDB、0
字符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是#pragma execution_character_set。它接受一个目标执行字符集,但只支持一个值,即“utf-8”。它允许用户指定utf-8执行字符集,并在VS2008和VS2010发布之后实现。这是在支持u8文本前缀之前完成的,现在真的不需要了。在这一点上,我们真的鼓励用户使用新的前缀,而不是#pragma executionŠcharacterŠu set。

当前问题摘要

“pragma setlocale”有很多问题。

  1. 它不能设置为UTF-8,这是一个主要的限制。
  2. 它只影响字符串和字符文本。
  3. 它实际上不能正确地处理DBCS字符集。

executionu characteru set pragma允许您将窄字符串编码为UTF-8,但它不支持任何其他字符集。此外,全局设置此参数的唯一方法是使用包含此pragma的头的/FI(force include)。试图以跨平台的方式编译包含非ASCII字符串的代码是非常困难的。

VS2015 Update 2中的新选项2

为了解决这些问题,有几个新的编译器命令行选项允许您指定源字符集和执行字符集。/source charset:选项可以采用IANA字符集名称或Windows代码页标识符(前缀为点)。/源字符集:|.NNNN如果传递了一个IANA名称,则使用 IMultiLanguage2::GetCharsetInfo . 代码页用于将编译器遇到的所有无BOM文件转换为其内部UTF-8格式。如果将UTF-8指定为源字符集,则根本不执行转换,因为编译器在内部使用UTF-8。如果指定的名称未知或检索代码页上的信息时发生其他错误,则会发出错误。一个限制是不能使用UTF-7、UTF-16或任何使用两个以上字节编码字符的DBCS字符集。此外,编译器可能会接受不是ASCII超集的代码页,但可能会导致许多有关意外字符的错误。/source charset选项会影响翻译单元中未自动识别的所有文件(请记住,我们会自动识别带有BOM的文件,也会识别没有BOM的UTF-16文件。)因此,不可能将UTF-8编码的文件和DBCS编码的文件放在同一个翻译单元中。/execution charset:|.NNNN选项使用与/source charset相同的查找机制来获取代码页。它控制如何生成窄字符和字符串文字。还有一个/utf-8选项,它是设置“/source”的同义词-charset:utf-8“和”/执行-charset:utf-8”.请注意,如果使用这些新选项中的任何一个,那么现在使用#pragma setlocale或#pragma execution character set都是错误的。在新选项和显式u8文本的使用之间,应该不再需要使用这些旧的pragma,特别是考虑到bug。但是,如果不使用新选项,现有的pragma将继续像以前一样工作。最后,还有一个new/validate charset选项,该选项将使用上述任何选项自动启用。可以使用/validate charset-关闭此功能,但不建议这样做。以前,在转换为内部UTF-8格式时,我们会对一些字符集进行一些验证,但是,我们不会检查UTF-8源文件,而是直接读取它们,这可能会在以后引起一些微妙的问题。无论是否有BOM,这个开关都可以验证UTF-8文件。

重温示例

通过在需要的地方正确地指定源字符集,现在无论源文件的编码是什么,结果都是相同的。此外,我们可以指定一个独立于源字符集的特定执行字符集,对于特定的执行字符集,结果应该是相同的。在表3中,您可以看到,不管源文件的编码是什么,我们现在都得到了完全相同的结果。绿色的数据表示与表1中的原始示例不同。表4显示了使用UTF-8的执行字符集的结果,表5使用GB2312作为执行字符集。表3–为每个源文件使用正确的源字符集的示例(当前代码页1252)。绿色表示与表1的差异。

文件编码 带物料清单的UTF-8 UTF-16LE带/或不带物料清单 UTF-8,无物料清单 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
转换为执行字符集
字符ch=’屰’;UTF-8->CP1252 0x3F型* 0x3F型* 0x3F型* 0x3F型*
char u8ch=u8’屰’;UTF-8->UTF-8 错误C2015 错误C2015 错误C2015 错误C2015
wcharu t wch=L’屰’;UTF-8->UTF-16LE 0x5C70型 0x5C70型 0x5C70型 0x5C70型
字符b[]=“屰”;UTF-8->CP1252 0x3F,0* 0x3F,0* 0x3F,0* 0x3F,0*
字符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 UTF-16LE带/或不带物料清单 UTF-8,无物料清单 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
转换为执行字符集
字符ch=’屰’;UTF-8->UTF-8 0xB0型 0xB0型 0xB0型 0xB0型
char u8ch=u8’屰’;UTF-8->UTF-8 错误C2015 错误C2015 错误C2015 错误C2015
wcharu t wch=L’屰’;UTF-8->UTF-16LE 0x5C70型 0x5C70型 0x5C70型 0x5C70型
字符b[]=“屰”;UTF-8->UTF-8 0xE5、0xB1、0xB0、0 0xE5、0xB1、0xB0、0 0xE5、0xB1、0xB0、0 0xE5、0xB1、0xB0、0
字符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 UTF-16LE带/或不带物料清单 UTF-8,无物料清单 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
转换为执行字符集
字符ch=’屰’;UTF-8->CP936 0xDB 0xDB 0xDB 0xDB
char u8ch=u8’屰’;UTF-8->UTF-8 错误C2015 错误C2015 错误C2015 错误C2015
wcharu t wch=L’屰’;UTF-8->UTF-16LE 0x5C70型 0x5C70型 0x5C70型 0x5C70型
字符b[]=“屰”;UTF-8->CP936 0x8C、0xDB、0 0x8C、0xDB、0 0x8C、0xDB、0 0x8C、0xDB、0
字符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 option 正确读取这些文件。除非没有其他选项,否则不要将/source字符集与utf-8以外的其他内容一起使用。将文件保存为Unicode(甚至没有BOM的UTF8)比使用DBCS编码更好。使用/execution charset或/utf-8有助于在Linux和Windows之间定位代码,因为Linux通常使用无BOM的utf-8文件和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-less文件的默认处理方式改为UTF-8,但是在更新中更改它可能会导致太多的无提示更改。UTF-8文件的验证应该能捕捉到几乎所有不正确假设的情况,所以我希望它会发生。

© 版权声明
THE END
喜欢就支持一下吧,技术咨询可以联系QQ407933975
点赞0 分享