13

在 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”,则文件的字节(忽略 0xFF、0xFE 的 Unicode 前导字节)为:<'A'> <0> <'B'> <0> <' C'> <0>

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

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

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

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

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

我已经在网络上搜索了许多网站(包括这个网站)来寻找这个看似微不足道的任务,但没有找到对这种行为的解释或一个很好的替代方案,它不涉及比我认为必要的更多代码(例如,A Google网络搜索也会产生相同的第二个代码片段作为可行的代码段)。

我发现唯一可行的是以下内容,我认为这是作弊,因为它需要直接访问 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 字符文件加载到 wstring 中的“犹太 STL 方式”是什么?

我在这里想念什么;它一定很愚蠢。

克里斯

4

1 回答 1

12

您一定对 SO 在 4 个半月后没有收到您的第一个问题的答案感到失望。这是一个好问题,大多数好问题都会在几分钟内得到回答(好或坏)。忽视你的两个可能原因是:

  • 您没有将其标记为“C++”,因此许多可能能够提供帮助的 C++ 程序员永远不会注意到它。(我现在将其标记为“C++”。)

  • 您的问题是关于 unicode 流处理的,这不是酷编码的想法。

阻碍您调查的误解似乎是:您似乎相信宽字符流std::wfstream和宽字符串std::wstring分别与“unicode 流”和“unicode 字符串”相同,特别是它们分别与 UTF-16 流和 UTF-16 字符串相同。这些都不是真的。

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

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

并且std::wstring( std::basic_string<wchar_t>) 是一种字符串类型,它只存储 的序列wchar_t,而不知道它们产生的编码(如果有的话)。

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

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

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

当您使用以下命令构建并打开输入流时:

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

它准备从“MyFile.txt”中提取字符(某些字母表)到类型的值中,wchar_t并且它将根据std::locale 在流上运行时指定的编码从文件中的字节序列中提取这些字符。提取。

您的代码没有std::locale为您的流指定一个,因此库的默认值生效。该默认值是全局 C++ 语言环境,而默认情况下又是 “C”语言环境;并且“C”语言环境假定 I/O 字节序列的“身份编码”,即 1 字节 = 1 个字符(将文本模式 I/O 的换行例外放在一边)。

因此,当您使用您std::istreambuf_iterator<wchar_t>提取字符时,提取过程通过将文件中的每个字节转换为wchar_t它附加到std::wstring wsData. 正如您所说,文件中的字节是:

0xFF、0xFE、'A'、0x00、'B'、0x00、'C'、0x00

前两个,您将其视为“unicode 前导字节”,确实是 UTF-16 字节顺序标记 (BOM),但在默认编码中,它们就是它们的本来面目。

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

0x00FF, 0x00FE, L'A', 0x0000, L'B', 0x0000, L'C', 0x0000

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

因为这正是正在发生的事情。

为了阻止这种情况发生,您需要在开始从流中提取字符之前做一些事情,告诉它它应该解码一个 UTF-16 字符序列。这样做的方法在概念上相当曲折。您需要具有imbue 的流,该流std::locale拥有 std::locale::facet一个实例化 std::codecvt<InternT, ExternT, StateT>(或从此类实例化),它将为流提供正确的方法,将 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);

这一新行的效果是“灌输”ifFile一个与它已经拥有的相同的新语言环境 - ifFile.getloc()- 但具有修改的编码器/解码器方面 - std::codecvt_utf16<wchar_t,0x10ffff,std::little_endian>。这个codecvt方面是将具有最大值的 UTF-16 字符解码0x10ffff为 little-endian wchar_t值(0x10ffff是 UTF-16 代码点的最大值)的方面。

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

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

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

请注意, order与应用 facetFE之前FF的相反codecvt,向我们展示了 little-endian 解码是按要求完成的。它必须是。只需通过删除 编辑新行std::little_endian,再次调试它,然后您会发现wsData变成的第一个元素0xFFFE 和其他三个宽字符成为 IICore象形字符集的象形图(如果您的调试器可以显示它们)。(现在,每当有同事惊讶地抱怨他们的代码将英文 Unicode 变成“中文”时,你就会知道一个可能的解释。)

如果您想在wsData没有前导 BOM 的情况下进行填充,您可以通过再次修改新行并替换std::little_endianstd::codecvt_mode(std::little_endian|std::consume_header)

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

只要您必须阅读的所有代码点都位于跨越 [0,0xffff] 的 UTF-16 Basic Multilingual Plane中,您就可以摆脱这种情况,并且您可能知道所有输入将永远遵守该约束。否则,16 位wchar_t不适合。代替:

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

并且该代码完全适合将任意 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> 自己子类化,根据这个答案

于 2013-05-20T17:10:44.957 回答