一、随机文件 I/O
1、文件指针
每个文件流类都包含一个文件指针,用于跟踪文件中的当前读/写位置。 当从文件中读取或写入某些内容时,读取/写入发生在文件指针的当前位置。 默认情况下,当打开文件进行读取或写入时,文件指针设置为文件的开头。 但是,如果以追加模式打开文件,则文件指针将移动到文件末尾,因此写入不会覆盖文件的任何当前内容。
2、使用 seekg() 和 seekp() 进行随机文件访问
到目前为止,我们所做的所有文件访问都是顺序的——也就是说,我们已经按顺序读取或写入文件内容。 但是,也可以进行随机文件访问——即,跳到文件中的各个点以读取其内容。 当您的文件充满记录并且您希望检索特定记录时,这可能很有用。 您可以直接跳到要检索的记录,而不是阅读所有记录直到找到您想要的记录。
随机文件访问是通过使用 seekg() 函数(用于输入)和 seekp() 函数(用于输出)操作文件指针来完成的。 如果您想知道,g 代表“get”,p 代表“put”。 对于某些类型的流, seekg() (改变读取位置)和 seekp() (改变写入位置)独立操作——但是,对于文件流,读取和写入位置总是相同的,所以 seekg 和 seekp 可以 互换使用。
seekg() 和 seekp() 函数采用两个参数。 第一个参数是确定要移动文件指针多少字节的偏移量。 第二个参数是一个 Ios 标志,它指定偏移参数应该从什么偏移。
正偏移量意味着将文件指针移向文件末尾,而负偏移量意味着将文件指针移向文件开头。
这里有些例子:
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
让我们使用 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
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);
std::cout << strData << '\n';
return 0;
}
结果输出以下:
is line 1
line 2
This is line 4
注意:某些编译器在与文本文件结合使用时(由于缓冲),在 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();
打印如下:
64
这是 sample.txt 的字节长度(假设最后一行后有回车)。
3、使用 fstream 同时读取和写入文件
fstream 类能够同时读取和写入文件!这里不可能在读写之间随意切换。一旦发生读取或写入,在两者之间切换的唯一方法是执行修改文件位置的操作(例如查找)。如果你真的不想移动文件指针(因为它已经在你想要的位置),可以寻找到当前位置:
// 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 示例。 我们将编写一个程序来打开一个文件,读取其内容,并将它找到的任何元音更改为“#”符号。
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;
}
其他有用的文件功能
要删除文件,只需使用 remove() 函数。
此外,如果流当前打开,is_open() 函数将返回 true,否则返回 false。
4、关于将指针写入磁盘的警告
虽然将变量流式传输到文件非常容易,但在处理指针时事情会变得更加复杂。请记住,指针只是保存它所指向的变量的地址。虽然可以在磁盘上读写地址,但这样做非常危险。这是因为变量的地址可能因执行而异。因此,尽管当您将该地址写入磁盘时,变量可能已经存在于地址 0x0012FF7C,但当您重新读取该地址时,它可能不再存在于该地址!
例如,假设您有一个名为 nValue 的整数,它位于地址 0x0012FF7C。您为 nValue 分配了值 5。您还声明了一个名为 *pnValue 的指针,该指针指向 nValue。 pnValue 保存 nValue 的地址 0x0012FF7C。您想保存这些以供以后使用,因此您将值 5 和地址 0x0012FF7C 写入磁盘。
之后,您再次运行程序并从磁盘读回这些值。您将值 5 读入另一个名为 nValue 的变量,该变量位于 0x0012FF78。您将地址 0x0012FF7C 读入一个名为 *pnValue 的新指针。因为当 nValue 位于 0x0012FF78 时 pnValue 现在指向 0x0012FF7C,所以 pnValue 不再指向 nValue,尝试访问 pnValue 会导致读取不到。
所以不要将地址写入文件。当您从磁盘读回它们的值时,最初位于这些地址的变量可能位于不同的地址,并且这些地址将无效。