求istreambuf_iterator <wchar_t>澄清,阅读Unicode字符的完整文本文件</wchar_t>

时间:2013-01-05 01:34:12

标签: c++ unicode wstring istream-iterator wifstream

在Scott Meyers的“Effective STL”一书中,有一个将整个文本文件读入std :: string对象的好例子:

std::string sData; 

/*** Open the file for reading, binary mode ***/
std::ifstream ifFile (“MyFile.txt”, std::ios_base::binary); // Open for input, binary mode

/*** Read in all the data from the file into one string object ***/
sData.assign (std::istreambuf_iterator <char> (ifFile),
              std::istreambuf_iterator <char> ());

请注意,它以8字节字符的形式读取。这非常有效。最近虽然我需要读取包含Unicode文本的文件(即每个字符两个字节)。但是,当我尝试(天真地)更改它以将数据从Unicode文本文件读取到std :: wstring对象时,如下所示:

std::wstring wsData; 

/*** Open the file for reading, binary mode ***/
std::wifstream ifFile (“MyFile.txt”, std::ios_base::binary); // Open for input, binary mode

/*** Read in all the data from the file into one string object ***/
wsData.assign (std::istreambuf_iterator <wchar_t> (ifFile),
               std::istreambuf_iterator <wchar_t> ());

我回来的字符串虽然是宽字符,但仍然具有备用空值。例如,如果文件包含Unicode字符串“ABC”,则文件的字节(忽略Unicode引导字节0xFF,0xFE)为:     &LT;” A”&GT; &℃,GT; &LT;” B”&GT; &℃,GT; &LT;” C”&GT; &℃,GT;

上面的第一个代码片段将正确地导致(char)字符串的以下内容:
    sData [0] ='A'
    sData [1] = 0x00
    sData [2] ='B'
    sData [3] = 0x00
    sData [4] ='C'
    sData [5] = 0x00

但是,当运行第二个代码片段时,会不合时宜地导致(wchar_t)字符串的以下内容:
    wsData [0] = L'A'
    wsData [1] = 0x0000
    wsData [2] = L'B'
    wsData [3] = 0x0000
    wsData [4] = L'C'
    wsData [5] = 0x0000

就好像文件仍在逐字节读取,然后只是简单地翻译成单独的wchar_t字符。

我原本以为std :: istreambuf_iterator,专门用于wchar_t,应该导致文件一次读取两个字节,不应该吗?如果没有,那么它的目的是什么呢?

我已经追溯到模板(没有简单的壮举;-),并且迭代器确实似乎仍然逐字节地读取文件并将其传递给其内部转换例程,该例程尽职地说明转换在每个之后完成字节(不仅在接收2个字节后)。

我已经搜索了网络上的一些网站(包括这个网站),看似这个看似微不足道的任务,但是没有找到这种行为的解释或一个不需要更多代码的好替代方案,而不是我认为应该是必要的(例如,谷歌搜索网络产生的第二个代码片段与可行的代码片段相同。

我发现的唯一可行的是以下内容,我认为这是一个作弊,因为它需要直接访问wstring的内部缓冲区,然后在那里键入 - 强制它。

std::wstring wsData; 

/*** Open the file for reading, binary mode ***/
std::wifstream ifFile (“MyFile.txt”, std::ios_base::binary); // Open for input, binary mode

wsData.resize (<Size of file in bytes> / sizeof (wchar_t));

ifFile.read ((char *) &wsData [0], <Size of file in bytes>);

哦,并预防不可避免的“为什么在二进制模式下打开文件,为什么不在文本模式下”问题,打开是故意的,好像文件在文本模式下打开(默认),这意味着CR / LF (“\ r \ n”或0x0D0A)序列将被转换为仅LF(“\ n”或0x0A)序列,而文件的纯字节读取将保留它们。无论如何,对于那些顽固分子来说,改变这一点并不令人惊讶,没有效果。

这里有两个问题,为什么第二种情况不能像人们预期的那样工作(即,这些迭代器发生了什么),以及你最喜欢的加载Unicode字符文件的“Kosher STL-way”进入一个wstring?

我在这里想念的是什么;它必须是愚蠢的东西。

克里斯

1 个答案:

答案 0 :(得分:11)

你必须对SO感到失望,因为你的第一个问题没有得到答案 4和半个月。这是一个很好的问题,大多数好问题都得到了回答 (好或坏)几分钟之内。忽视你的两个原因可能是:

  • 你没有标记它&#34; C ++&#34;,许多可能提供帮助的C ++程序员永远不会 注意到了(我现在已将其标记为&#34; C ++&#34;。)

  • 您的问题是关于unicode流处理,这是很酷的编码理念。

阻碍你调查的误解似乎是这样的:你似乎 相信宽字符流std::wfstream和宽字符串std::wstring, 分别与&#34; unicode stream&#34;相同和一个&#34; unicode字符串&#34;,特别是那个 它们分别与UTF-16流和UTF-16字符串相同。这些都不是真的。

std::wifstreamstd::basic_ifstream<wchar_t>)是一个转换输入的输入流 根据指定的内部序列wchar_t的外部字节序列 或外部序列的默认编码

同样,std::wofstreamstd::basic_ofstream<wchar_t>)是一个输出流 根据a将wchar_t的内部序列转换为外部字节序列 外部序列的指定或默认编码

std::wstringstd::basic_string<wchar_t>)是一种只存储的字符串类型 wchar_t的序列,不知道它们产生的编码 - 如果 - 任何 - 。

Unicode 是一系列字节序列编码 - UTF-8 / -16 / -32,还有一些更模糊的其他编码 - 与UTF- N 使用1或更多序列编码字母的原理相关 每个符号 N 位单位。 UTF-16显然是您尝试阅读的编码 进入std::wstring。你说:

  

我原本以为std :: istreambuf_iterator,专门用于wchar_t,应该导致文件一次读取两个字节,不应该吗?如果没有,那么它的目的是什么?

但是一旦你知道wchar_t不一定是2字节宽(它在微软的C库中, 32位和64位,但在GCC中它是4字节宽),还有一个UTF-16代码点(字符) 不需要适合2个字节(它可能需要4个),你会看到指定一个提取 单位wchar_t不能解码UTF-16流。

使用以下内容构造和打开输入流时

std::wifstream ifFile ("MyFile.txt", std::ios_base::binary);

准备从&#34; MyFile.txt&#34;中提取字符(某些字母)。成为价值观 类型为wchar_t,它将从字节序列中提取这些字符 根据{{​​3}}指定的编码生成文件 当它进行提取时,它在流上有效。

您的代码未为您的流指定std::locale,因此库的默认设置生效。 该默认值是全局C ++语言环境,默认情况下是 std::locale;和&#34; C&#34; locale假定 &#34;身份编码&#34; I / O字节序列,即1字节= 1个字符( 为文本模式I / O留出新行异常。

因此,当您使用std::istreambuf_iterator<wchar_t>时 提取字符,通过转换每个字节进行提取 在文件中wchar_t附加到std::wstring wsData。字节 在文件中,如你所说:

0xFF,0xFE,&#39; A&#39;,0x00,&#39; B&#39;,0x00,&#39; C&#39;,0x00

前两个,你打折为&#34; unicode前导字节&#34;,确实是一个 UTF-16字节顺序标记(BOM),但在默认编码中它们就是它们。

因此,正如您所观察到的那样,分配给wsData的宽字符是:

0x00FF,0x00FE,L&#39; A&#39;,0x0000,L&#39; B&#39;,0x0000,L&#39; C&#39;,0x0000

  

好像文件仍在逐字节读取,然后只是简单地翻译成单独的wchar_t字符。

因为它正是发生的事情。

要阻止这种情况发生,您需要在开始从流中提取字符之前执行某些操作 告诉它它应该解码UTF-16字符序列。这样做的方法 在概念上相当曲折。你需要"C" locale 拥有imbue的流拥有 std::locale这是一个实例化 std::locale::facet(或源自此类) 这将为流提供正确的方法,从解码UTF-16到wchar_t

但要点是你需要将正确的UTF-16编码器/解码器插入流中 在实践中它是(或应该)足够简单。我猜你的编译器是最近的MS VC ++。 如果那是对的,那么您可以通过以下方式修复代码:

  • #include <locale>#include <codecvt>添加到标题
  • 添加以下行:

    ifFile.imbue(std::locale(ifFile.getloc(),new std::codecvt_utf16<wchar_t,0x10ffff,std::little_endian>));

之后:

std::wifstream ifFile ("MyFile.txt", std::ios_base::binary);

这条新线的效果是“#im;”#34; ifFile具有相同的新区域设置 就像它已经拥有的那样 - ifFile.getloc() - 但是改进了编码器/解码器方面   - std::codecvt_utf16<wchar_t,0x10ffff,std::little_endian>。这个codecvt方面是 将最大值为0x10ffff的UTF-16字符解码为little-endian的字符 wchar_t个值(0x10ffff是UTF-16代码点的最大值)。

当您调试到如此修改的代码时,您现在会发现wsData只有4个宽字符长 那些人物是:

0xFEFF, L'A', L'B', L'C'

正如您所期望的那样,第一个是UTF-16小端BOM。

请注意,订单FEFF与申请前的订单相反 在codecvt方面,向我们展示了小端解码是按要求完成的。 它需要。只需删除std::little_endian即可修改新行, 再次调试,然后您会发现wsData的第一个元素变为0xFFFE 并且其他三个宽字符成为的象形图 std::codecvt<InternT, ExternT, StateT>象形文字 字符集(如果您的调试器可以显示它们)。 (现在,每当一位同事 他们惊讶地发现他们的代码正在将英文Unicode转换为&#34;中文&#34;, 你会知道一个可能的解释。)

如果您想要在没有前导BOM的情况下填充wsData,您可以执行此操作 再次修改新行并将std::little_endian替换为 std::codecvt_mode(std::little_endian|std::consume_header)

最后,您可能已经注意到新代码中的错误,即2字节wchar_t 宽度不足以表示0x100000和0x10ffff之间的UTF-16代码点 可以阅读。

只要你必须阅读的所有代码点都存在于此中,你就会侥幸逃脱 UTF-16 IICore, 跨越[0,0xffff],您可能知道所有输入都将永远遵守 约束。否则,16位wchar_t不适合用途。替换:

  • wchar_tchar32_t
  • std::wstringstd::basic_string<char32_t>
  • std::wifstreamstd::basic_ifstream<char32_t>

并且代码完全适合将abitrary UTF-16编码文件读入字符串。

(使用GNU C ++库的读者将发现自v4.7.2起 它尚未提供<codecvt>标准标头。标题<bits/codecvt.h>存在,并且有时会毕业到<codecvt>,但此时它只是 导出专业化class codecvt<char, char, mbstate_t>class codecvt<wchar_t, char, mbstate_t>,分别是身份 转换以及ASCII / UTF-8和wchar_t之间的转换。解决OP的问题 你需要子类std::codecvt<wchar_t,char,std::char_traits<wchar_t>::state_type> 你自己,按照Basic Multilingual Plane