經常在寫代碼的時候需要處理寬字符,ASCII 字符,在代碼中看到 wchar、char 等等。一般都是處理一個方法的時候發現需要的是某字符串,然后這邊有什么字符串,之后查一個轉換方法。還有對于 Unicode 、ANSI 這些不太分得清,所以花了一點時間看了一看。做個小結。
編碼介紹
ANSI
ANSI(American National Standards Institute) 其實并不算是一種固定編碼,可以理解為在不同國家,有著不同的解釋。例如在中國大陸,ANSI 編碼指的就是 GBK 編碼,在臺灣地區指的是 BIG5 編碼。所以一個場景下這種編碼是有問題的,比方說一個保存為 ANSI 編碼的文件,在不同區域的系統下,用記事本打開就會有問題,因為對文本的解釋是不同的。例如在中國的系統上保存,其實就是 GBK 編碼,然后在美國的系統上打開,會被當做 ASCII 編碼來解釋,就會出現問題。看不到想要的內容。(注:所以《 Windows 核心編程(第五版)》(下稱《核心編程》)2.1節作者說到:調用 strlen 會返回“以 0 結尾的一個 ANSI 單字節字符數組”中的字符數,這個表述是不準確的,之所以這么說是因為作者所在的國家顯然是 ASCII 編碼,但是拿到中文這里說就不恰當,可以說是作者的鍋也可以說是譯者的鍋。所以此書第二章所有講到 ANSI,都可以理解為 ASCII 編碼)
Unicode
Unicode 標準(使用多字符編碼)解決了 ASCII 編碼這種單字符編碼無法表示一些包含特別多字符的問題。官方的一段解釋 The Unicode Standard provides a unique number for every character, no matter what platform, device, application or language. ,其實就是把每個字符作為一個具體數字 。對于 Unicode 標準,存在多種編碼,例如:UTF-8 編碼,UTF-16 編碼等等。UTF(Unicode Transformation Format),指的是 Unicode 轉換格式。
UTF-8
以下引用《核心編程》原文:
UTF-8 將一些字符編碼為 1 個字節(可以說就是那些 ASCII 字符),一些字符編碼為 2 個字節,一些字符編碼為 3 個字節,一些字符編碼為 4 個字節。根據 Unicode 的數字不同來區分應該編碼為幾個字節,屬于
變長字節編碼。這樣的好處是顯而易見的,就是節省空間,壞處也是顯而易見的,處理一些字符編碼比較復雜的文本,顯然效率會差,至少要不斷判斷是幾個字節,計算長度就比較麻煩。
UTF-16
UTF-16 就比較雞賊了,如果細說就要扯到輔助平面和基本文字平面了,感覺意義都不是很大。簡單理解就是一般字符(文字基本都是這個范疇)編碼為 2 個字節,不一般的編碼為 4 個字節(也就是 2 個 2 字節)。關于 UTF-16 連《核心編程》都沒說,可見作者也是非常雞賊了。
UTF-32
UTF-32 這個算是最省事了,把 Unicode 值用 32 位無符號整數表示就得到了 UTF-32 的編碼了。缺點也是顯而易見的,賊占地方。
BOM頭
經常在 Code Page 中看到帶 BOM 頭和不帶 BOM 頭。這個跟編碼的大小端有關。對于這種多個字節的編碼存在一個大小端的問題。如何來區分編碼的大小端。Unicode 標準推薦使用一個 BOM(Byte Order Mark)來做區分。BOM 的字符編碼是0xFEFF,這個叫做零寬無中斷字符,這也解釋了為什么你在文件里邊去掉和添加 BOM 頭都不會影響排版。所以 BOM 頭的存在可以幫助判斷文本的編碼的大小端,如果沒有 BOM 頭的文本,在跨系統使用的時候,編輯器的實現可以做出兩種做法:1. 會根據系統是大小端強行解釋,這樣的問題是一旦兩個系統不一致,看到的內容也就完全不對了;2. 根據里邊的數據,做一個判斷,因為當大端被解釋成小端有可能會出現 Unicode 中不存在的字符(如 BOM 頭這個字符,0xFEFF存在,0xFFEF不存在)。在我看來顯然應該是第一種做法。具體理由按下不表了。
數據類型
char
1 個字節(8 bit)。用來表示 ASCII 編碼。
wchar_t
2 個字節(16 bit)。用來表示 Unicode 字符(UTF-16)。當寫出 wchar_t c = L'A'; 這行代碼的時候,編譯器會把L后邊的東西用 UTF-16 來編碼。值得一提的是wchar_t早期的 Microsoft 編譯器并不支持。在那個上古時期有這樣一個定義 typedef unsigned short wchar_t 。后來支持以后,編譯器搞了一個編譯開關 /Zc:wchar_t,有這個的才在編譯器定義這個數據類型,現在新建項目的時候會默認開啟了。
CHAR、WCHAR
按照《核心編程》的說法:
為了與 C 語言稍微有一些區分,Windows 開發團隊希望定義自己的數據類型。
- CHAR:
typedef char CHAR - WCHAR:
typedef wchar_t WCHAR - 指針:
// Pointer to 8-bit character(s) typedef CHAR *PCHAR; typedef CHAR *PSTR; typedef CONST CHAR *PCSTR // Pointer to 16-bit character(s) typedef WCHAR *PWCHAR; typedef WCHAR *PWSTR; typedef CONST WCHAR *PCWSTR
TCHAR
TCHAR c = TEXT('A')。這個可以理解為萬能類型,之所以這么說,可以看一下它的定義
#ifdef UNICODE
typedef WCHAR TCHAR, *PTCHAR, PTSTR;
typedef CONST WCHAR *PCTSTR;
#define __TEXT(quote) L##quote
#else
typedef CHAR TCHAR, *PTCHAR, PTSTR;
typedef CONST CHAR *PCTSTR;
#define __TEXT(quote) quote
#endif
#define TEXT(quote) __TEXT(quote)
所以看你的項目是否定義了 UNICODE 宏來決定 TCHAR 的類型,當然這個 UNICODE 宏還會影響 Windows API 調用函數版本的選擇,后邊細說。所以會看到大批文章告訴你解決什么編不過的問題都直接讓你用 TCHAR 和 TEXT()。但我覺得并沒有太大意義,至少我暫時想不到需要這兩個版本都支持的場景。項目使用哪種數據類型明確一點會比較好,會影響到效率,后邊細說。
函數
對于 Windows API 微軟都會提供兩個版本的例如 CreateWindowExW、CreateWindowExA,一個是寬字符版本,一個是單字符版本。當然如果你用CreateWindowEx,你會發現再配合 TCHAR 這套,顯然也可以正常使用。
#ifdef UNICODE
#define CreateWindowEx CreateWindowExW
#eles
#define CreateWindowEx CreateWindowExA
#endif
就是因為這個緣故。所以上邊我說會影響到函數版本的選擇。而效率問題,在 Windows Vista 上(當然可以理解為之后的版本也都如此) A 版本的函數其實只是一個轉換層,將傳入的 ASCII 字符轉換成 Unicode 字符,然后調用 W 版本。所以這中間會有一個分配內存的過程,顯然會有一個效率上的問題。所以其實現在寫代碼,非常推薦統一使用寬字符版本。
另外除了 Windows API 之外,C 運行庫,也有類似的操作。
#ifdef _UNICODE
#define _tcslen wcslen
#eles
#define _tcslen strlen
#endif
只不過使用的是 _UNICODE 宏。所以不想讓工程出現編碼的混亂,顯然 UNICODE、_UNICODE 是要成對出現的。事實上,現在用 Visual Studio 新建工程的時候,默認這兩個都會定義上的。
跨平臺的坑
對于 wchar_t 在 Windows 平臺是 UTF-16 編碼,是 2 個字節的長度。而在 Linux 上是 4 個字節的長度,GCC 編譯的時候會用 UTF-32 編碼。這里邊就會有一個不一致。要考慮編碼轉換問題。
最后
至此編程中需要的編碼,大致了解清楚了。Windows 編程中,除非有特殊需要,否則一律使用寬字符是最好的選擇。編碼則選擇 UTF-16 編碼。