目录
Unicode,UCS
UTF8
结论:
宽字符类型 wchar_t
locale
为什么需要宽字符类型
多字节字符串和宽字符串相互转换
最近使用到了 wchar_t 类型, 所以准备详细探究下, 没想到水还挺深, 网上的资料大多都是复制粘贴, 只有个结论, 也没个验证过程. 本文记录探究的过程及结论, 如有不对请指正.
Unicode,UCS
UCS(Universal Character Set)本质上就是一个字符集.
Unicode 的开发结合了国际标准化组织所制定的 ISO/IEC 10646, 即通用字符集(
Universal Character Set, UCS).Unicode 与 ISO/IEC 10646 在编码的运作原理相同, 但 The Unicode Standard 包含了更详尽的实现信息, 涵盖了更细节的主题, 诸如比特编码(bitwise encoding), 校对以及呈现等. 摘自()
所以也可以简单的理解为, Unicode 和 UCS 等价, 都是字符集.
UCS 编码的长度是 31 位, 可用 4 个字节表示, 可以表示 2 的 31 次方个字符. 如果两个字符的高位相同, 只有低 16 位不同, 则它们属于同一平面, 所以一个平面由 2 的 16 次方个字符组成. 目前大部分字符都位于第一个平面称为 BMP.BMP 的编码通常以 U+xxxx 这种形式表示, 其中 x 是 16 进制数.
比如中文 "你" 对应的 UCS 编码为 U+4f60,"好" 对应的 UCS 编码为 U+597d. 更多中文编码可以在 Unicode 编码表 http://www.chi2ko.com/tool/CJK.htm 中查询.
有了 UCS 编码, 任何一个字符在计算机中都最多可以用四个字节来表示, 称为码点.
UTF8
现在有了 UCS 字符集, 那么一个字符在计算机中真的要按四个字节 (UTF-32) 来存储吗?
答案是否定的, 一方面每个字符都按四字节来存储非常浪费空间, 因为大部分字符都在 BMP, 只有后 16 位有效, 前 16 位都是 0. 另一方面这与 c 语言不兼容, 在 c 语言中 0 字节表示字符串的结尾, 库函数 strlen 等函数依赖这一点, 如果按 UTF-32 存储, 其中有很多 0 字节并不表示字符串结尾.
Ken Thompson 发明了 UTF-8 编码, 可以很好的解决以上问题. Unicode 和 UTF-8 之间的转换关系表如下:
码点起值 | 码点终值 | 字节序列 | Byte1 | Byte2 | Byte3 | Byte4 | Byte5 | Byte6 |
---|---|---|---|---|---|---|---|---|
U+0000 | U+007F | 1 | 0xxxxxxx | |||||
U+0080 | U+07FF | 2 | 110xxxxx | 10xxxxxx | ||||
U+0800 | U+FFFF | 3 | 1110xxxx | 10xxxxxx | 10xxxxxx | |||
U+10000 | U+1FFFFF | 4 | 11110xxx | 10xxxxxx | 10xxxxxx | 10xxxxxx | ||
U+200000 | U+3FFFFFF | 5 | 111110xx | 10xxxxxx | 10xxxxxx | 10xxxxxx | 10xxxxxx | |
U+4000000 | U+7FFFFFFF | 6 | 1111110x | 10xxxxxx | 10xxxxxx | 10xxxxxx | 10xxxxxx | 10xxxxxx |
第一个字节要么最高位是 0(ASCII 码), 要么最高位都是 1, 最高位之后的 1 的个数决定了后面的有多少个字节也属于当前字符编码, 例如 111110xx, 最高位之后还有 4 个 1, 表示后面的 4 个字节属于当前编码. 后面的每个字节的最高位都是 10, 可以和第一个字节区分开来. 后面字节的 x 表示的就是 UCS 编码. 所以 UTF-8 就像一列火车, 第一个字节是车头, 包含了后面的哪几个字节也属于当前这列火车的信息, 后面的字节是车厢, 其中承载着 UCS 编码.
以中文字符 "你" 为例, 对应的 Unicode 为 "U+4f60", 二进制表示为 0100 1111 0110 0000. 按照表中的规则编码成 UTF-8 就是 11100100 10111101 10100000(0xe4 0xbd 0xa0).
结论:
Unicode 本质是字符集, 在这个集合中的任意一个字符都可以用一个四字节来表示.
UTF-8 是编码规则, 可以通过这个规则将 Unicode 字符集中任一字符对应的字节转换为另一个字节序列. UTF-8 只是编码规则中的一种, 其它的编码规则还有 UTF-16,UTF-32 等.
宽字符类型 wchar_t
在介绍宽字符前先了解下 locale. 因为多字节字符串和宽字符串的转换和 locale 相关.
locale
什么是 locale
区域设置(locale), 也称作 "本地化策略集","本地环境", 是表达程序用户地区方面的软件设定. 在 Linux 执行 locale 可以查看当前 locale 设置:
- Ubuntu@VM-0-16-Ubuntu:~$ locale
- LANG=zh_CN.UTF-8
- LANGUAGE=
- LC_CTYPE="zh_CN.UTF-8"
- LC_NUMERIC="zh_CN.UTF-8"
- LC_TIME="zh_CN.UTF-8"
- LC_COLLATE="zh_CN.UTF-8"
- LC_MONETARY="zh_CN.UTF-8"
- LC_MESSAGES="zh_CN.UTF-8"
- LC_PAPER="zh_CN.UTF-8"
- LC_NAME="zh_CN.UTF-8"
- LC_ADDRESS="zh_CN.UTF-8"
- LC_TELEPHONE="zh_CN.UTF-8"
- LC_MEASUREMENT="zh_CN.UTF-8"
- LC_IDENTIFICATION="zh_CN.UTF-8"
- LC_ALL=
可以将 locale 理解为一系列环境变量. locale 环境变量值的格式为 language_area.charset.languag 表示语言, 例如英语或中文; area 表示使用该语言的地区, 例如美国或者中国大陆; charset 表示字符集编码, 例如 UTF-8 或者 GBK.
这些环境变量会对日期格式, 数字格式, 货币格式, 字符处理等多个方面产生影响.
参考资料:
- locale wiki
- Environment Variables
如何设置系统默认的 locale
修改配置文件 / etc/default/locale, 比如要将 locale 设为 zh_CN.UTF-8, 添加如下语句 LANG=zh_CN.UTF-8
locale 环境变量有何作用
以 LC_TIME 为例, 该变量会影响 strftime()等函数. size_t strftime(char *str, size_t maxsize, const char *format, const struct tm *timeptr)
strftime 根据 format 中定义的格式化规则, 格式化结构 timeptr 表示的时间, 并把它存储在 str 中.
- #include <locale.h>
- #include <stdio.h>
- #include <time.h>
- int main () {
- time_t currtime;
- struct tm *timer;
- char buffer[80];
- time( &currtime );
- timer = localtime( &currtime );
- printf("Locale is: %s\n", setlocale(LC_TIME, "en_US.iso88591"));
- strftime(buffer,80,"%c", timer );
- printf("Date is: %s\n", buffer);
- printf("Locale is: %s\n", setlocale(LC_TIME, "zh_CN.UTF-8"));
- strftime(buffer,80,"%c", timer );
- printf("Date is: %s\n", buffer);
- printf("Locale is: %s\n", setlocale(LC_TIME, ""));
- strftime(buffer,80,"%c", timer );
- printf("Date is: %s\n", buffer);
- return(0);
- }
编译后运行结果如下:
- Locale is: en_US.iso88591
- Date is: Sun 07 Jul 2019 04:08:39 PM CST
- Locale is: zh_CN.UTF-8
Date is: 2019 年 07 月 07 日 星期日 16 时 08 分 39 秒
Locale is: zh_CN.UTF-8
Date is: 2019 年 07 月 07 日 星期日 16 时 08 分 39 秒
可以看到对 LC_TIME 设置不同的值后, 调用 strftime()会产生不同的结果.
char* setlocale (int category, const char* locale); 可以用来对当前程序进行地域设置.
category: 用于指定设置影响的范围, LC_CTYPE 影响字符分类和字符转换, LC_TIME 影响日期和时间的格式, LC_ALL 影响所有内容.
locale: 用于指定变量的值, 上例中分别使用了 "en_US.iso88591","zh_CN.UTF-8" 和空字符串 "","" 表示使用当前操作系统默认的区域设置.
参考资料:
setlocale() http://c.biancheng.net/ref/setlocale.html
为什么需要宽字符类型
"你好" 对应的 Unicode 分别为 "U+4f60" 和 "U+597d", 对应的 UTF-8 编码分别为 "0xe4 0xbd 0xa0" 和 "0xe5 0xa5 0xbd"
多字节字符串在编译后的可执行文件以 UTF-8 编码保存
- #include <stdio.h>
- #include <string.h>
- int main(void) {
- char s[] = "你好";
- size_t len = strlen(s);
- printf("len = %d\n", (int)len);
- printf("%s\n", s);
- return 0;
- }
编译后执行, 输出如下:
len = 6 你好
od 编译后的可执行文件, 可以发现 "你好" 以 UFT-8 编码保存, 也就是 "0xe4 0xbd 0xa0" 和 "0xe5 0xa5 0xbd"6 个字节.
strlen()函数只管结尾的 0 字节而不管字符串里存的是什么, 所以 len 是 6, 也就是 "你好" 的 UFT-8 编码的字节数.
printf("%s\n", s); 相当于将 "0xe4 0xbd 0xa0" 和 "0xe5 0xa5 0xbd"6 个字节 write 到当前终端的设备文件, 如果当前终端的驱动程序能识别 UTF-8 编码就能打印汉字, 如果当前字符终端的驱动程序不能识别 UTF-8 就打印不出汉字.
宽字符串在编译后可执行文件中以 Unicode 保存
- #include <wchar.h>
- #include <stdio.h>
- #include <locale.h>
- int main(void) {
- setlocale(LC_ALL, "zh_CN.UTF-8"); // 设置 locale
- wchar_t s[] = L"你好";
- size_t len = wcslen(s);
- printf("len = %d\n", (int)len);
- printf("%ls\n", s);
- return 0;
- }
编译后执行, 输出如下:
len = 2 你好
对编译后的可执行文件执行 od 命令, 可以找到如下这些字节:
- 0003020 001 \0 002 \0 ` O \0 \0 } Y \0 \0 \n \0 \0 \0
- 00020001 00004f60 0000597d 0000000a
00004f60 正是 "你" 对应的 Unicode,0000597d 是 "好" 对应的 Unicode. 所以对于宽字符串是按 Unicode 保存在可执行文件中的.
wchar_t 是宽字符类型. 在字符常量或者字符串前加 L 就表示宽字符常量或者宽字符串. 所以 len 是 2.
wcslen()和 strlen()不同, 不是见到 0 字节就结束而是要遇到 UCS 编码为 0 的字符才结束.
目前宽字符在内存中以 Unicode 进行保存, 但是要 write 到终端仍然需要以多字节编码输出, 这样终端驱动程序才能识别, 所以 printf 在内部把宽字符串转换成多字节字符串, 然后 write 出去. 这个转换过程受 locale 影响, setlocale(LC_ALL, "zh_CN.UTF-8"); 设置当前进程的 LC_ALL 为 zh_CN.UTF-8, 所以 printf 将 Unicode 转成多字节的 UTF-8 编码, 然后 write 到终端设备. 如果将 setlocale(LC_ALL, "zh_CN.UTF-8"); 改为 setlocale(LC_ALL, en_US.iso88591): 打印结果中将不会输出 "你好".
一般来说程序在内存计算时通常以宽字符编码, 存盘或者网络发送则用多字节编码.
多字节字符串和宽字符串相互转换
c 语言中提供了多字节字符串和宽字符串相互转换的函数.
- #include <stdlib.h>
- size_t mbstowcs(wchar_t *dest, const char *src, size_t n);
- size_t wcstombs(char *dest, const wchar_t *src, size_t n);
mbstowcs()将多字节字符串转换为宽字符串.
wcstombs()将宽字符串转换为多字节字符串.
考虑下面的例子:
- #include <locale.h>
- #include <stdio.h>
- #include <time.h>
- #include <stdlib.h>
- #include <wchar.h>
- #include <string.h>
- wchar_t* str2wstr(const char const* s) {
- const size_t buffer_size = strlen(s) + 1;
- wchar_t* dst_wstr = (wchar_t *)malloc(buffer_size * sizeof (wchar_t));
- wmemset(dst_wstr, 0, buffer_size);
- mbstowcs(dst_wstr, s, buffer_size);
- return dst_wstr;
- }
- void printBytes(const unsigned char const* s, int len) {
- for (int i = 0; i < len; i++) {
- printf("0x%02x", *(s + i));
- }
- printf("\n");
- }
- int main () {
- char s[10] = "你好"; // 内存中对应 0xe4 0xbd 0xa0 0xe5 0xa5 0xbd 0x00
- wchar_t ws[10] = L"你好"; // 内存中对应 0x60 0x4f 0x00 0x00 0x7d 0x59 0x00 0x00 0x00 0x00 0x00 0x00
- printf("Locale is: %s\n", setlocale(LC_ALL, "zh_CN.UTF-8")); //Locale is: zh_CN.UTF-8
- printBytes(s, 7); //0xe4 0xbd 0xa0 0xe5 0xa5 0xbd 0x00
- printBytes((char *)ws, 12); //0x60 0x4f 0x00 0x00 0x7d 0x59 0x00 0x00 0x00 0x00 0x00 0x00
- printBytes((char *)str2wstr(s), 12); //0x60 0x4f 0x00 0x00 0x7d 0x59 0x00 0x00 0x00 0x00 0x00 0x00
- return(0);
- }
编译后, 执行结果如下:
- Locale is: zh_CN.UTF-8
- 0xe4 0xbd 0xa0 0xe5 0xa5 0xbd 0x00
- 0x60 0x4f 0x00 0x00 0x7d 0x59 0x00 0x00 0x00 0x00 0x00 0x00
- 0x60 0x4f 0x00 0x00 0x7d 0x59 0x00 0x00 0x00 0x00 0x00 0x00
第二行输出也印证了我们之前说的多字节字符串在内存中以 UTF-8 存储,"0xe4 0xbd 0xa0 0xe5 0xa5 0xbd" 正是 "你好" 的 UTF-8 编码.
第三行输出印证了之前说的宽字符串在内存中以 Unicode 存储,"0x60 0x4f 0x00 0x00 0x7d 0x59 0x00 0x00" 正好是宽字符串 L"你好" 对应的 Unicode.
setlocale(LC_ALL, "zh_CN.UTF-8")设置 locale, 程序将以 UTF-8 解码宽字符串. 调用 mbstowcs()后, 可以看到 "你好" 的 UTF-8 编码 "0xe4 0xbd 0xa0 0xe5 0xa5 0xbd 0x00" 确实被转换成了 "你好" 对应的 Unicode "0x60 0x4f 0x00 0x00 0x7d 0x59 0x00 0x00 0x00 0x00 0x00 0x00".
如果将 setlocale(LC_ALL, "zh_CN.UTF-8")换成 setlocale(LC_ALL, "en_US.iso88591"); 那么最后一行的输出也就会不一样.
来源: https://www.cnblogs.com/gatsby123/p/11150472.html