C語言是 70 年代的產物,那個時候只有 ASCII,各個國家的字符編碼都還未成熟,所以C語言不可能從底層支持 GB2312、GBK、Big5、Shift-JIS 等國家編碼,也不可能支持 Unicode 字符集。
稍微有點C語言基本功的讀者可能認為C語言使用 ASCII 編碼,字符在存儲時會轉換成對應的 ASCII 碼值,這也是錯誤的,你被大學老師和教材誤導了!在C語言中,只有 char 類型的窄字符才使用 ASCII 編碼,char 類型的窄字符串、wchar_t 類型的寬字符和寬字符串都不使用 ASCII 編碼!
wchar_t 類型的寬字符和寬字符串使用 UTF-16 或者 UTF-32 編碼,這個在上節已經講到了,現在只剩下 char 類型的窄字符串(下面稱為窄字符串)沒有講了,這就是本節的重點。
對於窄字符串,C語言並沒有規定使用哪一種特定的編碼,只要選用的編碼能夠適應當前的環境即可,所以,窄字符串的編碼與操作系統和編譯器有關。
但是,可以肯定的說,在現代計算機中,窄字符串已經不再使用 ASCII 編碼了,因為 ASCII 編碼只能顯示字母、數字等英文字符,對漢語、日語、韓語等其它地區的字符無能為力。
討論窄字符串的編碼要從以下兩個方面下手。
源文件使用什么編碼
源文件用來保存我們編寫的代碼,它最終會被存儲到本地硬盤,或者遠程服務器,這個時候就要盡量壓縮文件體積,以節省硬盤空間或者網絡流量,而代碼中大部分的字符都是 ASCII 編碼中的字符,用一個字節足以容納,所以 UTF-8 編碼是一個不錯的選擇。
UTF-8 兼容 ASCII,代碼中的大部分字符可以用一個字節保存;另外 UTF-8 基於 Unicode,支持全世界的字符,我們編寫的代碼可以給全球的程序員使用,真正做到技術無國界。
常見的 IDE 或者編輯器,例如 Xcode、Sublime Text、Gedit、Vim 等,在創建源文件時一般也默認使用 UTF-8 編碼。但是 Visual Studio 是個奇葩,它默認使用本地編碼來創建源文件。
所謂本地編碼,就是像 GBK、Big5、Shift-JIS 等這樣的國家編碼(地區編碼);針對不同國家發行的操作系統,默認的本地編碼一般不同。簡體中文本的 Windows 默認的本地編碼是 GBK。
對於編譯器來說,它往往支持多種編碼格式的源文件。微軟編譯器、GCC、LLVM/Clang(內嵌於 Xcode 中)都支持 UTF-8 和本地編碼的源文件,不過微軟編譯器還支持 UTF-16 編碼的源文件。如果考慮到源文件的通用性,就只能使用 UTF-8 和本地編碼了。
窄字符串使用什么編碼
前面講到,用 puts 或者 printf 可以輸出窄字符串,代碼如下:
- #include <stdio.h>
- int main()
- {
- puts("C語言中文網");
- printf("http://abceng.net");
- return 0;
- }
"C語言中文網"
和"http://abceng.net"
就是需要被處理的窄字符串,程序運行后,它們會被載入到內存中。你看,這里面還包含了中文,肯定不能使用 ASCII 編碼了。
1) 微軟編譯器使用本地編碼來保存這些字符。不同地區的 Windows 版本默認的本地編碼不一樣,所以,同樣的窄字符串在不同的 Windows 版本下使用的編碼也不一樣。對於簡體中文版的 Windows,使用的是 GBK 編碼。
2) GCC、LLVM/Clang 編譯器使用和源文件相同的編碼來保存這些字符:如果源文件使用的是 UTF-8 編碼,那么這些字符也使用 UTF-8 編碼;如果源文件使用的是 GBK 編碼,那么這些字符也使用 GBK 編碼。
你看,對於代碼中需要被處理的窄字符串,不同的編譯器差別還是挺大的。不過可以肯定的是,這些字符始終都使用窄字符(多字節字符)編碼。
正是由於這些字符使用 UTF-8、GBK 等編碼,而不是使用 ASCII 編碼,所以它們才能包含中文。
那么,為什么很多初學者會誤認為C語言使用 ASCII 編碼呢?
不管是在課堂跟着老師學習,還是通過互聯網自學,初學者都是從處理英文開始的,對於英文來說,使用 GBK、UTF-8、ASCII 都是一樣的,GBK、UTF-8 都兼容 ASCII,初學者根本察覺不出用了哪種編碼。
另外,很多大學老師和書籍作者也經常會念叨,字符在存儲時會被轉換成對應的 ASCII 碼,在讀取時又會從 ASCII 碼轉換成對應的字符實體,大家需要熟悉 ASCII 編碼,它是C語言處理字符的基礎,這從很大程度上給初學者造成一種錯誤印象:C語言和 ASCII 編碼是綁定的,C語言使用 ASCII 編碼。
總結
對於 char 類型的窄字符,始終使用 ASCII 編碼。
對於 wchar_t 類型的寬字符和寬字符串,使用 UTF-16 或者 UTF-32 編碼,它們都是基於 Unicode 字符集的。
對於 char 類型的窄字符串,微軟編譯器使用本地編碼,GCC、LLVM/Clang 使用和源文件編碼相同的編碼。
另外,處理窄字符和處理寬字符使用的函數也不一樣:
- <stdio.h> 頭文件中的 putchar、puts、printf 函數只能用來處理窄字符;
- <wchar.h> 頭文件中的 putwchar、wprintf 函數只能用來處理寬字符。
你看,僅僅是字符的處理,C語言就能玩出這么多花樣,讓人捉摸不透,不容易學習。這是因為,C語言是一種較為底層和古老的語言,既有歷史遺留問題,又有貼近計算機底層的特性。不過,一旦搞明白這些繁雜的底層問題,你的編程內功將精進一個層次,這也許就是學習C語言的樂趣。
【拓展】編碼字符集和運行字符集
站在專業的角度講,源文件使用的字符集被稱為編碼字符集,也就是寫代碼的時候使用的字符集;程序中的字符或者字符串使用的字符集被稱為運行字符集,也就是程序運行后使用的字符集。
源文件需要保存到硬盤,或者在網絡上傳輸,使用的編碼要盡量節省存儲空間,同時要方便跨國交流,所以一般使用 UTF-8,這就是選擇編碼字符集的標准。
程序中的字符或者字符串,在程序運行后必須被載入到內存,才能進行后續的處理,對於這些字符來說,要盡量選用能夠提高處理速度的編碼,例如 UTF-16 和 UTF-32 編碼就能夠快速定位(查找)字符。
編碼字符集是站在存儲和傳輸的角度,運行字符集是站在處理或者操作的角度,所以它們並不一定相同。