UTF-16Unicode字符編碼五層次模型的第三層:字符編碼表(Character Encoding Form,也稱為"storage format")的一種實現方式。即把Unicode字符集的抽象碼位映射為16位長的整數(即碼元)的序列,用於數據存儲或傳遞。Unicode字符的碼位,需要1個或者2個16位長的碼元來表示,因此這是一個變長表示。

UTF是"Unicode/UCS Transformation Format"的首字母縮寫,即把Unicode字符轉換為某種格式之意。UTF-16正式定義於ISO/IEC 10646-1的附錄C,而RFC2781也定義了相似的做法。

UTF-16描述

Unicode的編碼空間從U+0000到U+10FFFF,共有1,112,064個碼位(code point)可用來映射字符。Unicode的編碼空間可以劃分為17個平面(plane),每個平面包含216(65,536)個碼位。17個平面的碼位可表示為從U+xx0000到U+xxFFFF,其中xx表示十六進制值從0016到1016,共計17個平面。第一個平面稱為基本多語言平面(Basic Multilingual Plane, BMP),或稱第零平面(Plane 0),其他平面稱為輔助平面(Supplementary Planes)。基本多語言平面內,從U+D800到U+DFFF之間的碼位區段是永久保留不映射到Unicode字符。UTF-16就利用保留下來的0xD800-0xDFFF區段的碼位來對輔助平面的字符的碼位進行編碼。

從U+0000至U+D7FF以及從U+E000至U+FFFF的碼位

第一個Unicode平面(碼位從U+0000至U+FFFF)包含了最常用的字符。該平面被稱為基本多語言平面,縮寫為BMP(Basic Multilingual Plane,BMP)。UTF-16與UCS-2編碼這個範圍內的碼位為16比特長的單個碼元,數值等價於對應的碼位。BMP中的這些碼位是僅有的可以在UCS-2中表示的碼位。

從U+10000到U+10FFFF的碼位

輔助平面(Supplementary Planes)中的碼位,在UTF-16中被編碼為一對16比特長的碼元(即32位元,4字節),稱作代理對(Surrogate Pair),具體方法是:

UTF-16解碼
lead \ trail DC00 DC01    …    DFFF
D800 10000 10001 103FF
D801 10400 10401 107FF
  ⋮
DBFF 10FC00 10FC01 10FFFF
  1. 碼位減去 0x10000,得到的值的範圍為20比特長的 0...0xFFFFF
  2. 高位的10比特的值(值的範圍為 0...0x3FF)被加上 0xD800 得到第一個碼元或稱作高位代理(high surrogate),值的範圍是 0xD800...0xDBFF。由於高位代理比低位代理的值要小,所以為了避免混淆使用,Unicode標準現在稱高位代理為前導代理(lead surrogates)。
  3. 低位的10比特的值(值的範圍也是 0...0x3FF)被加上 0xDC00 得到第二個碼元或稱作低位代理(low surrogate),現在值的範圍是 0xDC00...0xDFFF。由於低位代理比高位代理的值要大,所以為了避免混淆使用,Unicode標準現在稱低位代理為後尾代理(trail surrogates)。

上述算法可理解為:輔助平面中的碼位從U+10000到U+10FFFF,共計FFFFF個,即220=1,048,576個,需要20位來表示。如果用兩個16位長的整數組成的序列來表示,第一個整數(稱為前導代理)要容納上述20位的前10位,第二個整數(稱為後尾代理)容納上述20位的後10位。還要能根據16位整數的值直接判明屬於前導整數代理的值的範圍(210=1024),還是後尾整數代理的值的範圍(也是210=1024)。因此,需要在基本多語言平面中保留不對應於Unicode字符的2048個碼位,就足以容納前導代理與後尾代理所需要的編碼空間。這對於基本多語言平面總計65536個碼位來說,僅占3.125%。

由於前導代理、後尾代理、BMP中的有效字符的碼位,三者互不重疊,搜索是簡單的:一個字符編碼的一部分不可能與另一個字符編碼的不同部分相重疊。這意味着UTF-16是自同步(self-synchronizing)的:可以通過僅檢查一個碼元來判定給定字符的下一個字符的起始碼元。UTF-8也有類似優點,但許多早期的編碼模式就不是這樣,必須從頭開始分析文本才能確定不同字符的碼元的邊界。

由於最常有的字符都在基本多文種平面中,許多軟件處理代理對的部分往往得不到充分的測試。這導致了一些長期的bug與潛在安全漏洞,它們甚至存在於廣為流行且評價頗高的應用軟件中[1]

從U+D800到U+DFFF的碼位

Unicode標準規定U+D800...U+DFFF的值不對應於任何字符。

但是在使用UCS-2的時代,U+D800...U+DFFF內的值被占用,用於某些字符的映射。但只要不構成代理對,許多UTF-16編碼解碼還是能把這些不符合Unicode標準的字符映射正確的辨識、轉換成合規的碼元[2]。按照Unicode標準,這種碼元序列本來應算作編碼錯誤。

範例:

以U+10437編碼(𐐷)為例:

  1. 0x10437 減去 0x10000,結果為0x00437,二進制為 0000 0000 0100 0011 0111
  2. 分割它的上10位值和下10位值(使用二進制):0000 0000 0100 0011 0111
  3. 添加 0xD800 到上值,以形成高位0xD800 + 0x0001 = 0xD801
  4. 添加 0xDC00 到下值,以形成低位0xDC00 + 0x0037 = 0xDC37
  • 下表總結了一起示例的轉換過程,顏色指示碼點位如何分布在所述的UTF-16中。由UTF-16編碼過程中加入附加位的以黑色顯示。
字符 普通二進制 UTF-16二進制 UTF-16 十六進制
字符代碼
UTF-16BE
十六進制字節
UTF-16LE
十六進制字節
$ U+0024 0000 0000 0010 0100 0000 0000 0010 0100 0024 00 24 24 00
U+20AC 0010 0000 1010 1100 0010 0000 1010 1100 20AC 20 AC AC 20
𐐷 U+10437 0001 0000 0100 0011 0111 1101 1000 0000 0001 1101 1100 0011 0111 D801 DC37 D8 01 DC 37 01 D8 37 DC
𤭢 U+24B62 0010 0100 1011 0110 0010 1101 1000 0101 0010 1101 1111 0110 0010 D852 DF62 D8 52 DF 62 52 D8 62 DF

範例:UTF-16編碼程序

假設要將U+64321(16進位)轉成UTF-16編碼。因為它超過U+FFFF,所以他必須編譯成32位元(4個byte)的格式,如下所示:

V = 0x64321
Vx = V - 0x10000
= 0x54321
= 0101 0100 0011 0010 0001

Vh = 01 0101 0000 // Vx的高位部份的10 bits
Vl = 11 0010 0001 // Vx的低位部份的10 bits
w1 = 0xD800 //結果的前16位元初始值
w2 = 0xDC00 //結果的後16位元初始值

w1 = w1 | Vh
= 1101 1000 0000 0000
 |       01 0101 0000
= 1101 1001 0101 0000
= 0xD950

w2 = w2 | Vl
= 1101 1100 0000 0000
 |       11 0010 0001
= 1101 1111 0010 0001
= 0xDF21

所以這個字U+64321最後正確的UTF-16編碼應該是:

0xD950 0xDF21

而在小尾序中最後的編碼應該是:

0x50D9 0x21DF

因為這個字超過U+FFFF所以無法用UCS-2的格式編碼。

16進制編碼範圍 UTF-16表示方法(二進制) 10進制碼範圍 字節數量
U+0000 - U+FFFF xxxx xxxx xxxx xxxx - yyyy yyyy yyyy yyyy 0-65535 2
U+10000 - U+10FFFF 1101 10yy yyyy yyyy - 1101 11xx xxxx xxxx 65536-1114111 4

UTF-16比起UTF-8,好處在於大部分字符都以固定長度的字節(2字節)儲存,但UTF-16卻無法相容於ASCII編碼。

UTF-16的編碼模式

UTF-16的大尾序和小尾序儲存形式都在用。一般來說,以Macintosh製作或儲存的文字使用大尾序格式,以MicrosoftLinux製作或儲存的文字使用小尾序格式。

為了弄清楚UTF-16文件的大小尾序,在UTF-16文件的開首,都會放置一個U+FEFF字符作為Byte Order Mark(UTF-16 LE以 FF FE 代表,UTF-16 BE以 FE FF 代表),以顯示這個文字檔案是以UTF-16編碼,其中U+FEFF字符在UNICODE中代表的意義是 ZERO WIDTH NO-BREAK SPACE,顧名思義,它是個沒有寬度也沒有斷字的空白。

以下的例子有四個字符:「朱」(U+6731)、半角逗號(U+002C)、「聿」(U+807F)、「𪚥」(U+2A6A5)。

使用UTF-16編碼的例子
編碼名稱 編碼次序 編碼
BOM , 𪚥
UTF-16 LE 小尾序,不含BOM 31 67 2C 00 7F 80 69 D8 A5 DE
UTF-16 BE 大尾序,不含BOM 67 31 00 2C 80 7F D8 69 DE A5
UTF-16 LE 小尾序,包含BOM FF FE 31 67 2C 00 7F 80 69 D8 A5 DE
UTF-16 BE 大尾序,包含BOM FE FF 67 31 00 2C 80 7F D8 69 DE A5

UTF-16與UCS-2的關係

UTF-16可看成是UCS-2的父集。在沒有輔助平面字符(surrogate code points)前,UTF-16與UCS-2所指的是同一的意思。但當引入輔助平面字符後,就稱為UTF-16了。現在若有軟件聲稱自己支援UCS-2編碼,那其實是暗指它不能支援在UTF-16中超過2位元組的字集。對於小於0x10000的UCS碼,UTF-16編碼就等於UCS碼。

Microsoft Windows操作系統內核對Unicode的支持

Windows操作系統內核中的字符表示為UTF-16小尾序,可以正確處理、顯示以4字節存儲的字符。但是Windows API實際上僅能正確處理UCS-2字符,即僅以2字節存儲的,碼位小於U+FFFF的Unicode字符。其根源是Microsoft C++語言把 wchar_t 數據類型定義為16比特的unsigned short,這就與一個 wchar_t 型變量對應一個寬字符、可以存儲一個Unicode字符的規定相矛盾。相反,Linux平台的GCC編譯器規定一個 wchar_t 是4字節長度,可以存儲一個UTF-32字符,寧可浪費了很大的存儲空間。下例運行於Windows平台的C++程序可說明此點:

// 此源文件在Windows平台上必须保存为Unicode格式(即UTF-16小尾)
// 因为包含的汉字“𪚥”,不能在简体中文版Windows默认的代码页936(即GBK)中表示
// 该汉字在UTF-16小尾序中用4个字节表示
// Windows操作系统能正确显示这样的在UTF-16需用4字节表示的字符
// 但是Windows API不能正确处理这样的在UTF-16需用4字节表示的字符,把它判定为2个UCS-2字符

#include <windows.h>
#include <stdio.h>

int main()
{
	const wchar_t lwc[] = L"𪚥";

	MessageBoxW(NULL, lwc, lwc, MB_OK);

	int i = wcslen(lwc);
	printf("%d\n", i);
	int j = lstrlenW(lwc);
	printf("%d\n", j);

	return 0;
}

Windows 9x系統的API僅支持ANSI字符集,只支持部分的UCS-2轉換。1996年發布的Windows NT 4.0的API支持UCS-2。Windows 2000開始,Windows系統API開始支持UTF-16,並支持Surrogate Pair;但許多系統控件比如文本框和label等還不支持surrogate pair表示的字符,會顯示成兩個字符。Windows 7及更新的系統已經良好地支持了UTF-16,包括Surrogate Pair。

Windows API支持在UTF-16LE(wchar_t類型)與UTF-8(代碼頁CP_UTF8)之間的轉碼。例如:

#include <windows.h>
int main() {
	char a1[128], a2[128] = { "Hello" };
	wchar_t w = L'页';
	int n1, n2= 5;
	wchar_t w1[128];
	int m1 = 0;

	n1 = WideCharToMultiByte(CP_UTF8, 0, &w, 1, a1, 128, NULL, NULL);
	m1 = MultiByteToWideChar(CP_UTF8, 0, a2, n2, w1, 128);
}

參考文獻

  1. ^ Code in Apache Xalan 2.7.0 which can fail on surrogate pairs. Apache Foundation. [2012-03-23]. (原始內容存檔於2011-04-23). The code wrongly assumes it is safe to use substring on the input 
  2. ^ Python 2.6 decode of UTF16 does this on Linux, and it correctly handles surrogate pairs. All "CESU" decoders do it too, though they also mistranslate correct surrogate pairs into 2 characters

外部連結