c++ unicode转换中文_彻底弄懂UTF-8、Unicode、宽字符、locale
結論
寬字符類型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)
所以也可以簡單的理解為,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: %sn", setlocale(LC_TIME,"en_US.iso88591")); strftime(buffer,80,"%c", timer );printf("Date is: %sn", buffer);printf("Locale is: %sn", setlocale(LC_TIME,"zh_CN.UTF-8")); strftime(buffer,80,"%c", timer );printf("Date is: %sn", buffer);printf("Locale is: %sn", setlocale(LC_TIME,"")); strftime(buffer,80,"%c", timer );printf("Date is: %sn", 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 = %dn", (int)len);printf("%sn", s);return0;}
編譯后執行,輸出如下:
len = 6
你好
od編譯后的可執行文件,可以發現"你好"以UFT-8編碼保存,也就是“0xe4 0xbd 0xa0”和“0xe5 0xa5 0xbd”6個字節。
strlen()函數只管結尾的0字節而不管字符串里存的是什么,所以len是6,也就是“你好”的UFT-8編碼的字節數。
printf("%sn", 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 = %dn", (int)len);printf("%lsn", s);return0;}
編譯后執行,輸出如下:
len = 2
你好
對編譯后的可執行文件執行od命令,可以找到如下這些字節:
193 000302000100020` O 00} Y 00n 0001940002000100004f600000597d0000000a
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: %sn", 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 ");那么最后一行的輸出也就會不一樣。
需要更多點擊這里
總結
以上是生活随笔為你收集整理的c++ unicode转换中文_彻底弄懂UTF-8、Unicode、宽字符、locale的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 特斯拉宣布开启调价 Model 3/Y后
- 下一篇: mysql行复制_MySQL复制(一)-