完全弄懂UTF-八、Unicode、寬字符、locale


結論
html

寬字符類型wchar_tlinux

localeubuntu

爲何須要寬字符類型網絡

多字節字符串和寬字符串相互轉換函數

最近使用到了wchar_t類型,因此準備詳細探究下,沒想到水還挺深,網上的資料大多都是複製粘貼,只有個結論,也沒個驗證過程。本文記錄探究的過程及結論,若有不對請指正。編碼

Unicode、UCS操作系統

UCS(Universal Character Set)本質上就是一個字符集。.net

Unicode的開發結合了國際標準化組織所制定的 ISO/IEC 10646,即通用字符集(code

Universal Character Set, UCS)。Unicode 與 ISO/IEC 10646 在編碼的運做原理相同,但 The Unicode Standard 包含了更詳盡的實現信息、涵蓋了更細節的主題,諸如比特編碼(bitwise encoding)、校對以及呈現等。摘自(Unicode)orm

因此也能夠簡單的理解爲,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 111110x 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:~$ localeLANG=zh_CN.UTF-8LANGUAGE=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>intmain(){time_tcurrtime;structtm*timer;charbuffer[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);}

編譯後運行結果以下:

Localeis: en_US.iso88591Dateis: Sun07Jul201904:08:39PM CSTLocaleis: zh_CN.UTF-8Dateis:2019年07月07日 星期日16時08分39秒Localeis: zh_CN.UTF-8Dateis: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>intmain(void){chars[] ="你好";size_tlen =strlen(s);printf("len = %d\n", (int)len);printf("%s\n", s);return0;}

編譯後執行,輸出以下:

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>intmain(void){ setlocale(LC_ALL,"zh_CN.UTF-8");//設置localewchar_ts[] =L"你好";size_tlen = wcslen(s);printf("len = %d\n", (int)len);printf("%ls\n", s);return0;}

編譯後執行,輸出以下:

len = 2

你好

對編譯後的可執行文件執行od命令,能夠找到以下這些字節:

193 0003020001\0002\0` O \0\0} Y \0\0\n \0\0\01940002000100004f600000597d0000000a

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_tmbstowcs(wchar_t*dest,constchar*src,size_tn);size_twcstombs(char*dest,constwchar_t*src,size_tn);

mbstowcs()將多字節字符串轉換爲寬字符串。

wcstombs()將寬字符串轉換爲多字節字符串。

考慮下面的例子:

#include<locale.h>#include<stdio.h>#include<time.h>#include<stdlib.h>#include<wchar.h>#include<string.h>wchar_t* str2wstr(constcharconst* s) {constsize_tbuffer_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);returndst_wstr;}voidprintBytes(constunsignedcharconst* s,intlen){for(inti =0; i < len; i++) {printf("0x%02x ", *(s + i)); }printf("\n");}intmain(){chars[10] ="你好";//內存中對應0xe4 0xbd 0xa0 0xe5 0xa5 0xbd 0x00 wchar_tws[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-8printBytes(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-80xe40xbd0xa00xe50xa50xbd0x000x600x4f0x000x000x7d0x590x000x000x000x000x000x000x600x4f0x000x000x7d0x590x000x000x000x000x000x00

第二行輸出也印證了咱們以前說的多字節字符串在內存中以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 ");那麼最後一行的輸出也就會不同。

相關文章
相關標籤/搜索