多字节字符
本章介绍 C 语言如何处理非英语字符。
Unicode 简介
C 语言诞生时,只考虑了英语字符,使用 7 位的 ASCII 码表示所有字符。ASCII 码的范围是 0 到 127,也就是最多只能表示 100 多个字符,用一个字节就可以表示,所以char
类型只占用一个字节。
但是,如果处理非英语字符,一个字节就不够了,单单是中文,就至少有几万个字符,字符集就势必使用多个字节表示。
最初,不同国家有自己的字符编码方式,这样不便于多种字符的混用。因此,后来就逐渐统一到 Unicode 编码,将所有字符放入一个字符集。
Unicode 为每个字符提供一个号码,称为码点(code point),其中 0 到 127 的部分,跟 ASCII 码是重合的。通常使用“U+十六进制码点”表示一个字符,比如U+0041
表示字母A
。
Unicode 编码目前一共包含了 100 多万个字符,码点范围是 U+0000 到 U+10FFFF。完整表达整个 Unicode 字符集,至少需要三个字节。但是,并不是所有文档都需要那么多字符,比如对于 ASCII 码就够用的英语文档,如果每个字符使用三个字节表示,就会比单字节表示的文件体积大出三倍。
为了适应不同的使用需求,Unicode 标准委员会提供了三种不同的表示方法,表示 Unicode 码点。
- UTF-8:使用 1 个到 4 个字节,表示一个码点。不同的字符占用的字节数不一样。
- UTF-16:对于 U+0000 到 U+FFFF 的字符(称为基本平面),使用 2 个字节表示一个码点。其他字符使用 4 个字节。
- UTF-32:统一使用 4 个字节,表示一个码点。
其中,UTF-8 的使用最为广泛,因为对于 ASCII 字符(U+0000 到 U+007F),它只使用一个字节表示,这就跟 ASCII 的编码方式完全一样。
C 语言提供了两个宏,表示当前系统支持的编码字节长度。这两个宏都定义在头文件limits.h
。
MB_LEN_MAX
:任意支持地区的最大字节长度,定义在limits.h
。MB_CUR_MAX
:当前语言的最大字节长度,总是小于或等于MB_LEN_MAX
,定义在stdlib.h
。
字符的表示方法
字符表示法的本质,是将每个字符映射为一个整数,然后从编码表获得该整数对应的字符。
C 语言提供了不同的写法,用来表示字符的整数号码。
\123
:以八进制值表示一个字符,斜杠后面需要三个数字。\x4D
:以十六进制表示一个字符,\x
后面是十六进制整数。\u2620
:以 Unicode 码点表示一个字符(不适用于 ASCII 字符),码点以十六进制表示,\u
后面需要 4 个字符。\U0001243F
:以 Unicode 码点表示一个字符(不适用于 ASCII 字符),码点以十六进制表示,\U
后面需要 8 个字符。
printf("ABC\n");
printf("\101\102\103\n");
printf("\x41\x42\x43\n");
上面三行都会输出“ABC”。
printf("\u2022 Bullet 1\n");
printf("\U00002022 Bullet 1\n");
上面两行都会输出“• Bullet 1”。
多字节字符的表示
C 语言预设只有基本字符,才能使用字面量表示,其它字符都应该使用码点表示,并且当前系统还必须支持该码点的编码方法。
所谓基本字符,指的是所有可打印的 ASCII 字符,但是有三个字符除外:@
、$
、`
。
因此,遇到非英语字符,应该将其写成 Unicode 码点形式。
char* s = "\u6625\u5929";
printf("%s\n", s); // 春天
上面代码会输出中文“春天”。
如果当前系统是 UTF-8 编码,可以直接用字面量表示多字节字符。
char* s = "春天";
printf("%s\n", s);
注意,\u + 码点
和\U + 码点
的写法,不能用来表示 ASCII 码字符(码点小于0xA0
的字符),只有三个字符除外:0x24
($
),0x40
(@
)和0x60
(`
)。
char* s = "\u0024\u0040\u0060";
printf("%s\n", s); // @$`
上面代码会输出三个 Unicode 字符“@$`”,但是其它 ASCII 字符都不能用这种表示法表示。
为了保证程序执行时,字符能够正确解读,最好将程序环境切换到本地化环境。
setlocale(LC_ALL, "");
上面代码中,使用setlocale()
切换执行环境到系统的本地化语言。setlocale()
的原型定义在头文件locale.h
,详见标准库部分的《locale.h》章节。
像下面这样,指定编码语言也可以。
setlocale(LC_ALL, "zh_CN.UTF-8");
上面代码将程序执行环境,切换到中文环境的 UTF-8 编码。
C 语言允许使用u8
前缀,对多字节字符串指定编码方式为 UTF-8。
char* s = u8"春天";
printf("%s\n", s);
一旦字符串里面包含多字节字符,就意味着字符串的字节数与字符数不再一一对应了。比如,字符串的长度为 10 字节,就不再是包含 10 个字符,而可能只包含 7 个字符、5 个字符等等。
setlocale(LC_ALL, "");
char* s = "春天";
printf("%d\n", strlen(s)); // 6
上面示例中,字符串s
只包含两个字符,但是strlen()
返回的结果却是 6,表示这两个字符一共占据了 6 个字节。
C 语言的字符串函数只针对单字节字符有效,对于多字节字符都会失效,比如strtok()
、strchr()
、strspn()
、toupper()
、tolower()
、isalpha()
等不会得到正确结果。
宽字符
上一小节的多字节字符串,每个字符的字节宽度是可变的。这种编码方式虽然使用起来方便,但是很不利于字符串处理,因此必须逐一检查每个字符占用的字节数。所以除了这种方式,C 语言还提供了确定宽度的多字节字符存储方式,称为宽字符(wide character)。
所谓“宽字符”,就是每个字符占用的字节数是固定的,要么是 2 个字节,要么是 4 个字节。这样的话,就很容易快速处理。
宽字符有一个单独的数据类型 wchar_t,每个宽字符都是这个类型。它属于整数类型的别名,可能是有符号的,也可能是无符号的,由当前实现决定。该类型的长度为 16 位(2 个字节)或 32 位(4 个字节),足以容纳当前系统的所有字符。它定义在头文件wchar.h
里面。
宽字符的字面量必须加上前缀“L”,否则 C 语言会把字面量当作窄字符类型处理。
setlocale(LC_ALL, "");
wchar_t c = L'牛';
printf("%lc\n", c);
wchar_t* s = L"春天";
printf("%ls\n", s);
上面示例中,前缀“L”在单引号前面,表示宽字符,对应printf()
的占位符为%lc
;在双引号前面,表示宽字符串,对应printf()
的占位符为%ls
。
宽字符串的结尾也有一个空字符,不过是宽空字符,占用多个字节。
处理宽字符,需要使用宽字符专用的函数,绝大部分都定义在头文件wchar.h
。
多字节字符处理函数
mblen()
mblen()
函数返回一个多字节字符占用的字节数。它的原型定义在头文件stdlib.h
。
int mblen(const char* mbstr, size_t n);
它接受两个参数,第一个参数是多字节字符串指针,一般会检查该字符串的第一个字符;第二个参数是需要检查的字节数,这个数字不能大于当前系统单个字符占用的最大字节,一般使用MB_CUR_MAX
。
它的返回值是该字符占用的字节数。如果当前字符是空的宽字符,则返回0
;如果当前字符不是有效的多字节字符,则返回-1
。
setlocale(LC_ALL, "");
char* mbs1 = "春天";
printf("%d\n", mblen(mbs1, MB_CUR_MAX)); // 3
char* mbs2 = "abc";
printf("%d\n", mblen(mbs2, MB_CUR_MAX)); // 1
上面示例中,字符串“春天”的第一个字符“春”,占用 3 个字节;字符串“abc”的第一个字符“a”,占用 1 个字节。
wctomb()
wctomb()
函数(wide character to multibyte)用于将宽字符转为多字节字符。它的原型定义在头文件stdlib.h
。
int wctomb(char* s, wchar_t wc);
wctomb()
接受两个参数,第一个参数是作为目标的多字节字符数组,第二个参数是需要转换的一个宽字符。它的返回值是多字节字符存储占用的字节数量,如果无法转换,则返回-1
。
setlocale(LC_ALL, "");
wchar_t wc = L'牛';
char mbStr[10] = "";
int nBytes = 0;
nBytes = wctomb(mbStr, wc);
printf("%s\n", mbStr); // 牛
printf("%d\n", nBytes); // 3
上面示例中,wctomb()
将宽字符“牛”转为多字节字符,wctomb()
的返回值表示转换后的多字节字符占用 3 个字节。
mbtowc()
mbtowc()
用于将多字节字符转为宽字符。它的原型定义在头文件stdlib.h
。
int mbtowc(
wchar_t* wchar,
const char* mbchar,
size_t count
);
它接受 3 个参数,第一个参数是作为目标的宽字符指针,第二个参数是待转换的多字节字符指针,第三个参数是多字节字符的字节数。
它的返回值是多字节字符的字节数,如果转换失败,则返回-1
。
setlocale(LC_ALL, "");
char* mbchar = "牛";
wchar_t wc;
wchar_t* pwc = &wc;
int nBytes = 0;
nBytes = mbtowc(pwc, mbchar, 3);
printf("%d\n", nBytes); // 3
printf("%lc\n", *pwc); // 牛
上面示例中,mbtowc()
将多字节字符“牛”转为宽字符wc
,返回值是mbchar
占用的字节数(占用 3 个字节)。
wcstombs()
wcstombs()
用来将宽字符串转换为多字节字符串。它的原型定义在头文件stdlib.h
。
size_t wcstombs(
char* mbstr,
const wchar_t* wcstr,
size_t count
);
它接受三个参数,第一个参数mbstr
是目标的多字节字符串指针,第二个参数wcstr
是待转换的宽字符串指针,第三个参数count
是用来存储多字节字符串的最大字节数。
如果转换成功,它的返回值是成功转换后的多字节字符串的字节数,不包括尾部的字符串终止符;如果转换失败,则返回-1
。
下面是一个例子。
setlocale(LC_ALL, "");
char mbs[20];
wchar_t* wcs = L"春天";
int nBytes = 0;
nBytes = wcstombs(mbs, wcs, 20);
printf("%s\n", mbs); // 春天
printf("%d\n", nBytes); // 6
上面示例中,wcstombs()
将宽字符串wcs
转为多字节字符串mbs
,返回值6
表示写入mbs
的字符串占用 6 个字节,不包括尾部的字符串终止符。
如果wcstombs()
的第一个参数是 NULL,则返回转换成功所需要的目标字符串的字节数。
mbstowcs()
mbstowcs()
用来将多字节字符串转换为宽字符串。它的原型定义在头文件stdlib.h
。
size_t mbstowcs(
wchar_t* wcstr,
const char* mbstr,
size_t count
);
它接受三个参数,第一个参数wcstr
是目标宽字符串,第二个参数mbstr
是待转换的多字节字符串,第三个参数是待转换的多字节字符串的最大字符数。
转换成功时,它的返回值是成功转换的多字节字符的数量;转换失败时,返回-1
。如果返回值与第三个参数相同,那么转换后的宽字符串不是以 NULL 结尾的。
下面是一个例子。
setlocale(LC_ALL, "");
char* mbs = "天气不错";
wchar_t wcs[20];
int nBytes = 0;
nBytes = mbstowcs(wcs, mbs, 20);
printf("%ls\n", wcs); // 天气不错
printf("%d\n", nBytes); // 4
上面示例中,多字节字符串mbs
被mbstowcs()
转为宽字符串,成功转换了 4 个字符,所以该函数的返回值为 4。
如果mbstowcs()
的第一个参数为NULL
,则返回目标宽字符串会包含的字符数量。