要想瞭解UTF-8編碼規則,請參考我的文章:http://blog.csdn.net/sheismylife/article/details/8570015
在我的另一篇文章"UTF-8編碼實測" http://blog.csdn.net/sheismylife/article/details/8571726 中,我使用了boost::locale庫的代碼來解碼UTF-8. 現在來仔細研究一下解碼的演算法:
如何分辨leading byte和continuation bytes呢?關鍵在於任何一個continuation byte都以10開始。下面的函數可以協助判斷是否為continuation byte:
bool is_trail(char ci) { unsigned char c = ci; return (c & 0xC0) == 0x80;}
因為0xC0二進位格式是1100 0000,和c按位與後也就是低六位全部設定為0,進保留c的高兩位.
而0x80二進位格式是1000 0000, 如果兩者相等,說明c的高兩位是10,因此c是continuation byte。返回true。
有了這個函數,判斷一個位元組是否為leading byte也很簡單:
bool is_lead(char ci) { return !is_trail(ci);}
再看一下函數trail_length, 該函數通過分析一個leading byte來確定continuation bytes的長度。
int trail_length(char ci) { unsigned char c = ci; if(c < 128) return 0; if(BOOST_LOCALE_UNLIKELY(c < 194)) return -1; if(c < 224) return 1; if(c < 240) return 2; if(BOOST_LOCALE_LIKELY(c <=244)) return 3; return -1;}
如果c < 128, 說明就是一個ASCII字元,用一個位元組表示。因此conitunuation bytes長度為0,utf-8編碼總長度為1.
因為11011111就是223,只要c在區間[128, 224), 說明continuation bytes長度為1,utf-8編碼總長度為2.
繼續推理,11101111等於239, 因此在[224, 240)區間的,continuation bytes長度為2,utf-8編碼總長度為3.
11110111等於247, 因此在[240, 248)區間的,continuation bytes長度為3,utf-8編碼總長度為4. 但是這裡限定的區間實際上是[240, 244] 不明白為什麼,可能還有什麼編碼規則我不清楚。
現在來看一下utf.hpp中的代碼,http://www.boost.org/doc/libs/1_53_0/libs/locale/doc/html/utf_8hpp_source.html
00192 template<typename Iterator>00193 static code_point decode(Iterator &p,Iterator e)00194 {00195 if(BOOST_LOCALE_UNLIKELY(p==e))00196 return incomplete;00197 00198 unsigned char lead = *p++;00199 00200 // First byte is fully validated here00201 int trail_size = trail_length(lead);00202 00203 if(BOOST_LOCALE_UNLIKELY(trail_size < 0))00204 return illegal;00205 00206 //00207 // Ok as only ASCII may be of size = 000208 // also optimize for ASCII text00209 //00210 if(trail_size == 0)00211 return lead;00212 00213 code_point c = lead & ((1<<(6-trail_size))-1);00214 00215 // Read the rest00216 unsigned char tmp;00217 switch(trail_size) {00218 case 3:00219 if(BOOST_LOCALE_UNLIKELY(p==e))00220 return incomplete;00221 tmp = *p++;00222 if (!is_trail(tmp))00223 return illegal;00224 c = (c << 6) | ( tmp & 0x3F);00225 case 2:00226 if(BOOST_LOCALE_UNLIKELY(p==e))00227 return incomplete;00228 tmp = *p++;00229 if (!is_trail(tmp))00230 return illegal;00231 c = (c << 6) | ( tmp & 0x3F);00232 case 1:00233 if(BOOST_LOCALE_UNLIKELY(p==e))00234 return incomplete;00235 tmp = *p++;00236 if (!is_trail(tmp))00237 return illegal;00238 c = (c << 6) | ( tmp & 0x3F);00239 }00240 00241 // Check code point validity: no surrogates and00242 // valid range00243 if(BOOST_LOCALE_UNLIKELY(!is_valid_codepoint(c)))00244 return illegal;00245 00246 // make sure it is the most compact representation00247 if(BOOST_LOCALE_UNLIKELY(width(c)!=trail_size + 1))00248 return illegal;00249 00250 return c;00251 00252 }
decode函數負責從字串中解析一個utf-8編碼(可能包含1-4位元組),返回對應的code point,一個uint32_t的整數。
總是假定第一個位元組是leading byte,然後用trail_length擷取continuation bytes的長度,如果為0,說明就是ASCII字元,直接返回。
210行到250行處理的都是非ASCII字元。先注意switch/case用法,這裡沒有break語句,也就是說,如果trail_size為3,會先執行case 3:裡面的語句,然後再依次執行case 2和 case 1(不需要匹配)。這是來自switch/case的特殊文法。參考:http://msdn.microsoft.com/en-us/library/k0t5wee3.aspx
當然用迴圈能更好的表達,但是不知道Artyom為什麼選擇這種寫法?效能更高嗎?至少有一條,如果有代碼掃描工具的話,會認為這裡三段代碼重複,會警示告資訊。:)
現在再看一下之前UTF-8編碼一文中引用的wiki的例子:
現在看一個來自wiki的例子示範如何對字元€進行UTF-8編碼:step 1:擷取€的Unicode code point,是0xU+20ACstep 2:0xU+20AC範圍在U+07FF和U+FFFF之間,因此用三位元組表示。step 3:0xU+20AC的二進位碼是:10000010101100,14位長,要想表示3位元組編碼,必須湊成16 bits.因此高位補上兩個0,變成2位元組16位長:0010000010101100,我下面稱為數值串。step 4: 根據規則,添加一個leading byte,開頭是1110,那麼這個leading byte還有4個bit需要填充,從數值串高位取4個bit來,leading byte變成了:11100010,而數值串值為000010101100step 5: 第一個continuation byte高位應該是10,還缺少6 bits,從數值串中按高位取6 bit,這樣第一個continuation byte為:10000010,而數值串變為101100step 6: 第二個continuation byte高位也應該是10,還缺少6 bits, 從數值串取6 bits,這樣第二個continuation byte為:10101100最終編碼形成的三位元組:11100010 10000010 10101100寫成16進位就是0xE282AC
解碼也就是編碼的逆過程,從leading byte中抽取低4位,從兩個continuation bytes中都抽取低6位,拼接成16bits的整數,然後轉換類型變成uint32_t的整數。
下面的代碼其實就是取出低四位:
code_point c = lead & ((1<<(6-trail_size))-1);
這是個很好的技巧,總結一下可以寫成一個函數, 函數接受兩個參數,一個是要提取bit的x,一個是提取的位元。
uint8_t GetLowNBit(uint8_t x, uint8_t n) { return x & ((1<<n)-1);}
為什麼這裡用6-trail_size,純粹是觀察出來的規律。
2位元組utf-8編碼時,leading byte以110開頭,因此要提取低5位元值, 6-trail_size=6-1=5,剛好能夠提取低5位。
3位元組utf-8編碼,leading byte以1110開頭,因此要提取低4位元值,6-trail_size=6-2=4.
4位元組utf-8編碼,leading byte以11110開頭,因此要提取低3位元值,6-trail_size=6-3=3.
所以這裡用6,Artyom的觀察力很敏銳。
提取出leading byte的低位元據後,現在要提取continuation bytes的低位元據,也就是那個switch/case的功能。
這就很簡單了,tmp & 0x3F就是取出低6位元據,因為continuation byte永遠都是10開頭。每次取出低6位後,將c左移6位然後按位或,就達到合并bit成新的整數的目的。
前面已經解釋過swtich/case在without break的用法。這裡就會運行兩次,取出後面兩個continuation bytes的低6位元據,併合並。
UTF-8解碼演算法分析完成。