Unicode 和 ANSI 文件 I/O,逐行处理






3.28/5 (15投票s)
本文介绍了一个非常简单的库,它提供了从磁盘文件读取和写入文本行的功能,同时支持 ANSI 和 Unicode。
引言
大多数应用程序都需要以某种形式在硬盘上存储数据。最常见的形式是文本文件,其中每条信息都由一行或多行文本表示。文件解析器逐行将文件读取到字符串中,并将字符串传递给后续的处理程序。
创建这样的解析器似乎非常简单,但有一个主要问题——文件不能是 Unicode 格式。至少不是简单的方式——CRT 和 STL 都不支持将 Unicode 文本写入文件。此外,即使是 ANSI,我知道的唯一真正简单的读写文本行的方法是使用 STL 流,有些人(例如我 :))可能不太喜欢。
出于这些原因,我决定编写一个小库,它将提供以 ANSI 和 Unicode 格式将文本行写入/从磁盘文件读取的功能,并使其尽可能易于使用。
在本文中,我将逐步介绍创建该库的过程,并尝试解释其中所有不那么显而易见的方面。难度级别设置为“初学者/很少使用 C++”,因此请予以考虑。
设计考量
在编写任何代码时,我们首先需要问自己我们的目标是什么。在这种情况下,我们最终想要的是一组具有两个基本用途的函数:
- 将文本行写入文件。
- 从文件中读取文本行。
以上陈述中的关键词是“文件”、“文本”和“行”。“文件”指的是硬盘上的文件,就像我们都知道的那样。“文本”指的是一系列字符。我们不会以任何方式解释这些字符,没有数字、布尔值等。“行”指的是以“换行符”终止的文本片段。换行符是用于指示文本片段中行尾的特殊字符。该字符的代码因字符编码而异(SBCS、MBCS、Unicode……CodeProject 上有一篇关于字符编码的出色文章 这里)。
首先,我们需要确定文件的格式。就我们的目的而言,文件将包含 ANSI(单字节)或 Unicode 字符编码的文本行。仅此而已,没有 BOM 或其他任何东西。我们库的目标不是创建符合标准的 Unicode 文件,而仅仅是将字符串(单字节 ANSI 或 Unicode)中的字符存储在文件中,然后将它们读回,与之前完全相同。请注意,您不应在同一个文件中混合 ANSI 和 Unicode,也不应在 Unicode 文件上使用 ANSI 函数或反之,除非您确切知道自己在做什么。
为了处理文件,我们将使用 stdio.h 中的标准 C 函数。其他选项包括 Win32 API(不可移植)和 STL 流(对于我们的目的来说有点大材小用)。
这应该足够进行设计了,让我们开始实际实现。我们将从编写函数开始。
实现 LineToFile() 函数
LineToFile()
函数的目的是将字符串中的文本写入文本文件。此函数的两个显而易见的参数是字符串和文本文件。返回值应指示函数的成功或失败。
bool LineToFile(FILE* f, const std::string& s);
足够简单。这里唯一有趣的部分是字符串参数是通过 **const 引用**传递的——这意味着函数不会创建自己的字符串副本进行工作(从而节省时间和内存),但会对其原始字符串具有只读访问权限。
此声明的问题在于它只能用于 ANSI 字符串。我们需要为 Unicode 字符串创建第二个函数。这就是 **函数重载** 发挥作用的地方。通过函数重载,我们可以创建具有相同名称但参数不同的多个函数。在编译代码时,编译器将根据参数决定调用哪个函数,并且(假设函数具有相同的目的)程序员不必为不同类型的参数的同一操作记住多个函数名称。现在我们将为 LineToFile()
函数创建两个重载,一个用于 ANSI,一个用于 Unicode。
bool LineToFile(FILE* f, const std::string& s); bool LineToFile(FILE* f, const std::wstring& s);
这样就好多了,现在我们可以同时传递 ANSI 字符串和 Unicode 字符串作为参数,并调用适当的函数。
让我们看看这些函数是如何实现的
bool LineToFile(FILE* f, const std::string& s) { // write the string to the file size_t n = fwrite(s.c_str(), sizeof(char), s.size(), f); // write line break to the file fputc('\n', f); // return whether the write operation was successful return (n == length); }; bool LineToFile(FILE* f, const std::wstring& s) { // write the string to the file size_t n = fwrite(s.c_str(), sizeof(wchar_t), s.size(), f); // write line break to the file fputwc(L'\n', f); // return whether the write operation was successful return (n == s.size()); };
这相当简单。我们只需获取字符串的缓冲区,并使用 fwrite()
将其内容复制到文件中,然后是适当的换行符(使用 fputc()
)。ANSI 版本和 Unicode 版本之间的区别很小,我们只需确保使用适当的数据类型和函数。从现在开始(为了本文的长度),我将始终只描述其中一个重载。它们之间的差异很小,您可以随时下载源代码。
传递给函数的字符串参数可以是 STL 字符串、零终止字符串或字符串常量。这是可能的,因为当您将零终止字符串传递给函数时,会创建一个具有相应内容的临时 std::string
,并用于函数调用。(这会带来一些危险,我在本文的“高级”部分对此进行了讨论。)
现在让我们实现读取函数……
实现 LineFromFile() 函数
同样,该函数将有两个重载——一个用于 ANSI 字符串,另一个用于 Unicode 字符串。返回值将再次指示成功或失败,参数将是用于读取的文件和一个用于保存结果行文本的字符串变量。
bool LineFromFile(FILE* f, std::string& s); bool LineFromFile(FILE* f, std::wstring& s);
请注意,这次对字符串变量的引用不是 const 的。这意味着参数必须是实际的 std::string
变量,这正是我们想要达到的目标。该函数实现如下:
bool LineFromFile(FILE* f, std::wstring& s) { // reset string s.clear(); // read one char at a time while (true) { // read char wint_t c = fgetwc(f); // check for EOF if (c == WEOF) return false; // check for EOL if (c == L'\n') return true; // append this character to the string s += c; }; };
这也非常直接。我们一次读取一个字符(使用 fgetc()
)并将它附加到字符串变量,除了以下情况:
- 如果我们刚刚读取的字符是换行符。这意味着当前行的末尾已到达。函数将返回
true
,字符串变量将包含我们刚刚读取的行文本。 - 如果我们刚刚读取的字符是文件结束 (EOF) 字符,函数将返回
false
,这表示文件末尾已到达。EOF 字符之前应始终是换行符,如果文件是使用我们的LineToFile()
函数写入的,那么它就是。这意味着当函数返回false
时,字符串变量应该为空。但是,如果函数返回false
且 EOF 前面没有换行符,则字符串变量将包含函数在文件结束前读取的所有内容。
使用这些函数
使用上述函数非常简单。您需要做的就是以 **二进制模式** 打开文件(因为我们不希望进行任何翻译),使用 fopen()
并使用正确的参数调用函数。要将行写入文本文件,然后再将其读回内存,您将:
// prepare some strings std::string s1 = "string 1"; char* s2 = "string 2"; // open the file for writing FILE* f = fopen("file.dat", "wb"); // write strings LineToFile(f, s1); LineToFile(f, s2); LineToFile(f, "string 3"); // reopen the file for reading fclose(f); f = fopen("file.dat", "rb"); // read all lines std::string strLine; while (LineFromFile(f, strLine)) { // do whatever with strLine }; // close the file fclose(f);
扩展功能
当您查看 LineToFile()
和 LineFromFile()
函数时,您会注意到它们使用了两个常量——一个换行符和一个 EOF 字符。如果我们更改这些常量为其他值会怎样?
例如,考虑将空格字符用作换行符,将换行符用作 EOF。现在我们可以不是一次一行地读取整个文本文件,而是可以一次一个单词地读取(如果需要,可以对整个文件重复)。而我们所要做的就是更改两个常量!
更改这两个常量的值可能非常有用,如上所示,所以为什么不给用户一个更改它的选项呢?我们将这样更改函数声明:
bool LineToFile(FILE* f, const std::string& s, int eol = '\n'); bool LineToFile(FILE* f, const std::wstring& s, wint_t eol = L'\n');; bool LineFromFile(FILE* f, std::string& s, int eol = '\n', int eof = EOF); bool LineFromFile(FILE* f, std::wstring& s, wint_t eol = '\n', wint_t eof = WEOF);
如果您不理解参数声明中的赋值运算符,请知道这称为 **默认参数**。当您为参数分配默认值时,您就给了用户一个选择,让他们可以选择是否要为该参数指定值。如果不指定,将使用默认值。这样,以下所有函数调用都是有效的:
LineToFile(f, myString); LineToFile(f, myString, '\t'); LineToFile(f, myString, ' ', '\n');
我们在保持函数调用简洁的同时,为用户提供了更多的控制权(如果他们需要的话)。
函数体不会有太大变化,只是用参数 eol
替换了常量 '\n'
,用参数 eof
替换了常量 EOF
。这应该足够清楚了,但如果您需要查看实际的函数体,请参阅源文件。
高级
正如我之前提到的,使用 char*
或 LineToFile()
函数的字符串常量存在一个问题。它工作正常,但是……
必须创建一个临时的 std::string
变量,其中包含适当的内容,然后将其用于函数。问题是,这个临时变量的创建是不必要的,甚至可能失败,因为新变量(有时)必须创建自己的缓冲区来保存字符串数据。
为了避免不必要的分配,我们可以为零终止字符串编写单独的 LineToFile()
函数重载。这些函数看起来像这样:
bool LineToFile(FILE* f, const wchar_t* const s, wint_t eol = L'\n', size_t length = -1) { // check if the pointer is valid if (!s) { return false; }; // calculate the string's length if (length==-1) { length = wcslen(s); }; // write the string to the file size_t n = fwrite(s, sizeof(wchar_t), length, f); // write line break to the file fputwc(eol, f); // return whether the write operation was successful return (n == length); };
注意 length
参数。如果我们不想浪费时间再次计算字符串的长度,或者只想写入字符串的一部分,可以使用它。
现在我们有了零终止字符串的独立函数,我们也可以放弃旧的 WriteToFile()
函数体,只将它们用作新函数的接口:
inline bool LineToFile(FILE* f, const std::string& s, int eol = '\n') { return LineToFile(f, s.c_str(), eol, s.size()); }; inline bool LineToFile(FILE* f, const std::wstring& s, wint_t eol = L'\n') { return LineToFile(f, s.c_str(), eol, s.size()); };
请注意,我将这两个函数声明为 inline
。这(在这种情况下)将节省生成的代码中的几个汇编指令,并使其运行速度稍快一些(因为它不会创建一个单独的“函数”来调用 LineToFile()
的其他重载)。
再次,我们在不增加任何不希望的复杂性的情况下改进了库。
结束语
该库不尝试生成符合 Unicode 标准的文件。它(通常)也无法加载由其他程序生成的 Unicode 文件。该库的唯一目的是提供一个简单的接口,用于在文件中存储和恢复 Unicode 和 ANSI 字符串。话虽如此,使用此库创建的 Unicode 文件**可以**在记事本和其他 Unicode 感知的文本编辑器中阅读。
致谢
我想特别感谢 **John R. Shaw** 先生,他提出了一些非常好的建议,主要是关于本文“高级”部分中描述的内容。有关详细信息,请参阅此页面底部的讨论。
历史
- 2005 年 10 月 26 日 - **重大**更新,大部分文章被重写。
- 2005 年 10 月 25 日 -
LineToFile()
现在接受const std::string&
而不是std::string
。 - 2005 年 10 月 23 日 - 添加了用于自定义 EOL 和 EOF 的可选参数。
- 2005 年 10 月 20 日 - 添加了关于字符串中特殊字符的注释。
- 2005 年 10 月 19 日 - 首次发布。