最近使用到了wchar_t类型,因此准备详细探究下,没想到水还挺深,网上的资料大多都是复制粘贴,只有个结论,也没个验证过程。本文记录探究的过程及结论,若有不对请指正。linux
UCS(Universal Character Set)本质上就是一个字符集。
Unicode的开发结合了国际标准化组织所制定的 ISO/IEC 10646,即通用字符集(
Universal Character Set, UCS)。Unicode 与 ISO/IEC 10646 在编码的运做原理相同,但 The Unicode Standard 包含了更详尽的实现信息、涵盖了更细节的主题,诸如比特编码(bitwise encoding)、校对以及呈现等。摘自(Unicode)
因此也能够简单的理解为,Unicode和UCS等价,都是字符集。ubuntu
UCS编码的长度是31位,可用4个字节表示,能够表示2的31次方个字符。若是两个字符的高位相同,只有低16位不一样,则它们属于同一平面,因此一个平面由2的16次方个字符组成。目前大部分字符都位于第一个平面称为BMP。BMP的编码一般以U+xxxx这种形式表示,其中x是16进制数。
好比中文“你”对应的UCS编码为U+4f60,“好”对应的UCS编码为U+597d。更多中文编码能够在Unicode编码表中查询。网络
有了UCS编码,任何一个字符在计算机中都最多能够用四个字节来表示,称为码点。函数
如今有了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编码。.net
以中文字符“你”为例,对应的Unicode为"U+4f60",二进制表示为0100 1111 0110 0000。按照表中的规则编码成UTF-8就是11100100 10111101 10100000(0xe4 0xbd 0xa0)。code
Unicode本质是字符集,在这个集合中的任意一个字符均可以用一个四字节来表示。orm
UTF-8是编码规则,能够经过这个规则将Unicode字符集中任一字符对应的字节转换为另外一个字节序列。UTF-8只是编码规则中的一种,其它的编码规则还有UTF-16,UTF-32等。
在介绍宽字符前先了解下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。
这些环境变量会对日期格式,数字格式,货币格式,字符处理等多个方面产生影响。
参考资料:
修改配置文件/etc/default/locale,好比要将locale设为zh_CN.UTF-8,添加以下语句LANG=zh_CN.UTF-8
以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()
“你好”对应的Unicode分别为"U+4f60"和"U+597d”,对应的UTF-8编码分别为“0xe4 0xbd 0xa0”和“0xe5 0xa5 0xbd”
#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就打印不出汉字。
#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命令,能够找到以下这些字节:
193 0003020 001 \0 002 \0 ` O \0 \0 } Y \0 \0 \n \0 \0 \0 194 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 ");
那么最后一行的输出也就会不同。