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

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

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等價,都是字符集。ubuntu

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編碼。.net

以中文字符「你」爲例,對應的Unicode爲"U+4f60",二進制表示爲0100 1111 0110 0000。按照表中的規則編碼成UTF-8就是11100100 10111101 10100000(0xe4 0xbd 0xa0)。code

結論

Unicode本質是字符集,在這個集合中的任意一個字符均可以用一個四字節來表示。orm

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 ");那麼最後一行的輸出也就會不同。

相關文章
相關標籤/搜索