# 附錄?A.?字符編碼
**目錄**
+ [1\. ASCII碼](apas01.html)
+ [2\. Unicode和UTF-8](apas02.html)
+ [3\. 在Linux C編程中使用Unicode和UTF-8](apas03.html)
## 1.?ASCII碼
ASCII碼的取值范圍是0~127,可以用7個bit表示。C語言中`char`型變量的大小規定為一字節,如果存放ASCII碼則只用到低7位,高位為0。以下是ASCII碼表:
**圖?A.1.?ASCII碼表**

絕大多數計算機的一個字節是8位,取值范圍是0~255,而ASCII碼并沒有規定編號為128~255的字符,為了能表示更多字符,各廠商制定了很多種ASCII碼的擴展規范。注意,雖然通常把這些規范稱為擴展ASCII碼(Extended ASCII),但其實它們并不屬于ASCII碼標準。例如以下這種擴展ASCII碼由IBM制定,在字符終端下被廣泛采用,其中包含了很多表格邊線字符用來畫界面。
**圖?A.2.?IBM的擴展ASCII碼表**

在圖形界面中最廣泛使用的擴展ASCII碼是ISO-8859-1,也稱為Latin-1,其中包含歐洲各國語言中最常用的非英文字母,但畢竟只有128個字符,某些語言中的某些字母沒有包含。如下表所示。
**圖?A.3.?ISO-8859-1**

編號為128~159的是一些控制字符,在上表中沒有列出。
## 2.?Unicode和UTF-8
為了統一全世界各國語言文字和專業領域符號(例如數學符號、樂譜符號)的編碼,ISO制定了ISO 10646標準,也稱為UCS(Universal Character Set)。UCS編碼的長度是31位,可以表示2<sup>31</sup>個字符。如果兩個字符編碼的高位相同,只有低16位不同,則它們屬于一個平面(Plane),所以一個平面由2<sup>16</sup>個字符組成。目前常用的大部分字符都位于第一個平面(編碼范圍是U-00000000~U-0000FFFD),稱為BMP(Basic Multilingual Plane)或Plane 0,為了向后兼容,其中編號為0~256的字符和Latin-1相同。UCS編碼通常用U-xxxxxxxx這種形式表示,而BMP的編碼通常用U+xxxx這種形式表示,其中x是十六進制數字。在ISO制定UCS的同時,另一個由廠商聯合組織也在著手制定這樣的編碼,稱為Unicode,后來兩家聯手制定統一的編碼,但各自發布各自的標準文檔,所以UCS編碼和Unicode碼是相同的。
有了字符編碼,另一個問題就是這樣的編碼在計算機中怎么表示。現在已經不可能用一個字節表示一個字符了,最直接的想法就是用四個字節表示一個字符,這種表示方法稱為UCS-4或UTF-32,UTF是Unicode Transformation Format的縮寫。一方面這樣比較浪費存儲空間,由于常用字符都集中在BMP,高位的兩個字節通常是0,如果只用ASCII碼或Latin-1,高位的三個字節都是0。另一種比較節省存儲空間的辦法是用兩個字節表示一個字符,稱為UCS-2或UTF-16,這樣只能表示BMP中的字符,但BMP中有一些擴展字符,可以用兩個這樣的擴展字符表示其它平面的字符,稱為Surrogate Pair。無論是UTF-32還是UTF-16都有一個更嚴重的問題是和C語言不兼容,在C語言中0字節表示字符串結尾,庫函數`strlen`、`strcpy`等等都依賴于這一點,如果字符串用UTF-32存儲,其中有很多0字節并不表示字符串結尾,這就亂套了。
UNIX之父Ken Thompson提出的UTF-8編碼很好地解決了這些問題,現在得到廣泛應用。UTF-8具有以下性質:
* 編碼為U+0000~U+007F的字符只占一個字節,就是0x00~0x7F,和ASCII碼兼容。
* 編碼大于U+007F的字符用2~6個字節表示,每個字節的最高位都是1,而ASCII碼的最高位都是0,因此非ASCII碼字符的表示中不會出現ASCII碼字節(也就不會出現0字節)。
* 用于表示非ASCII碼字符的多字節序列中,第一個字節的取值范圍是0xC0~0xFD,根據它可以判斷后面有多少個字節也屬于當前字符的編碼。后面每個字節的取值范圍都是0x80~0xBF,見下面的詳細說明。
* UCS定義的所有2<sup>31</sup>個字符都可以用UTF-8編碼表示出來。
* UTF-8編碼最長6個字節,BMP字符的UTF-8編碼最長三個字節。
* 0xFE和0xFF這兩個字節在UTF-8編碼中不會出現。
具體來說,UTF-8編碼有以下幾種格式:
U-00000000?–?U-0000007F:? 0xxxxxxx
U-00000080?–?U-000007FF:? 110xxxxx?10xxxxxx
U-00000800?–?U-0000FFFF:? 1110xxxx?10xxxxxx?10xxxxxx
U-00010000?–?U-001FFFFF:? 11110xxx?10xxxxxx?10xxxxxx?10xxxxxx
U-00200000?–?U-03FFFFFF:? 111110xx?10xxxxxx?10xxxxxx?10xxxxxx?10xxxxxx
U-04000000?–?U-7FFFFFFF:? 1111110x?10xxxxxx?10xxxxxx?10xxxxxx?10xxxxxx?10xxxxxx
第一個字節要么最高位是0(ASCII字節),要么最高兩位都是1,最高位之后1的個數決定后面有多少個字節也屬于當前字符編碼,例如111110xx,最高位之后還有四個1,表示后面有四個字節也屬于當前字符的編碼。后面每個字節的最高兩位都是10,可以和第一個字節區分開。這樣的設計有利于誤碼同步,例如在網絡傳輸過程中丟失了幾個字節,很容易判斷當前字符是不完整的,也很容易找到下一個字符從哪里開始,結果頂多丟掉一兩個字符,而不會導致后面的編碼解釋全部混亂了。上面的格式中標為x的位就是UCS編碼,最后一種6字節的格式中x位有31個,可以表示31位的UCS編碼,UTF-8就像一列火車,第一個字節是車頭,后面每個字節是車廂,其中承載的貨物是UCS編碼。UTF-8規定承載的UCS編碼以大端表示,也就是說第一個字節中的x是UCS編碼的高位,后面字節中的x是UCS編碼的低位。
例如U+00A9(?字符)的二進制是10101001,編碼成UTF-8是11000010 10101001(0xC2 0xA9),但不能編碼成11100000 10000010 10101001,UTF-8規定每個字符只能用盡可能少的字節來編碼。
## 3.?在Linux C編程中使用Unicode和UTF-8
目前各種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]](bi01.html#bibli.unicodefaq "UTF-8 and Unicode FAQ, http://www.cl.cam.ac.uk/~mgk25/unicode.html"),讀者可進一步參考這篇文章。
- Linux C編程一站式學習
- 歷史
- 前言
- 部分?I.?C語言入門
- 第?1?章?程序的基本概念
- 第?2?章?常量、變量和表達式
- 第?3?章?簡單函數
- 第?4?章?分支語句
- 第?5?章?深入理解函數
- 第?6?章?循環語句
- 第?7?章?結構體
- 第?8?章?數組
- 第?9?章?編碼風格
- 第?10?章?gdb
- 第?11?章?排序與查找
- 第?12?章?棧與隊列
- 第?13?章?本階段總結
- 部分?II.?C語言本質
- 第?14?章?計算機中數的表示
- 第?15?章?數據類型詳解
- 第?16?章?運算符詳解
- 第?17?章?計算機體系結構基礎
- 第?18?章?x86匯編程序基礎
- 第?19?章?匯編與C之間的關系
- 第?20?章?鏈接詳解
- 第?21?章?預處理
- 第?22?章?Makefile基礎
- 第?23?章?指針
- 第?24?章?函數接口
- 第?25?章?C標準庫
- 第?26?章?鏈表、二叉樹和哈希表
- 第?27?章?本階段總結
- 部分?III.?Linux系統編程
- 第?28?章?文件與I/O
- 第?29?章?文件系統
- 第?30?章?進程
- 第?31?章?Shell腳本
- 第?32?章?正則表達式
- 第?33?章?信號
- 第?34?章?終端、作業控制與守護進程
- 第?35?章?線程
- 第?36?章?TCP/IP協議基礎
- 第?37?章?socket編程
- 附錄?A.?字符編碼
- 附錄?B.?GNU Free Documentation License Version 1.3, 3 November 2008
- 參考書目
- 索引