C++ STL IO流 與 Unicode (UTF-16 UTF-8) 的協同工作(轉載)

寫的真不錯,受益匪淺呀
要是身邊多些這樣善於鑽研的程序員朋友就好了
凡用到文件讀寫,輸入輸出,就得和編碼、Unicode 打交道。這系列實驗來測試一下 C++ STL 的 IO流 對 ANSI 編碼、Unicode 編碼的支持特性,看能否找到一個自動識別編碼,自動轉碼的解決方案。從基礎開始,一步一步來:
 
平臺 Win32 XP sp3 + VS2008. (+ Boost 1.36.0)
 
實驗 01:
#include<string>
#include<iostream>
#include<locale>
using namespace std;
 
locale prevloc;
locale loc("chs");
 
string str1("string class");
string str2("漢字與字符");
wstring wstr1(L"wstring class");          //去掉L前綴則編譯錯誤
wstring wstr2(L"漢字與字符");
 
prevloc = cout.imbue(locale(""));
cout<<"Default Locale: "<<prevloc.name()<<endl;
cout<<"System Locale: "<<locale("").name()<<endl;
cout<<"C風格字符串/n"<<L"w-string/n"<<str1<<'/n'<<str2<<'/n'<<endl;
 
prevloc = wcout.imbue(loc);   //若去掉此句,則wstr2無法正常輸出
wcout<<"Default Locale: "<<prevloc.name().c_str()<<endl;    //若不加 .c_str() 則編譯錯誤
wcout<<"chs Locale Name: "<<loc.name().c_str()<<endl;
wcout<<"C-string/n"<<"C風格字符串/n"<<L"寬字符串/n"<<wstr1<<'/n'<<wstr2<<'/n'<<endl;
 
結論:
        1.cout 與 string 配合使用,wcout 與 wstring 配合使用,交錯則編譯錯誤(類型問題)
        2.wstring 初始化時需用 L"xxx" 的寬字符形式,同樣 string 初始化時不能加 L 前綴
        3.默認locale ("C")下 cout 可以正常輸出 C風格字符串與std::string類型,包括漢字也能正常顯示
    但對 L"xxx" 寬字符串無能爲力
          默認locale ("C")下 wcout 不能輸出中文,包括C風格字符串、寬字符串與std::wstring
    設定系統 locale ("chs")後,正常輸出寬字符串與std::wstring,但 C風格字符串 中的漢字無法顯示
 
        總之,string cout "C-style 字符串" 自成體系
                  wstring wcout L"寬字符串" 自成體系,但 wcout 要選擇 locale 後才能正常輸出中文。
 
實驗 02:
cout.imbue(locale(""));
wcout.imbue(locale(""));
 
string  str3 ( "abc漢字");
wstring wstr3(L"abc漢字");
 
cout<<"str1 length: "<<str1.length()<<'/n'; // 12
cout<<"str2 length: "<<str2.length()<<'/n'; // 10
cout<<"str3 length: "<<str3.length()<<'/n'; // 7
cout<<str2[0]<<' '<<str2[1]<<'/n'// 輸出:?
cout<<endl;
wcout<<L"wstr1 length: "<<wstr1.length()<<'/n'; // 13
wcout<<L"wstr2 length: "<<wstr2.length()<<'/n'; // 5
wcout<<L"wstr3 length: "<<wstr3.length()<<'/n'; // 5
wcout<<wstr2[0]<<' '<<wstr2[1]<<'/n';   // 輸出:漢 字
 
結論:
        4.std::string 內部以 char 類型儲存字符,當有漢字時以雙字節存儲,此時 length() 給出
    字符串所佔字節數而不是字符數
          std::wstring 內部以 wchar_t 類型存儲字符,字母漢字統一都是雙字節,此時 length()
    給出是正確的字符數。
        5.當std::string中有漢字存在時,通過下標訪問不能得到正確的字符。這是顯而易見的,
    一方面字符寬度不統一無法隨機訪問,另一方面 std::string[] 返回 char 類型。std::wstring
    不存在此問題。
 
實驗 03:
// test.txt 爲 ANSI 編碼(GB2312),內容爲以上 str1 ~ str3 的3行。
#include<fstream>
 
string str;
wstring wstr;
 
ifstream fin("test.txt");
//fin.imbue(locale(""));
while(fin>>str)
    cout<<str<<'/n';
fin.close();
 
wifstream wfin("test.txt");
//wfin.imbue(locale(""));
//wfin.imbue(locale(".936"));
while(wfin>>wstr)
    wcout<<wstr<<'/n';
wfin.close();
 
結論:
       6.std::ifstream 讀取 ANSI 編碼正常,std::wifstream 讀取 ANSI 編碼錯誤...默認 locale("C") 不能識別中文字符
          std::wifstream 設置 imbue(locale("")) 或 locale(".936") 後正常讀取。936 爲 GB2312 的代碼頁。
 
 實驗 04:
 test.txt 爲 Shift-JIS 編碼,內容爲
 うみねこのなく頃に

 程序代碼同實驗3
 ifstream 輸出爲
 偆傒偹偙偺側偔崰偵
 wifstream 設定 imbue(locale("")) 後輸出相同
 
結論:
       7.顯而易見的,其他地區的編碼無法正確識別。這也是很多日本遊戲和文本文件運行
    或讀取時產生亂碼的原因。
 
 實驗 05:
 test.txt 爲 Shift-JIS 編碼,內容同上
 ifstream 與 wifstream 都添加 imbue(locale("jpn")) 或 locale(".932")
932 爲 Shift-JIS 的代碼頁
 輸出爲:
 偆傒偹偙偺側偔崰偵
 うみねこのなく頃に
 
 
結論:
       8.這裏可以看出一個顯著性差異。wifstream 在讀取時按照 Shift-JIS 編碼將其轉換爲
    Unicode 儲存,在 wcout 輸出時又按照 ANSI (GB2312) 轉換,其結果是 —— 正確顯示
    了其他地區編碼的字符。而 ifstream 與 cout 則缺少那兩步轉換,結果與上例相同
    以後的實驗將不再考慮 ifstream 而只實驗 wifstream。
 
 實驗 06:
 test.txt 存爲 UTF-16 編碼(Win32 默認的 little endian),內容同上。
 wifstream 設定爲 imbue(locale(".1200"))
 1200 爲 UTF-16 的 code page
 
 結果,運行出錯...發現是 imbue(locale(".1200")); 這句的問題
 試着將 ".1200" 改爲 ".936" 則運行正常,輸出亂碼。(936是 GB2312 的代碼頁)
 翻 MSDN 時在 Code Page 那頁1200 UTF-16 後面發現一行小字:
 "available only to managed applications"...鬱悶
 看來用 locale 轉Unicode的想法到此結束了?記得 STL 書中貌似說過,locale 的名
 字在各平臺上是不統一的,因爲關係到各平臺的支持問題。這樣的話,要麼自己寫
 代碼,要麼就只好用 API 顯式轉換了:MultiByteToWideChar
 另外,在 setlocale 函數說明中也寫到,UTF-8 和 UTF-7 等每字符有可能大於2字節
 的編碼不被支持,所以 UTF-8 也只能用 MultiByteToWideChar 轉咯...
 目前大概只能得出結論 C++ STL locale 在 Win32 平臺上支持不完善吧
 
 實驗 07: 用 API 重寫讀文件部分代碼
#include<windows.h>

HANDLE hFile;
if(INVALID_HANDLE_VALUE != (hFile = CreateFileW(L"test.txt",
        GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL))){

    int iFileLength, iUniTest, i;
    iFileLength = GetFileSize(hFile,NULL);
    char *pBuffer, *pText;
    pBuffer = new char[iFileLength+2];
    DWORD dwBytesRead;


    ReadFile(hFile,pBuffer,iFileLength,&dwBytesRead,NULL);
    CloseHandle(hFile);
    pBuffer[iFileLength] = '/0';
    pBuffer[iFileLength + 1] = '/0';
 
    iUniTest = IS_TEXT_UNICODE_SIGNATURE | IS_TEXT_UNICODE_REVERSE_SIGNATURE;
    if(IsTextUnicode(pBuffer,iFileLength,&iUniTest)){
        pText = pBuffer + 2;
        iFileLength -= 2;
        if(iUniTest & IS_TEXT_UNICODE_REVERSE_SIGNATURE){
            for(i = 0;i < iFileLength; i+=2)
                swap(pText[i],pText[i+1]);
        }
        wstr = (wchar_t*)(pBuffer+2);
    }
    delete [] pBuffer;
    wcout<<wstr<<'/n';
}
 
        輸出正確。以上程序段自動識別 Unicode 編碼文件開頭的 0xFFFE 標記判斷是 Little Endian 還是
    Big Endian 並做相應轉換。但是代碼量較大,且與 C++ 的 IO流 很不搭調...
 
結論:
       9.可以看到,只是把輸入內容去掉UTF-16開頭的0xFFFE,直接把內存指針改爲
    wchar_t* 後 std::wstring 即可正確識別,說明程序中的寬字符存儲格式實際上用的就是
    UTF-16 little endian
 
 實驗 08:
 不死心又去翻了 boost 庫,發現 codecvt_null 這個好東西,看下實現是把文件存儲內容
 按照 wchar_t 爲單位直接讀入內存不做任何轉換。這其實不正好是 UTF-16 需要做的麼
 以下把 test.txt 存爲 UTF-16 little endian 再次實驗
#include<boost/archive/codecvt_null.hpp>

wifstream wfin(L"test.txt");
locale utf16(loc, new boost::archive::codecvt_null<wchar_t>);
wfin.imbue(utf16);
while(wfin>>wstr){
    wcout<<wstr<<endl;
}
wfin.close();
 
輸出正確。
 
結論:
       10. 看來可以把 codecvt_null 作爲 UTF-16 的 codecvt_facet 讀入 locale
    來使用,避免使用類似上面 API 那麼多代碼。
 
 實驗 09:
 將 test.txt 存爲 UTF-16 Big Endian ,內容不變。程序不變
 
無法輸出任何內容。
結論:
       11. wcout 不認識 big endian 的 wchar_t ...
    看來想讀取 UTF-16 Big Endian,僅靠 codecvt_null 還不夠。稍微翻了一下
    《C++ 輸入輸出流與本地化》這本書,現在可以考慮寫一個自己的 codecvt_facet
    了。有了 codecvt_null 的代碼,稍作改動即可用於 UTF-16 big endian。雖說有了
    現在的知識自己寫個 utf-16 的codecvt_facet 也可以,但效率大概比不上 boost 裏的。
 
代碼準備:用類似的方法寫出了自己的 codecvt_utf16 和 codecvt_utf16_reverse 兩個
codecvt_facet...然後繼續實驗。自己寫的內容放入咱自己的頭文件吧:codecvt_utf.h,
內容加入自己的 namespace : tvt
 
 實驗 10: 用 codecvt_utf.h 代替 codecvt_null.hpp。用 codecvt_utf16 和
 codecvt_utf16_reverse 實現 little endian 與 big endian 的輸入。

wifstream wfin(L"test.txt");
locale utf16(loc,new tvt::codecvt_utf16<wchar_t>);
wfin.imbue(utf16);
while(wfin>>wstr){
    wcout<<wstr<<endl;
}
wfin.close();
///////////////////////////////////////
wifstream wfin(L"test.txt");
locale utf16(loc,new tvt::codecvt_utf16_reverse<wchar_t>);
wfin.imbue(utf16);
while(wfin>>wstr){
    wcout<<wstr<<endl;
}
wfin.close();
 
第一段程序讀取 UTF-16 little endian 編碼的 text.txt 正確輸出
第二段程序讀取 UTF-16 big endian 編碼的 text.txt 正確輸出
 
UTF-16 的轉碼順利完成。下面考慮 UTF-8 ,寫法類似。在 boost 庫中繼續尋找,發現
這個東東 boost/detail/utf8_codecvt_facet.hpp 。看下說明,不支持直接使用此文件,這文件
是專門提供其他 boost 組件使用的。僅 include 它的話編譯出問題。再尋找到同名的 cpp 文件
後即可看到 do_in do_out 這兩個轉碼關鍵的虛函數。有了上面 UTF-16 的基礎,我們類似可寫
出 UTF-8 的轉碼 codecvt_facet。我給他起名爲 codecvt_utf8, 依然加入 codecvt_utf.h 文件。
現在此文件有一兩百行了。經試驗可正確輸入 UTF-8 編碼。
 
對應編碼有了處理方法後,下一個問題是編碼識別。
 
實驗 11:
wchar_t wc;
wchar_t buf[2];
wifstream wfin(L"text.txt");
wfin.read(&wc,1);
wfin.read(&buf[0],2);
 
將 wc 和 buf 的內容按2進制或16進制輸出。
結論:
       12. wistream.read(buffer,count) 操作每次讀入 count 個字節,但將每個字節存入一個
 wchar_t 類型的 buffer[i] 中。其實 buffer 中每個 wchar_t 的高位都字節是 0 ...
 
 實驗 12:
 加入判斷條件,在 wfin 中自動加入合適的 utf16 facet,使得自動識別並讀取
 little endian 和 big endian 編碼的文件:

wchar_t buf[2];
wifstream wfin(L"test.txt");
wfin.read(buf,2);

if(buf[0] == wchar_t(0xFF) && buf[1] == wchar_t(0xFE)){
    cout<<"little endian"<<endl;
    wfin.imbue(locale(loc,new tvt::codecvt_utf16<wchar_t>));
}
else if(buf[0] == wchar_t(0xFE) && buf[1] == wchar_t(0xFF)){
    cout<<"big endian"<<endl;
    wfin.imbue(locale(loc,new tvt::codecvt_utf16_reverse<wchar_t>));
}
while(wfin>>wstr){
    wcout<<wstr<<endl;
}
 
對於兩種編碼的 text.txt 都實現了自動識別並正確讀取。輸出正確!
 
結論:
       13.UFT-16在傳輸時幾乎都會加上 0xFFFE 等傳輸標誌很容易判斷,即使沒有, Win32 下
    也有 IsTextUnicode 這 API 用專門方法判斷。UTF-8 就很麻煩了,開頭不一定都有 BOM 標
    記,與各地區字符集一樣都可以用一個或多字節表示一個字符,編碼長度不固定,如果是
    很長一段 ASCII 字符,那麼用 UTF-8 和 GB2312 編碼出來結果一樣,就很難分辨
 
代碼準備:經過一段時間思考,打算用這種算法。先讀取前3字節,若是 BOM 頭標記最好。若
不是則排除 UTF-16 ,下面集中力量分辨 UTF-8 與 ANSI 。從頭開始尋找第一個 >127 的字節
若此字節內容 < 0xC0 或 >0xEF 則可判斷不是 UTF-8 。否則,根據 UTF-8 的規則,在後面1 或
2 字節中看開頭兩位是不是 10 。若不是則斷定不是 UTF-8 ,否則就算得到一個 UTF-8 字符。
如果能夠找到 10個 滿足條件的 UTF-8 字符就判斷爲 UTF-8 編碼。若未到 10 個即遇到文件結
尾,那麼找到 UTF-8 字符數大於 1 即斷定爲 UTF-8 否則斷定爲 ANSI ...
用這種方式選擇對應轉碼 facet:
wistrm.imbue(std::locale(wistrm.getloc(), new codecvt_utf8));
 
按以上想法寫成函數 int IsStreamUnicode(std::wistream &wistrm); UTF-16 LE 返回1,BE 返回2,
UTF-8 返回3,否則返回 0 (判斷爲ANSI)
 
實驗 13:
 
std::wifstream wfin(L"test.txt");
if(!tvt::IsStreamUnicode(wfin))
    wfin.imbue(loc);
while(wfin>>wstr)
    wcout<<wstr<<endl;
 
 在我試驗的各種情況下,均能自動識別 UTF-16 LE UTF-16 BE UTF-8 與 ANSI 編碼
 並正確設定轉碼 locale .
 
 
-------------------------------------------------------------------------------------
8小時後,關於後續實驗的補充:
 
使用中發現某些情況下 UTF-16 的讀寫出現問題,特別是有換行符或某字節中編碼剛好
等於控制符時。經過反覆測試認定是 讀寫mode 問題。在讀寫 Unicode 文件時,
wifstream 與 wofstream 都設定爲 ios_base::binary 模式即可。後來又補充了一個添加
BOM 頭的小東西。爲了使用簡便把 utf_16 的 template 也去掉了。最終情形使用起來
像這個樣子:
 
#include<iostream>
#include<fstream>
#include<codecvt_utf.h>
using namespace std;
 
wstring wstr;
wcout.imbue(locale(""));
 
// Open the Input and Output Files:
std::wifstream wfin(L"test.txt", ios_base::binary);
std::wofstream wfout(L"testout.txt", ios_base::binary);
 
// Set Output Format and Write BOM tag:
wfout.imbue(locale(locale(""), new tvt::codecvt_utf16));
wfout<<tvt::utf_bom;
 
// Detect the Format of the Input File
if(!tvt::IsStreamUnicode(wfin))
    wfin.imbue(locale(""));
 
// Read and Write
//while(wfin>>wstr){
//    wcout<<wstr<<endl;
//    wfout<<wstr<<endl;
//}
 
// Another way:
while(getline(wfin,wstr)){
    wcout<<wstr<<endl;
    wfout<<wstr<<endl;
}
 
// Close Files:
wfin.close();
wfout.close();
 
讀寫測試全部通過!
 
感謝 記事本、EditPlus 和 HxDen 的大力支持...

 至此,關於 Unicode 編碼和 C++ STL IO流 的協作算是大功告成了吧,呵呵。以後有需要再
在實踐中改進

 花了整整一天時間 + 8 小時 = = 還算有價值吧,因爲在網上看到很多人都在問且沒有結果
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章