您当前的位置:首页 > 电脑百科 > 程序开发 > 编程百科

彻底弄懂UTF-8、Unicode、宽字符、locale

时间:2019-07-09 09:55:26  来源:  作者:

最近使用到了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 之间的转换关系表如下:

码点起值码点终值字节序列Byte1Byte2Byte3Byte4Byte5Byte6U+0000U+007F10xxxxxxxU+0080U+07FF2110xxxxx10xxxxxxU+0800U+FFFF31110xxxx10xxxxxx10xxxxxxU+10000U+1FFFFF411110xxx10xxxxxx10xxxxxx10xxxxxxU+200000U+3FFFFFF5111110xx10xxxxxx10xxxxxx10xxxxxx10xxxxxxU+4000000U+7FFFFFFF61111110x10xxxxxx10xxxxxx10xxxxxx10xxxxxx10xxxxxx

第一个字节要么最高位是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
", setlocale(LC_TIME, "en_US.iso88591"));
 strftime(buffer,80,"%c", timer );
 printf("Date is: %s
", buffer);
 printf("Locale is: %s
", setlocale(LC_TIME, "zh_CN.UTF-8"));
 strftime(buffer,80,"%c", timer );
 printf("Date is: %s
", buffer);
 printf("Locale is: %s
", setlocale(LC_TIME, ""));
 strftime(buffer,80,"%c", timer );
 printf("Date is: %s
", 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
", (int)len);
 printf("%s
", s);
 return 0;
}

编译后执行,输出如下:

len = 6
你好

od编译后的可执行文件,可以发现"你好"以UFT-8编码保存,也就是“0xe4 0xbd 0xa0”和“0xe5 0xa5 0xbd”6个字节。

strlen()函数只管结尾的0字节而不管字符串里存的是什么,所以len是6,也就是“你好”的UFT-8编码的字节数。

printf("%s ", 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
", (int)len);
 printf("%ls
", s);
 return 0;
}

编译后执行,输出如下:

len = 2
你好

对编译后的可执行文件执行od命令,可以找到如下这些字节:

193 0003020 001  002  ` O   } Y   
   
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("
");
}
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
", 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 ");那么最后一行的输出也就会不一样。



Tags:UTF-8   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
"UTF-8" 是标准写法,php 在 Windows 下边英文不区分大小写,所以也可以写成 "utf-8"。"UTF-8" 也可以把中间的"-"省略,写成 "UTF8"。一般程序都能识别,但也有例外(如下文),为了严格...【详细内容】
2021-04-19  Tags: UTF-8  点击:(264)  评论:(0)  加入收藏
首先说一下背景:由于计算机是美国人发明的,因此最早只有127个字母被编码到计算机中,也就是大小写英文字母、数字和一些符号,这个编码表称为ASCII编码。例如:大写字母A的编码是65,...【详细内容】
2020-10-13  Tags: UTF-8  点击:(68)  评论:(0)  加入收藏
今天给大家来点儿干货,不知道非程序猿的小哥哥,小姐姐会不会&hellip;&hellip;找不到北在工作中比如常见的开发工具,git,idea当我们用到命令窗口时,字符集总是显示一堆乱码,今天小...【详细内容】
2020-09-28  Tags: UTF-8  点击:(248)  评论:(0)  加入收藏
如果你使用MySQL数据库,请记住一点:你应该总是使用utf8mb4编码,而不是utf8编码。MySQL编码实现标准的UTF-8字符编码可以用4个字节去编码最多21位字符,这几乎包括了世界上所有的...【详细内容】
2020-09-02  Tags: UTF-8  点击:(87)  评论:(0)  加入收藏
在计算机中,所有的数据在存储和运算时都要使用二进制数值表示(因为计算机用高电平和低电平分别表示1和0),而具体用哪些二进制数字表示哪个符号,当然每个人都可以约定自己的一套(...【详细内容】
2019-09-24  Tags: UTF-8  点击:(162)  评论:(0)  加入收藏
最近使用到了wchar_t类型,所以准备详细探究下,没想到水还挺深,网上的资料大多都是复制粘贴,只有个结论,也没个验证过程。本文记录探究的过程及结论,如有不对请指正。Unicode、UCSU...【详细内容】
2019-07-09  Tags: UTF-8  点击:(324)  评论:(0)  加入收藏
那么什么是编码?什么是 UTF-8? MySQL 简史 为什么这件事情会让人如此抓狂 总结最近我遇到了一个 bug,我试着通过 Rails 在以“utf8”编码的 MariaDB 中保存一个 UTF-8 字符串,...【详细内容】
2019-05-05  Tags: UTF-8  点击:(362)  评论:(0)  加入收藏
utf-8和Unicode到底有什么区别?是存储方式不同?编码方式不同?它们看起来似乎很相似,但是实际上他们并不是同一个层次的概念...【详细内容】
2019-04-17  Tags: UTF-8  点击:(682)  评论:(0)  加入收藏
▌简易百科推荐
本文分为三个等级自顶向下地分析了glibc中内存分配与回收的过程。本文不过度关注细节,因此只是分别从arena层次、bin层次、chunk层次进行图解,而不涉及有关指针的具体操作。前...【详细内容】
2021-12-28  linux技术栈    Tags:glibc   点击:(3)  评论:(0)  加入收藏
摘 要 (OF作品展示)OF之前介绍了用python实现数据可视化、数据分析及一些小项目,但基本都是后端的知识。想要做一个好看的可视化大屏,我们还要学一些前端的知识(vue),网上有很多比...【详细内容】
2021-12-27  项目与数据管理    Tags:Vue   点击:(2)  评论:(0)  加入收藏
程序是如何被执行的&emsp;&emsp;程序是如何被执行的?许多开发者可能也没法回答这个问题,大多数人更注重的是如何编写程序,却不会太注意编写好的程序是如何被运行,这并不是一个好...【详细内容】
2021-12-23  IT学习日记    Tags:程序   点击:(9)  评论:(0)  加入收藏
阅读收获✔️1. 了解单点登录实现原理✔️2. 掌握快速使用xxl-sso接入单点登录功能一、早期的多系统登录解决方案 单系统登录解决方案的核心是cookie,cookie携带会话id在浏览器...【详细内容】
2021-12-23  程序yuan    Tags:单点登录(   点击:(8)  评论:(0)  加入收藏
下载Eclipse RCP IDE如果你电脑上还没有安装Eclipse,那么请到这里下载对应版本的软件进行安装。具体的安装步骤就不在这赘述了。创建第一个标准Eclipse RCP应用(总共分为六步)1...【详细内容】
2021-12-22  阿福ChrisYuan    Tags:RCP应用   点击:(7)  评论:(0)  加入收藏
今天想简单聊一聊 Token 的 Value Capture,就是币的价值问题。首先说明啊,这个话题包含的内容非常之光,Token 的经济学设计也可以包含诸多问题,所以几乎不可能把这个问题说的清...【详细内容】
2021-12-21  唐少华TSH    Tags:Token   点击:(10)  评论:(0)  加入收藏
实现效果:假如有10条数据,分组展示,默认在当前页面展示4个,点击换一批,从第5个开始继续展示,到最后一组,再重新返回到第一组 data() { return { qList: [], //处理后...【详细内容】
2021-12-17  Mason程    Tags:VUE   点击:(14)  评论:(0)  加入收藏
什么是性能调优?(what) 为什么需要性能调优?(why) 什么时候需要性能调优?(when) 什么地方需要性能调优?(where) 什么时候来进行性能调优?(who) 怎么样进行性能调优?(How) 硬件配...【详细内容】
2021-12-16  软件测试小p    Tags:性能调优   点击:(20)  评论:(0)  加入收藏
Tasker 是一款适用于 Android 设备的高级自动化应用,它可以通过脚本让重复性的操作自动运行,提高效率。 不知道从哪里听说的抖音 app 会导致 OLED 屏幕烧屏。于是就现学现卖,自...【详细内容】
2021-12-15  ITBang    Tags:抖音防烧屏   点击:(25)  评论:(0)  加入收藏
11 月 23 日,Rust Moderation Team(审核团队)在 GitHub 上发布了辞职公告,即刻生效。根据公告,审核团队集体辞职是为了抗议 Rust 核心团队(Core team)在执行社区行为准则和标准上...【详细内容】
2021-12-15  InfoQ    Tags:Rust   点击:(25)  评论:(0)  加入收藏
最新更新
栏目热门
栏目头条