utf-8 不用考慮位元組序(轉)

lynon發表於2018-04-26

轉自:

https://blog.csdn.net/willib/article/details/51583044


這篇文章的目的是希望你能在看完後對字元的編碼和子節相關的東西,以及寬字元型別在不同平臺之間的處理能有一個清晰的認識,有出入的地方,感謝指正。


字元編碼

“電腦只有二進位制,人腦才有亂碼”,凡是我們看到的亂碼都是由字元編碼引起的。如果對於字元編碼沒有一個清晰的認識,那麼各種各樣的編碼格式在你的腦海裡肯定是混亂的。首先,我們知道一個位元組是由八個二進位制位組成的,用十六進位制表示就是0x00,這八個二進位制位可以組合成256種不同的狀態。最開始計算機只是在美國用的,為了在計算機的終端顯示可見的東西,他們就把這8個二進位制位所組成的狀態約定成一些指定的字元,0x20以下的稱為控制字元,如換行,反白之類的。然後他們又把空格啊,標點符號啊,數字字母之類的也指定成了特定的狀態表示。這樣一直約定到了127號,就算把英文字元編碼完了,它們全都是用的一個位元組來表示,這些編碼就被稱為“ascii”。但是當時發明計算機的人也沒想到計算機能發展到除了他們美帝以外其他一些國家居然也可以用了,但是各個國家之間的語言字元多種多樣,除了“ascii”編碼的那些外,還有很多的字元沒能在終端顯示出來。所以他們繼續把127號以後的進行編碼,從128一直編到了255,這些又稱為“擴充套件字元”。


但是,當計算機進入中國後,由於我們的漢子博大精深,那區區的一兩百個狀態怎麼夠啊,所以我們當然也開始了自己的編碼指定。因為最開始的考慮不周,如沒有考慮到少數名族語言之類的。單漢字編碼就有了好幾種。“GB2312”:127號之前的字元保持不變,直接廢掉了127之後的那些擴充套件字元,約定兩個127號之後的子節組合在一起編碼成一個漢字,前面一個位元組稱為高子節,後面一個位元組稱為低位元組,都是127號之後的子節。“GBK”:前127號保持不變,不再要求兩個位元組都是127號之後的子節了,只要第一個位元組,也就是高子節,是127號之後的子節就行,第二個位元組(低位元組)不管是不是127號之後的子節都可以。“GB18030”:對“GBK”又進行了擴充套件,增加了更多的字元表示。這一系列的中文編碼又總稱位“DBCS“。


單中文就出現了這麼多的編碼格式,在世界範圍內來看,其他的國家的情況也差不多。這些各種各樣的編碼都是你認你的我認我的,根本都沒有一個統一的共識,這樣的編碼資訊就無法實現資訊的傳輸和共享了,得,要想認別國的字元,那你得裝一套別國的編碼系統。在這樣的情況下,國際標準化組織(iso)就開始著手解決這個問題了,他們廢除了國家地區性的編碼,統一制定了一個編碼格式,”Universal Multiple-Octet Coded Character Set”,簡稱UCS俗稱 “unicode“。


unicode有兩種格式,UCS2和UCS4,它們採用定長編碼,UCS2指定2個位元組編碼一個字元,UCS4指定4個位元組編碼一個字元,在這樣的約定下,所有國家的字元都採用這樣的約定格式來進行重新編碼,原有的ascii保持編碼格式不變,只是將它擴充套件成了2個位元組或者是4個位元組。當然,你可能也發現了,這樣的編碼指定也存在一些問題,UCS2根本就不能編碼出所有的字元,UCS4卻可能是文字的長度成倍的增加,因為一些字元本可以用一個位元組或者是兩個位元組就可以編碼的。所以基於這樣一些原因,而且隨著網際網路的出現,就有了後來的 UTF-8,UTF-16,UTF-32,它們都是Unicode編碼格式的具體實現方式。UTF-8和UTF-16採用變長的編碼方式,utf-8約定可以用1-4個位元組來表示一個字元,utf-16可以用2個或者是4個位元組來編碼一個字元。utf-16可以說是ucs2的擴充套件,而utf-32和ucs4基本相同。關於utf-8的具體編碼方式,我建議你可以看一下後面第二個連結阮一峰老師的文章,講得很清楚。


位元組序

上面我們說了字元編碼,在計算機中還有位元組序這個概念了,你肯定也聽過大端序列和小端序列這個說法。在幾乎所有的機器上,多位元組物件都被儲存為連續的位元組序列,物件的地址為所使用位元組序列中最小的地址,因為不同機器之間處理器和系統的不同,位元組序有大端序列(big-endian)和小端序列(little-endian)之分。大端序列指的是在物件的起始地址儲存高序序列,小端序列指的是在物件的起始地址儲存低序位元組。如一個int型別的值0x01234567,用小端序列表示為:67 45 23 01,用大端序列表示為:01 23 45 67. 當然,在上面我們說的字元編碼中,utf-8時沒有大小端序列之分的。 經測試在Intel處理器上的win7 64位系統,Ubuntu 32位系統,OS X系統均為小端位元組序。當然在我們常用的Intel處理器都是採用的小端位元組序。可以用下面的程式碼來測試位元組序:

  1. #include <iostream>  
  2. union {  
  3.     short   s; //2 bytes  
  4.     char    c[sizeof(short)];  
  5. }endian;  
  6.   
  7. int main()  
  8. {  
  9.     endian.s = 0x0102;  
  10.   
  11.     if(endian.c[0] == 1 && endian.c[1] == 2)  
  12.         std::cout << "big endian" << std::endl;  
  13.     else if (endian.c[0] == 2 && endian.c[1] == 1)  
  14.         std::cout << "little endian" << std::endl;  
  15.     else  
  16.         std::cout << "unknow" << std::endl;  
  17.   
  18.     return 0;  
  19. }  

寬字元型別(wchar_t)的跨平臺處理

當我們需要把以前寫的Windows程式進行跨平臺處理時,如果原來的工程採用的Unicode編碼,我想你肯定首先想到了,wchar_t型別在Windows和Linux平臺下的大小時不一樣的,Windows下采用的是2位元組編碼一個字元,基於utf-16,Linux下采用的是4個位元組編碼一個字元,基於utf-32。兩個平臺下的wchar_t型別sizeof出來的大小不同,那同一份程式碼進行跨平臺處理的時候,會不會出問題呢,這個或許你就有點犯難了。


在這裡,我們首先應該明確一點,wchar_t型別在Windows和Linux平臺下位元組大小的不同,對我們程式本身的跨平臺性沒有任何影響,你Windows下是怎麼處理wchar_t的,那麼在Linux下就怎麼處理,相應的介面和操作都不用改變。不會對資料的讀取產生錯誤。但是有一點,這些是基於這樣一個事實的,就是你沒有在兩個平臺之間對不同平臺下產生的檔案進行讀取。如我目前所做的專案中,在程式碼實現上,需要把wchar_t型別的一些資料輸出到檔案儲存,然後在後續的程式碼中進行讀寫。在不同平臺下產生的這個檔案,wchar_t字元的編碼方式肯定是不一樣的,所以不能把Windows下生成的檔案,直接拿到Linux下面來進行讀寫,如果這樣做,那麼讀寫錯誤是肯定會發生的。還有一點,不能貿然的新增gcc 編譯項 -fshort-wchar,強制將Linux平臺下的wchar_t指定成兩個位元組,因為這樣做,只會改變你在程式碼中自己實現的部分,而內部庫或者是第三方庫中用到的介面和函式都是沒有變的,仍然採用的是4位元組編碼。如,std::wstring, QT中的QString等。


對於這點,在專案中我擬定了兩個方案,方案一是在程式碼中讀寫檔案的部分,寫入檔案的時候,把wchar_t型別的資料轉成utf-8的編碼格式來儲存,讀取的時候把utf-8編碼的資料讀出來後再轉成平臺對應的wchar_t字元,兩個平臺下都採用同樣的解決辦法。在windows下可以採用系統函式WideCharToMultiByte()和MultiByteToWideChar()來進行轉換,如下面把寬字元轉成UTF-8的列子:

  1. #include < windows.h >  
  2.    
  3. std::string to_utf8(const wchar_t* buffer, int len)  
  4. {  
  5.     int nChars = ::WideCharToMultiByte(  
  6.         CP_UTF8,  
  7.         0,  
  8.         buffer,  
  9.         len,  
  10.         NULL,  
  11.         0,  
  12.         NULL,  
  13.         NULL);  
  14.     if (nChars == 0) return "";  
  15.    
  16.     string newbuffer;  
  17.     newbuffer.resize(nChars) ;  
  18.     ::WideCharToMultiByte(  
  19.         CP_UTF8,  
  20.         0,  
  21.         buffer,  
  22.         len,  
  23.         const_castchar* >(newbuffer.c_str()),  
  24.         nChars,  
  25.         NULL,  
  26.         NULL);   
  27.    
  28.     return newbuffer;  
  29. }  
  30.    
  31. std::string to_utf8(const std::wstring& str)  
  32. {  
  33.     return to_utf8(str.c_str(), (int)str.size());  
  34. }  

而在Linux下同樣採用系統函式iconv()來轉換,具體用法下面會提到,這裡先省略。當然,如果你現在專案中的編譯器支援C++11那就好辦了,而我現在windows上的編輯器還是vs2008,C++11在語言上提供了對utf-8的支援,提供了一些新的型別和函式來處理,如下:

  1. // convert UTF-8 string to wstring  
  2. std::wstring utf8_to_wstring (const std::string& str)  
  3. {  
  4.     std::wstring_convert<std::codecvt_utf8<wchar_t>> myconv;  
  5.     return myconv.from_bytes(str);  
  6. }  
  7.   
  8. // convert wstring to UTF-8 string  
  9. std::string wstring_to_utf8 (const std::wstring& str)  
  10. {  
  11.     std::wstring_convert<std::codecvt_utf8<wchar_t>> myconv;  
  12.     return myconv.to_bytes(str);  
  13. }  

因為目前專案需要相容以前Windows下生成的資料檔案,所以最後採用了方案二來做,方案二的辦法是保持Windows下wchar_t型別的資料讀寫方式和編碼都不變。只在Linux下面做文章,Linux下的資料讀寫需要根據Windows下wchar_t的編碼來進行,也就是,在Linux下寫入wchar_t型別的資料的時候,需要先把Linux下的wchar_t的編碼格式轉成Windows下的wchar_t編碼格式,讀的時候同理,需要先把讀出來的Windows下的wchar_t資料,轉成Linux下的wchar_t資料,這樣就能實現兩個平臺下的資料讀寫,而且夜相容了以前的資料檔案。在Linux下的轉換函式也採用的是iconv(),在標頭檔案<iconv.h>中。iconv()的用法如下:

  1. int encodingConvert(const char *tocode, const char *fromcode,  
  2.                     char *inbuf, size_t inlength, char *outbuf, size_t outlength)  
  3. {  
  4. #ifndef _WIN32  
  5.   
  6.     char **inbuffer = &inbuf;  
  7.     char **outbuffer = &outbuf;  
  8.   
  9.     iconv_t cd;  
  10.     size_t ret;  
  11.     cd = iconv_open(tocode, fromcode);  
  12.     if((size_t)cd == -1)  
  13.         return -1;  
  14.     ret = iconv(cd, inbuffer, &inlength, outbuffer, &outlength);  
  15.     if(ret == -1)  
  16.         return -1;  
  17.     iconv_close(cd);  
  18. #endif  
  19.   
  20.     return 0;  
  21. }  

注意iconv()函式在處理的時候,傳進來的inbuffer應該傳一個副本,因為iconv()函式在處理的時候會改變inbuffer,iconv()是對inbuffer中的字元一個一個的進行轉換,然後儲存到outbuffer中。可以用類似下面的程式碼來做具體的處理,主要傳進去的編碼需要指定具體的大端序列還是小端序列,因為Linux下預設的好像是大端的,所以指定詳細點總沒有錯的。

  1. //把wchar_t資料寫到檔案中, 檔案中的資料都是以windows下的格式來存放。  
  2. int WriteWstringToBuffer(const std::wstring &wstr, void *buffer)  
  3. {  
  4.     int inLength = (wstr.size() + 1) * sizeof(wchar_t);  
  5.     int outLength = inLength / 2;  //從Linux下寫入wchar_t的資料到檔案的長度  
  6.     char *inBuffer = new char[inLength]();  
  7.     memcpy(inBuffer, wstr.c_str(), inLength);  
  8.     char *outBuffer = new char[outLength]();  
  9.       
  10.     int ret = EncodingCovert("UTF-16LE""UTF-32LE", inBuffer, inLength, outBuffer, outLength);  
  11.       
  12.     memcpy(buffer, outBuffer, outLength);  
  13.       
  14.     delete[] inBuffer;  
  15.     delete[] outBuffer;  
  16.       
  17.     return (ret == -1)?ret:0;  
  18. }  
  19.   
  20. //從檔案中讀取wchar_t資料, 檔案中的資料都是以windows下的格式來存放。  
  21. int ReadWstringFromBuffer(std::wstring &wstr, void *buffer, int bufLength)  
  22. {  
  23.     int inLength = bufLength;  
  24.     int outLength = inLength * 2;  
  25.     char *inBuffer = new char[inLength]();  
  26.     memcpy(inBuffer, buffer, inLength);  
  27.     char *outBuffer = new char[outLength]();  
  28.       
  29.     int ret = EncodingCovert("UTF-32LE""UTF-LE", inBuffer, inLength, outBuffer, outLength);  
  30.       
  31.     wstr = outBuffer;  
  32.       
  33.     delete[] inBuffer;  
  34.     delete[] outBuffer;  
  35.       
  36.     return (ret == -1)?ret:0;  
  37. }  

在進行這樣的轉換處理後,Linux和Windows下就能互相交換和讀寫wchar_t型別的資料了,而不用擔心資料的讀寫錯誤。


字元編碼部分參考文章:

ASCII、Unicode、GBK和UTF-8字元編碼的區別聯絡

字元編碼筆記:ASCII,Unicode和UTF-8


相關文章