• C++ STL IO流 与 Unicode (UTF-16 UTF-8) 的协同工作


    09年研究技术的大神真的好多,本文测试有很多错误,更正后发布下(可能与编辑器相关)。

    file.imbue(locale(file.getloc(), new codecvt_utf8<wchar_t, 0x10FFFF, consume_header>));

    读取UTF-8编码的文件:

    #include<string>
    #include<iostream>
    #include<locale>
    #include <fstream>
    #include <codecvt>
    #include <io.h>
    #include <fcntl.h>
    using namespace std;
    
    int main()
    {
    	wstring wstr;
    
    	wifstream file("test.txt");
    	file.imbue(locale(file.getloc(), new codecvt_utf8<wchar_t, 0x10FFFF, consume_header>));
    	
    	_setmode(_fileno(stdout), _O_U8TEXT);
    	//wcout.imbue(locale("chs"));//在console输出
    	
    	while (file >> wstr)
    		wcout << wstr << '
    ';
    	file.close();
    }


    转载地址:
    凡用到文件读写,输入输出,就得和编码、Unicode 打交道。这系列实验来测试一下 C++ STL 的 IO流 对 ANSI 编码、Unicode 编码的支持特性,看能否找到一个自动识别编码,自动转码的解决方案。从基础开始,一步一步来:
     
    平台 Win7  VS2013
     
    实验 01:

    #include<string>
    #include<iostream>
    #include<locale>
    using namespace std;
    
    int main()
    {
    	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风格字符串
    " << L"w-string
    " << str1 << '
    ' << str2 << '
    ' << 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
    " << "C风格字符串
    " << L"宽字符串
    " << wstr1 << '
    ' << wstr2 << '
    ' << endl;
    }
    

    输出
    Default Locale: C
    System Locale: Chinese (Simplified)_People's Republic of China.936
    C风格字符串
    00,963,004string class
    汉字与字符
    
    Default Locale: C
    chs Locale Name: Chinese (Simplified)_People's Republic of China.936
    C-string
    C
    宽字符串
    wstring class
    汉字与字符
    
    请按任意键继续. . .


     
    结论:
            1.cout 与 string 配合使用,wcout 与 wstring 配合使用,交错则编译错误(类型问题)
            2.wstring 初始化时需用 L"xxx" 的宽字符形式,同样 string 初始化时不能加 L 前缀
            3.默认locale ("C")下 cout 可以正常输出 C风格字符串与std::string类型,包括汉字也能正常显示
        但对 L"xxx" 宽字符串无能为力
              默认locale ("C")下 wcout 不能输出中文,包括C风格字符串(可以输出 wcout << "你好"; wcout << L"你好";不能输出)、宽字符串与std::wstring
        设定系统 locale ("chs")后,正常输出宽字符串与std::wstring,但 C风格字符串 中的汉字无法显示
     
            总之,string cout "C-style 字符串" 自成体系
                      wstring wcout L"宽字符串" 自成体系,但 wcout 要选择 locale 后才能正常输出中文(不包括C风格字符串)。

    console输出包含非ASCII字符的宽字符串的方法:
    1、WINAPI WriteConsoleW
    2、_setmode
    3、locale或者STL的imbue
     
    实验 02:
    #include<string>
    #include<iostream>
    #include<locale>
    using namespace std;
    
    int main()
    {
    	cout.imbue(locale(""));
    	wcout.imbue(locale(""));
    
    	string str1("string class");
    	string str2("汉字与字符");
    
    	string  str3("abc汉字");
    	wstring wstr1(L"wstring class");
    	wstring wstr2(L"汉字与字符");
    	wstring wstr3(L"abc汉字");
    
    	cout << "str1 length: " << str1.length() << '
    '; // 12
    	cout << "str2 length: " << str2.length() << '
    '; // 10
    	cout << "str3 length: " << str3.length() << '
    '; // 7
    	cout << str2[0] << " " << str2[1] << '
    ';  // 输出:?
    	cout << endl;
    	wcout << L"wstr1 length: " << wstr1.length() << '
    '; // 13
    	wcout << L"wstr2 length: " << wstr2.length() << '
    '; // 5
    	wcout << L"wstr3 length: " << wstr3.length() << '
    '; // 5
    	wcout << wstr2[0] << " " << wstr2[1] << '
    ';   // 输出:汉 字
    }
    

    输出:
    str1 length: 12
    str2 length: 10
    str3 length: 7
    ?
    
    wstr1 length: 13
    wstr2 length: 5
    wstr3 length: 5
    汉 字
    请按任意键继续. . .





    结论:
            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<string>
    #include<iostream>
    #include<locale>
    #include <fstream>
    using namespace std;
    
    int main()
    {
    	string str;
    	wstring wstr;
    
    	ifstream fin("test.txt");
    	//fin.imbue(locale(""));
    	while (fin >> str)
    		cout << str << '
    ';
    	fin.close();
    
    	wifstream wfin("test.txt");
    	//wfin.imbue(locale(""));
    	//wfin.imbue(locale(".936"));
    	while (wfin >> wstr)
    		wcout << wstr << '
    ';
    	wfin.close();
    }

    输出:
     
    abc汉字
    wstring
    class
    汉字与字符
    abc汉字
    
    abc汉字
    wstring
    class
    汉字与字符
    abc汉字
    请按任意键继续. . .

    结论:
           6.std::ifstream 读取 ANSI 编码正常,std::wifstream 读取 ANSI 编码错误(也正常)…默认 locale("C") 不能识别中文字符(可以识别中文字符,当做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] = ”;
        pBuffer[iFileLength + 1] = ”;

     

        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<<‘ ’;
    }
     
            输出正确。以上程序段自动识别 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 小时 = = 还算有价值吧,因为在网上看到很多人都在问且没有结果
     
     ===========分隔线============
     另附:现在来看用 c++ 的 IO stream locale 系列实现转码并不是一个经济的选择,如果用 STLport 的话还好些,用 VC STL 则存在较严重的效率问题:

    Keep it simple!
    作者:N3verL4nd
    知识共享,欢迎转载。
  • 相关阅读:
    你的程序够健壮么?我看未必。。。
    POJ 3415 Max Sum of Max-K-sub-sequence (线段树+dp思想)
    Android ARM汇编语言
    关于索引删除的策略IndexDeletionPolicy
    深度学习领域的一些大牛
    框架学习之道:PE框架简介
    PropertyPlaceholderConfigurer类的使用注意
    hdu 4622 Reincarnation (后缀自动机)
    总结showModalDialog在开发中的一些问题
    android端向服务器提交请求的几种方式
  • 原文地址:https://www.cnblogs.com/lgh1992314/p/5834827.html
Copyright © 2020-2023  润新知