C/C++对Unicode编码的处理

http://blog.chinaunix.net/uid-23414687-id-2425175.html


 MultiByteToWideChar(CP_ACP,0,p,-1,tUni,1);//调用windows的API函数将中文内码转换成  UNICODE码
这个函数的具体参数可以查询MSDN,其功能为将多字符转换为宽字符,还对应有一个将宽字节转换为多字节的函数。
   outUnic[j++]=tUni[0];//tUni中为转换好的中文Unicode内码

http://www.ibm.com/developerworks/cn/linux/l-cn-ccppglb/

编码转换方法

一般的编码转换,直接做映射的不太可能,需要比较多的工作量,大多情况下还是选择 Unicode 作为转换的中介。

使用库函数

如前文所说,JAVA 的 String 对象是以 Unicode 编码存在的,所以 JAVA 程序员主要关心的是读入时判断字节流的编码,从而确保可以正确的转化为 Unicode 编码;相比之下,C/C++ 将外部文件读出的数据存为字符数组、或者是 string 类型;而 wstring 才是符合 Unicode 编码的双字节数组。一般常用的方法是 C 标准库的 wcstombs、mbstowcs 函数,和 windows API 的 MultiByteToWideChar 与 WideCharToMultiByte 函数来完成向 Unicode 的转入和转出。

这里以 MBs2WCs 函数的实现说明 GB2312 向 Unicode 的转换的主要过程:

清单 1. 多字节字符串向宽字节字符串转换
wchar_t * MBs2WCs(const char* pszSrc){ 
    wchar_t* pwcs = NULL; 
    intsize = 0; 
    #ifdefined(_linux_) 
        setlocale(LC_ALL, "zh_CN.GB2312"); 
        size = mbstowcs(NULL,pszSrc,0); 
        pwcs = new wchar_t[size+1]; 
        size = mbstowcs(pwcs, pszSrc, size+1); 
        pwcs[size] = 0; 
    #else
        size = MultiByteToWideChar(20936, 0, pszSrc, -1, 0, 0); 
        if(size <= 0) 
            returnNULL; 
        pwcs = new wchar_t[size]; 
        MultiByteToWideChar(20936, 0, pszSrc, -1, pwcs, size); 
    #endif 
    returnpwcs; 
 }

相应的,WCs2MBs 可以将宽字符串转化为字节流。

清单 2. 宽字节字符串向多字节字符串转换
char* WCs2MBs(const wchar_t * wcharStr){ 
    char* str = NULL; 
    intsize = 0; 
    #ifdefined(_linux_) 
        setlocale(LC_ALL, "zh_CN.UTF8"); 
        size = wcstombs( NULL, wcharStr, 0); 
        str = new char[size + 1]; 
        wcstombs( str, wcharStr, size); 
        str[size] = '\0'; 
    #else
        size = WideCharToMultiByte( CP_UTF8, 0, wcharStr, -1, NULL, NULL, NULL, NULL ); 
        str = new char[size]; 
        WideCharToMultiByte( CP_UTF8, 0, wcharStr, -1, str, size, NULL, NULL ); 
    #endif 
    returnstr; 
 }

Linux 的 setlocale 的具体使用可以参阅有 C/C++ 文档,它关系到文字、货币单位、时间等很多格式问题。Windows 相关的代码中 20936 和宏定义 CP_UTF8 是 GB2312 编码对应的的 Code Page[ 类似的 Code Page 参数可以从 MSDN的 Encoding Class 有关信息中获得 ]。

这里需要特别指出的是 setlocale 的第二个参数,Linux 和 Windows 是不同的:

  • 1. 笔者在 Eclipse CDT + MinGW 下使用 [country].[charset](如 zh_CN.gb2312 或 zh_CN.UTF8)的格式并不能通过编码转换测试,但可以使用 Code Page,即可以写成 setlocale(LC_ALL, ".20936") 这样的代码。这说明,这个参数与编译器无关,而与系统定义有关,而不同操作系统对于已安装字符集的定义是不同的。
  • 2. Linux 系统下可以参见 /usr/lib/locale/ 路径,系统所支持的 locale 都在这里。转换成 UTF8 时,并不需要 [country] 部分一定是 zh_CN,en_US.UTF8 也可以正常转换。

另外,标准 C 和 Win32 API 函数返回值是不同的,标准 C 返回的 wchar_t 数组或者是 char 数组都没有字符串结束符,需要手动赋值,所以 Linux 部分的代码要有区别对待。

最后,还要注意应当在调用这两个函数后释放分配的空间。如果将 MBs2WCs 和 WCs2MBs 的返回值分别转化为 wstring 和 string,就可以在它们函数体内做 delete,这里为了代码简明,故而省略,但请读者别忘记。 


http://hi.baidu.com/ytwirvexplbbtvq/item/0ed1fe3b2c4c7a8ac2cf2922


字符集编码与 C/C++ 源文件字符编译乱弹(转)

最近在看国际化编程 (i18ninternationalization) 的东西,也弄清楚了点字符集有关的一些问题,其实网上的一些牛人已经将字符集、Unicode 等相关的问题说的很清楚了,我在这里引用他们的总结并自己小结一下心得,并且实验一下在编译时,源代码自身的字符集与编译生成工具之间的问题。

locale与字符集

locale,中文有时翻译成“现场”,还不如叫英文的locale好,它的意思是“一套和地域有关的习惯而形成的程序运行上下文”,它由很多方面 (category) 组成,比如:某个地区的人们习惯怎样表示他们的货币金额 (LC_MONETARY) ,是用 "$100",还是用 "¥100";习惯怎么表示十进制多位数 (LC_NUMERIC) ,是每一千位进行分隔 "100,000",还是每一万位进行分隔 "10,0000";习惯怎么表示日期时间 (LC_TIME) ,是日-月-年的方式 "30-01-1999",还是年-月-日的方式 "1999-01-30",等等还有其它一些方面,不过其中我们最关心的是一个叫 LC_CTYPE 的,CTYPE 的含义大概是:Character Type(字符类型),它表示某个地区的字符用哪个字符集进行编码。还有LC_ALL,它是其它所有方面的并集。

C 标准库中设置 locale 的函数是:setlocale(),MSDN VC10 参考:Language and Country/Region Strings

字符集(Character-Set)按照发明顺序和继承关系,有以下常用的几种:

  1. ASCII

    ANSI 发布的字符编码标准,编码空间 0x00-0x7F,占用1个字节,上学时学的 C 语言书后面的字符表中就是它,因为使用这个字符集中的字符就已经可以编写 C 程序源代码了,所以给这个字符集起一个 locale 名叫 C,所有实现的 C 语言运行时和系统运行时,都应该有这个 C locale,因为它是所有字符集中最小的一个,设置为其它 locale 时可能由于不存在而出错,但设置 C 一定不会出错,比如:当 Linux 的 LANG 配置出错时,所有的 LC_* 变量就会被自动设置为最小的 C locale。MSDN VC10 参考:Code Pages

  2. ISO-8859-1

    ISO 发布的字符编码标准,又称 Latin-1 字符集,编码空间 0x00-0xFF,占用1个字节,可以编码大多数的西欧地区语言。参考:ISO IEC 8859-1

  3. GB2312,GBK,GB18030

    GB 系列是由中国国标局发布的字符编码方案(其中 GBK 不是正式标准),后期发布的版本兼容之前的,是之前的超集。

    参考:中文的几个编码 by blade2001

    • GB2312

      为1-2字节变长编码,汉字区中编码 6763 个字符。

    • GBK

      是微软对 GB2312 的扩展,后由国标局作为指导性标准,为1-2字节变长编码,编码 21886 个字符,分为汉字区和图形符号区。汉字区编码 21003 个字符,支持CJK汉字(简体、繁体、常用日韩文),Windows 代码页为 CP936。

    • GB18030

      为1-2-4字节变长编码,汉字区编码 27484 个字符,支持CJK汉字、常用藏文、蒙文、维吾尔文等,Windows 代码页为 CP54936。

上面的所有编码都将ASCII作为自己的子集实现,所以这些字符集又叫做本地化的(Native)ANSI 字符集。

一般在程序中为了支持国际化,在程序初始化时,将 locale 设置为系统配置的 Native ANSI 字符集,即执行:setlocale(LC_ALL, "")

ASCII、ISO-8859-1 这种用1个字节编码的字符集,叫做单字节字符集(SBCS - Single-Byte Character Set)。

GB 系列这种用1-2、4个不等字节编码的字符集,叫做多字节字符集(MBCS - Multi-Byte Character Set)。

由 SBCS 编码的数据可以随机访问,从任意字节偏移开始解编码都能保证解析出的字符和后继字符是正确的。

而由MBCS编码的数据,只能将其作为字节流进行解析,如果从随机任意的字节偏移开始解编码,有可能定位到切断一个字符的中间位置,导致后继解析出的字符连续出错。作为字节流时,是从某个标识位置进行解析字符,比如从数据的开始位置,或从每个新行符 '\n' 之后开始解析字符。

Unicode 的理解

参考:谈谈 Unicode 编码,简要解释 UCS、UTF、BMP、BOM 等名词 by fmddlmyy

首先,Unicode 它不是一个东西,它至少涉及3个方面:Code Point,UCS,UTF。

每个地区、国家都有自己的 Native ANSI 字符集,虽然它们在 ASCII 子集部分是相同的,但其它部分都不尽相同,如果一个字符仅在一个特定的Native ANSI 字符集中编码,那么好,如果用户使用别的字符集,那么无论如何也无法解析和表现这个字符。怎样让你的文本数据同时可以包含英、法、德、中、日、阿拉伯甚至世界上所有可能的、完全的字符?办法似乎只有一个,就是在世界范围内对所有可能的字符进行穷举编码,这就是最初提出Unicode 的原因,全称为 Universal Multiple-Octet Code

  1. Code Point

    在实际编码之前先给每个穷举到的字符指定一个序号,叫它 Code Point,把它当做是数学概念,和用几个字节存储无关,只要发布Unicode 的标准化组织(ISO 和 unicode.org)愿意,将新出现的字符继续向后编号就可以了,既然数学序号,就没有什么不够用的问题。编号时有一些原则,就是越常用的字符越靠前,编号到一定数量后,发现差不多了,常用字符都编完了,截止于此将之前的编号组成的子集叫做基本多文种平面(BMP - Basic Multilingual Plane),在 BMP 里的字符,只要4位16进制数就可以表示,当然在 BMP 以外的字符则需要使用5位或更多16进制数表示。比如:"汉" 字在 BMP 里,它的 Code Point 可以表示为 U+6C49。常用CJK汉字都落在 BMP 内,所以都能用U+HHHH 的形式表示其 Code Point。Windows 下有个字符映射表(charmap.exe)的工具,可以列举每个字符的Code Point、字体支持、字符集之间的关系。

  2. UCS

    有了 Code Point 后就可以规定它的字符集,叫做 UCS - Unicode Character Set,它和存储有关,用2个字节存储 Code Point 叫做 UCS-2,用4个字节存储的叫做 UCS-4,UCS-2 可以编码并存储 BMP 中的所有字符,而如果不够用了(要用到 BMP 外的字符),则可以使用 UCS-4。通常交流中提到 Unicode,如果不特指,就指代的是 UCS-2。

    UCS 和 Native ANSI 字符集采用的 MBCS 编码是不同的,UCS 不将 ASCII 作为自己的子集,无论什么情况 UCS 总使用定长的字节来编码字符,UCS-2 使用2个字节,UCS-4 使用4个字节,而不是 Native ANSI 字符集中可能采用的变长编码。比如:"A" 在 GB2312 中编码为 0x41,而在 UCS-2 中编码为 0x0041。

    当存储多字节编码的数据并且不将其作为字节流解析时,就要考虑保存数据的大小端问题(Endian),可以使用 BOM(Byte Order Mark)标识一个 UCS 字符数据块是采用 Big Endian 还是 Little Endian 进行存储:在 Unicode 概念中有一个字符,它的 Code Point 为 U+FEFF,实际上它不映射到任何地区、国家中的可能字符,即是一个不可能存在字符的 Code Point((-_-^),Unicode 标准对它的注释为:ZERO WIDTH NO-BREAK SPACE),当开始处理 UCS 数据块时,UCS 标准建议先处理这个 ZERO WIDTH NO-BREAK SPACE 字符,比如 UCS-2 数据块,如果一开始读到/写入的字节序列是 FF FE(8 进制:377 376),那么说明后续的 UCS-2 按 Little Endian 存储;如果是 FE FF(8进制:376 377),则说明后续的 UCS-2 按 Big Endian 存储。

    采用定长的 UCS 有一个好处,就是可以像 SBCS 一样随机访问数据块中的任何字符,当然这里的随机偏移单位不是每字节:当用 UCS-2,是每2字节随机偏移,当用 UCS-4 时,是每4字节随机偏移。

    但是 UCS 也有缺点,一是有些浪费:比如用 UCS-2,如果在一个数据块中只使用对应于 ASCII 中的字符,那么有一半存储都被浪费掉了,因为对应于ASCII 中的字符,它的 UCS-2 编码实际上是它的 ASCII 编码加上填0的高1字节组成的2字节编码,那种使用16进制编辑器打开文件后隔一列为0的字符文件就是这种情况。二是和 ASCII 不兼容,由于太多的已有系统使用 ASCII(或 Native ANSI)了,这点使 UCS 和其它系统对接时有点麻烦。

  3. UTF

    UTF - Unicode Transformation Format,作为 Unicode 的传输编码,是对 UCS 再次编码映射得到的字符集,能够一定程度上解决上面 UCS 的2个缺点。UTF-8 是以8位为单元对 UCS-2 进行再次编码映射,是当前网络传输、存储优选的字符集。UTF-8 使用8位单元(1字节)变长编码,并将 ASCII 作为子集,这样就可以将 UTF-8 当做一种 MBCS 的 Native ANSI 字符集的实现,因此 UTF-8 需要使用1字节流方式解析字符。处于BMP 中的CJK汉字,使用 UTF-8 编码时通常会映射到3字节序列,而 GB 系列字符集中的CJK汉字通常为2字节序列。

    UTF-8 和所有的 Native ANSI 字符集一样:当数据块中只有 ASCII 子集部分的字符时,是无法区分这个数据块用哪种 Native ANSI 字符集进行编码的,因为这部分的编码映射关系对于所有的 Native ANSI 字符集是共享的,只有当未来数据块中包含像CJK汉字这种在 ASCII 子集之外的字符时,采用不同 Native ANSI 字符集的数据块才会表现出不同。

    不过有一种方法可以让数据块标识自己使用的是 UTF-8 编码(即使字符内容都在 ASCII 内),这对于文本编辑器等应用很有用,它们可以使用这个标识判断文件当前使用的字符集,以便未来插入 ASCII 之外的字符时决定如何编码。这个标识方法就是使用 UCS-2 中 BOM 的 UTF-8 编码,其1字节流为:EF BB BF(8 进制:357 273 277)。当数据块的开始有这个流时就说明后续字符采用 UTF-8 编码。因为 UTF-8 使用1字节流方式处理,这时 BOM 已经失去其在 UCS-2 中作为标识字节序大小端的作用,而仅把 EF BB BF 作为 UTF-8 编码的标识功能(Magic),有时就叫它UTF-8 Signature。但并非所有能处理 UTF-8 数据的应用都假定有 Signature 这个标识功能的存在:微软的应用大多都支持 UTF-8 Signature,但在开源领域,比如 Linux 下有相当多的程序都不支持 UTF-8 Signature。

源文件字符集与编译

在 ISO C99 中有了宽字符处理的标准,例程大多在 wchar.h 中声明,并且有了 wchar_t 这么一个类型。不管哪种 C 编译器和标准/RT库实现,wchar_t 通常都可以认为是存储 UCS 字符的类型,C 语言语法中也使用前缀的 L 字符来说明一个字符常量、字符串字面量在编译时采用 UCS 编码。

VC8 cl的实现中,默认的编译选项将 wchar_t 做为内建类型(选项:/Zc:wchar_t),此时 sizeof(wchar_t) 为 2,可存储 UCS-2 编码。Linux GCC 4 的实现中,sizeof(wchar_t) 为 4,可存储 UCS-4 编码。MinGW 和 Cygwin 的 GCC 4 中,sizeof(wchar_t) 为 2。

如此有这么一个疑问:

  1. 源文件程序语法中的字符编码指示。

  2. 源文件自身的字符编码。

这2者有何种联系?于是我做了如下实验,试着搞明白编译器对上面2者的处理作用。分别实验了3个我常用的编译工具集:VC8、MinGW GCC、Linux GCC。

  • 先看当程序语法中使用非 wchar_t 字符编码指示时的情况,按照教科书上的说法这种字符串字面量编译时使用 ASCII 编码,因为其中有汉字,因此我把它想象成用某种 Native ANSI 字符集进行编码,在随后的测试和调试中便可判断这种假定是否正确。

    使用记事本、iconv 等工具将上面的源文件做出5份不同的字符集编码的出来:GBK、UCS-2 LE、UCS-2 LE(BOM)、UTF-8、UTF-8(BOM)。其中 LE 表示UCS-2 采用 Little Endian 字节序存储;带 BOM 的表示:在文件头有 BOM 标识,对于 UTF-8 来说就是 Signature,没有带 BOM 的就没有这个文件头标识。

    我编译生成了上面的程序后,查看了3处字符串的编码:

    1. 内存中的字符串:使用 gdb、VC 等调试工具,跟踪 memcpy() 时向 buf[] 中复制的字符数据。

    2. 可执行文件中的字串常量:使用 WinHex、hd 等16进制查看工具,在编译生成的对象文件和可执行映像文件中查找字符串字面量的中间部分 "ABC 123"。

    3. 该程序写入的文件:该程序使用 fwrite() 向某个命令行参数指定的文件写入 buf[] 中的字符数据,查看这个写入文件的编码。

    实验结果



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章