最近使用到了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)

所以也可以简单的理解为,Unicode和UCS等价,都是字符集。

UCS编码的长度是31位,可用4个字节表示,可以表示2的31次方个字符。如果两个字符的高位相同,只有低16位不同,则它们属于同一平面,所以一个平面由2的16次方个字符组成。目前大部分字符都位于第一个平面称为BMP。BMP的编码通常以U+xxxx这种形式表示,其中x是16进制数。

比如中文“你”对应的UCS编码为U+4f60,“好”对应的UCS编码为U+597d。更多中文编码可以在Unicode编码表中查询。

有了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。

这些环境变量会对日期格式,数字格式,货币格式,字符处理等多个方面产生影响。

参考资料:

  1. locale wiki
  2. 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()

为什么需要宽字符类型

“你好”对应的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命令,可以找到如下这些字节:

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 ");那么最后一行的输出也就会不一样。

彻底弄懂UTF-8、Unicode、宽字符、locale的更多相关文章

  1. [转帖]彻底弄懂UTF-8、Unicode、宽字符、locale

    彻底弄懂UTF-8.Unicode.宽字符.locale linux后端开发   已关注   彻底弄懂UTF-.Unicode.宽字符.locale unicode 是字符集 utf-8是编码格式.. ...

  2. 【编码】彻底弄懂ASCII、Unicode、UTF-8之间的关系

    计算机中的所有字符,说到底都是用二进制的0.1的排列组合来表示的,因此就需要有一个规范,来枚举规定每个字符对应哪个0.1的排列组合,这样的规范就是字符集. ASCII 全称是“美国信息交换标准码”(A ...

  3. 宽字符wchar_t和窄字符char区别和相互转换

    转自:http://blog.csdn.net/nodeathphoenix/article/details/7416725 1.    首先,说下窄字符char了,大家都很清楚,就是8bit表示的b ...

  4. 宽字符,Ansic和Unicode

    电脑发展的初期,只是在美国等英文国家使用,英文只有26个字母和其它字符,一个字节最多可以表示256个字符,如字母"A"用0x41(二进制01000001)表示,字母"a& ...

  5. 彻底弄懂 Unicode 编码

    彻底弄懂 Unicode 编码 今天,在学习 Node.js 中的 Buffer 对象时,注意到它的 alloc 和 from 方法会默认用 UTF-8 编码,在数组中每位对应 1 字节的十六进制数. ...

  6. [c/c++] programming之路(25)、字符串(六)——memset,Unicode及宽字符,strset

    一.memset #include<stdio.h> #include<stdlib.h> #include<memory.h> void *mymemset(vo ...

  7. 宽字符、多字节、unicode、utf-8、gbk编码转化

    今天遇到一个编码的问题,困惑了我很长时间,所以就简要的的了解了一下常用的编码类型. 我们最常见的是assic编码,它是一种单字节编码,对多容纳256个字符. 我们在编程的时候经常遇到unicode,u ...

  8. 宽字符与Unicode (c语言 汉语字符串长度)

    在C语言中,我们使用char来定义字符,占用一个字节,最多只能表示128个字符,也就是ASCII码中的字符.计算机起源于美国,char 可以表示所有的英文字符,在以英语为母语的国家完全没有问题. 但是 ...

  9. 宽字符(UNICODE)字符集

    推荐使用宽字符(UNICODE)字符集,严格使用宽字符集的函数和定义.具体参考https://blog.csdn.net/qq_22642239/article/details/84822485

随机推荐

  1. crossplatform----文本编辑器工具Atom安装

    1.简介 Atom 是 Github 专门为程序员推出的一个跨平台文本编辑器.具有简洁和直观的图形用户界面,并有很多有趣的特点:支持CSS,HTML,JavaScript等网页编程语言.它支持宏,自动 ...

  2. html5 页面元素插件

    1. 滚动条 jquery.nicescroll 正常引用方式: 设置区域高度 var bodyHeight = $(document.body).height(); $("#XXXXXXX ...

  3. DevOps技术路线图

    来自roadmap.sh github地址 自己整理的百度脑图中文版

  4. [WPF]获取鼠标指针下的元素

    原文:[WPF]获取鼠标指针下的元素   [WPF]获取鼠标指针下的元素 周银辉 以前写过一些GetElementUnderMouse之类的函数,要用到坐标换算而显得有些麻烦(特别是当元素有XXXTr ...

  5. Stream转Byte数组

    //获得当前文件目录 string rootPath = Directory.GetCurrentDirectory(); string path = rootPath + "Your Fi ...

  6. 【Linux】查看物理CPU个数、核数、逻辑CPU个数

    ①物理cpu数:主板上实际插入的cpu数量,可以数不重复的 physical id 有几个(physical id) cat /proc/cpuinfo| grep "physical id ...

  7. shell产生随机数

    #!/bin/bash # 每次调用$RANDOM都会返回不同的随机整数. # 一般范围为: - (有符号的16-bit整数). MAXCOUNT= count= echo echo "$M ...

  8. NBU客户端安装失败

    该服务器是阿里云上的centos6.8,是用来做oracle服务器,买来之后进行测试,发现没有安装图形化界面,还有部分包也没有安装.在oracle安装完成之后,尝试安装NBU的客户端,结果发生报错:T ...

  9. 有什么很好的软件是用 Qt 编写的?(尘中远)

    作者:尘中远链接:http://www.zhihu.com/question/19630324/answer/19365369来源:知乎 http://www.cnblogs.com/grandyan ...

  10. CoolFormat(Qt Creator也可管理VC的Project)

    http://download.csdn.net/download/akof1314/8457593 https://github.com/akof1314/CoolFormat http://dow ...