C现代方法(第25章)笔记——国际化特性

第25章 国际化特性

——即使你的计算机说英语,它也可能产自日本

过去,C语言并不十分适合在非英语国家(地区)使用。C语言最初假定字符都是单字节的,并且所有计算机都能识别字符#[\]^{|}~,因为这些字符都需要在C程序中用到。遗憾的是这些假定并不是在世界的任何地方都适用。因此,创建C89的专家又添加了新的特性和函数库,以使C语言更加国际化

1994年,针对ISO C标准的修正草案Amendment1被批准通过,这一增强的C89版本有时也称为C94C95这一草案通过双联符语言特性以及<iso646.h><wchar.h><wctype.h>提供了对国际化编程的额外函数库支持C99以通用字符名的形式为国际化提供了更多的支持。C1X继续以<uchar.h>改进了这些支持。本章介绍C语言的所有国际化特性,这些特性可能来自C89Amendment1C99C1X。虽然来自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。影响两个字符串比较函数(strcollstrxfrm)的行为。[这两个函数都在<string.h>(23.6节)中声明。]
  • LC_CTYPE。影响<ctype.h>(23.5节)中的函数(isdigitisxdigit除外)的行为。同时还影响本章讨论的多字节函数和宽字符函数的行为。
  • LC_MONETARY。影响由localeconv函数返回的货币格式信息。
  • LC_NUMERIC。影响格式化输入/输出函数(如printfscanf)使用的小数点字符以及<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函数

char *setlocale(int category, const char *locale);

setlocale函数修改当前的地区,可以是针对一个类项的,也可以是针对所有类项的。如果第一个参数是LC_COLLATELC_CTYPELC_MONETARYLC_NUMERICLC_TIME之一,那么setlocale调用只会影响一个类项。如果第一个参数是LC_ALL,调用就会影响所有类项。C标准对第二个参数仅定义了两种可能值:"C"""。如果有其他地区,则由具体的实现自行处理

在任意程序执行开始时,都会隐式执行调用:

setlocale(LC_ALL, "C");
//当地区设置为"C"时,库函数按“正常”方式执行,小数点是一个点。

如果在程序运行起来后想改变地区,就需要显式调用setlocale函数。用""作为第二个参数调用setlocale函数可以切换到本地(nativelocale)模式。这种模式下程序会适应本地的环境。C标准并没有定义切换到本地模式的具体影响。setlocale函数的有些实现会检查当前的运行环境[与getenv函数(26.2节)的方式一样],查找特定名字(可能与表示类项的宏同名)的环境变量;有些实现则根本什么都不做。(C标准并没有要求setlocale有什么特定的作用。当然,如果库中的setlocale什么都不做,那么这个库在一些地区可能不会卖得很好。)

补充小知识:对于除"C"""以外的其他地区,不同的编译器之间有很大的差异。GNUC库(称为glibc)提供了"POSIX"地区,该地区与""一样。glibc用于Linux,允许在需要的时候增加额外的地区。地区的格式为语言[_地域][.码集][@指定符]

其中方括号中的项是可选的。语言的可能值列在ISO 639标准中,“地域”来自另一个标准(ISO 3166),“码集”指明字符集或字符集的编码方案。下面给出了几个例子:

"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命令的用法之一是获取所有可用地区的列表,这可以通过在命令行输入下面的语句来实现:

locale -a

地区信息正变得越来越重要,因此统一字符联盟(Unicode Consortium)设立了一个泛区域数据仓库(Common Locale Data Repository,CLDR)项目来建立标准的地区集合。

setlocale函数调用成功时,它会返回一个指向字符串的指针,这个字符串与新地区的类项相关联。(例如,这个字符串可能就是地区名字自身。)如果调用失败,setlocale函数返回空指针。

setlocale函数也可以当作查询函数使用。如果第二个参数是空指针,setlocale函数会返回一个指向字符串的指针,这个字符串与当前地区类项相关联。这一特性在第一个参数为LC_ALL时特别有用,因为可以获取所有类项的当前设置。setlocale函数返回的字符串可以(通过复制到变量中)保存起来,以便日后调用setlocale函数时使用。


25.1.3 localeconv函数

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_digitsCHAR_MAX十进制小数点后的数字个数
p_cs_precedesCHAR_MAX如果currency_symbol在非负值之前,则为1;如果currency_symbol在数值之后,则为0
n_cs_precedesCHAR_MAX如果currency_symbol在负值之前,则为1;如果currency_symbol在数值之后,则为0
p_sep_by_spaceCHAR_MAX把currency_symbol和数值符号字符串与非负值分隔开(见表25-4)
n_sep_by_spaceCHAR_MAX把currency_symbol和数值符号字符串与负值分隔开(见表25-4)
p_sign_posnCHAR_MAX用于非负值时positive_sign的位置(见表25-5)
n_sign_posnCHAR_MAX用于负值时negative_sign 的位置(见表25-5)

表25-3 lconv结构的char类型成员(国际格式化)

名称在“C”地区中的值描述
int_frac_digitsCHAR_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_spacen_sep_by_spaceint_p_sep_by_spaceint_n_sep_by_space值的含义。成员p_sep_by_spacen_sep_by_space的含义在C99中有所改变。在C89中,它们只有两种可能的值:1currency_symbol和货币量之间有空格)和0currency_symbol和货币量之间没有空格)。

表25-4 ...sep_by_space成员的值

含义
0货币符号与量之间没有空格
1如果货币符号与量的符号相邻,用空格把它们与量分隔开;否则,用空格把货币符号与量分隔开
2如果货币符号与量的符号相邻,用空格把它们分隔开;否则,用空格把量的符号与量分隔开

表25-5解释了成员p_sign_posnn_sign_posnint_p_sign_posnint_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_digits22
p_cs_precedes10
n_cs_precedes10
p_sep_by_space02
n_sep_by_space02
p_sign_posn11
n_sign_posn11
int_curr_symbol"USD ""EUR "
int_frac_digits22
int_p_cs_precedes10
int_n_cs_precedes10
int_p_sep_by_space12
int_n_sep_by_space12
int_p_sign_posn11
int_n_sign_posn11

表25-7是把7593.86格式化成上述两个地区的货币数值的情况,具体形式与数值符号以及是本地化还是国际化有关。

表25-7 美国(美元)和芬兰(欧元)货币数值对比

美国芬兰
本地格式(正数)$7,593.867 593,86 EUR
本地格式(负数)-$7,593.86- 7 593,86 EUR
国际化格式(正数)USD7,593.86
国际化格式(负数)-USD7,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_MAXMB_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作为前缀:

L'a'

而宽字面串也需要用字母L作为前缀:

L"abc"

此字符串表示一个含有宽字符L'a'L'b'L'c'并且后跟一个空的宽字符的数组。


25.2.3 Unicode和通用字符集

多字节字符和宽字符的差异在讨论Unicode时比较明显。UnicodeUnicode联盟(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 10646Unicode保持一致。因为Unicode和通用字符集非常相似,所以本书经常将这两个术语互换使用。

Unicode最初只有65536个字符(16位所能表示的字符数目),后来发现这是不够的,现在Unicode的字符已超过100000个。(欲了解最新版本,请访问Unicode官方网站。)Unicode的前65536个字符(包括最常用的字符)称作基本多语种平面(Basic Multilingual Plane,BMP)


25.2.4 Unicode编码

Unicode为每一个字符分配一个唯一的数(称为码点)。可以有多种方式使用字节来表示这些码点。我将介绍两种简单的方法,一种使用宽字符,另一种使用多字节字符

UCS-2是一种宽字符编码方案,它把每一个Unicode码点存储为两个字节USC-2可以表示基本多语种平面上的所有字符(码点在十六进制的0000FFFF之间),但是不能够表示不属于BMPUnicode字符。

另一种流行的方式是8位的UCS转换格式(UTF-8),该方案使用多字节字符UTF-8是由Ken Thompson和他在贝尔实验室的同事RobPike1992年设计的(就是设计B语言的那个Ken ThompsonB语言是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~00007F0xxxxxxx
000080~0007FF110xxxxx 10xxxxxx
000800~00FFFF1110xxxx 10xxxxxx 10xxxxxx
010000~10FFFF11110xxx 10xxxxxx 10xxxxxx 10xxx

UTF-8读取码点值中的位,将其分为几组(由表25-8中的x来表示),并把每一组分配给不同的字节。最简单的情况是码点在0~7F范围(ASCII字符)内,此时只要在原数的7位之前加一个0即可。

码点在80~7FF范围(包括所有的Latin-1字符)内时,需要将码点值的位分为两组,一组5位另一组6位。5位组的前缀为1106位组的前缀为10。例如,字符ä的码点为E4(十六进制)11100100(二进制)。在UTF-8中可以将其表示为双字节序列1100001110100100。注意高亮的部分,连起来就是00011100100

如果字符的码点落在800~FFFF范围(包含基本多语种平面中的剩余字符)内,那么需要3字节。其他的Unicode字符(大多数很少用到)都分配4字节。UTF-8有以下几个有用的性质:

  • 128ASCII字符中的每一个字符都可以用一个字节表示。仅由ASCII字符组成的字符串在UTF-8中保持不变。
  • 对于UTF-8字符串中的任意字节,如果其最左边的位是0,那么它一定是ASCII字符,因为其他所有字节都以1开始。
  • 多字节字符的第一个字节指明了该字符的长度。如果字节开头1的个数为2,那么这个字符的长度为2字节。如果字节开头1的个数为34,那么这个字符的长度分别为3字节或4字节。
  • 在多字节序列中,每隔一个字节就以10作为最左边的位。

最后三个性质特别重要,因为它们可以保证一个多字节字符中的字节序列不会是另一个有效的多字节字符。这样一来,简单地进行字节比较就可以从多字节字符串中搜索一个特定的字符或字符序列。

现在来看看UTF-8相比于UCS-2的优缺点。UCS-2的优点在于,字符都是以最自然的格式存储的。UTF-8的优点在于,它能处理所有的Unicode字符(而不仅仅是BMP中的字符)、所需的空间比UCS-2少且兼容ASCIIUCS-2用于WindowsNT操作系统,但不如UTF-8流行;使用4字节的新版本(UCS-4)正在逐渐取代UCS-2的地位。一些系统把UCS-2扩展为一种多字节编码方案,方法是允许用可变数量的字节对来表示字符(UCS-2使用一个字节对来表示字符)。这样的编码方案称为UTF-16,它的优点是能够兼容UCS-2


25.2.5 多字节/宽字符转换函数

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

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函数来确定是否可以把宽字符字符串转换为有效的多字节字符:

int wccheck(wchar_t *wcs) 
{ 
    char buf[MB_LEN_MAX]; 
    int n; 
    
    for (wctomb(NULL, 0); ; ++wcs) 
        if ((n = wctomb (buf, *wcs)) <= 0) 
            return1;       /* invalid character */ 
        else if (buf[n-1] == '\0') 
            return 0;      /* all characters are valid */ 
} 

顺便说一下,mblenmbtowcwctomb都可以用来测试多字节编码是否依赖状态。当传递空指针作为char*类型的参数时,如果多字节字符的编码是依赖状态的,那么上述每种函数都会返回非零值,否则返回0


25.2.6 多字节/宽字符串转换函数

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。例如,程序

#include <stdio.h> 
int main(void) 
{ 
    printf("hello, world\n"); 
    return 0; 
}

可以写成

??=include <stdio.h> 
int main(void) 
??< 
    printf("hello, world??/n"); 
    return 0; 
??> 

尽管三联符很少用到,但遵循C89C99标准的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中,8d组成一个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节)stdinstdoutstderr在程序刚开始执行时是没有倾向性的。]使用字节输入/输出函数在流上执行操作会使流成为面向字节的,使用宽字符输入/输出函数执行操作会使流成为面向宽字符的。流的倾向性可以调用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 格式化宽字符输入/输出函数

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>中的对应函数
fwprintffprintf
fwscanffscanf
swprintfSnprintf、sprintf
swscanfsscanf
vfwprintfvfprintf
vfwscanfvfscanf
vswprintfvsnprintf、vsprintf
vswscanfvsscanf
vwprintfvprintf
vwscanfvscanf
wprintfprintf
wscanfscanf

这一组中的所有函数有以下几个共同特性:

  • 都有包含宽字符的格式串。
  • …printf函数返回输出的字符数量,但现在是对宽字符计数。
  • %n转换说明表示到目前为止输出(…printf函数)读入(…scanf函数)的宽字符的数量。

fwprintffprintf还有以下不同;

  • %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类型的数组。它类似于sprintfsnprintf,但不完全等同于这两个函数。类似于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 宽字符输入/输出函数

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>中的对应函数
fgetwcfgetc
fgetwsfgets
fputwcfputc
fputwsfputs
fwide
getwcgetc
getwchargetchar
putwcputc
putwcharputchar
ungetwcungetc

除非特别说明,可以认为表25-13中所列出的<wchar.h>中的函数和<stdio.h>中的对应函数行为一致。但是,多数对应函数之间有一点细微的差别。为了指示错误或者文件结尾条件,<stdio.h>中的一些字符输入/输出函数返回EOF,但<wchar.h>中的对应函数返回WEOF

还有一个问题会影响宽字符输入函数。调用读取单字符的函数(fgetwcgetwcgetwchar)时,可能会因为输入流中的字节不能组成有效的宽字符或者可用的字节不够而导致调用失败。这样会造成编码错误,进而导致函数将EILSEQ存入errno并返回WEOFfgetws函数(读取宽字符串)也可能因为编码错误而失败,这种情况下它会返回空指针。


宽字符输出函数也可能遇到编码错误。用于输出单字符的函数(fputwcputwcputwchar)在出现编码错误时将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 宽字符串数值转换函数
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>中的对应函数
wcstodstrtod
wcstofstrtof
wcstoldstrtold
wcstolstrtol
wcstollstrtoll
wcstoulstrtoul
wcstoullstrtoull
25.5.4.2 宽字符串复制函数
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>中的对应函数
wcscpystrcpy
wcsncpystrncpy
wmemcpymemcpy
wmemmovememmove
25.5.4.3 宽字符串拼接函数
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>中的对应函数
wcscatstrcat
wcsncatstrncat
25.5.4.4 宽字符串比较函数
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>中的对应函数
wcscmpstrcmp
wcscollstrcoll
wcsncmpstrncmp
wcsxfrmstrxfrm
wmemcmpmemcmp
25.5.4.5 宽字符串搜索函数
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>中的对应函数
wcschrstrchr
wcscspnstrcspn
wcspbrkstrpbrk
wcsrchrstrrchr
wcsspnstrspn
wcsstrstrstr
wcstokstrtok
wmemchrmemchr

wcstok函数与strtok函数作用相同,但由于有第三个参数,所以用法略有不同。(strtok函数只有两个参数。)要了解wcstok的工作原理,首先需要回顾一下strtok的行为。

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节中的例子。假设strpq声明
如下:

wchar_t str[] = L" April 28,1998";
wchar_t *p, *q;

最初的wcstok调用用str作为第一个参数:

p = wcstok(str, L" \t", &q); 

现在p指向April的第一个字符,April之后有一个空的宽字符。用空指针作为第一个参数、&q作为第三个参数调用wcstok,可以从上次停下来的地方继续搜索:

p = wcstok(NULL, L" \t,", &q); 

在这个调用之后,p指向28的第一个字符,现在28的后面有一个用于终止的空的宽字符。再次调用wcstok可以定位年:

p = wcstok(NULL, L" \t", &q);
//p现在指向1998的第一个字符。
25.5.4.6 其他函数
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>中的对应函数
wcslenstrlen
wmemsetmemset

25.5.5 宽字符时间转换函数

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个函数(mbrlenmbrtowcwcrtombmbsrtowcswcsrtombs)与<stdlib.h>中的多字节/宽字符转换函数以及多字节/宽字符串转换函数相对应。<wchar.h>中的函数具有一个额外的参数——一个指向mbstate_t类型变量的指针。这个变量记录多字节字符序列向宽字符序列转换(或反向转换)的当前转换状态。因此,<wchar.h>中的函数是“可再次启动的”:以前一次函数调用中修改过的指向mbstate_t类型变量的指针作为参数,可以用该调用的转换状态“再次启动”函数。这样的好处之一是可以让两个函数共享同样的转换状态。例如,处理单个多字节字符构成的字符串时,mbrtowcmbsrtowcs函数调用可以共享同一个mbstate_t类型变量。

存储在mbstate_t类型变量中的转换状态包括当前迁移状态和多字节字符内的当前位置。将mbstate_t类型变量的字节设为0会使其处于初始转换状态,这意味着还没有开始处理多字节字符,且初始迁移状态有效:

mbstate_t state; 
... 
memset (&state, '\0', sizeof(state)); 

&state传递给任何一个可再次启动的函数,将导致从初始转换状态开始进行转换。一旦在这些函数中修改了mbstate_t类型变量,该变量就不能用于转换不同的多字节字符序列了,也不能用于反向的转换,否则会导致未定义的行为。改变某个地区的LC_CTYPE之后使用该变量也会导致未定义的行为。

25.5.6.1 单字节/宽字符转换函数
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 转换状态函数
int mbsinit(const mbstate_t *ps);

这一组只有一个函数mbsinit。如果ps是空指针或者它指向一个描述初始转换状态的mbstate_t型变量,函数返回非零值。

25.5.6.3 可重启的多字节/宽字符转换函数
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>中的mblenmbtowcwctomb函数(在25.2节讨论过)的可重启动版本。新函数mblenmbtowcwctomb<stdlib.h>中的对应函数有如下区别:

  • mbrlenmbrtowcwcrtomb函数新增了一个参数ps。当这些函数中的任一函数被调用时,相应的参数指向一个mbstate_t类型的变量;函数会在这个变量中存储转换状态。如果与ps对应的实参是空指针,函数将使用内部变量来存储转换状态(在程序执行的一开始,这个变量设置为初始转换状态)。
  • s参数是空指针时,旧版的mblenmbtowcwctomb函数在多字节字符编码依赖状态时返回非零值,否则返回0。新版的函数不具有该行为。
  • mbrlenmbrtowcwcrtomb函数的返回值为size_t类型而不是int类型,旧版函数的返回值为int类型。

调用mbrlen等同于调用

mbrtowc(NULL, s, n, ps)

但当ps是空指针时,使用内部变量的地址来代替。如果s是空指针,调用mbrtowc等同于调用

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等同于

wcrtomb(buf, L'\0', ps)

这里buf是内部缓冲区。否则,wcrtombwc从宽字符转换为多字节字符,并将其存于s指向的数组中。如果wc是空的宽字符,wcrtomb中存储空字节,如果必要,前面还可以放一个迁移序列用于存储初始迁移状态。这种情况下,调用中所用的mbstate_t类型变量置为初始转换状态。wcrtomb返回所存储的字节数,包括迁移序列。如果wc不是有效的宽字符,函数返回-1并将EILSEQ存于errno中。

25.5.6.4 可重启动的多字节/宽字符串转换函数
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);

mbsrtowcswcsrtombs函数是<stdlib.h>中的mbstowcswcstombs函数(在25.2节讨论过)的可重启动版本。mbsrtowcswcsrtombs函数与<stdlib.h>中的对应函数基本一样,只有如下区别。

  • mbsrtowcswcsrtombs都有一个额外的参数ps。当它们中的一个函数被调用时,对应的参数指向一个mbstate_t类型的变量,函数将使用该变量存储转换状态。如果ps对应的参数是空指针,函数将使用内部变量来存储转换状态。(在程序一开始执行时,这个变量设置为初始转换状态。)这两个函数在转换过程中都会更新状态。如果转换因为遇到空字符而停止,mbstate_t型变量将置为初始转换状态。
  • src参数表示包含待转换字符的数组(源数组),它是一个指向指针的指针。(在旧版的mbstowcs函数和wcstombs函数中,对应参数只是一个普通指针。)这个变化使得mbsrtowcswcsrtombs可以记录转换停止的位置。如果转换因为达到空字符而停止,则把src指向的指针设置为空;否则使该指针刚好越过上一次转换成功的源字符。
  • dst参数有可能是空指针,在这种情况下不存储已转换的字符,也不修改src指向的指针。
  • 当这两个函数在源数组里遇到无效字符时,它们会将EILSEQ存于errno中(同时返回-1,而mbstowcswcstombs函数仅返回-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 宽字符分类函数

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>中相应的函数对该字符的宽字符版本返回真。唯一的例外是宽的空白字符(不是空格)中属于打印字符的那些字符,用iswgraphiswpunct分类的结果与用isgraphispunct分类的结果不同。例如,使isgraph返回真的字符可能会使iswgraph返回假。


25.6.2 可扩展的宽字符分类函数

int iswctype(wint_t wc, wctype_t desc); 
wctype_t wctype(const char *property);

前面讨论的每一个宽字符分类函数都可以测试一个固定的条件。wctypeiswctype函数(被设计为同时使用)可以用于测试其他条件。

wctype函数的参数是一个描述一类宽字符类的字符串,它返回一个表示这个类的wctype_t类型值。例如,调用

wctype("upper")

返回一个wctype_t类型的值表示大写字母类。C99标准要求允许用以下字符串作为wctype的参数:

"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函数返回非零值。例如,调用

iswctype(wc, wctype("alnum"))

等价于iswalnum(wc)。如果传递给wctype的字符串不是上面列出的标准字符串,则wctypeiswctype尤其有用。


25.6.3 宽字符大小写映射函数

wint_t towlower(wint_t wc); 
wint_t towupper(wint_t wc);

towlowertowupper函数分别是tolowertoupper对应的宽字符版本。例如,towlower在参数是大写字母时返回参数的小写形式;否则,保持参数不变并将其返回。一般说来,处理宽字符时会有一些突发情况。例如,某个字母在当前地区可能有多种小写字母,在这种情况下towlower可以返回其中任意一个。


25.6.4 可扩展的宽字符大小写映射函数

wint_t towctrans(wint_t wc, wctrans_t desc);
wctrans_t wctrans(const char *property);

wctranstowctrans函数一起使用,以支持一般性的宽字符大小写映射。

wctrans函数的参数是一个字符串,用于描述字符的大小写映射。它返回一个wctrans_t类型的值来表示该映射关系。例如,调用

wctrans("tolower")

返回一个表示从大写字母向小写字母映射的wctrans_t类型值。C99标准要求字符串"tolower""toupper"可以作为wctrans的参数。具体实现中还可以提供其他的字符串。哪些字符串可以用作wctrans的合法参数依赖于当前地区的LC_CTYPE类项。"tolower""toupper"在所有地区都合法。如果当前地区不支持传递给wctrans的字符串,函数返回0

调用towctrans函数需要用到两个参数:wc(宽字符)desc(wctrans返回的值)towctrans根据desc所指定的大小写映射关系,将wc映射为另一个宽字符。例如,调用

towctrans(wc, wctrans("tolower"))

等价于

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_tchar32_tchar16_t是一个无符号整数类型,和uint_least16_t相同,用来保存长度为16位的字符,通常用于保存UTF-16编码的字符;char32_t也是一个无符号整数类型,和uint_least32_t相同,用来保存长度为32位的字符,通常用于保存UTF-32编码的字符。


25.7.1 带u、U和u8前缀的字面串

C99相比,C1X的另一个显著变化是支持uUu8前缀的字面串,以及uU前缀的字符常量。u前缀的字面串用于在程序编译期间创建一个元素类型为char16_t的静态数组,带u前缀的字符常量是宽字符常量,它的类型是char16_t,例如:

char16_t c = u'a'; 
char16_t * p = u"Aye aye sir!\n"; 

U前缀的字面串用于在程序编译期间创建一个元素类型为char32_t的静态数组,带U前缀的字符常量是宽字符常量,它的类型是char32_t,例如:

char32_t d = U'a'; 
char32_t * q = U"Yes captain!\n"; 

u8前缀只适用于字面串,用来明确指定字面串采用UTF-8编码方案,例如:

char s [] = u8"As you wish!\n"; 
//注意!在u、U、u8和它们后面的"之间不能有任何空白,否则将导致语法错误。

25.7.2 可重启动的多字节/宽字符转换函数

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等同于调用

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且转换状态是未指定的

函数c16rtombchar16_t类型的宽字符转换为多字节字符。如果参数s为空指针,则该函数等同于

c16rtomb(buf, L'\0', ps)

否则,该函数计算将参数c16中的宽字符转换成多字节字符需要几个字节,并将转换后的结果保存到参数s所指向的内存位置,但是至多保存MB_CUR_MAX个字节。如果参数c16中是空宽字符,则转换和保存的是以任意迁移序列为前导的空字节,这个迁移序列用于恢复初始迁移状态。这种情况下,调用中所用的mbstate_t类型变量置为初始转换状态。

此函数的返回值是转换并保存的字节数,包括任何迁移序列。如果参数c16的值不代表有效的宽字符,将发生编码错误:保存的值是EILSEQ并且返回值是(size_t)-1

函数mbrtoc32用于将多字节字符转换为char32_t类型的宽字符。如果s是空指针,调用mbrtoc32等同于调用

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为空指针,则该函数等同于

c32rtomb (buf, L'\0', ps)

否则,该函数计算将参数c32中的宽字符转换成多字节字符需要几个字节,并将转换后的结果保存到参数s所指向的内存位置,但是至多保存MB_CUR_MAX个字节。如果参数c32中是空宽字符,则转换和保存的是以任意迁移序列为前导的空字节,这个迁移序列用于恢复初始迁移状态。这种情况下,调用中所用的mbstate_t类型变量置为初始转换状态。

此函数的返回值是转换并保存的字节数,包括任何迁移序列。如果参数c32的值不代表有效的宽字符,将发生编码错误:保存的值是EILSEQ并且返回值是(size_t)-1


问与答

问1setlocale函数可以返回多长的地区信息字符串?

答:不存在最大长度。这就引发了一个问题:如果不知道字符串的长度,如何为字符串设置空间呢?当然,答案就是动态存储分配。下面这个程序段(基于HarbisonSteele写的《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); 

现在可以先切换到另一个地区,然后再恢复到旧的地区:

setlocale(LC_ALL, ""); /* switches to native locale */ 
... 
setlocale(LC_ALL, old_locale); /* restorees old locale */ 

问2:为什么C语言同时提供多字节字符和宽字符呢?两者选其一难道不够吗?

答:这两种编码分别用于不同的目的。多字节字符用于输入/输出目的很方便,因为输入/输出设备经常是面向字节的。但是宽字符更适用于程序内部,因为每个宽字符占有相同的空间。因此,程序可以读入多字节字符输入,把它转换为便于程序内部操作的宽字符格式,然后再把宽字符转换回用于输出的多字节格式

问3Unicode通用字符集(UCS)看起来很相似,两者的区别是什么?

答:这两者所包含的字符一样,而且表示字符所用的码点也一样。不过,Unicode不仅仅是一个字符集。例如,Unicode支持“双向显示”。有些语言(包括阿拉伯语和希伯来语)允许从右向左书写,而不是从左向右书写。Unicode可以用于指定字符的显示顺序,它允许文本中同时包含从左向右显示的字符和从右向左显示的字符。


写在最后

本文是博主阅读《C语言程序设计:现代方法(第2版·修订版)》时所作笔记,日后会持续更新后续章节笔记。欢迎各位大佬阅读学习,如有疑问请及时联系指正,希望对各位有所帮助,Thank you very much!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

New_Teen

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值