65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.28/5 (15投票s)

2005年10月19日

CPL

10分钟阅读

viewsIcon

115228

downloadIcon

1969

本文介绍了一个非常简单的库,它提供了从磁盘文件读取和写入文本行的功能,同时支持 ANSI 和 Unicode。

引言

大多数应用程序都需要以某种形式在硬盘上存储数据。最常见的形式是文本文件,其中每条信息都由一行或多行文本表示。文件解析器逐行将文件读取到字符串中,并将字符串传递给后续的处理程序。

创建这样的解析器似乎非常简单,但有一个主要问题——文件不能是 Unicode 格式。至少不是简单的方式——CRT 和 STL 都不支持将 Unicode 文本写入文件。此外,即使是 ANSI,我知道的唯一真正简单的读写文本行的方法是使用 STL 流,有些人(例如我 :))可能不太喜欢。

出于这些原因,我决定编写一个小库,它将提供以 ANSI 和 Unicode 格式将文本行写入/从磁盘文件读取的功能,并使其尽可能易于使用。

在本文中,我将逐步介绍创建该库的过程,并尝试解释其中所有不那么显而易见的方面。难度级别设置为“初学者/很少使用 C++”,因此请予以考虑。

设计考量

在编写任何代码时,我们首先需要问自己我们的目标是什么。在这种情况下,我们最终想要的是一组具有两个基本用途的函数:

  1. 将文本行写入文件。
  2. 从文件中读取文本行。

以上陈述中的关键词是“文件”、“文本”和“行”。“文件”指的是硬盘上的文件,就像我们都知道的那样。“文本”指的是一系列字符。我们不会以任何方式解释这些字符,没有数字、布尔值等。“行”指的是以“换行符”终止的文本片段。换行符是用于指示文本片段中行尾的特殊字符。该字符的代码因字符编码而异(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())并将它附加到字符串变量,除了以下情况:

  1. 如果我们刚刚读取的字符是换行符。这意味着当前行的末尾已到达。函数将返回 true,字符串变量将包含我们刚刚读取的行文本。
  2. 如果我们刚刚读取的字符是文件结束 (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 日 - 首次发布。
© . All rights reserved.