28-7 随机文件 IO

文件指针

每个文件流类都包含一个文件指针,用于跟踪文件中的当前读/写位置。当从文件中读取或写入数据时,读/写操作发生在文件指针指向的当前位置。默认情况下,以读或写模式打开文件时,文件指针会指向文件开头。但是,如果以追加模式打开文件,文件指针会移动到文件末尾,以避免写入操作覆盖文件的现有内容。

使用 seekg() 和 seekp() 进行随机文件访问

到目前为止,我们所有的文件访问都是顺序的——也就是说,我们按顺序读取或写入文件内容。然而,也可以进行随机文件访问——即跳转到文件中的不同位置读取其内容。当文件包含大量记录,而您只想检索特定记录时,这种方法非常有用。您无需读取所有记录直到找到所需记录,而是可以直接跳转到想要检索的记录。

随机文件访问是通过操作文件指针来实现的,可以使用 seekg() 函数(用于输入)或 seekp() 函数(用于输出)。其中,g 代表“获取”(get),p 代表“写入”(put)。对于某些类型的流,seekg()(改变读取位置)和 seekp()(改变写入位置)是独立运行的——但是,对于文件流,读取位置和写入位置始终相同,因此 seekg 和 seekp 可以互换使用。

seekg() 和 seekp() 函数接受两个参数。第一个参数是偏移量,用于确定文件指针要移动的字节数。第二个参数是 iOS 标志,用于指定偏移量参数的起始位置。

Ios 查找标志 意义
beg 偏移量是相对于文件开头的(默认值)。
cur 偏移量是相对于文件指针的当前位置而言的。
end 偏移量是相对于文件末尾的。

正偏移量表示将文件指针向文件末尾移动,而负偏移量表示将文件指针向文件开头移动。

以下是一些例子:

inf.seekg(14, std::ios::cur); // move forward 14 bytes
inf.seekg(-18, std::ios::cur); // move backwards 18 bytes
inf.seekg(22, std::ios::beg); // move to 22nd byte in file
inf.seekg(24); // move to 24th byte in file
inf.seekg(-28, std::ios::end); // move to the 28th byte before end of the file

移动到文件开头或结尾很容易:

inf.seekg(0, std::ios::beg); // move to beginning of file
inf.seekg(0, std::ios::end); // move to end of file

警告:
在文本文件中,定位到文件开头以外的位置可能会导致意外行为。
在编程中,换行符('\n')实际上是一种抽象概念。

  • 在 Windows 系统中,换行符表示为连续的 CR(回车符)和 LF(换行符)字符(因此占用 2 个字节的存储空间)。
  • 在 Unix 系统中,换行符表示为 LF(换行符)字符(因此占用 1 个字节的存储空间)。

根据文件的编码方式,无论向哪个方向查找超过换行符,都需要消耗数量不等的字节,这意味着结果会因使用的编码方式而异。
此外,在某些操作系统中,文件末尾可能会填充零字节(值为 0 的字节)。对这类文件进行文件定位(或从文件末尾偏移一定距离)会产生不同的结果。

为了让您了解它们的工作原理,我们用 seekg() 函数和上一课中创建的输入文件举个例子。该输入文件如下所示:

This is line 1
This is line 2
This is line 3
This is line 4

以下是一个例子

#include <fstream>
#include <iostream>
#include <string>

int main()
{
    std::ifstream inf{ "Sample.txt" };

    // If we couldn't open the input file stream for reading
    if (!inf)
    {
        // Print an error and exit
        std::cerr << "Uh oh, Sample.txt could not be opened for reading!\n";
        return 1;
    }

    std::string strData;

    inf.seekg(5); // move to 5th character
    // Get the rest of the line and print it, moving to line 2
    std::getline(inf, strData);
    std::cout << strData << '\n';

    inf.seekg(8, std::ios::cur); // move 8 more bytes into file
    // Get rest of the line and print it
    std::getline(inf, strData);
    std::cout << strData << '\n';

    inf.seekg(-14, std::ios::end); // move 14 bytes before end of file
    // Get rest of the line and print it
    std::getline(inf, strData); // undefined behavior
    std::cout << strData << '\n';

    return 0;
}

这将产生以下结果:

img

根据文件的编码方式,第三行可能会得到不同的结果。

seekg() 和 seekp() 更适合用于二进制文件。您可以通过以下方式以二进制模式打开上述文件:

std::ifstream inf {"Sample.txt", std::ifstream::binary};

另外两个有用的函数是 tellg() 和 tellp(),它们返回文件指针的绝对位置。这可以用来确定文件的大小:

std::ifstream inf {"Sample.txt"};
inf.seekg(0, std::ios::end); // move to end of file
std::cout << inf.tellg();

在作者的电脑上,打印结果如下:

img

这就是 Smple.txt 的长度(以字节为单位)(假设最后一行后面有一个换行符)。

作者注:
前一个例子的结果64是在 Windows 系统上运行的。如果在 Unix 系统上运行该示例,由于 Unix 系统中换行符的表示形式较小,因此会得到60不同的结果。如果文件末尾填充了零字节,则可能会得到其他结果。

使用 fstream 同时读取和写入文件

fstream 类几乎可以同时读取和写入文件!但需要注意的是,读取和写入操作不能随意切换。一旦完成读取或写入操作,切换的唯一方法是执行修改文件位置的操作(例如 seek)。如果您不想移动文件指针(因为它已经位于您想要的位置),您可以始终 seek 到当前位置:

// assume iofile is an object of type fstream
iofile.seekg(iofile.tellg(), std::ios::beg); // seek to current file position

如果你不这样做,可能会发生许多奇怪而离奇的事情。

(注:虽然看起来这样iofile.seekg(0, std::ios::cur)做也行得通,但有些编译器似乎会将其优化掉。)

还有一点需要注意:与 ifstream 不同,我们可以通过判断while (inf)是否还有更多内容要读取来判断是否还有内容要读取,但这在 fstream 中行不通。

让我们用 fstream 函数做一个文件 I/O 示例。我们将编写一个程序,打开一个文件,读取其内容,并将找到的所有元音字母替换为“#”符号。

#include <fstream>
#include <iostream>
#include <string>

int main()
{
    // Note we have to specify both in and out because we're using fstream
    std::fstream iofile{ "Sample.txt", std::ios::in | std::ios::out };

    // If we couldn't open iofile, print an error
    if (!iofile)
    {
        // Print an error and exit
        std::cerr << "Uh oh, Sample.txt could not be opened!\n";
        return 1;
    }

    char chChar{}; // we're going to do this character by character

    // While there's still data to process
    while (iofile.get(chChar))
    {
        switch (chChar)
        {
            // If we find a vowel
            case 'a':
            case 'e':
            case 'i':
            case 'o':
            case 'u':
            case 'A':
            case 'E':
            case 'I':
            case 'O':
            case 'U':

                // Back up one character
                iofile.seekg(-1, std::ios::cur);

                // Because we did a seek, we can now safely do a write, so
                // let's write a # over the vowel
                iofile << '#';

                // Now we want to go back to read mode so the next call
                // to get() will perform correctly.  We'll seekg() to the current
                // location because we don't want to move the file pointer.
                iofile.seekg(iofile.tellg(), std::ios::beg);

                break;
        }
    }

    return 0;
}

运行上述程序后,我们的 Sample.txt 文件将如下所示:

img

其他有用的文件功能

要删除文件,只需使用 remove() 函数即可。

此外,is_open() 函数会在流当前打开时返回 true,否则返回 false。

关于写入磁盘指针的警告

将变量写入文件非常容易,但处理指针时情况就变得复杂了。请记住,指针仅仅保存着它所指向变量的地址。虽然可以读写磁盘地址,但这极其危险。这是因为变量的地址可能因每次执行而改变。因此,即使某个变量在您将地址 0x0012FF7C 写入磁盘时位于该地址,但当您再次读取该地址时,它可能已经不在那里了!

例如,假设你有一个名为 nValue 的整数,其地址为 0x0012FF7C。你将 nValue 的值赋为 5。你还声明了一个名为 *pnValue 的指针,指向 nValue。pnValue 保存了 nValue 的地址 0x0012FF7C。你想将这些信息保存下来以备后用,因此你将值 5 和地址 0x0012FF7C 写入磁盘。

几周后,你再次运行程序并从磁盘读取这些值。你将值 5 读取到名为 nValue 的变量中,该变量的地址为 0x0012FF78。你将地址 0x0012FF7C 读取到一个名为 *pnValue 的新指针中。由于 nValue 的地址为 0x0012FF78,而 pnValue 现在指向 0x0012FF7C,因此 pnValue 不再指向 nValue,尝试访问 pnValue 会导致问题。

警告
不要将内存地址写入文件。从磁盘读取变量值时,原本位于这些地址的变量可能位于不同的地址,导致这些地址无效。

posted @ 2025-12-08 09:30  游翔  阅读(24)  评论(0)    收藏  举报