您的位置:首页 > 编程语言 > C语言/C++

C语言宽字符——字符集与字符编码和宽字符之间的关系

2013-04-13 09:18 218 查看
前言:

距上一篇博文,已经是3个月的时间了,忙碌着项目开发,无暇顾及博客。现在项目总算是结束了一个段落,是该总结的时候。4月份将会更新几篇文章,都是在项目中遇到的问题,然后再深入了解之后总结出来的,希望通过这个平台能与更多的人有更多的交流。

正文:

我在做日志管理这一部分内容的时候,碰到了这样一个问题:程序运行到时间处理的库函数时,如 ctime, strftime, localtime,程序就会立即断错误,搜索之后才知道这种情况很有可能是因为前面的程序出现了内存泄露或越界,用 valgrind 调试的时候,它总是会在这么一行程序上警告:

wcsncpy(log->name, pu_desc->name, sizeof(log->name)-1);
两个结构体成员的 name 均定义成 wchar_t name[32],所以使用了 wcsncpy。开始的时候怎么都没看出来哪里出问题了,我还特意在复制的大小值那里减1,就是为了预防越界。后来用 gdb 跟踪到这里,gdb 显示第三个参数传递的是 127,的确是没错,因为在 GNU 的 C 库里面,wchar_t 是4个字节的,又回头查阅了 wcsncpy 的帮助手册,才看到这个函数要求传递的第三个参数是宽字符的个数,而不是字节数。传递了
127 个宽字符,肯定就越界了。

除了这个问题以外,在数据库处理的时候也碰到了字符乱码的问题,因为这个名称是要求用中文显示的,后来虽然问题解决了,但是一直搞不清楚UTF-8,宽字符之间到底是一种怎么样的关系,更别说再扯到其他的 UTF-16 等等,就彻底糊涂了。经过这几天的深入了解,总算是有些头绪了,现在以问题列表的形式将学习内容总结如下:

一、什么是ISO 10646,Unicode,UCS,UTF-8,UTF-16,UTF-32,UCS-2,UCS-4?它们到底存在什么样的关系?

1. 字符集和字符编码

要弄清这些,先要明白两个概念:字符集(character set)和字符编码(character encoding)。ISO 10646是标准,UCS 和 Unicode 是字符集,剩下的都是字符编码。字符集是一个表,指定字符对应的数值(称为
code point),而字符编码是指这些 code points 的表现形式。举例说明,比如字符
€ 的字符集数值是8364(通常用十六进制表示为U+20AC),而是用UTF-8字符编码的结果就是E2 82 AC。其中 UCS 这个字符集就是由 ISO 10646 标准定义的,全称是Universal
Character Set。而 Unicode 是从一些多语言软件制作厂家组织在一起的 Unicode Project 中诞生的。UCS-2和UCS-4字符编码是由ISO 10646标准制定,而UTF-8到UTF-32是由Unicode Project制定,UTF的全称是
Unicode Transmission Format(也可以是 UCS Transmission Format)。

2. ISO 10646 Project 和 Unicode Project

这两个独立的项目组织,开始的时候都有各自的字符集(UCS 和 Unicode)。但是在1991年的时候他们意识到这样对整个世界来说并不是好事,开始将成果统一在一起,使用统一的字符编码表,但是仍然以两个独立组织存在。随后的Unicode标准都有相对应的ISO 10646的标准,区别上也不是很大,只不过Unicode标准写的更详细明了,排版也很优秀,因此很适合作为开发的参考手册使用,除此之外,Unicode还有一些字符比较、排序等等的算法说明,而
ISO 10646 标准就是一张字符集表而已,字符处理算法则定义在其他的标准上。

3. UCS-2 和 UCS-4 编码

顾名思义,这两个字符编码分别对应用2个字节和4个字节表示一个字符。起初 ISO 10646 标准定义的都是31位的字符集,也就是UCS-4,但是直到2001年才出现第一个超过了16位编码的字符(16位以内能表示的字符集称为 Basic Multilingual Plane,简称BMP),因此使用 UCS-2 表示 BMP 的字符,而UCS-4能表示所有的
Unicode 字符。

4. UTF-8, UTF-16 和 UTF-32 编码

Unicode起初则是用16位来定义字符集,也就是UCS-2。后来发现不够用了,于是将超过2个字节表示的字符扩展到21位,并将字符集空间(从0x00xxxx xxxx - 0x10xxxx xxxx)分成16个部分(每个部分称为一个 Plane),高5位从 0x00 - 0x10 分别代表这16个Plane,每一个Plane都有 2^16 个编码空间。

超过16位的如何编码呢?Unicode 字符集定义了 surrogate pair 的16位字节对,也就是需要32位来表示,编码规则是将字符值减去0x10000,得到一个20位的编码范围,20位中的高10位加上0xD800得到 lead surrogate(范围是0xD800 - 0xDBFF), 低10位的加上 0xDC00
得到trail surrogate(范围是 0xDC00 - 0xDFFF),注意因为10位所能表示的编码范围是 0x000 - 0x03FF, 因此这两个 surrogate 字节不会冲突。但是它们会和 BMP 字符冲突,为了解决这个问题, Unicode 要求从0xD800 - 0xDFFF 作为保留范围,不能用来定义字符,而且要求其他 UTF 组的编码也要遵守这个规则(但是实际上,很多软件并不遵守,所以这种编码方式也导致了很多bug的存在)。

这个需要用21位才能表示完整的Unicode字符集也就是 UTF-16 了,BMP 部分的编码方式与UCS-2完全相同,采用2个字节表示,超过 BMP 的只能用4个字节的 surrogate pair 4个字节来表示,因此这是一个变长的字符集。于是就有了 UTF-32,它采用固定的4个字节来表示字符,只不过它的字符集范围是和
UTF-16 一样的,所以除了字符集范围以外,UTF-32 和 UCS-4 是一样的,都是用4个字节来表示,而且21位以内的编码方式完全相同。

另外由于用多字节来表示一个字符,就会出现字节序的问题,因此在这些编码的后面加上BE(Big Endian)或LE(Little Endian)来表示大端和小端字节序的编码,不加这些后缀则根据所处环境来决定。于是就有 UCS-2, UCS-2BE, UCS-2LE, UCS-4,UCS-4LE, UCS-4BE, UTF-16, UTF-16BE, UTF16-LE, UTF-32, UTF-32BE 和 UTF-32LE
这些字符编码。除此之外,还有一种以BOM(Byte Order Mask)来区分字节序的方法,读者可自行查找相关资料。

至于 UTF-8 字符编码则是一个充满想象的设计,之所以名字中含有8,是因为它特殊的编码方式决定了它不受字节序的影响。具体说来如下所示:
(1) 128以内的,也就是 0x00-0x7F, 兼容 ASCII 编码,均用1个字节表示。
(2) 超过128的用2-6个字节表示,起始字节用多个1加一个0后加数据来表示,1的个数代表总共用多少个字节来表示一个字符,比如110x xxxx就表示用2个字节表示一个字符(这两个字节包括起始字节),后续的字节用 10xx xxxx 的形式,均以 10 开头,数据则填充在所有剩下的位上面(也就是 x 的位置,包括起始字节没有用掉的位)。比如上面说的 € 字符,它的Unicode值是
U+20AC,拆成二进制位就是 0010 0000 1010 1100, UTF-8的编码就是 1110 0010 1000 0010 1010 1100,也就是E2 82 AC(数据部分用下划线标明)起始字节的 E(1110) 也就表示是用3个字节来表示这个字符。

UTF-8的编码规则开始看起来可能会很复杂,需要多看几次才能明白,但是看过之后,应该就会明白为什么它不受字节序的影响,因为所有128以内的字节均是以0开头,而128以外的都是以1开头,其中128以外的起始字节是不可能跟后续字节冲突,因为最少也是以 110x 开头(表示2个字节)。而后续的字节都是以10开头,并且无论大小端,都是从起始字节按照顺序往下排的。除此之外,UTF-8编码还有一个优点,它是符合C语言字符串的要求,以
'\0' 结束,因此所有的普通字符串操作如 strcpy 等对 UTF-8 编码的字符完全有效,而 UTF-16, UTF-32等中间填充大量的 '\0' 字符,则不能使用这些函数。不过有人可能认为 UTF-8 编码有些种族歧视,因为对西文字符,只要用1到2个字节就可以表示,而从中东开始往东亚的方向,所需要的字节数逐渐增加,(比如中文在 UTF-8 编码中普遍使用3个字节来表示)不过对于内部存储不再成问题的今天,这个已经不算什么了。

5. 补充

有一个叫 unicode 的小程序,可以打印字符的 Unicode 编码,比如像下图所示为“中”的 Unicode 编码



只关注编码部分,输出结果的第一行显示“中”在 Unicode 字符集的数值是 U+4E2D, 对应的 UTF-8 和 UTF-16BE 的编码分别是 E4 B8 AD 和 4E2D(读者可自行按照编码规则来检验 UTF-8 编码)

二、C语言的宽字符又跟这些有什么关系?

为什么 C 语言会有一个宽字符的类型?这要涉及到两个概念,内部表示(Internal representation)和外部表示(External representation),前者表示字符是如何保存在内存里面的,也就是字符在程序运行时的表现形式;而后者表示字符是如何存放在外部介质上以及在外部通信的表现形式。历史上这两个本是同一个概念,但是随着字符集的逐渐扩大,也就独立出两个概念。上面所说的编码方式全部是外部表示,也就是我们通常所说的某个文件采用何种编码。C语言定义的宽字符则是内部表示,即这些字符在程序中的表现形式。
然而在C语言的标准里面,没有指定 wchar_t 的具体长度,只是说这个类型能表示所有的字符。在 GNU 的 C 库,wchar_t 是和 UCS-4 相同的,即统一使用4个字节来表示所有宽字符,而不像UCS-2的两个字节或UTF-8的变长字节。但是如果在某些嵌入式系统中,把 wchar_t 定义成 char 也不是没有可能,因此可移植的程序中不推荐使用 wchar_t。
另外除了 wchar_t 以外,还有 wint_t 类型对应 int 类型。除此之外,还有两个宏定义 WCHAR_MIN 和 WCHAR_MAX 分别表示宽字符的最小长度和最大长度,文件结束符 EOF 也有相对应的宽字符表示 WEOF。

三、什么时候该使用宽字符?什么时候可以不用?

如果你所需要的又只是简单的复制移动操作,那完全可以不用宽字符,但是如果需要诸如查找字符的个数、字符串排序等操作,就必须要转换成宽字符了。不过对复制移动操作,需要注意的是,当外部表示是用 UTF-8 的编码方式时,可以采用 str 组的字符串操作,而对于其他的编码,那就只能使用 mem 组的内存处理函数,否则会造成数据的丢失。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: