C++ 编译器对字符编码的要求和处理方式

http://www.voidcn.com/article/p-ebfyuuxr-ny.html


  • 长期以来,很多人都清楚,一旦C++源码中直接使用了中文,这样的源码想要跨平台(I18N)会非常困难。

随着:

  • Windows下:MSVC2010成为主流
  • Linux下:GCC升级到4.6

C++中的中文问题 才算有了一个比较优雅的、跨平台的Workaround。

(本文讨论编译器范围:GCC4.6+, MSVC2010sp1+ 。本文属于QString系列,但暂不涉及QString)

C++ 中文问题

要在C++中正确使用中文,必须要了解下面两个概念:

源码字符集(the source character set)

源码文件是使用何种编码保存的

执行字符集(the execution character set)

可执行程序内保存的是何种编码(程序执行时内存中字符串编码)

C++98的问题: 既没有规定源码字符集,也没有规定执行字符集

这个... 如何理解?不妨看个例子

例子

这个要求高么?

  • 一个简单的C++程序,只是希望它能在简体中文Windows、正体中文Windows、英文版Windows、Linux、MAC OS...下的结果一致。

//main.cpp
int main()
{
    char mystr[] = "老老实实的学问,来不得半点马虎";
    return sizeof mystr;
}

可以试着反问自己两个问题

  • 这个源码文件是何种编码保存的?(有确定答案么?)
  • mystr中是什么内容?(有确定答案么?)

对C++来说,这两个都不确定。

  • 固定平台的话,还能忍忍
  • 要跨平台的话,这种东西...

GCC

在GCC下,这两个都可以使用你自己喜好的编码(如果不指定,默认都是UTF8)

-finput-charset=charset
-fexec-charset=charset

除了前两个选项外,还有一个:

-fwide-exec-charset=charset

wide? 不妨先猜一下它是干嘛的

MSVC

MSVC没有类似前面的选项。

源码字符集如何解决?

有BOM么,有则按BOM解释,无则使用本地Locale字符集(随系统设置而变)

执行字符集如何解决?

使用本地Locale字符集(随系统设置而变)

挺霸道哈(当然,源码中可以使用#pragma setlocale("..."),但功能很有限,比如Windows没有utf8的locale,所以...)。

另外,和GCC对应的wide-exec-charset呢?

宽执行字符集如何解决?

不妨先考虑一下

怎么办?

这才两个编译器,看起来就这么复杂了。而C++编译器的数目远大于2.

要想跨平台,必须确保这两个字符集都是“确定”的,而能胜任该任务的字符集,似乎理想的也只能是...

UTF-8方案

  • 如果我们将源码保存成utf8,执行字符集也选为utf8,那么天下将太平了。使用非ASCII字符的源码文件也就可以在不同国家的用户间无障碍流通了 ;-).

源码保存成UTF-8没有什么困难,但是,执行字符集需要是UTF-8。没那么简单

对GCC来说,这个问题很简单(默认的编码选项足够了):

  • 只要源码文件保存成utf8即可(带或不带BOM均可)
  • 早期的gcc不接收带BOM的utf8源码文件,现在,至少在GCC4.6中,这一限制不再存在。

对MSVC来说,这个问题异常复杂:

  • 对MSVC2003来说,只要源码保存成不带BOM的utf8即可
  • 对MSVC2005、(没在SP1基础上装热补丁的)MSVC2008来说。完全没办法
  • 直到MSVC2010sp1,才算提供了一个解决方案。源码保存成带BOM的utf8,utf16,...,然后添加

#pragma execution_character_set("utf-8")

要想跨GCC4.6+和MSVC2010sp1+,我们需要取它们的交集:也就是

  • 源码保存成带BOM的utf8
  • 为MSVC添加#pragma

//main.cpp

#if _MSC_VER >= 1600
#pragma execution_character_set("utf-8")
#endif

int main()
{
    char mystr[] = "老老实实的学问,来不得半点马虎";
    return sizeof mystr;
}

C++11

等到MSVC支持C++11的String Literals之时,我们就没必要用那个蹩脚的pragma了,直接

    char mystr[] = u8"老老实实的学问,来不得半点马虎";

即可(尽管现在在GCC下没问题,但要跨平台,估计要等到Visual C++ 12了)。

有个问题?

C++98中不是有个wchar_t么,它不是用来表示unicode字符的么?

Unicode 4.0标准的5.2节是如何说的:

The width of wchar_t is compiler-specific and can be as small as 8 bits. Consequently, programs that need to be portable across any C or C++ compilershould not use wchar_t for storing Unicode text. The wchar_t type is intended forstoring compiler-defined wide characters, which may be Unicode characters in some compilers.

在回头看看GCC的选项

-fwide-exec-charset=charset

尽管GCC为其提供的默认编码是UTF16或UTF32(取决于wchar_t的宽度),但该编码是可以随意设置的。

尽管这个东西不保证跨平台,也很不好玩, 但是,由于在windows下面wchar_t用来表示utf16字符,而且直接对应系统API接口,所以在类型char16_t普及之前,还是很重要的。

C++11执行字符集

前面提到的u8就是C++11为“执行字符集”所做的努力之一。

新明确规定了utf8、utf16和utf32这3种执行字符集。

char*

u8"中文"

char16_t*

u"中文"

char32_t*

U"中文"

可是C++11并没有规定源码字符集

const char* mystr=u8"中文";

C++标准对编译器说,我不管这个文件的具体编码是什么,但你必须给我生成对应utf8编码的字节流。

编译器似乎有点傻了吧?不知道源文件的编码,我如何转换

于是:

MSVC说:源码文件必须有BOM,不然我就认为你是本地locale的编码

GCC说:我认为你就是utf8编码,除非通过命令行通知我其他编码

在C++11标准下,对源码编码 简单的处理办法还是,使用带BOM的UTF8保存。


UTF—8与UTF—8(无bom)格式相比有什么不同

BOM——Byte Order Mark,就是字节序标记


在UCS 编码中有一个叫做"ZERO WIDTH NO-BREAK SPACE"的字符,它的编码是FEFF。而FFFE在UCS中是不存在的字符,所以不应该出现在实际传输中。UCS规范建议我们在传输字节流前,先传输字符"ZERO WIDTH NO-BREAK SPACE"。这样如果接收者收到FEFF,就表明这个字节流是Big-Endian的;如果收到FFFE,就表明这个字节流是Little-Endian的。因此字符"ZERO WIDTH NO-BREAK SPACE"又被称作BOM。


UTF-8不需要BOM来表明字节顺序,但可以用BOM来表明编码方式。字符"ZERO WIDTH NO-BREAK SPACE"的UTF-8编码是EF BB BF。所以如果接收者收到以EF BB BF开头的字节流,就知道这是UTF-8编码了。


UTF-8编码的文件中,BOM占三个字节。如果用记事本把一个文本文件另存为UTF-8编码方式的话,用UE打开这个文件,切换到十六进制编辑状态就可以看到开头的FFFE了。这是个标识UTF-8编码文件的好办法,软件通过BOM来识别这个文件是否是UTF-8编码,很多软件还要求读入的文件必须带BOM。可是,还是有很多软件不能识别BOM。

参考

[C/C++] 各类C/C++编译器对UTF-8源码文件的兼容性测试(VC、GCC、BCB)


Admin
2012年7月27日
名人名言:迎着阳光开放的花朵才美丽,伴着革命理想的爱情才甜蜜。——莫贵英 

作者:zyl910


  在不合平台上开辟C/C++法度时,为了避免源码文件乱码,得采取UTF-8编码来存储源码文件。然则很多编译器对UTF-8源码文件兼容性不佳,于是我做了一些测试,解析了最佳保存规划。


一、测试法度


  为了测试编译器对UTF-8源码文件兼容性,我编写了如许的一个测试法度——



//#if _MSC_VER >= 1600    // VC2010

//#pragma execution_character_set("utf-8")
//#endif

#include <stdio.h>
#include <locale.h>
#include <string.h>
#include <wchar.h>

char* psa = "\u4e00字A";
wchar_t* pdw = L"\u4e00字W";

int main(int argc, char* argv[])
{
char* pa;
wchar_t* pw;

setlocale(LC_ALL, ""); // 应用体系当前代码页.

// char
printf("len<%d>=%d,str=%s\t//", sizeof(char), strlen(psa), psa);
for(pa=psa; *pa!=0; ++pa) printf(" %.2X", (unsigned char)*pa);
printf("\n");

// wchar_t
printf("len<%d>=%d,str=%ls\t//", sizeof(wchar_t), wcslen(pdw), pdw);
for(pw=pdw; *pw!=0; ++pw) printf(" %.4X", (unsigned int)*pw);
printf("\n");

return 0;
}


 



  若是体系默认编码是GB2312(如中文Windows体系),该法度的输出成果应是——
len<1>=5,str=一字A // D2 BB D7 D6 41
len<2>=3,str=一字W // 4E00 5B57 0057


  若是体系默认编码是UTF-8(如Linux体系),该法度的输出成果应是——
len<1>=7,str=一字A // E4 B8 80 E5 AD 97 41
len<4>=3,str=一字W // 4E00 5B57 0057


  注:
1. “len”旁尖括号内的是字符类型的宽度。char类型一般是1字节。而wchar_t类型跟编译器与操纵体系有关,Windows平台下一般2字节,Linux平台下一般4字节。
2. “len<?>=”右侧的数字是字符个数。用char类型,一个汉字的GB2312编码是2个字符,一个汉字的UTF-8编码一般是3个字符。而对于wchar_t类型,一个汉字一般是1个字符。
3. “str=”右侧的是所显示的字符串。
4. “//”右侧用于显示每一个字符的值。



二、测试成果


  须要测试这些方面——
1. 分别测试不合操纵体系下的多种编译器。
2. 无签名的UTF-8与带签名的UTF-8。UTF-8存储规划分别有两种,一是无签名的UTF-8,另一是带签名的UTF-8,这两种规划的差别是——是否存在签名字符(BOM)。
3. 履行字符集。VC2010增长了“#pragma execution_character_set("utf-8")”,指导char的履行字符集是UTF-8编码。


  按照上方的请求,制订好了测试项目,分别有Window平台下的测试与Linux平台下的测试。
  Window平台下的测试有——
[VC6, noBOM]:VC6.0 sp1,源码应用无签名的UTF-8编码。
[VC6, BOM]:VC6.0 sp1,源码应用带签名的UTF-8编码。
[VC2003, noBOM]:VC2003 sp1,源码应用无签名的UTF-8编码。
[VC2003, BOM]:VC2003 sp1,源码应用带签名的UTF-8编码。
[VC2005, noBOM]:VC2005 sp1,源码应用无签名的UTF-8编码。
[VC2005, BOM]:VC2005 sp1,源码应用带签名的UTF-8编码。
[VC2010, noBOM]:VC2010 sp1,源码应用无签名的UTF-8编码。
[VC2010, BOM]:VC2010 sp1,源码应用带签名的UTF-8编码。
[VC2010, noBOM, execution_character_set]:VC2010 sp1,源码应用无签名的UTF-8编码,并应用“#pragma execution_character_set("utf-8")”。
[VC2010, BOM, execution_character_set]:VC2010 sp1,源码应用带签名的UTF-8编码,并应用“#pragma execution_character_set("utf-8")”。
[BCB6, noBOM]:Borland C++ Builder 6.0,源码应用无签名的UTF-8编码。
[BCB6, BOM]:Borland C++ Builder 6.0,源码应用带签名的UTF-8编码。
[gcc(mingw), noBOM]:MinGW中的GCC 4.6.2,源码应用无签名的UTF-8编码。
[gcc(mingw), BOM]:MinGW中的GCC 4.6.2,源码应用带签名的UTF-8编码。


  Linux平台下的测试有——
[gcc(fedora), noBOM, chs]:Fedora 17自带的GCC 4.7.0,源码应用无签名的UTF-8编码,体系说话设为“简体中文”。
[gcc(fedora), BOM, chs]:Fedora 17自带的GCC 4.7.0,源码应用带签名的UTF-8编码,体系说话设为“简体中文”。
[gcc(fedora), noBOM, eng]:Fedora 17自带的GCC 4.7.0,源码应用无签名的UTF-8编码,体系说话设为“英语”。
[gcc(fedora), BOM, eng]:Fedora 17自带的GCC 4.7.0,源码应用带签名的UTF-8编码,体系说话设为“英语”。


  测试成果汇总如下(分号“;”后的是我写的注释)——



[VC6, noBOM]

len<1>=9,str=u4e00瀛桝 // 75 34 65 30 30 E5 AD 97 41 ; VC6无法辨认“\u”转义符,直接输出了“u4e00”。
len<2>=7,str=u4e00瀛梂 // 0075 0034 0065 0030 0030 701B 6882

[VC6, BOM]
无法编译! ; 因BOM字符被编译器当做了错误的语句。

[VC2003, noBOM]
len<1>=0,str= // ; 编译器无法辨认字符串。
len<2>=3,str=一瀛梂 // 4E00 701B 6882

[VC2003, BOM]
len<1>=0,str= //
len<2>=3,str=一字W // 4E00 5B57 0057

[VC2005, noBOM]
len<1>=6,str=一瀛桝 // D2 BB E5 AD 97 41
len<2>=3,str=一瀛梂 // 4E00 701B 6882

[VC2005, BOM]
len<1>=5,str=一字A // D2 BB D7 D6 41
len<2>=3,str=一字W // 4E00 5B57 0057

[VC2010, noBOM]
len<1>=6,str=一瀛桝 // D2 BB E5 AD 97 41 ; “字A”的UTF-8编码为“E5 AD 97 41”,编译器将它们辨认为GB2312编码的“瀛桝”,并将其存储为GB2312字符串。
len<2>=3,str=一瀛梂 // 4E00 701B 6882 ; “字W”的UTF-8编码为“E5 AD 97 57”,编译器将它们辨认为GB2312编码的“瀛梂”,并将其存储为UTF-16字符串。

[VC2010, BOM]
len<1>=5,str=一字A // D2 BB D7 D6 41 ; 因带有BOM,编译器正确的辨认了字符串,并将其存储为GB2312字符串。
len<2>=3,str=一字W // 4E00 5B57 0057 ; 因带有BOM,编译器正确的辨认了字符串,并将其存储为UTF-16字符串。

[VC2010, noBOM, execution_character_set]
len<1>=8,str=一鐎涙 // D2 BB E7 80 9B E6 A1 9D ; “\u4e00”被辨认为“一”,并存储为GB2312编码“D2 BB”。“字A”的UTF-8编码为“E5 AD 97 41”,编译器将它们辨认为GB2312编码的“瀛桝”,并存储为UTF-8编码的“E7 80 9B E6 A1 9D”。但显示时体系默认是 GB2312 编码。
len<2>=3,str=一瀛梂 // 4E00 701B 6882

[VC2010, BOM, execution_character_set]
len<1>=6,str=一瀛桝 // D2 BB E5 AD 97 41 ; “\u4e00”被辨认为“一”,并存储为GB2312编码“D2 BB”。“字A”的UTF-8编码为“E5 AD 97 41”,编译器正确的将其存储为UTF-8编码。但显示时体系默认是 GB2312 编码。
len<2>=3,str=一字W // 4E00 5B57 0057

[BCB6, noBOM]
len<1>=6,str=一瀛桝 // D2 BB E5 AD 97 41
len<2>=3,str=一瀛梂 // 4E00 701B 6882

[BCB6, BOM]
无法编译! ; 因BOM字符被编译器当做了错误的语句。

[gcc(mingw), noBOM]
len<1>=7,str=涓€瀛桝 // E4 B8 80 E5 AD 97 41 ; 存储为UTF-8编码。但显示时体系默认是 GB2312 编码。
len<2>=3,str=一字W // 4E00 5B57 0057

[gcc(mingw), BOM]
len<1>=7,str=涓€瀛桝 // E4 B8 80 E5 AD 97 41
len<2>=3,str=一字W // 4E00 5B57 0057


[gcc(fedora), noBOM, chs]
len<1>=7,str=一字A // E4 B8 80 E5 AD 97 41 ; 存储为UTF-8编码。显示时体系默认是 zh_CN.utf8 编码,正常输出。
len<4>=3,str=一字W // 4E00 5B57 0057

[gcc(fedora), BOM, chs]
len<1>=7,str=一字A // E4 B8 80 E5 AD 97 41
len<4>=3,str=一字W // 4E00 5B57 0057

[gcc(fedora), noBOM, eng]
len<1>=7,str=一字A // E4 B8 80 E5 AD 97 41 ; 存储为UTF-8编码。显示时体系默认是 en_US.utf8 编码,正常输出。
len<4>=3,str=一字W // 4E00 5B57 0057

[gcc(fedora), BOM, eng]
len<1>=7,str=一字A // E4 B8 80 E5 AD 97 41
len<4>=3,str=一字W // 4E00 5B57 0057







三、成果解析


  调查测试成果,我们起首可以发明以下几点——  
VC6和BCB6都无法编译带签名UTF-8编码的代码文件,它们会将签名字符(BOM)当做错误的语句。
VC6无法辨认“\u”转义符。
VC2003无法辨认UTF-8编码的char。



3.1 道理解析


  Windows下的测试以VC2010最为典范,以此为例来讲解。


  在编译过程中,处理惩罚字符串时会涉及下面两种字符集——
源码字符集(the source character set):源码文件是应用何种编码保存的。
履行字符集(the execution character set):可履行法度内保存的是何种编码。


  要想使法度不会乱码,必须满足——
1) 编译器正确辨认了源码字符集,从而获得正确的字符串数据。
2) 运行景象的编码与履行字符集雷同。运行景象的编码可经由过程setlocale函数来设备,“setlocale(LC_ALL, "")”默示应用体系默认编码。对于简体中文Windows来说一般是GB2312,若是履行字符集雷同,那就能正常显示,不然会乱码。


  VC2010是如许处理惩罚的——
源码字符集:若是有签名字符,就按它的编码来解析;不然应用本地Locale字符集。
履行字符集:对于char类型,若是有“#pragma execution_character_set”,就按它的编码来存储字符串;不然应用本地Locale字符集。对于wchar_t类型,老是应用UTF-16编码。


  当源码应用带签名的UTF-8编码时,VC2010能正确的辨认源码字符集是UTF-8。然后因没有“#pragma execution_character_set”,履行字符集是本地Locale字符集——
[VC2010, BOM]
len<1>=5,str=一字A // D2 BB D7 D6 41 ; 因带有BOM,编译器正确的辨认了字符串,并将其存储为GB2312字符串。
len<2>=3,str=一字W // 4E00 5B57 0057 ; 因带有BOM,编译器正确的辨认了字符串,并将其存储为UTF-16字符串。


  当源码应用无签名的UTF-8编码时,VS2010因找不到签名字符,源码字符集被误认为是本地Locale字符集。然后因没有“#pragma execution_character_set”,履行字符集是本地Locale字符集——
[VC2010, noBOM]
len<1>=6,str=一瀛桝 // D2 BB E5 AD 97 41 ; “字A”的UTF-8编码为“E5 AD 97 41”,编译器将它们辨认为GB2312编码的“瀛桝”,并将其存储为GB2312字符串。
len<2>=3,str=一瀛梂 // 4E00 701B 6882 ; “字W”的UTF-8编码为“E5 AD 97 57”,编译器将它们辨认为GB2312编码的“瀛梂”,并将其存储为UTF-16字符串。


  当应用“#pragma execution_character_set("utf-8")”设备了履行字符集为UTF-8后,景象变得更错杂了。我们先看看VC2010能正确辨认源码字符集的带签名文件——
[VC2010, BOM, execution_character_set]
len<1>=6,str=一瀛桝 // D2 BB E5 AD 97 41 ; “\u4e00”被辨认为“一”,并存储为GB2312编码“D2 BB”。“字A”的UTF-8编码为“E5 AD 97 41”,编译器正确的将其存储为UTF-8编码。但显示时体系默认是 GB2312 编码。
len<2>=3,str=一字W // 4E00 5B57 0057


  再看看无签名时的景象。VS2010因找不到签名字符,源码字符集被误认为是本地Locale字符集,即误将UTF-8辨认为GB2312。然后按照履行字符集,又转换编码为UTF-8进行存储。最后在运行时因默认编码是GB2312,再次误将UTF-8辨认为GB2312——
[VC2010, noBOM, execution_character_set]
len<1>=8,str=一鐎涙 // D2 BB E7 80 9B E6 A1 9D ; “\u4e00”被辨认为“一”,并存储为GB2312编码“D2 BB”。“字A”的UTF-8编码为“E5 AD 97 41”,编译器将它们辨认为GB2312编码的“瀛桝”,并存储为UTF-8编码的“E7 80 9B E6 A1 9D”。但显示时体系默认是 GB2312 编码。
len<2>=3,str=一瀛梂 // 4E00 701B 6882


  从上方这2个例子中,发明VC2010存在一个Bug——“#pragma execution_character_set”对“\u”转义字符无效,“\u”转义字符老是应用本地Locale字符集,而不是履行字符集。



3.2 GCC解析


  GCC的源码字符集与履行字符集默认是UTF-8编码,这是因为如今的Linux体系大多应用UTF-8编码。就算调剂了Linux体系说话后,只是区域产生了变更,字符编码依然是UTF-8。所以我们的法度在“简体中文”与“英语”下,均能正确的显示中文字符。


  MinGW中的GCC也是如许的,源码字符集与履行字符集默认是UTF-8编码。然则简体中文的Windows的默认编码是GB2312,会将printf输出UTF-8字符串误认为是GB2312,造成乱码。



3.2 最佳规划


  若是字符串常量中没有非ASCII字符,建议源码文件应用无签名的UTF-8编码,如许能支撑早期的编译器。
  若是字符串常量中含有非ASCII字符,建议源码文件应用带签名的UTF-8编码,如许能使大多半编译器正确的处理惩罚源码字符集。


  补充——
1. 重视前提仅是“字符串常量中没有非ASCII字符”。若是是从外部文件或其他路子获得非ASCII字符串,只要选择了合适的字符串函数,无签名UTF-8编码的源码文件也是能行的。
2. VC2010新增的“#pragma execution_character_set”用于明白请求UTF-8字符串的场合。因为Windows没有UTF-8的locale,实用性较小,



参考文献——
《ISO/IEC 9899:1999 (C99)》。ISO/IEC,1999。www.open-std.org/jtc1/sc22/wg14/www/docs/n1124.pdf
《C99标准》。yourtommy。http://blog.csdn.net/yourtommy/article/details/7495033
《QString乱谈(2) 》。dbzhang800。http://blog.csdn.net/dbzhang800/article/details/7540905


 


源码下载—— 
http://files.cnblogs.com/zyl910/testwchar.rar




迎着阳光开放的花朵才美丽,伴着革命理想的爱情才甜蜜。——莫贵英
相关文章
相关标签/搜索