文章目录
- [第25章 国际化特性](#第25章 国际化特性)
-
- [25.1 <locale.h>: 本地化](#25.1 <locale.h>: 本地化)
-
- [25.1.1 类项](#25.1.1 类项)
- [25.1.2 setlocale函数](#25.1.2 setlocale函数)
- [25.1.3 localeconv函数](#25.1.3 localeconv函数)
- [25.2 多字节字符和宽字符](#25.2 多字节字符和宽字符)
-
- [25.2.1 多字节字符](#25.2.1 多字节字符)
- [25.2.2 宽字符](#25.2.2 宽字符)
- [25.2.3 Unicode和通用字符集](#25.2.3 Unicode和通用字符集)
- [25.2.4 Unicode编码](#25.2.4 Unicode编码)
- [25.2.5 多字节/宽字符转换函数](#25.2.5 多字节/宽字符转换函数)
- [25.2.6 多字节/宽字符串转换函数](#25.2.6 多字节/宽字符串转换函数)
- [25.3 双联符和三联符](#25.3 双联符和三联符)
-
- [25.3.1 三联符](#25.3.1 三联符)
- [25.3.2 双联符](#25.3.2 双联符)
- [25.3.3 <iso646.h>: 拼写替换](#25.3.3 <iso646.h>: 拼写替换)
- [25.4 通用字符名(C99)](#25.4 通用字符名(C99))
- [25.5 <wchar.h>: 拓展的多字节和宽字符实用工具](#25.5 <wchar.h>: 拓展的多字节和宽字符实用工具)
-
- [25.5.1 流的倾向性](#25.5.1 流的倾向性)
- [25.5.2 格式化宽字符输入/输出函数](#25.5.2 格式化宽字符输入/输出函数)
- [25.5.3 宽字符输入/输出函数](#25.5.3 宽字符输入/输出函数)
- [25.5.4 通用的宽字符串实用工具](#25.5.4 通用的宽字符串实用工具)
-
- [25.5.4.1 宽字符串数值转换函数](#25.5.4.1 宽字符串数值转换函数)
- [25.5.4.2 宽字符串复制函数](#25.5.4.2 宽字符串复制函数)
- [25.5.4.3 宽字符串拼接函数](#25.5.4.3 宽字符串拼接函数)
- [25.5.4.4 宽字符串比较函数](#25.5.4.4 宽字符串比较函数)
- [25.5.4.5 宽字符串搜索函数](#25.5.4.5 宽字符串搜索函数)
- [25.5.4.6 其他函数](#25.5.4.6 其他函数)
- [25.5.5 宽字符时间转换函数](#25.5.5 宽字符时间转换函数)
- [25.5.6 扩展的多字节/宽字符转换实用工具](#25.5.6 扩展的多字节/宽字符转换实用工具)
-
- [25.5.6.1 单字节/宽字符转换函数](#25.5.6.1 单字节/宽字符转换函数)
- [25.5.6.2 转换状态函数](#25.5.6.2 转换状态函数)
- [25.5.6.3 可重启的多字节/宽字符转换函数](#25.5.6.3 可重启的多字节/宽字符转换函数)
- [25.5.6.4 可重启动的多字节/宽字符串转换函数](#25.5.6.4 可重启动的多字节/宽字符串转换函数)
- [25.6 <wctype.h>:宽字符分类和映射实用工具(C99)](#25.6 <wctype.h>:宽字符分类和映射实用工具(C99))
-
- [25.6.1 宽字符分类函数](#25.6.1 宽字符分类函数)
- [25.6.2 可扩展的宽字符分类函数](#25.6.2 可扩展的宽字符分类函数)
- [25.6.3 宽字符大小写映射函数](#25.6.3 宽字符大小写映射函数)
- [25.6.4 可扩展的宽字符大小写映射函数](#25.6.4 可扩展的宽字符大小写映射函数)
- [25.7 <uchar.h>: 改进的Unicode支持(C1X)](#25.7 <uchar.h>: 改进的Unicode支持(C1X))
-
- [25.7.1 带u、U和u8前缀的字面串](#25.7.1 带u、U和u8前缀的字面串)
- [25.7.2 可重启动的多字节/宽字符转换函数](#25.7.2 可重启动的多字节/宽字符转换函数)
- 问与答
- 写在最后
第25章 国际化特性
------即使你的计算机说英语,它也可能产自日本。
过去,
C
语言并不十分适合在非英语国家(地区)使用。C
语言最初假定字符都是单字节的,并且所有计算机都能识别字符#
、[
、\
、]
、^
、{
、|
、}
和~
,因为这些字符都需要在C
程序中用到。遗憾的是这些假定并不是在世界的任何地方都适用。因此,创建C89
的专家又添加了新的特性和函数库,以使C
语言更加国际化。
1994
年,针对ISO C
标准的修正草案Amendment1
被批准通过,这一增强的C89
版本有时也称为C94
或C95
。这一草案通过双联符语言特性以及<iso646.h>
、<wchar.h>
和<wctype.h>
提供了对国际化编程的额外函数库支持 。C99
以通用字符名的形式为国际化提供了更多的支持。C1X
继续以<uchar.h>
改进了这些支持。本章介绍C
语言的所有国际化特性,这些特性可能来自C89
、Amendment1
、C99
或C1X
。虽然来自Amendment1
的修改事实上先于C99
,但我们也将其标记为C99
的修改。
<locale.h>头(25.1节)
提供了允许程序员针对特定的"地区"(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数。多字节字符和宽字符(25.2节)
使程序可以工作在更大的字符集上,例如亚洲国家的字符集。通过双联符
、三联符
和<iso646.h>(25.3节)
可以在一些不支持某些C
语言编程中常用字符的机器上编写程序。通用字符名(25.4节)
允许程序员把通用字符集中的字符嵌入程序的源代码中。<wchar.h>(25.5节)
提供了用于宽字符输入/输出以及宽字符串操作的函数。<wctype.h>头(25.6节)
提供了宽字符分类函数和大小写映射函数。最后,<uchar.h>头(25.7节)
提供了Unicode
字符处理函数。
25.1 <locale.h>: 本地化
<locale.h>
提供的函数用于控制C
标准库中对于不同的地区会产生不一样的行为的部分。[地区(locale)
通常是国家或者说某种特定语言的地理区域。]
在标准库中,依赖地区的部分包括以下几项:
数字量的格式
。例如,在一些地区小数点是圆点(297.48
),在另一些地区则是逗号(297,48)
。货币量的格式
。例如,不同国家或地区的货币符号不同。字符集
。字符集通常依赖于特定地区的语言。亚洲国家或地区所需的字符集通常比西方国家或地区大得多。日期和时间的表示形式
。例如,一些地区习惯在写日期时先写月(8/24/2012
),而另一些地区习惯先写日(24/8/2012
)。
25.1.1 类项
通过修改地区,程序可以改变它的行为来适应世界的不同区域。但地区改动可能会影响库的许多部分,其中一部分可能是我们不希望改变的。幸好,我们不需要同时对库的所有部分进行改动。实际上,可以使用下列宏中的一种来指定一个类项:
LC_COLLATE
。影响两个字符串比较函数(strcoll
和strxfrm
)的行为。[这两个函数都在<string.h>(23.6节)
中声明。]LC_CTYPE
。影响<ctype.h>(23.5节)
中的函数(isdigit
和isxdigit
除外)的行为。同时还影响本章讨论的多字节函数和宽字符函数的行为。LC_MONETARY
。影响由localeconv
函数返回的货币格式信息。LC_NUMERIC
。影响格式化输入/输出函数(如printf
和scanf
)使用的小数点字符以及<stdlib.h>
中的数值转换函数(如strtod,26.2节
),还会影响localeconv
函数返回的非货币格式信息。LC_TIME
。影响strftime
函数(在<time.h>
中声明,26.3节
)的行为,该函数将时间转换成字符串。在C99
中,还会影响wcsftime函数(25.5节)
的行为。
C
语言的具体实现中可以提供其他类项,并定义上面未列出的以LC_
开头的宏。例如,大多数UNIX
系统提供了一个LC_MESSAGES
类项,它会影响系统的肯定和否定响应格式。
25.1.2 setlocale函数
c
char *setlocale(int category, const char *locale);
setlocale
函数修改当前的地区,可以是针对一个类项的,也可以是针对所有类项的。如果第一个参数是LC_COLLATE
、LC_CTYPE
、LC_MONETARY
、LC_NUMERIC
或LC_TIME
之一,那么setlocale
调用只会影响一个类项。如果第一个参数是LC_ALL
,调用就会影响所有类项。C
标准对第二个参数仅定义了两种可能值:"C"
和""
。如果有其他地区,则由具体的实现自行处理。
在任意程序执行开始时,都会隐式执行调用:
c
setlocale(LC_ALL, "C");
//当地区设置为"C"时,库函数按"正常"方式执行,小数点是一个点。
如果在程序运行起来后想改变地区,就需要显式调用setlocale
函数 。用""
作为第二个参数调用setlocale
函数可以切换到本地(nativelocale)模式
。这种模式下程序会适应本地的环境。C
标准并没有定义切换到本地模式的具体影响。setlocale
函数的有些实现会检查当前的运行环境[与getenv函数(26.2节)
的方式一样],查找特定名字(可能与表示类项的宏同名)的环境变量;有些实现则根本什么都不做。(C
标准并没有要求setlocale
有什么特定的作用。当然,如果库中的setlocale
什么都不做,那么这个库在一些地区可能不会卖得很好。)
补充小知识
:对于除"C"
和""
以外的其他地区,不同的编译器之间有很大的差异。GNU
的C
库(称为glibc
)提供了"POSIX"
地区,该地区与""
一样。glibc
用于Linux
,允许在需要的时候增加额外的地区。地区的格式为语言[_地域][.码集][@指定符]
。其中方括号中的项是可选的 。语言的可能值列在
ISO 639
标准中,"地域"来自另一个标准(ISO 3166
),"码集"指明字符集或字符集的编码方案。下面给出了几个例子:
c"swedish" "en_GB" "en_IE" "fr_CH
"en_IE"
地区有几种变体,包括"en_IE@euro"(使用欧元)
、"en_IE.iso88591"(使用ISO/IEC8859-1字符集)
、"en_IE.iso885915@euro"(使用ISO/IEC8859-15字符集和欧元)
以及"en_IE.utf8"(使用通用字符集的UTF-8编码方案)
。
Linux
和其他一些版本的UNIX
支持locale
命令,该命令可以用于获取地区信息。locale
命令的用法之一是获取所有可用地区的列表,这可以通过在命令行输入下面的语句来实现:
bashlocale -a
地区信息正变得越来越重要,因此
统一字符联盟(Unicode Consortium)
设立了一个泛区域数据仓库(Common Locale Data Repository,CLDR)
项目来建立标准的地区集合。
当setlocale
函数调用成功时,它会返回一个指向字符串的指针,这个字符串与新地区的类项相关联。(例如,这个字符串可能就是地区名字自身。)如果调用失败,setlocale
函数返回空指针。
setlocale
函数也可以当作查询函数使用。如果第二个参数是空指针,setlocale
函数会返回一个指向字符串的指针,这个字符串与当前地区类项相关联。这一特性在第一个参数为LC_ALL
时特别有用,因为可以获取所有类项的当前设置。setlocale
函数返回的字符串可以(通过复制到变量中)保存起来,以便日后调用setlocale
函数时使用。
25.1.3 localeconv函数
c
struct lconv *localeconv(void);
虽然可以通过调用setlocale
函数来获取当前地区的信息,但setlocale
函数可能不是以最有效的形式返回信息的。为了找到关于当前地区的很具体的信息(小数点字符是什么,货币符号是什么),只需要用到声明在<locale.h>
中的另一个函数localeconv
。
localeconv
函数返回指向struct lconv
类型结构的指针。该结构的成员包含了当前地区的详细信息 。该结构具有静态存储期,以后可以通过调用localeconv
函数或者setlocale
函数来修改。在使用上述函数之一擦除结构信息之前,请确保已经从lconv
结构中提取了所需要的信息。
lconv
结构中的一些成员具有char*
类型,另一些成员则具有char
类型。表25-1
列出了char*
类型的成员,其中前三个成员描述了非货币数值的格式,其他成员则处理货币数值。此表还给出了"C"
地区(默认情况)中每个成员的值,值为""
意味着"不可用"
。
表25-1 lconv
结构的char*
类型的成员
名称 | 在"C"地区中的值 | 描述 |
---|---|---|
decimal_point(非货币类) | "." | 十进制小数点字符 |
thousands_sep(非货币类) | "" | 在十进制小数点前用来分隔数字组的字符 |
grouping(非货币类) | "" | 数字组的大小 |
mon_decimal_point(货币类) | "" | 十进制小数点字符 |
mon_thousands_sep(货币类) | "" | 在十进制小数点前用来分隔数字组的字符 |
mon_grouping(货币类) | "" | 数字组的大小 |
positive_sign(货币类) | "" | 表示非负值的字符串 |
negative_sign(货币类) | "" | 表示负值的字符串 |
currency_symbol(货币类) | "" | 本地货币符号 |
int_curr_symbol(货币类) | "" | 国际货币符号① |
①分隔符(常常是空格或者点)后边跟着3
个字母的缩写。例如,瑞士、英国和美国的国际货币符号分别是"CHF"
、"GBP"
和"USD"
。
这里需要特别说明一下成员
grouping
和成员mon_grouping
。这两个字符串中的每个字符都说明了一组数字的大小。(分组工作是从十进制小数点开始自右向左进行的。)值CHAR_MAX
说明不需要继续分组了,0
说明前面的元素应该用于其余的数字。例如,字符串"\3"
(\3
的后边跟着\0
)说明第一组应该有3
个数字,以后所有其他数字也应该以3
为单位分组。
lconv
结构的char
类型成员分为两组。第一组的成员(见表25-2
)影响货币数值的本地格式化,第二组的成员(见表25-3
)影响货币数值的国际格式化。表25-3
中只有一个成员不是C99
新增的。如表25-2
和表25-3
所示,"C"
地区中每个char
类型成员的值为CHAR_MAX
,表示"不可用"。
表25-2 lconv
结构的char
类型成员(本地格式化)
名称 | 在"C"地区中的值 | 描述 |
---|---|---|
frac_digits | CHAR_MAX | 十进制小数点后的数字个数 |
p_cs_precedes | CHAR_MAX | 如果currency_symbol在非负值之前,则为1;如果currency_symbol在数值之后,则为0 |
n_cs_precedes | CHAR_MAX | 如果currency_symbol在负值之前,则为1;如果currency_symbol在数值之后,则为0 |
p_sep_by_space | CHAR_MAX | 把currency_symbol和数值符号字符串与非负值分隔开(见表25-4) |
n_sep_by_space | CHAR_MAX | 把currency_symbol和数值符号字符串与负值分隔开(见表25-4) |
p_sign_posn | CHAR_MAX | 用于非负值时positive_sign的位置(见表25-5) |
n_sign_posn | CHAR_MAX | 用于负值时negative_sign 的位置(见表25-5) |
表25-3 lconv
结构的char
类型成员(国际格式化)
名称 | 在"C"地区中的值 | 描述 |
---|---|---|
int_frac_digits | CHAR_MAX | 十进制小数点后的数字个数 |
int_p_cs_precedes① | CHAR_MAX | 如果int_curr_symbol在非负值之前,则为1;如果int_curr_symbol在数值之后,则为0 |
int_n_cs_precedes① | CHAR_MAX | 如果int_curr_symbol在负值之前,则为1;如果 int_curr_symbol在数值之后,则为0 |
int_p_sep_by_space① | CHAR_MAX | 把int_curr_symbol和数值符号字符串与非负值分隔开(见表25-4) |
int_n_sep_by_space① | CHAR_MAX | 把int_curr_symbol和数值符号字符串与负值分隔开(见表25-4) |
int_p_sign_posn① | CHAR_MAX | 用于非负值时positive_sign的位置(见表25-5) |
int_n_sign_posn① | CHAR_MAX | 用于负值时negative_sign的位置(见表25-5) |
① 仅C99
有。
表25-4
解释了成员p_sep_by_space
、n_sep_by_space
、int_p_sep_by_space
和int_n_sep_by_space
值的含义。成员p_sep_by_space
和n_sep_by_space
的含义在C99
中有所改变。在C89
中,它们只有两种可能的值:1
(currency_symbol
和货币量之间有空格)和0
(currency_symbol
和货币量之间没有空格)。
表25-4 ...sep_by_space
成员的值
值 | 含义 |
---|---|
0 | 货币符号与量之间没有空格 |
1 | 如果货币符号与量的符号相邻,用空格把它们与量分隔开;否则,用空格把货币符号与量分隔开 |
2 | 如果货币符号与量的符号相邻,用空格把它们分隔开;否则,用空格把量的符号与量分隔开 |
表25-5
解释了成员p_sign_posn
、n_sign_posn
、int_p_sign_posn
和int_n_sign_posn
的含义。
表25-5 ...sign_posn
成员的值
值 | 含义 |
---|---|
0 | 量和货币符号的外面有圆括号 |
1 | 量的符号在量和货币符号的前面 |
2 | 量的符号在量和货币符号的后面 |
3 | 量的符号刚好在货币符号的前面 |
4 | 量的符号刚好在货币符号的后面 |
为了说明
lconv
结构的成员如何随着地区的不同而不同,下面来看两个示例。表25-6
显示了lconv
货币成员用于美国和芬兰两国时的常见值(芬兰使用欧元作为货币)。
表25-6 lconv
货币成员用于美国和芬兰两国时的常见值
成员 | 美国 | 芬兰 |
---|---|---|
mon_decimal_point | "." | "," |
mon_thousands_sep | "," | " " |
mon_grouping | "\3" | "\3" |
positive_sign | "" | "" |
negative_sign | "-" | "-" |
currency_symbol | "$" | "EUR" |
frac_digits | 2 | 2 |
p_cs_precedes | 1 | 0 |
n_cs_precedes | 1 | 0 |
p_sep_by_space | 0 | 2 |
n_sep_by_space | 0 | 2 |
p_sign_posn | 1 | 1 |
n_sign_posn | 1 | 1 |
int_curr_symbol | "USD " | "EUR " |
int_frac_digits | 2 | 2 |
int_p_cs_precedes | 1 | 0 |
int_n_cs_precedes | 1 | 0 |
int_p_sep_by_space | 1 | 2 |
int_n_sep_by_space | 1 | 2 |
int_p_sign_posn | 1 | 1 |
int_n_sign_posn | 1 | 1 |
表25-7
是把7593.86
格式化成上述两个地区的货币数值的情况,具体形式与数值符号以及是本地化还是国际化有关。
表25-7 美国(美元)和芬兰(欧元)货币数值对比
美国 | 芬兰 | |
---|---|---|
本地格式(正数) | $7,593.86 | 7 593,86 EUR |
本地格式(负数) | -$7,593.86 | - 7 593,86 EUR |
国际化格式(正数) | USD | 7,593.86 |
国际化格式(负数) | -USD | 7,593.86 |
请记住
C
语言的库函数不能自动格式化货币量,需要由程序员使用lconv
结构中的信息来完成格式化。
25.2 多字节字符和宽字符
程序在适应不同地区的过程中最大的难题之一就是字符集的问题 。北美地区主要使用
ASCII
字符集及其扩展,包括Latin-1(7.3节)
;其他地区的情况较为复杂。在许多国家,计算机采用类似于ASCII
的字符集,但是缺少了某些字符。25.3节
将进一步讨论这个问题。其他国家或地区,尤其是在亚洲则面临着另一个问题:书写的语言需要巨大的字符集,字符个数通常是以千计的。
因为定义已经把char
类型值的大小限制为1
字节,所以通过改变char
类型的含义来处理更大的字符集显然是不可能的。取而代之的是,C
语言允许编译器提供一种扩展字符集。这种字符集可以用于编写C
程序(例如,在注释和字符串中),也可以用于程序运行的环境中,或者两者都有。C
语言提供了两种对扩展字符集进行编码的方法:多字节字符(multibyte character)
和宽字符(wide character)
。C
语言还提供了把一种编码转换成另外一种编码的函数。
25.2.1 多字节字符
在多字节字符编码中,用一个或多个字节表示一个扩展字符 。根据字符的不同,字节的数量可能发生变化。
C
语言要求任何扩展字符集必须包含特定的基本字符(即字母、数字、运算符、标点符号和空白字符)。这些字符都必须是单字节的。其他字节可以解释为多字节字符的开始。
一些多字节字符集依靠状态相关编码(state-dependent encoding)
。在这类编码中,每个多字节字符序列都以初始迁移状态(initial shift state)
开始。以后遇到的特定字节(称为迁移序列)会改变迁移状态,从而影响后续字节的含义。例如,日本的JIS
编码混合使用单字节码与双字节码,嵌在字符串中的"转义序列"说明何时对单字节模式和双字节模式进行切换。(与之相反,Shift-JIS
编码不是状态相关的。每个字符要求一个或者两个字节,但是双字节字符的第一个字节总可以区别于单字节字符。)
在任何编码中,无论迁移状态如何,
C
标准都要求始终用零字节来表示空字符。而且,零字节不能是多字节字符的第二个(或者更后面的)字节。
C
语言库提供了两个与多字节字符相关的宏:MB_LEN_MAX
和MB_CUR_MAX
,这两个宏说明了多字节字符中字节的最大数量。宏MB_LEN_MAX
(定义在<limits.h>
中)给出了任意支持地区的最大值,而宏MB_CUR_MAX
(定义在<stdlib.h>
中)则给出了当前地区的最大值。(改变地区可能会影响多字节字符的解释。)显然,宏MB_CUR_MAX
不可能大过宏MB_LEN_MAX
。
任何字符串都可能包含多字节字符,尽管字符串的长度指的是字符串中字节的数目(由strlen
函数确定)而不是字符的数目。特别地,...printf
和...scanf
函数调用中的格式串可以包含多字节字符。因此,C99
标准把术语多字节字符串定义为字符串的同义词。
25.2.2 宽字符
另外一种对扩展字符集进行编码的方法是使用
宽字符(wide character)
。宽字符是一种整数,其值代表字符。不同于长度可变的多字节字符,特定实现中所支持的所有宽字符有着相同的字节数。宽字符串是指由宽字符组成的字符串,其末尾有一个空宽字符(数值为零的宽字符)。
宽字符具有wchar_t
类型(在<stddef.h>
和其他一些头中声明),wchar_t
必须是可以表示任何支持地区的最大扩展字符集的整数类型
。例如,如果两个字节足够表示任何扩展字符集,那么可以把wchar_t
定义成unsigned short int
。
C
语言支持宽字符常量和宽字面串 。宽字符常量类似于普通的字符常量,但需要有字母L
作为前缀:
c
L'a'
而宽字面串也需要用字母L
作为前缀:
c
L"abc"
此字符串表示一个含有宽字符L'a'
、L'b'
和L'c'
并且后跟一个空的宽字符的数组。
25.2.3 Unicode和通用字符集
多字节字符和宽字符的差异在讨论
Unicode
时比较明显。Unicode
是Unicode联盟(Unicode Consortium)
开发的巨大字符集。Unicode
联盟是由一些计算机制造商成立的,目的在于创建用于计算机的国际化字符集。Unicode
的前256
个字符与Latin-1
一样(所以Unicode
的前128
个字符与ASCII
字符集相匹配)。但是Unicode
所包括的范围远远超过Latin-1
,提供的字符几乎可以满足所有现代语言和旧式语言的需求。Unicode
还包括许多专用符号,如在数学和音乐中使用的符号。Unicode
标准最早出版于1991
年。
Unicode
与国际标准ISO/IEC 10646
紧密相关,该标准定义了一种称为通用字符集(Universal CharacterSet,UCS)
的字符编码方案。UCS
是国际标准化组织(ISO
)开发的,差不多与Unicode
同一时间启动。尽管UCS
最初和Unicode
不同,但二者后来统一了。ISO
现在与Unicode
联盟紧密合作,以确保ISO/IEC 10646
和Unicode
保持一致。因为Unicode
和通用字符集非常相似,所以本书经常将这两个术语互换使用。
Unicode
最初只有65536
个字符(16
位所能表示的字符数目),后来发现这是不够的,现在Unicode
的字符已超过100000
个。(欲了解最新版本,请访问Unicode
官方网站。)Unicode
的前65536个
字符(包括最常用的字符)称作基本多语种平面(Basic Multilingual Plane,BMP)
。
25.2.4 Unicode编码
Unicode
为每一个字符分配一个唯一的数(称为码点
)。可以有多种方式使用字节来表示这些码点。我将介绍两种简单的方法,一种使用宽字符,另一种使用多字节字符。
UCS-2
是一种宽字符编码方案,它把每一个Unicode
码点存储为两个字节 。USC-2
可以表示基本多语种平面上的所有字符(码点在十六进制的0000
和FFFF
之间),但是不能够表示不属于BMP
的Unicode
字符。
另一种流行的方式是
8
位的UCS
转换格式(UTF-8
),该方案使用多字节字符 。UTF-8
是由Ken Thompson
和他在贝尔实验室的同事RobPike
于1992
年设计的(就是设计B
语言的那个Ken Thompson
,B
语言是C
语言的前身)。UTF-8
的一个有用的性质就是ASCII
字符在UTF-8
中保持不变:每个字符都是一个字节且使用同样的二进制编码。所以,设计用于读取UTF-8
数据的软件同样可以处理ASCII
数据,而不需要任何改变。基于这些原因,UTF-8
广泛用于因特网上基于文本的应用程序(如网页和电子邮件)。
在UTF-8
中每个码点需要1~4
字节。UTF-8
中常用字符所需的字节数较少,如表25-8
所示。
表25-8 UTF-8
编码
码点范围(十六进制) | UTF-8字节序列(二进制) |
---|---|
000000~00007F | 0xxxxxxx |
000080~0007FF | 110xxxxx 10xxxxxx |
000800~00FFFF | 1110xxxx 10xxxxxx 10xxxxxx |
010000~10FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxx |
UTF-8
读取码点值中的位,将其分为几组(由表25-8
中的x
来表示),并把每一组分配给不同的字节。最简单的情况是码点在0~7F
范围(ASCII
字符)内,此时只要在原数的7
位之前加一个0
即可。
码点在80~7FF
范围(包括所有的Latin-1
字符)内时,需要将码点值的位分为两组,一组5
位另一组6
位。5
位组的前缀为110
,6
位组的前缀为10
。例如,字符ä
的码点为E4(十六进制)
或11100100
(二进制)。在UTF-8
中可以将其表示为双字节序列11000011
10100100
。注意高亮的部分,连起来就是00011100100
。
如果字符的码点落在
800~FFFF
范围(包含基本多语种平面中的剩余字符)内,那么需要3
字节。其他的Unicode
字符(大多数很少用到)都分配4
字节。UTF-8
有以下几个有用的性质:
128
个ASCII
字符中的每一个字符都可以用一个字节表示。仅由ASCII
字符组成的字符串在UTF-8
中保持不变。- 对于
UTF-8
字符串中的任意字节,如果其最左边的位是0
,那么它一定是ASCII
字符,因为其他所有字节都以1
开始。 - 多字节字符的第一个字节指明了该字符的长度。如果字节开头
1
的个数为2
,那么这个字符的长度为2
字节。如果字节开头1
的个数为3
或4
,那么这个字符的长度分别为3
字节或4
字节。 - 在多字节序列中,每隔一个字节就以
10
作为最左边的位。
最后三个性质特别重要,因为它们可以保证一个多字节字符中的字节序列不会是另一个有效的多字节字符。这样一来,简单地进行字节比较就可以从多字节字符串中搜索一个特定的字符或字符序列。
现在来看看
UTF-8
相比于UCS-2
的优缺点。UCS-2
的优点在于,字符都是以最自然的格式存储的。UTF-8
的优点在于,它能处理所有的Unicode
字符(而不仅仅是BMP
中的字符)、所需的空间比UCS-2
少且兼容ASCII
。UCS-2
用于WindowsNT
操作系统,但不如UTF-8
流行;使用4
字节的新版本(UCS-4
)正在逐渐取代UCS-2
的地位。一些系统把UCS-2
扩展为一种多字节编码方案,方法是允许用可变数量的字节对来表示字符(UCS-2
使用一个字节对来表示字符)。这样的编码方案称为UTF-16
,它的优点是能够兼容UCS-2
。
25.2.5 多字节/宽字符转换函数
c
int mblen(const char *s, size_t n); //来自<stdlib.h>
int mbtowc(wchar_t * restrict pwc,
const char * restrict s,
size_t n); //来自<stdlib.h>
int wctomb(char *s, wchar_t wc); //来自<stdlib.h>
尽管
C89
引入了多字节字符和宽字符的概念,它只提供了5
个函数来处理这些字符。现在介绍一下这些函数,它们都属于<stdlib.h>
头。C99的<wchar.h>
和<wctype.h>
头新增了许多多字节和宽字符函数,25.5节
和25.6节
将加以讨论。
C89
的多字节/宽字符函数分为两组。第一组把多字节格式的单个字符转换为宽字符格式,或者进行反向转换 。这些函数的行为依赖于当前地区的LC_CTYPE
类项。如果多字节编码是依赖状态的,函数的行为还依赖于当前的转换状态。转换状态不仅包含当前在多字节字符中的位置,还包含当前的迁移状态。以空指针作为char*
类型参数的值来调用这些函数会导致函数的内部转换状态设为初始转换状态。该状态表明当前没有正在处理的多字节字符,且初始迁移状态有效。对函数的后续调用会更新其内部转换状态。
mblen
函数检测第一个参数是否指向形成有效多字节字符的字节序列。如果是,则函数返回字符中的字节数;如果不是,则函数返回-1
。作为一种特殊情况,如果函数的第一个参数指向空字符,则mblen
函数返回0
。函数的第二个参数限制了mblen
函数将检测的字节的数量,通常情况下会传递MB_CUR_MAX
。
下面的函数来自P.J.Plauger
的《C标准库 》一书,它使用mblen
函数来确定字符串是否由有效的多字节字符构成。如果s
指向有效字符串,则函数返回0
。
c
int mbcheck(const char *s)
{
int n;
for (mblen(NULL, 0); ; s += n)
if ((n = mblen(s, MB_CUR_MAX)) <= 0)
return n;
}
mbcheck
函数有两点需要特别说明一下。首先是mblen(NULL,0)
的神秘调用。此调用把mblen
的内部转换状态设置为初始转换状态(针对多字节编码依赖状态的情况)。其次是有关终止的问题。要记住s
指向的是以空字符结尾的普通字符串。当mblen
函数遇到这个空字符时将返回0
,这样会导致mbcheck
函数返回。如果mblen
因为遇到无效的多字节字符而返回-1
,那么mbcheck
会提前返回。
mbtowc
函数把(第二个参数指向的)多字节字符转换为宽字符 。第一个参数指向函数用于存储结果的wchar_t
类型变量,第三个参数限制了mbtowc
函数将检测的字节的数量。mbtowc
函数返回和mblen
函数一样的值:如果多字节字符有效,则返回多字节字符中字节的数量;如果多字节字符无效,则返回-1
;如果第二个参数指向空字符,则返回0
。
wctomb
函数把宽字符(第二个参数)转换为多字节字符,并把该多字节字符存储到第一个参数指向的数组中 。wctomb
函数可以向数组中存储多达MB_LEN_MAX
个字符,但是在最后不附加空字符。如果宽字符能与有效的多字节字符相对应,wctomb
函数会返回多字节字符中字节的数量,否则返回-1
。(注意,如果要求转换空的宽字符,wctomb
函数返回1
。)
下面这个函数(也来自Plauger
的《C标准库 》一书)使用wctomb
函数来确定是否可以把宽字符字符串转换为有效的多字节字符:
c
int wccheck(wchar_t *wcs)
{
char buf[MB_LEN_MAX];
int n;
for (wctomb(NULL, 0); ; ++wcs)
if ((n = wctomb (buf, *wcs)) <= 0)
return --1; /* invalid character */
else if (buf[n-1] == '\0')
return 0; /* all characters are valid */
}
顺便说一下,mblen
、mbtowc
和wctomb
都可以用来测试多字节编码是否依赖状态。当传递空指针作为char*
类型的参数时,如果多字节字符的编码是依赖状态的,那么上述每种函数都会返回非零值,否则返回0
。
25.2.6 多字节/宽字符串转换函数
c
size_t mbstowcs(wchar_t * restrict pwcs,
const char * restrict s,
size_t n); //来自<stdlib.h>
size_t wcstombs(char * restrict s,
const wchar_t * restrict pwcs,
size_t n); //来自<stdlib.h>
剩下的C89
多字节/宽字符函数把包含多字节字符的字符串转换为宽字符字符串,或者进行反向转换。如何进行转换依赖于当前地区的LC_CTYPE
类项。
mbstowcs
函数把多字节字符序列转换为宽字符 。函数的第二个参数指向包含待转换的多字节字符的数组,第一个参数指向宽字符数组,第三个参数限制了可以存储在数组中的宽字符数量。当达到上限或者遇到(存储在宽字符数组中的)空字符时,mbstowcs
函数就停止。函数会返回修改的数组元素的数量(不包括末尾的空的宽字符)。如果遇到无效的多字节字符,mbstowcs
函数返回-1
(强制转换为size_t
类型)。
wcstombs
函数和mbstowcs
函数正好相反:它把宽字符序列转换为多字节字符 。函数的第二个参数指向宽字符串,第一个参数指向用于存储多字节字符的数组,第三个参数限制了可以存储在数组中的字节的数量。当达到上限或者遇到(自己存入的)空字符时,wcstombs
函数就停止。函数会返回存储的字节数量(不包括用于终止的空字符)。如果遇到无法对应任何多字节字符的宽字符,wcstombs
函数返回-1
(强制转换为size_t
类型)。
mbstowcs
函数假设要转换的字符串以初始迁移状态开始。由wcstombs
函数产生的字符串始终是以初始迁移状态开始的。
25.3 双联符和三联符
某些国家或地区的程序员常常因为键盘缺少
C
语言需要的字符而无法进入C程序。在欧洲尤其如此,那里的老式键盘提供的是欧洲语言所用的古老字符而不是C
语言需要的字符,如#
、[
、\
、]
、^
、{
、|
、}
和~
。C89
引入了三联符
(表示问题字符的三字符编码)来解决这一问题。但是三联符没能流行起来,所以标准的Amendment1
增加了两处改进:双联符
和<iso646.h>
头,前者比三联符易读,后者定义了表示特定C运算符的宏。
25.3.1 三联符
三联序列(trigraph sequence,或者简称为三联符)
是一种三字符编码,它可以用于替代ASCII字符 。表25-9
给出了三联符的完整列表。所有三联符都以??
开始,这样做虽然并不足够醒目,但至少便于发现。
表25-9 三联序列
三联序列 | 等价的ASCII码 |
---|---|
??= | # |
??( | [ |
??/ | | |
??) | ] |
??' | ^ |
??< | { |
??! | | |
??> | } |
??- | ~ |
三联符可以自由地替换成等价的ASCII
码。例如,程序
c
#include <stdio.h>
int main(void)
{
printf("hello, world\n");
return 0;
}
可以写成
c
??=include <stdio.h>
int main(void)
??<
printf("hello, world??/n");
return 0;
??>
尽管三联符很少用到,但遵循C89
或C99
标准的C
编译器都必须能接受三联符。这个特性有时可能会导致问题。
请注意 !!在字面串中请小心放置
??
,因为编译器可能会把它视为三联符的开始标志。如果发生这种情况,那么通过在第二个?
字符的前面放置字符\
来把第二个字符?
变成转义序列。?\?
这样组合的结果就不会被看作三联符的开始了。
25.3.2 双联符
因为三联符较难读懂,所以
C89
标准的Amendment1
增加了双联符(digraph)
表示法。顾名思义,双联符只需要两个字符而不是三个 。双联符可以用于替代表25-10
中的6
个记号。
表25-10 双联符
双联符 | 记号 |
---|---|
<: | [ |
:> | ] |
<% | { |
%> | } |
%: | # |
%:%: | ## |
双联符(不同于三联符)是记号的替代品,而不是字符的替代品 。因此,字面串或字符常量中的双联符不会被识别出来。例如,字符串"<::>"
长度为4
,它包括字符<
、:
、:
和>
,而不包括字符[
和]
。相反,字符串"??(??)"
长度为2
,因为编译器将三联序列??(
替换为[
,并把三联序列??)
替换为]
。
双联符比起三联符来说功能更有限。
- 第一,如我们所见,双联符在字面串和字符常量中不起作用,所以在这些情况下仍然需要三联符。
- 第二,双联符不能为字符
\
、^
、|
和~
提供替代的表示方法。接下来讨论的<iso646.h>
可以解决这一问题。
25.3.3 <iso646.h>: 拼写替换
<iso646.h>
头相当简单。它只定义了表25-11
所示的11
个宏,除此之外什么都没有。每一个宏表示一个包含字符&
、|
、~
、!
或^
的C
运算符。这样一来,即使键盘上缺少这些字符,也仍然能够使用表中列出的运算符。
表25-11 <iso646.h>
中的宏定义
宏 | 值 |
---|---|
and | && |
and_eq | &= |
bitand | & |
bitor | | |
compl | ~ |
not | ! |
not_eq | != |
or | || |
or_eq | |= |
xor | ^ |
xor_eq | ^= |
这个头的名字源于ISO/IEC 646
,这是用于类ASCII
字符集的旧版标准。该标准允许"国别变体",各个国家或地区可以用本地字符替换特定的ASCII
字符,从而导致双联符和<iso646.h>
试图解决的那个问题。
25.4 通用字符名(C99)
25.2节
讨论了通用字符集(UCS)
,它与Unicode
紧密相关。C99
提供了一种专门的特性------通用字符名,它允许我们在程序源代码中使用UCS
字符。
通用字符名类似于转义序列 。但是,普通的转义序列只能出现于字符常量和字面串中,而通用字符名还可以用于标识符。这个特性允许程序员在为变量、函数等命名时使用他们的本地语言。
可以用2
种方式书写通用字符名(\udddd
和\Udddddddd
),每个d
都是一个十六进制的数字。在格式\Udddddddd
中,8
个d
组成一个8
位的十六进制数用于标识目标字符的UCS
码点。格式\udddd
可以用于码点的十六进制值为FFFF
或更小的字符,包括基本多语种平面上的所有字符。
例如,希腊字母
β
的UCS
码点是000003B2
,所以该字符的通用字符名为\U000003B2
(或者是\U000003b2
,因为大小写在十六进制中无所谓)。因为UCS
码点的十六进制前4
位是0
,所以也可以使用\u
表示法,将字符写为\u03B2
或\u03b2
。(与Unicode
相匹配的)UCS
码点的值可以在Unicode
官方网站的CodeCharts
页面上找到。
并不是所有的通用字符名都可以用于标识符,C99
标准列出了哪些通用字符名可以用于标识符。此外,标识符不能以表示数字的通用字符名开头。
25.5 <wchar.h>: 拓展的多字节和宽字符实用工具
<wchar.h>
头提供了宽字符输入/输出和宽字符串处理的函数。<wchar.h>
头中的绝大部分函数都是其他头(主要是<stdio.h>
和<string.h>
)中函数的宽字符版本。
<wchar.h>
头声明了以下一些类型和宏:
mbstate_t
:把多字节字符序列转换为宽字符序列或进行反向转换时,可以用这个类型的值来存储转换状态。wint_t
:一种整数类型,它的值表示扩展字符。WEOF
:一个表示wint_t
类型值的宏,该wint_t
类型值与任何扩展字符不同。WEOF
的用法与EOF
很相似,通常用于指明错误或文件末尾条件。
注意 !!<wchar.h>
为宽字符提供了函数但没有为多字节字符提供函数 。这是因为C
的普通库函数能够处理多字节字符,所以不需要专门的函数。例如,fprintf
函数允许格式串包含多字节字符。
大多数宽字符函数的行为与标准库其他地方的某个函数一致。通常,所做的修改仅仅是把参数和返回值的类型从char
改成了wchar_t
(或者从char *
改成了wchar_t *
)。另外,表示字符计数的参数和返回值用宽字符而不是字节的个数来衡量。在本节下面的内容中,将指出与每个宽字符函数对应的库函数(如果存在的话)。这里不会详细讨论宽字符函数,除非它与相应的"非宽"版本有显著差异。
25.5.1 流的倾向性
在讨论
<wchar.h>
提供的输入/输出函数前,先理解流的倾向性(stream orientation)
是很重要的,这个概念在C89
中并不存在。
每个流要么是面向字节的(传统方式),要么是面向宽字符的(把数据当成宽字符写入流中) 。第一次打开流时,它没有倾向性。[特别地,标准流(22.1节)stdin
、stdout
和stderr
在程序刚开始执行时是没有倾向性的。]使用字节输入/输出函数在流上执行操作会使流成为面向字节的,使用宽字符输入/输出函数执行操作会使流成为面向宽字符的。流的倾向性可以调用fwide
函数进行选择(本节后面会讲到)。流只要保持打开状态,就能保持其倾向性。调用freopen函数(22.2节)
重新打开流会删除其倾向性。
往面向宽字符的流中写入宽字符时,首先将宽字符转换为多字节字符然后再存入与流相关的文件。相反,当从面向宽字符的流中读取输入时,需要把流中的多字节字符转换为宽字符。文件中的多字节编码与程序中的字符和字符串编码相类似,不同之处在于,文件中的编码可能包含空字节。
每一个面向宽字符的流都有一个相关联的mbstate_t
对象,该对象用于记录流的转换状态。当写入流中的宽字符不能与任何多字节字符相对应,或者从流中读取的字符序列不能构成有效的多字节字符时,会出现编码错误。在上述任何一种情况下,EILSEQ
宏(定义在<errno.h>
头中)的值会存储到errno变量(24.2节)
中,以指明错误的性质。
一旦流是面向字节的,对其应用宽字符输入/输出函数就不合法了。类似地,对面向宽字符的流应用字节输入/输出函数也是不合法的。其他流函数可以用于两种倾向性的流,不过对于面向宽字符的流有以下几点需要特别考虑:
- 面向宽字符的二进制流受限于文本文件和二进制文件的文件定位限制。
- 对面向宽字符的流执行文件定位操作之后,宽字符输出函数也许会覆盖多字节字符的一部分。这样会导致文件的其他部分处于不确定的状态。
- 对面向宽字符的流调用
fgetpos函数(22.7节)
会获取流的mbstate_t
对象,使其成为与流相关联的fpos_t
对象的一部分。
以后如果使用该fpos_t
对象来调用fsetpos函数(22.7节)
,mabstate_t
对象会恢复以前的值。
25.5.2 格式化宽字符输入/输出函数
c
int fwprintf(FILE * restrict stream,
const wchar_t * restrict format, ...);
int fwscanf(FILE * restrict stream,
const wchar_t * restrict format, ...);
int swprintf(wchar_t * restrict s, size_t n,
const wchar_t * restrict format, ...);
int swscanf(const wchar_t * restrict s,
const wchar_t * restrict format, ...);
int vfwprintf(FILE * restrict stream,
const wchar_t * restrict format, va_list arg);
int vfwscanf(FILE * restrict stream,
const wchar_t * restrict format, va_list arg);
int vswprintf(wchar_t * restrict s, size_t n,
const wchar_t * restrict format, va_list arg);
int vswscanf(const wchar_t * restrict s,
const wchar_t * restrict format, va_list arg);
int vwprintf(const wchar_t * restrict format, va_list arg);
int vwscanf(const wchar_t * restrict format, va_list arg);
int wprintf(const wchar_t * restrict format, ...);
int wscanf(const wchar_t * restrict format, ...);
这一组函数是<stdio.h>
中的格式化输入/输出函数(在22.3节
讨论过)的宽字符版本 。<wchar.h>
中的函数的参数类型为wchar_t *
而不是char *
,但函数的行为与<stdio.h>
中的函数基本相同。表25-12
给出了<stdio.h>
中的函数与宽字符函数的对应关系。如果没有特别说明,表中左边一列的函数与它右边的函数功能相同。
表25-12 格式化的宽字符输入/输出函数及其在<stdio.h>
中的对应函数
<wchar.h>函数 | <stdio.h>中的对应函数 |
---|---|
fwprintf | fprintf |
fwscanf | fscanf |
swprintf | Snprintf、sprintf |
swscanf | sscanf |
vfwprintf | vfprintf |
vfwscanf | vfscanf |
vswprintf | vsnprintf、vsprintf |
vswscanf | vsscanf |
vwprintf | vprintf |
vwscanf | vscanf |
wprintf | printf |
wscanf | scanf |
这一组中的所有函数有以下几个共同特性:
- 都有包含宽字符的格式串。
...printf
函数返回输出的字符数量,但现在是对宽字符计数。%n
转换说明表示到目前为止输出(...printf函数)
或读入(...scanf函数)
的宽字符的数量。
fwprintf
和fprintf
还有以下不同;
%c
转换说明用于参数为int
类型的情况。如果存在长度指定符l
(转换为%lc
),则假定参数的类型为wint_t
。在上述两种情形下,相应的参数都输出为宽字符。%s
转换说明用于指向字符数组的指针,该字符数组可以包括多字节字符。(fprintf
对多字节字符没有特殊规定。)如果存在长度指定符l
(%ls
),相应的参数应该是包含宽字符的数组。在上述两种情形下,数组里的字符都输出为宽字符。(用于fprintf
时,%ls
转换说明也表示宽字符数组,但是在输出之前会将数组中的字符转换为多字节字符。)
fwscanf
函数不同于fscanf
函数,它读取宽字符 。%c
、%s
和%[
转换需要特别提一下。这些转换符都可以读取宽字符,并在存入字符数组前将其转换为多字节字符。fwscanf
使用mbstate_t
对象来记录这一过程中的转换状态;每次转换开始时,把该对象设置为0
。如果存在长度指定符l
(转换分别为%lc
、%ls
和%l[
),那么输入字符不需要转换,而是直接存入wchar_t
型的数组元素中。因此,如果希望把宽字符字符串中的字符存为宽字符,需要使用%ls
。如果用%s
而不是%ls
,宽字符能够从输入流中读出,但是在存储之前会被转换为多字节字符。
swprintf
将宽字符写入wchar_t
类型的数组。它类似于sprintf
和snprintf
,但不完全等同于这两个函数。类似于snprintf
函数,它用参数n
来限制需要输出的(宽)字符的数目,但swprintf
返回实际输出的宽字符的数目(不包括空字符)。在这一点上,它类似于sprintf
函数而非snprintf
函数,swprintf
函数返回没有长度限制的情况下应输出的字符数(不包括空字符)。如果待输出的宽字符数目为n或者更多,swpritf
函数返回负值,这与sprintf
函数和snprintf
函数均不一样。
vswprintf
函数与swprintf
函数等价,只是用arg
取代了swprintf
函数的可变参数列表。与swprintf
函数(类似但不等同于sprintf
函数和snprintf
函数)一样,vswprintf
函数是vsprintf
函数和vsnprintf
函数的结合。如果尝试输出n
个或者更多个宽字符,vswprintf
函数返回一个负整数,这与swprintf
函数类似。
25.5.3 宽字符输入/输出函数
c
wint_t fgetwc(FILE *stream);
wchar_t *fgetws(wchar_t * restrict s, int n, FILE * restrict stream);
wint_t fputwc(wchar_t c, FILE *stream);
int fputws(const wchar_t * restrict s, FILE *restrict stream);
int fwide(FILE *stream, int mode);
wint_t getwc(FILE *stream);
wint_t getwchar(void);
wint_t putwc(wchar_t c, FILE *stream);
wint_t putwchar(wchar_t c);
wint_t ungetwc(wint_t c, FILE *stream);
这一组函数是<stdio.h>
中的字符输入/输出函数(在22.4节
讨论过)的宽字符版本 。表25-13
给出了<stdio.h>
中的函数与宽字符函数的对应关系。如表所示,fwide
是唯一的全新函数。
表25-13 宽字符输入输出函数及其在<stdio.h>
中的对应函数
<wchar.h>函数 | <stdio.h>中的对应函数 |
---|---|
fgetwc | fgetc |
fgetws | fgets |
fputwc | fputc |
fputws | fputs |
fwide | --- |
getwc | getc |
getwchar | getchar |
putwc | putc |
putwchar | putchar |
ungetwc | ungetc |
除非特别说明,可以认为表25-13
中所列出的<wchar.h>
中的函数和<stdio.h>
中的对应函数行为一致。但是,多数对应函数之间有一点细微的差别。为了指示错误或者文件结尾条件,<stdio.h>
中的一些字符输入/输出函数返回EOF
,但<wchar.h>
中的对应函数返回WEOF
。
还有一个问题会影响宽字符输入函数 。调用读取单字符的函数(
fgetwc
、getwc
和getwchar
)时,可能会因为输入流中的字节不能组成有效的宽字符或者可用的字节不够而导致调用失败。这样会造成编码错误,进而导致函数将EILSEQ
存入errno
并返回WEOF
。fgetws
函数(读取宽字符串)也可能因为编码错误而失败,这种情况下它会返回空指针。
宽字符输出函数也可能遇到编码错误 。用于输出单字符的函数(
fputwc
、putwc
和putwchar
)在出现编码错误时将EILSEQ
存入errno
并返回WEOF
。但用于输出宽字符字符串的fputws
函数有所不同:它在出现编码错误时返回EOF
(而不是WEOF
)。
fwide
函数在C89
函数中没有相对应的函数。fwide
函数用于确定流的当前倾向性,如果需要还可以设置流的倾向性 。mode
参数决定函数的行为。
mode>0
:如果没有倾向性,尝试使流面向宽字符。mode<0
:如果没有倾向性,尝试使流面向字节。mode=0
:不改变倾向性。
如果流已经有了倾向性,fwide
不会改变其倾向性。
fwide
返回的值依赖于函数调用后流的倾向性 。如果流为面向宽字符的,返回的值为正;如果流为面向字节的,返回的值为负
;如果流没有倾向性,返回0
。
25.5.4 通用的宽字符串实用工具
<wchar.h>
头提供了许多函数来对宽字符串
进行操作。它们是<stdlib.h>
和<string.h>
中函数的宽字符版本。
25.5.4.1 宽字符串数值转换函数
c
double wcstod(const wchar_t * restrict nptr,
wchar_t ** restrict endptr);
float wcstof(const wchar_t * restrict nptr,
wchar_t ** restrict endptr);
long double wcstold(const wchar_t * restrict nptr,
wchar_t ** restrict endptr);
long int wcstol(const wchar_t * restrict nptr,
wchar_t ** restrict endptr, int base);
long long int wcstoll(const wchar_t * restrict nptr,
wchar_t ** restrict endptr, int base);
unsigned long int wcstoul(
const wchar_t * restrict nptr,
wchar_t ** restrict endptr, int base);
unsigned long long int wcstoull(
const wchar_t * restrict nptr,
wchar_t ** restrict endptr, int base);
这一组函数是<stdlib.h>
中的数值转换函数(将在26.2节
讨论)的宽字符版本。<wchar.h>
中的函数的参数类型为wchar_t *
和wchar_t **
而不是char *
和char **
,但它们的行为与<stdlib.h>
中的函数基本一样。表25-14
给出了<stdlib.h>
中的函数及其对应的宽字符版本。
表25-14 宽字符串数值转换函数及其在<stdlib.h>
中的对应函数
<wchar.h>函数 | <stdlib.h>中的对应函数 |
---|---|
wcstod | strtod |
wcstof | strtof |
wcstold | strtold |
wcstol | strtol |
wcstoll | strtoll |
wcstoul | strtoul |
wcstoull | strtoull |
25.5.4.2 宽字符串复制函数
c
wchar_t *wcscpy(wchar_t * restrict s1,
const wchar_t * restrict s2);
wchar_t *wcsncpy(wchar_t * restrict s1,
const wchar_t * restrict s2, size_t n);
wchar_t *wmemcpy(wchar_t * restrict s1,
const wchar_t * restrict s2, size_t n);
wchar_t *wmemmove(wchar_t *s1, const wchar_t *s2, size_t n);
这一组函数是<string.h>
中的字符串复制函数(在23.6节
讨论过)的宽字符版本。<wchar.h>
头中的函数的参数类型为wchar_t *
而不是char *
,但它们的行为与<string.h>
中的函数基本一致。表25-15
给出了<string.h>
中的函数及其对应的宽字符版本。
表25-15 宽字符串复制函数及其在<string.h>
中的对应函数
<wchar.h>函数 | <string.h>中的对应函数 |
---|---|
wcscpy | strcpy |
wcsncpy | strncpy |
wmemcpy | memcpy |
wmemmove | memmove |
25.5.4.3 宽字符串拼接函数
c
wchar_t *wcscat(wchar_t * restrict s1, const wchar_t * restrict s2);
wchar_t *wcsncat(wchar_t * restrict s1,
const wchar_t * restrict s2, size_t n);
这一组函数是<string.h>
中的字符串拼接函数(在23.6节
讨论过)的宽字符版本。<wchar.h>
中的函数的参数类型是wchar_t *
而不是char *
,但它们的行为与<string.h>
中的函数基本一样。表25-16
给出了<string.h>
中的函数及其对应的宽字符版本。
表25-16 宽字符串拼接函数及其在<string.h>
中的对应函数
<wchar.h>函数 | <string.h>中的对应函数 |
---|---|
wcscat | strcat |
wcsncat | strncat |
25.5.4.4 宽字符串比较函数
c
int wcscmp(const wchar_t *s1, const wchar_t *s2);
int wcscoll(const wchar_t *s1, const wchar_t *s2);
int wcsncmp(const wchar_t *s1, const wchar_t *s2, size_t n);
size_t wcsxfrm(wchar_t * restrict s1,
const wchar_t * restrict s2, size_t n);
int wmemcmp(const wchar_t * s1, const wchar_t * s2, size_t n);
这一组函数是<string.h>
中的字符串比较函数(在23.6节
讨论过)的宽字符版本。<wchar.h>
中的函数的参数类型是wchar_t *
而不是char *
,但它们的行为与<string.h>
中的函数基本一样。表25-17
给出了<string.h>
中的函数及其对应的宽字符版本。
表25-17 宽字符串比较函数及其在<string.h>
中的对应函数
<wchar.h>函数 | <string.h>中的对应函数 |
---|---|
wcscmp | strcmp |
wcscoll | strcoll |
wcsncmp | strncmp |
wcsxfrm | strxfrm |
wmemcmp | memcmp |
25.5.4.5 宽字符串搜索函数
c
wchar_t *wcschr(const wchar_t *s, wchar_t c);
size_t wcscspn(const wchar_t *s1, const wchar_t *s2);
wchar_t *wcspbrk(const wchar_t *s1, const wchar_t *s2);
wchar_t *wcsrchr(const wchar_t *s, wchar_t c);
size_t wcsspn(const wchar_t *s1, const wchar_t *s2);
wchar_t *wcsstr(const wchar_t *s1, const wchar_t *s2);
wchar_t *wcstok(wchar_t * restrict s1,
const wchar_t * restrict s2,
wchar_t ** restrict ptr);
wchar_t *wmemchr(const wchar_t *s, wchar_t c, size_t n);
这一组函数是<string.h>
中的字符串搜索函数(在23.6节
讨论过)的宽字符版本。<wchar.h>
中的函数的参数类型是wchar_t *
和wchar_t **
而不是char *
和char **
,但它们的行为与<string.h>
中的函数基本一样。表25-18
给出了<string.h>
中的函数及其对应的宽字符版本。
表25-18 宽字符串搜索函数及其在<string.h>
中的对应函数
<wchar.h>函数 | <string.h>中的对应函数 |
---|---|
wcschr | strchr |
wcscspn | strcspn |
wcspbrk | strpbrk |
wcsrchr | strrchr |
wcsspn | strspn |
wcsstr | strstr |
wcstok | strtok |
wmemchr | memchr |
wcstok
函数与strtok
函数作用相同,但由于有第三个参数,所以用法略有不同 。(strtok
函数只有两个参数。)要了解wcstok
的工作原理,首先需要回顾一下strto
k的行为。
23.6节
讲到strtok
在字符串中搜索一个"记号",就是一系列不包含特定分隔符的字符。调用strtok(s1,s2)
会在s1
中搜索一系列不包含在s2
中的非空字符 。strtok
函数会在记号末尾的字符后面存储一个空字符作为标记,然后返回一个指针指向记号的首字符。
以后可以调用strtok
函数在同一字符串中搜索更多的记号。调用strtok(NULL,s2)
就可以继续上一次的strtok
函数调用。和上一次调用一样,strtok
函数会用一个空字符来标记记号的末尾,然后返回一个指针指向记号的首字符。这个过程可以持续进行,直到strtok
函数返回空指针,这表明找不到符合要求的记号。
strtok
的一个问题是在搜索的时候使用静态变量来记录,这样就无法同时对两个或更多个字符串进行搜索 。而wcstok
由于多了一个参数,不存在这一问题。
wcstok
的前两个参数与strtok
是相同的(当然,它们指向宽字符串)。第三个参数ptr
将指向wchr_t *
类型的变量。函数将在这个变量中存储信息,使得之后调用wcstok
时能够继续扫描同一个字符串(当第一个参数为空指针时)。当通过后续的wcstok
调用继续进行搜索时,用指向同一个变量的指针作为第三个参数;这个变量的值在wcstok
函数调用之间不能改变。
为了了解
wcstok
的工作原理,让我们再来看看23.6节
中的例子。假设str
、p
和q
声明如下:
c
wchar_t str[] = L" April 28,1998";
wchar_t *p, *q;
最初的wcstok
调用用str
作为第一个参数:
c
p = wcstok(str, L" \t", &q);
现在p
指向April
的第一个字符,April
之后有一个空的宽字符。用空指针作为第一个参数、&q
作为第三个参数调用wcstok
,可以从上次停下来的地方继续搜索:
c
p = wcstok(NULL, L" \t,", &q);
在这个调用之后,p
指向28
的第一个字符,现在28
的后面有一个用于终止的空的宽字符。再次调用wcstok
可以定位年:
c
p = wcstok(NULL, L" \t", &q);
//p现在指向1998的第一个字符。
25.5.4.6 其他函数
c
size_t wcslen(const wchar_t *s);
wchar_t *wmemset(wchar_t *s, wchar_t c, size_t n);
这一组函数是<string.h>
中的其他字符串函数(在23.6节
讨论过)的宽字符版本。<wchar.h>
中的函数的参数类型是wchar_t *
而不是char *
,但它们的行为与<string.h>
中的函数基本一样。表25-19
给出了<string.h>
中的函数及其对应的宽字符版本。
表25-19 宽字符串其他函数与<string.h>
中的对应函数
<wchar.h>函数 | <string.h>中的对应函数 |
---|---|
wcslen | strlen |
wmemset | memset |
25.5.5 宽字符时间转换函数
c
size_t wcsftime(wchar_t * restrict s, size_t maxsize,
const wchar_t * restrict format,
const struct tm * restrict timeptr);
wcsftime
函数是<time.h>
头中的strftime
函数(将在26.3节
讨论)的宽字符版本。
25.5.6 扩展的多字节/宽字符转换实用工具
本节讨论
<wchar.h>
中用于在多字节字符和宽字符之间进行转换的函数。其中有5
个函数(mbrlen
、mbrtowc
、wcrtomb
、mbsrtowcs
和wcsrtombs
)与<stdlib.h>
中的多字节/宽字符转换函数以及多字节/宽字符串转换函数相对应。<wchar.h>
中的函数具有一个额外的参数------一个指向mbstate_t
类型变量的指针 。这个变量记录多字节字符序列向宽字符序列转换(或反向转换)的当前转换状态。因此,<wchar.h>
中的函数是"可再次启动的" :以前一次函数调用中修改过的指向mbstate_t
类型变量的指针作为参数,可以用该调用的转换状态"再次启动"函数。这样的好处之一是可以让两个函数共享同样的转换状态。例如,处理单个多字节字符构成的字符串时,mbrtowc
和mbsrtowcs
函数调用可以共享同一个mbstate_t
类型变量。
存储在mbstate_t
类型变量中的转换状态包括当前迁移状态和多字节字符内的当前位置。将mbstate_t
类型变量的字节设为0
会使其处于初始转换状态,这意味着还没有开始处理多字节字符,且初始迁移状态有效:
c
mbstate_t state;
...
memset (&state, '\0', sizeof(state));
把&state
传递给任何一个可再次启动的函数,将导致从初始转换状态开始进行转换。一旦在这些函数中修改了mbstate_t
类型变量,该变量就不能用于转换不同的多字节字符序列了,也不能用于反向的转换,否则会导致未定义的行为。改变某个地区的LC_CTYPE
之后使用该变量也会导致未定义的行为。
25.5.6.1 单字节/宽字符转换函数
c
wint_t btowc(int c);
int wctob(wint_t c);
这一组函数把单字节字符转换为宽字符,或执行反向转换。
如果c
等于EOF
或者在初始迁移状态时c(
强制转换为unsignedchar
)不是有效的单字节符号,那么btowc
函数返回WEOF
。否则,btowc
返回c
的宽字符表示。
wctob
函数执行btowc
的反向操作。如果c
在初始迁移状态时没有对应的多字节字符,则返回EOF
;否则返回c
的单字节表示。
25.5.6.2 转换状态函数
c
int mbsinit(const mbstate_t *ps);
这一组只有一个函数mbsinit
。如果ps
是空指针或者它指向一个描述初始转换状态的mbstate_t
型变量,函数返回非零值。
25.5.6.3 可重启的多字节/宽字符转换函数
c
size_t mbrlen(const char * restrict s, size_t n, mbstate_t * restrict ps);
size_t mbrtowc(wchar_t * restrict pwc,
const char * restrict s, size_t n,
mbstate_t * restrict ps);
size_t wcrtomb(char * restrict s, wchar_t wc, mbstate_t * restrict ps);
这一组函数是
<stdlib.h>
中的mblen
、mbtowc
和wctomb
函数(在25.2节
讨论过)的可重启动版本。新函数mblen
、mbtowc
和wctomb
与<stdlib.h>
中的对应函数有如下区别:
mbrlen
、mbrtowc
和wcrtomb
函数新增了一个参数ps
。当这些函数中的任一函数被调用时,相应的参数指向一个mbstate_t
类型的变量;函数会在这个变量中存储转换状态。如果与ps
对应的实参是空指针,函数将使用内部变量来存储转换状态(在程序执行的一开始,这个变量设置为初始转换状态)。- 当
s
参数是空指针时,旧版的mblen
、mbtowc
和wctom
b函数在多字节字符编码依赖状态时返回非零值,否则返回0
。新版的函数不具有该行为。 mbrlen
、mbrtowc
和wcrtomb
函数的返回值为size_t
类型而不是int
类型,旧版函数的返回值为int
类型。
调用
mbrlen
等同于调用
c
mbrtowc(NULL, s, n, ps)
但当ps
是空指针时,使用内部变量的地址来代替。如果s
是空指针,调用mbrtowc
等同于调用
c
mbrtowc(NULL, "", 1, ps)
否则,mbrtowc
至多检查由s
指向的n
个字节来判断是否已处理完一个有效的多字节字符。(注意,在函数调用之前可能已经在处理多字节字符了,这由ps
指向的mbstate_t
类型变量来记录。)如果是这样,这些字节将被转换为宽字符。只要pwc
不为空,就把该宽字符存于pwc
指向的位置。如果该字符是空的宽字符,把函数调用中使用的mbstate_t
类型变量置为初始转换状态。
mbrtowc
有多种可能的返回值 。如果转换产生了空的宽字符,其返回值为0
。如果转换产生了非空的宽字符,则返回一个范围在1~n
的数,该返回值是用于完成多字节字符的字节数。如果s
指向的n
个字节不足以完成多字节字符(尽管这些字节本身是有效的),则返回-2
。最后,如果出现编码错误(函数遇到了不能形成有效的多字节字符的字节),则返回-1
;在这种情况下,mbrtowc
仍会将EILSEQ
存于errno
中。
如果s
是空指针,调用wcrtomb
等同于
c
wcrtomb(buf, L'\0', ps)
这里buf
是内部缓冲区。否则,wcrtomb
将wc
从宽字符转换为多字节字符,并将其存于s
指向的数组中。如果wc
是空的宽字符,wcrtomb
中存储空字节,如果必要,前面还可以放一个迁移序列用于存储初始迁移状态。这种情况下,调用中所用的mbstate_t
类型变量置为初始转换状态。wcrtomb
返回所存储的字节数,包括迁移序列。如果wc
不是有效的宽字符,函数返回-1
并将EILSEQ
存于errno
中。
25.5.6.4 可重启动的多字节/宽字符串转换函数
c
size_t mbsrtowcs(wchar_t * restrict dst,
const char ** restrict src,
size_t len,
mbstate_t * restrict ps);
size_t wcsrtombs(char * restrict dst,
const wchar_t ** restrict src,
size_t len,
mbstate_t * restrict ps);
mbsrtowcs
和wcsrtombs
函数是<stdlib.h>
中的mbstowcs
和wcstombs
函数(在25.2节
讨论过)的可重启动版本。mbsrtowcs
和wcsrtombs
函数与<stdlib.h>
中的对应函数基本一样,只有如下区别。
mbsrtowcs
和wcsrtombs
都有一个额外的参数ps
。当它们中的一个函数被调用时,对应的参数指向一个mbstate_t
类型的变量,函数将使用该变量存储转换状态。如果ps
对应的参数是空指针,函数将使用内部变量来存储转换状态。(在程序一开始执行时,这个变量设置为初始转换状态。)这两个函数在转换过程中都会更新状态。如果转换因为遇到空字符而停止,mbstate_t
型变量将置为初始转换状态。src
参数表示包含待转换字符的数组(源数组),它是一个指向指针的指针。(在旧版的mbstowcs
函数和wcstombs
函数中,对应参数只是一个普通指针。)这个变化使得mbsrtowcs
和wcsrtombs
可以记录转换停止的位置。如果转换因为达到空字符而停止,则把src
指向的指针设置为空;否则使该指针刚好越过上一次转换成功的源字符。dst
参数有可能是空指针,在这种情况下不存储已转换的字符,也不修改src
指向的指针。- 当这两个函数在源数组里遇到无效字符时,它们会将
EILSEQ
存于errno
中(同时返回-1
,而mbstowcs
和wcstombs
函数仅返回-1
)。
25.6 <wctype.h>:宽字符分类和映射实用工具(C99)
<wctype.h>
头是<ctype.h>头(23.5节)
的宽字符版本 。<ctype.h>
提供了两类函数:字符分类函数
(如isdigit
,测试一个字符是否是数字)和字符映射函数
(如toupper
,把小写字母转换为大写字母)。<wctype.h>
为宽字符提供了类似的函数,但与<ctype.h>
有一点重要区别:<wctype.h>
中的一些函数是"可扩展的",这意味着它们可以执行自定义的字符分类和映射。
<wctype.h>
声明了三个类型和一个宏。wint_t
类型和WEOF
宏在25.5节
中讨论过。另外两种类型是wctype_t
(其值表示特定于地区的字符分类)和wctrans_t
(其值表示特定于地区的字符映射)。
<wctype.h>
中的大部分函数要求参数为wint_t
类型。这个参数的值必须是一个宽字符
(wchar_t
类型的值)或WEOF
,传递其他参数会引起未定义的行为。
<wctype.h>
中函数的行为受当前地区的LC_CTYPE
类项的影响。
25.6.1 宽字符分类函数
c
int iswalnum(wint_t wc);
int iswalpha(wint_t wc);
int iswblank(wint_t wc);
int iswcntrl(wint_t wc);
int iswdigit(wint_t wc);
int iswgraph(wint_t wc);
int iswlower(wint_t wc);
int iswprint(wint_t wc);
int iswpunct(wint_t wc);
int iswspace(wint_t wc);
int iswupper(wint_t wc);
int iswxdigit(wint_t wc);
对于每一个宽字符分类函数,如果它的参数有特定的性质,则返回非零值
。表25-20
列出了每个函数测试的性质。
表25-20 宽字符分类函数
函数 | 测试 |
---|---|
iswalnum(wc) | wc是否是字母或数字 |
iswalpha(wc) | wc是否是字母 |
iswblank(wc) | wc是否是标准空白① |
iswcntrl(wc) | wc是否是控制字符 |
iswdigit(wc) | wc是否是十进制数字 |
iswgraph(wc) | wc是否是打印字符(空格除外) |
iswlower(wc) | wc是否是小写字母 |
iswprint(wc) | wc是否是打印字符(包含空格) |
iswpunct(wc) | wc是否是标点符号 |
iswspace(wc) | wc是否是空白字符 |
iswupper(wc) | wc是否是大写字母 |
iswxdigit(wc) | wc是否是十六进制数字 |
①标准空白字符是空格(L' '
)和水平制表符(L'\t'
)。
表25-20
的描述中忽略了宽字符的一些细节 。例如,C99
标准中iswgraph
的定义指出,该函数"对任意给定的宽字符,测试iswprint
为真且iswspace
为假",因此存在这样的可能性:多个宽字符都可以被认作"空格"。
在大多数情况下,宽字符分类函数与<ctype.h>
中对应的函数一致:如果<ctype.h>
中的函数对某个字符返回非零值(表明"真"),那么<wctype.h>
中相应的函数对该字符的宽字符版本返回真。唯一的例外是宽的空白字符(不是空格)中属于打印字符的那些字符,用iswgraph
和iswpunct
分类的结果与用isgraph
和ispunct
分类的结果不同。例如,使isgraph
返回真的字符可能会使iswgraph
返回假。
25.6.2 可扩展的宽字符分类函数
c
int iswctype(wint_t wc, wctype_t desc);
wctype_t wctype(const char *property);
前面讨论的每一个宽字符分类函数都可以测试一个固定的条件。wctype
和iswctype
函数(被设计为同时使用)可以用于测试其他条件。
wctype
函数的参数是一个描述一类宽字符类的字符串,它返回一个表示这个类的wctype_t
类型值。例如,调用
c
wctype("upper")
返回一个wctype_t
类型的值表示大写字母类。C99
标准要求允许用以下字符串作为wctype
的参数:
c
"alnum" "alpha" "blank" "cntrl" "digit" "graph"
"lower" "print" "punct" "space" "upper" "xdigit"
其他字符串可以由实现提供。哪些字符串可以用作wctype
的合法参数依赖于当前地区的LC_CTYPE
类项。上面列出的12
个字符串在所有地区都合法。如果当前地区不支持传递给wctype
的字符串,函数返回0
。
调用
iswctype
函数需要用到两个参数:wc(宽字符)
和desc(wctype返回的值)
。如果wc
属于与desc
相对应的字符类,那么iswctype
函数返回非零值
。例如,调用
c
iswctype(wc, wctype("alnum"))
等价于iswalnum(wc)
。如果传递给wctype
的字符串不是上面列出的标准字符串,则wctype
和iswctype
尤其有用。
25.6.3 宽字符大小写映射函数
c
wint_t towlower(wint_t wc);
wint_t towupper(wint_t wc);
towlower
和towupper
函数分别是tolower
和toupper
对应的宽字符版本。例如,towlower
在参数是大写字母时返回参数的小写形式;否则,保持参数不变并将其返回。一般说来,处理宽字符时会有一些突发情况。例如,某个字母在当前地区可能有多种小写字母,在这种情况下towlower
可以返回其中任意一个。
25.6.4 可扩展的宽字符大小写映射函数
c
wint_t towctrans(wint_t wc, wctrans_t desc);
wctrans_t wctrans(const char *property);
wctrans
和towctrans
函数一起使用,以支持一般性的宽字符大小写映射。
wctrans
函数的参数是一个字符串,用于描述字符的大小写映射。它返回一个wctrans_t
类型的值来表示该映射关系。例如,调用
c
wctrans("tolower")
返回一个表示从大写字母向小写字母映射的wctrans_t
类型值。C99
标准要求字符串"tolower"
和"toupper"
可以作为wctrans
的参数。具体实现中还可以提供其他的字符串。哪些字符串可以用作wctrans
的合法参数依赖于当前地区的LC_CTYPE
类项。"tolower"
和"toupper"
在所有地区都合法。如果当前地区不支持传递给wctrans
的字符串,函数返回0
。
调用
towctrans
函数需要用到两个参数:wc(宽字符)
和desc(wctrans返回的值)
。towctrans
根据desc
所指定的大小写映射关系,将wc
映射为另一个宽字符。例如,调用
c
towctrans(wc, wctrans("tolower"))
等价于
c
towlower(wc)
与实现定义的大小写映射一起使用时,towctrans
特别有用。
25.7 <uchar.h>: 改进的Unicode支持(C1X)
在
C99
中,可以用wchar_t
类型的变量保存宽字符。尽管绝大多数计算机系统开始支持Unicode
字符集,使用wchar_t
类型保存的字符也都是Unicode
字符,但是C
语言没有规定这种类型的长度,再加上不同的操作系统使用不同的Unicode
编码方案,这就影响了文本的交换以及程序的可移植性。
举例来说,Windows
使用UTF-16
编码,因为单一16
位只能表示基本多语种平面内的字符,它使用的实际上是变长UTF-16
编码:对于基本多语种平面内的字符,使用一个16
位来表示;对于其他字符,则使用两个16
位来表示(代理对)。与Windows
不同,Linux
直接使用32
位的UTF-32
来编码字符。因为长度不统一,所以当程序在不同的平台之间移植时,就需要做麻烦的转换工作。
从
C11
开始,标准库提供了头<uchar.h>
并定义了两种具有明确长度的宽字符类型,它们分别是char16_t
和char32_t
。char16_t
是一个无符号整数类型,和uint_least16_t
相同,用来保存长度为16
位的字符,通常用于保存UTF-16
编码的字符;char32_t
也是一个无符号整数类型,和uint_least32_t
相同,用来保存长度为32
位的字符,通常用于保存UTF-32
编码的字符。
25.7.1 带u、U和u8前缀的字面串
和
C99
相比,C1X
的另一个显著变化是支持u
、U
和u8
前缀的字面串,以及u
和U
前缀的字符常量。带u
前缀的字面串用于在程序编译期间创建一个元素类型为char16_t
的静态数组,带u
前缀的字符常量是宽字符常量,它的类型是char16_t
,例如:
c
char16_t c = u'a';
char16_t * p = u"Aye aye sir!\n";
带U
前缀的字面串用于在程序编译期间创建一个元素类型为char32_t
的静态数组,带U
前缀的字符常量是宽字符常量,它的类型是char32_t
,例如:
c
char32_t d = U'a';
char32_t * q = U"Yes captain!\n";
u8
前缀只适用于字面串,用来明确指定字面串采用UTF-8
编码方案,例如:
c
char s [] = u8"As you wish!\n";
//注意!在u、U、u8和它们后面的"之间不能有任何空白,否则将导致语法错误。
25.7.2 可重启动的多字节/宽字符转换函数
c
size_t mbrtoc16(char16_t * restrict pc16, const char * restrict s, size_t n,
mbstate_t * restrict ps);
size_t c16rtomb(char * restrict s, char16_t c16, mbstate_t restrict ps);
size_t mbrtoc32(char32_t * restrict pc32, const char * restrict s, size_t n,
mbstate_t * restrict ps);
size_t c32rtomb(char * restrict s, char32_t c32, mbstate_t * restrict ps);
这些函数拥有一个参数ps
,它是指向mbstate_t
的指针,可用于完整地描述受这些函数影响的多字节字符序列的当前转换状态。如果与ps
对应的实参是空指针,函数将使用一个mbstate_t
类型的内部变量来存储转换状态。在程序启动时,这个变量被初始化到一个起始的转换状态。
函数
mbrtoc16
用来将多字节字符转换为用char16_t
类型来表示的宽字符。如果s
是空指针,调用mbrtoc16
等同于调用
c
mbrtoc16(NULL, "", 1, ps)
否则,mbrtoc16
至多检查由s
指向的n
个字节,以确定完成下一个多字节字符所需要的字节数(包括任何迁移序列)。如果能够确定s
中的下一个多字节字符是完整且有效的,则将其转换为相应的16
位宽字符,并保存在pc16
指向的位置(如果pc16
不是空指针的话)。
如果宽字符是变长编码的(比如
UTF-16
代理对),可能需要执行该函数一次以上。换句话说,上一次调用只是得到了宽字符编码的前一部分。后续的调用不会消费额外的输入,还是在指定的n
个字节内处理,并转换和保存下一个宽字符。
如果转换后的结果是一个空宽字符,则ps指向的转换状态恢复到最初的时候。表25-21
列出了该函数的返回值及其含义:
表25-21 mbrtoc16
函数的转换结果
返回值 | 含义 |
---|---|
0 | 转换后的结果是空宽字符 |
1~n | 实际用了几个字节完成的宽字符转换 |
(size_t)-3 | 本次调用是延续上一次的调用,并已成功转换和保存宽字符 |
(size_t)-2 | 接下来的n个字节不足以表示一个多字节字符,但它依然可能是有效的,只是需要后面的字节才能完整表示 |
(size_t)-1 | 编码错误,接下来的n个字节不能表示一个完整有效的多字节字符。此时,errno的值是EILSEQ且转换状态是未指定的 |
函数
c16rtomb
将char16_t
类型的宽字符转换为多字节字符 。如果参数s
为空指针,则该函数等同于
c
c16rtomb(buf, L'\0', ps)
否则,该函数计算将参数c16
中的宽字符转换成多字节字符需要几个字节,并将转换后的结果保存到参数s
所指向的内存位置,但是至多保存MB_CUR_MAX
个字节。如果参数c16
中是空宽字符,则转换和保存的是以任意迁移序列为前导的空字节,这个迁移序列用于恢复初始迁移状态。这种情况下,调用中所用的mbstate_t
类型变量置为初始转换状态。
此函数的返回值是转换并保存的字节数,包括任何迁移序列。如果参数c16
的值不代表有效的宽字符,将发生编码错误:保存的值是EILSEQ
并且返回值是(size_t)-1
。
函数
mbrtoc32
用于将多字节字符转换为char32_t
类型的宽字符。如果s
是空指针,调用mbrtoc32
等同于调用
c
mbrtoc32(NULL, "", 1, ps)
否则,mbrtoc32
至多检查由s
指向的n
个字节,以确定完成下一个多字节字符所需要的字节数(包括任何迁移序列)。如果能够确定s
中的下一个多字节字符是完整且有效的,则将其转换为相应的32位宽字符,并保存在pc32
指向的位置(如果pc32
不是空指针的话)。
如果宽字符是变长编码的(这对于
UTF-32
来说是不可能的,但是库函数不会预设任何具体的编码方案),可能需要执行该函数一次以上。换句话说,上一次调用只是得到了宽字符编码的前一部分。后续的调用不会消费额外的输入,还是在指定的n个字节内处理,并转换和保存下一个宽字符。
如果转换后的结果是一个空宽字符,则ps
指向的转换状态恢复到最初的时候。表25-22
列出了该函数的返回值及其含义:
表25-22 mbrtoc32
函数的转换结果
返回值 | 含义 |
---|---|
0 | 转换后的结果是空宽字符 |
1~n | 实际用了几个字节完成的宽字符转换 |
(size_t)-3 | 本次调用是延续上一次的调用,并已成功转换和保存宽字符 |
(size_t)-2 | 接下来的n个字节不足以表示一个多字节字符,但它依然可能是有效的,只是需要后面的字节才能完整表示 |
(size_t)-1 | 编码错误,接下来的n个字节不能表示一个完整有效的多字节字符。此时,errno的值是EILSEQ且转换状态是未指定的 |
函数
c32rtomb
用于将char32_t
类型的宽字符转换为多字节字符 。如果参数s
为空指针,则该函数等同于
c
c32rtomb (buf, L'\0', ps)
否则,该函数计算将参数c32
中的宽字符转换成多字节字符需要几个字节,并将转换后的结果保存到参数s
所指向的内存位置,但是至多保存MB_CUR_MAX
个字节。如果参数c32
中是空宽字符,则转换和保存的是以任意迁移序列为前导的空字节,这个迁移序列用于恢复初始迁移状态。这种情况下,调用中所用的mbstate_t
类型变量置为初始转换状态。
此函数的返回值是转换并保存的字节数,包括任何迁移序列。如果参数c32
的值不代表有效的宽字符,将发生编码错误:保存的值是EILSEQ
并且返回值是(size_t)-1
。
问与答
问1 :
setlocale
函数可以返回多长的地区信息字符串?
答:不存在最大长度 。这就引发了一个问题:如果不知道字符串的长度,如何为字符串设置空间呢?当然,答案就是动态存储分配
。下面这个程序段(基于Harbison
和Steele
写的《C
语言参考手册》一书中的类似示例)说明了如何确定需要的空间数量,动态地分配内存,然后再把地区信息复制到此内存空间中:
c
char *temp, *old_locale;
temp = setlocale(LC_ALL, NULL);
if (temp == NULL) {
/* locale information not available */
}
old_locale = malloc (strlen (temp) + 1);
if (old_locale == NULL) {
/* memory allocation failed */
}
strcpy(old_locale, temp);
现在可以先切换到另一个地区,然后再恢复到旧的地区:
c
setlocale(LC_ALL, ""); /* switches to native locale */
...
setlocale(LC_ALL, old_locale); /* restorees old locale */
问2 :为什么
C
语言同时提供多字节字符和宽字符呢?两者选其一难道不够吗?
答:这两种编码分别用于不同的目的 。多字节字符用于输入/输出目的很方便,因为输入/输出设备经常是面向字节的。但是宽字符更适用于程序内部,因为每个宽字符占有相同的空间。因此,程序可以读入多字节字符输入,把它转换为便于程序内部操作的宽字符格式,然后再把宽字符转换回用于输出的多字节格式。
问3 :
Unicode
和通用字符集(UCS)
看起来很相似,两者的区别是什么?
答:这两者所包含的字符一样,而且表示字符所用的码点也一样 。不过,Unicode
不仅仅是一个字符集。例如,Unicode
支持"双向显示"。有些语言(包括阿拉伯语和希伯来语)允许从右向左书写,而不是从左向右书写。Unicode
可以用于指定字符的显示顺序,它允许文本中同时包含从左向右显示的字符和从右向左显示的字符。
写在最后
本文是博主阅读《C语言程序设计:现代方法(第2版·修订版)》时所作笔记,日后会持续更新后续章节笔记。欢迎各位大佬阅读学习,如有疑问请及时联系指正,希望对各位有所帮助,Thank you very much!