寫的真不錯,受益匪淺呀
要是身邊多些這樣善於鑽研的程序員朋友就好了
凡用到文件讀寫,輸入輸出,就得和編碼、Unicode 打交道。這系列實驗來測試一下 C++ STL 的 IO流 對 ANSI 編碼、Unicode 編碼的支持特性,看能否找到一個自動識別編碼,自動轉碼的解決方案。從基礎開始,一步一步來:
平臺 Win32 XP sp3 + VS2008. (+ Boost 1.36.0)
實驗 01:
#include<string>
#include<iostream>
#include<locale>
#include<iostream>
#include<locale>
using namespace std;
locale prevloc;
locale loc("chs");
locale loc("chs");
string str1("string class");
string str2("漢字與字符");
wstring wstr1(L"wstring class"); //去掉L前綴則編譯錯誤
wstring wstr2(L"漢字與字符");
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;
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;
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風格字符串 中的漢字無法顯示
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(""));
wcout.imbue(locale(""));
string str3 ( "abc漢字");
wstring wstr3(L"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;
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'; // 輸出:漢 字
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
字符串所佔字節數而不是字符數
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;
wstring wstr;
ifstream fin("test.txt");
//fin.imbue(locale(""));
while(fin>>str)
cout<<str<<'/n';
fin.close();
//fin.imbue(locale(""));
while(fin>>str)
cout<<str<<'/n';
fin.close();
wifstream wfin("test.txt");
//wfin.imbue(locale(""));
//wfin.imbue(locale(""));
//wfin.imbue(locale(".936"));
while(wfin>>wstr)
wcout<<wstr<<'/n';
wfin.close();
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 的代碼頁。
std::wifstream 設置 imbue(locale("")) 或 locale(".936") 後正常讀取。936 爲 GB2312 的代碼頁。
實驗 04:
test.txt 爲 Shift-JIS 編碼,內容爲
うみねこのなく頃に
test.txt 爲 Shift-JIS 編碼,內容爲
うみねこのなく頃に
程序代碼同實驗3
ifstream 輸出爲
偆傒偹偙偺側偔崰偵
wifstream 設定 imbue(locale("")) 後輸出相同
偆傒偹偙偺側偔崰偵
wifstream 設定 imbue(locale("")) 後輸出相同
結論:
7.顯而易見的,其他地區的編碼無法正確識別。這也是很多日本遊戲和文本文件運行
或讀取時產生亂碼的原因。
或讀取時產生亂碼的原因。
實驗 05:
test.txt 爲 Shift-JIS 編碼,內容同上
ifstream 與 wifstream 都添加 imbue(locale("jpn")) 或 locale(".932")
test.txt 爲 Shift-JIS 編碼,內容同上
ifstream 與 wifstream 都添加 imbue(locale("jpn")) 或 locale(".932")
932 爲 Shift-JIS 的代碼頁
輸出爲:
輸出爲:
偆傒偹偙偺側偔崰偵
うみねこのなく頃に
うみねこのなく頃に
結論:
8.這裏可以看出一個顯著性差異。wifstream 在讀取時按照 Shift-JIS 編碼將其轉換爲
Unicode 儲存,在 wcout 輸出時又按照 ANSI (GB2312) 轉換,其結果是 —— 正確顯示
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
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
試着將 ".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 平臺上支持不完善吧
的編碼不被支持,所以 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;
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
wchar_t* 後 std::wstring 即可正確識別,說明程序中的寬字符存儲格式實際上用的就是
UTF-16 little endian
實驗 08:
不死心又去翻了 boost 庫,發現 codecvt_null 這個好東西,看下實現是把文件存儲內容
按照 wchar_t 爲單位直接讀入內存不做任何轉換。這其實不正好是 UTF-16 需要做的麼
以下把 test.txt 存爲 UTF-16 little endian 再次實驗
不死心又去翻了 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();
wfin.close();
輸出正確。
結論:
10. 看來可以把 codecvt_null 作爲 UTF-16 的 codecvt_facet 讀入 locale
來使用,避免使用類似上面 API 那麼多代碼。
來使用,避免使用類似上面 API 那麼多代碼。
實驗 09:
將 test.txt 存爲 UTF-16 Big Endian ,內容不變。程序不變
將 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 的輸入。
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();
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;
locale utf16(loc,new tvt::codecvt_utf16_reverse<wchar_t>);
wfin.imbue(utf16);
while(wfin>>wstr){
wcout<<wstr<<endl;
}
wfin.close();
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 ...
wchar_t 類型的 buffer[i] 中。其實 buffer 中每個 wchar_t 的高位都字節是 0 ...
實驗 12:
加入判斷條件,在 wfin 中自動加入合適的 utf16 facet,使得自動識別並讀取
little endian 和 big endian 編碼的文件:
加入判斷條件,在 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 標
也有 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;
if(!tvt::IsStreamUnicode(wfin))
wfin.imbue(loc);
while(wfin>>wstr)
wcout<<wstr<<endl;
在我試驗的各種情況下,均能自動識別 UTF-16 LE UTF-16 BE UTF-8 與 ANSI 編碼
並正確設定轉碼 locale .
並正確設定轉碼 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);
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.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(""));
wfin.imbue(locale(""));
// Read and Write
//while(wfin>>wstr){
// wcout<<wstr<<endl;
// wfout<<wstr<<endl;
//}
// wcout<<wstr<<endl;
// wfout<<wstr<<endl;
//}
// Another way:
while(getline(wfin,wstr)){
wcout<<wstr<<endl;
wfout<<wstr<<endl;
}
wcout<<wstr<<endl;
wfout<<wstr<<endl;
}
// Close Files:
wfin.close();
wfout.close();
wfout.close();
讀寫測試全部通過!
感謝 記事本、EditPlus 和 HxDen 的大力支持...
至此,關於 Unicode 編碼和 C++ STL IO流 的協作算是大功告成了吧,呵呵。以後有需要再
在實踐中改進
花了整整一天時間 + 8 小時 = = 還算有價值吧,因爲在網上看到很多人都在問且沒有結果