最近使用到了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。
這些環境變量會對日期格式,數字格式,貨幣格式,字符處理等多個方面產生影響。
參考資料:
- 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> 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 ");那么最后一行的輸出也就會不一樣。