目前各種Linux發行版都支持UTF-8編碼,當前系統的語言和字元編碼設置保存在一些環境變數中,可以通過locale
命令查看:
$ locale LANG=en_US.UTF-8 LC_CTYPE="en_US.UTF-8" LC_NUMERIC="en_US.UTF-8" LC_TIME="en_US.UTF-8" LC_COLLATE="en_US.UTF-8" LC_MONETARY="en_US.UTF-8" LC_MESSAGES="en_US.UTF-8" LC_PAPER="en_US.UTF-8" LC_NAME="en_US.UTF-8" LC_ADDRESS="en_US.UTF-8" LC_TELEPHONE="en_US.UTF-8" LC_MEASUREMENT="en_US.UTF-8" LC_IDENTIFICATION="en_US.UTF-8" LC_ALL=
常用漢字也都位於BMP中,所以一個漢字的存儲通常占3個位元組。例如編輯一個C程序:
#include <stdio.h> int main(void) { printf("你好\n"); return 0; }
源檔案是以UTF-8編碼存儲的:
$ od -tc nihao.c 0000000 # i n c l u d e < s t d i o . 0000020 h > \n \n i n t m a i n ( v o i 0000040 d ) \n { \n \t p r i n t f ( " 344 275 0000060 240 345 245 275 \ n " ) ; \n \t r e t u r 0000100 n 0 ; \n } \n 0000107
其中八進制的344 375 240
(十六進制e4 bd a0
)就是“你”的UTF-8編碼,八進制的345 245 275
(十六進制e5 a5 bd
)就是“好”。把它編譯成目標檔案,"你好\n"
這個字元串就成了這樣一串位元組:e4 bd a0 e5 a5 bd 0a 00
,漢字在其中仍然是UTF-8編碼的,一個漢字占3個位元組,這種字元在C語言中稱為多位元組字元(Multibyte Character)。運行這個程序相當於把這一串位元組write
到當前終端的設備檔案。如果當前終端的驅動程式能夠識別UTF-8編碼就能打印出漢字,如果當前終端的驅動程式不能識別UTF-8編碼(比如一般的字元終端)就打印不出漢字。也就是說,像這種程序,識別漢字的工作既不是由C編譯器做的也不是由libc
做的,C編譯器原封不動地把源檔案中的UTF-8編碼複製到目標檔案中,libc
只是當作以0結尾的字元串原封不動地write
給內核,識別漢字的工作是由終端的驅動程式做的。
但是僅有這種程度的漢字支持是不夠的,有時候我們需要在C程序中操作字元串裡的字元,比如求字元串"你好\n"
中有幾個漢字或字元,用strlen
就不靈了,因為strlen
只看結尾的0位元組而不管字元串裡存的是什麼,求出來的是位元組數7。為了在程序中操作Unicode字元,C語言定義了寬字元(Wide Character)類型wchar_t
和一些庫函數。在字元常量或字元串字面值前面加一個L就表示寬字元常量或寬字元串,例如定義wchar_t c = L'你';
,變數c
的值就是漢字“你”的31位UCS編碼,而L"你好\n"
就相當於{L'你', L'好', L'\n', 0}
,wcslen
函數就可以取寬字元串中的字元個數。看下面的程序:
#include <stdio.h> #include <locale.h> int main(void) { if (!setlocale(LC_CTYPE, "")) { fprintf(stderr, "Can't set the specified locale! " "Check LANG, LC_CTYPE, LC_ALL.\n"); return 1; } printf("%ls", L"你好\n"); return 0; }
寬字元串L"你好\n"
在原始碼中當然還是存成UTF-8編碼的,但編譯器會把它變成4個UCS編碼0x00004f60 0x0000597d 0x0000000a 0x00000000
保存在目標檔案中,按小端存儲就是60 4f 00 00 7d 59 00 00 0a 00 00 00 00 00 00 00
,用od
命令查看目標檔案應該能找到這些位元組。
$ gcc hihao.c $ od -tx1 a.out
printf
的%ls
轉換說明表示把後面的參數按寬字元串解釋,不是見到0位元組就結束,而是見到UCS編碼為0的字元才結束,但是要write
到終端仍然需要以多位元組編碼輸出,這樣終端驅動程式才能識別,所以printf
在內部把寬字元串轉換成多位元組字元串再write
出去。事實上,C標準並沒有規定多位元組字元必須以UTF-8編碼,也可以使用其它的多位元組編碼,在運行時根據環境變數確定當前系統的編碼,所以在程序開頭需要調用setlocale
獲取當前系統的編碼設置,如果當前系統是UTF-8的,printf
就把UCS編碼轉換成UTF-8編碼的多位元組字元串再write
出去。一般來說,程序在做內部計算時通常以寬字元編碼,如果要存檔或者輸出給別的程序,或者通過網絡發給別的程序,則採用多位元組編碼。
關於Unicode和UTF-8本節只介紹了最基本的概念,部分內容出自[Unicode FAQ],讀者可進一步參考這篇文章。