1
0
wiki/docs/开发/C/多字节字符.md

307 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
id: 多字节字符
title: 多字节字符
sidebar_position: 21
data: 2022年3月30日
---
本章介绍 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个字符。
```c
printf("ABC\n");
printf("\101\102\103\n");
printf("\x41\x42\x43\n");
```
上面三行都会输出“ABC”。
```c
printf("\u2022 Bullet 1\n");
printf("\U00002022 Bullet 1\n");
```
上面两行都会输出“• Bullet 1”。
## 多字节字符的表示
C 语言预设只有基本字符,才能使用字面量表示,其它字符都应该使用码点表示,并且当前系统还必须支持该码点的编码方法。
所谓基本字符,指的是所有可打印的 ASCII 字符,但是有三个字符除外:`@`、`$`、`` ` ``。
因此,遇到非英语字符,应该将其写成 Unicode 码点形式。
```c
char* s = "\u6625\u5929";
printf("%s\n", s); // 春天
```
上面代码会输出中文“春天”。
如果当前系统是 UTF-8 编码,可以直接用字面量表示多字节字符。
```c
char* s = "春天";
printf("%s\n", s);
```
注意,`\u + 码点`和`\U + 码点`的写法,不能用来表示 ASCII 码字符(码点小于`0xA0`的字符),只有三个字符除外:`0x24``$``0x40``@`)和`0x60``` ` ``)。
```c
char* s = "\u0024\u0040\u0060";
printf("%s\n", s); // @$`
```
上面代码会输出三个 Unicode 字符“@$`”,但是其它 ASCII 字符都不能用这种表示法表示。
为了保证程序执行时,字符能够正确解读,最好将程序环境切换到本地化环境。
```c
setlocale(LC_ALL, "");
```
上面代码中,使用`setlocale()`切换执行环境到系统的本地化语言。`setlocale()`的原型定义在头文件`locale.h`详见标准库部分的《locale.h》章节。
像下面这样,指定编码语言也可以。
```c
setlocale(LC_ALL, "zh_CN.UTF-8");
```
上面代码将程序执行环境,切换到中文环境的 UTF-8 编码。
C 语言允许使用`u8`前缀,对多字节字符串指定编码方式为 UTF-8。
```c
char* s = u8"春天";
printf("%s\n", s);
```
一旦字符串里面包含多字节字符就意味着字符串的字节数与字符数不再一一对应了。比如字符串的长度为10字节就不再是包含10个字符而可能只包含7个字符、5个字符等等。
```c
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 语言会把字面量当作窄字符类型处理。
```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`。
```c
int mblen(const char* mbstr, size_t n);
```
它接受两个参数,第一个参数是多字节字符串指针,一般会检查该字符串的第一个字符;第二个参数是需要检查的字节数,这个数字不能大于当前系统单个字符占用的最大字节,一般使用`MB_CUR_MAX`。
它的返回值是该字符占用的字节数。如果当前字符是空的宽字符,则返回`0`;如果当前字符不是有效的多字节字符,则返回`-1`。
```c
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`。
```c
int wctomb(char* s, wchar_t wc);
```
`wctomb()`接受两个参数,第一个参数是作为目标的多字节字符数组,第二个参数是需要转换的一个宽字符。它的返回值是多字节字符存储占用的字节数量,如果无法转换,则返回`-1`。
```c
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`。
```c
int mbtowc(
wchar_t* wchar,
const char* mbchar,
size_t count
);
```
它接受3个参数第一个参数是作为目标的宽字符指针第二个参数是待转换的多字节字符指针第三个参数是多字节字符的字节数。
它的返回值是多字节字符的字节数,如果转换失败,则返回`-1`。
```c
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`。
```c
size_t wcstombs(
char* mbstr,
const wchar_t* wcstr,
size_t count
);
```
它接受三个参数,第一个参数`mbstr`是目标的多字节字符串指针,第二个参数`wcstr`是待转换的宽字符串指针,第三个参数`count`是用来存储多字节字符串的最大字节数。
如果转换成功,它的返回值是成功转换后的多字节字符串的字节数,不包括尾部的字符串终止符;如果转换失败,则返回`-1`。
下面是一个例子。
```c
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`。
```c
size_t mbstowcs(
wchar_t* wcstr,
const char* mbstr,
size_t count
);
```
它接受三个参数,第一个参数`wcstr`是目标宽字符串,第二个参数`mbstr`是待转换的多字节字符串,第三个参数是待转换的多字节字符串的最大字符数。
转换成功时,它的返回值是成功转换的多字节字符的数量;转换失败时,返回`-1`。如果返回值与第三个参数相同,那么转换后的宽字符串不是以 NULL 结尾的。
下面是一个例子。
```c
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`,则返回目标宽字符串会包含的字符数量。