随机文件I / O
文件指针
每个文件流类都包含一个文件指针,用于跟踪文件中的当前读/写位置。当从文件中读取或写入某些内容时,读取/写入将发生在文件指针的当前位置。默认情况下,打开文件进行读取或写入时,文件指针将设置为文件的开头。但是,如果在附加模式下打开文件,则文件指针将移动到文件末尾,因此写入不会覆盖文件的任何当前内容。
使用seekg()和seekp()进行随机文件访问
到目前为止,我们所做的所有文件访问都是顺序的,也就是说,我们已按顺序读取或写入文件内容。但是,也可以进行随机文件访问,也就是说,跳过文件中的各个点来读取其内容。当您的文件已满,并且您希望检索特定记录时,这可能很有用。您可以直接跳到要检索的记录,而不是在找到所需记录之前读取所有记录。
通过使用seekg()函数(用于输入)和seekp()函数(用于输出)操作文件指针来完成随机文件访问。如果你想知道,g代表“get”而p代表“put”。对于某些类型的流,seekg()(更改读取位置)和seekp()(更改写入位置)独立运行,但是,对于文件流,读取和写入位置始终相同,因此seekg和seekp可以是可互换使用。
seekg()和seekp()函数有两个参数。第一个参数是一个偏移量,用于确定移动文件指针的字节数。第二个参数是Ios标志,指定偏移参数应偏移的内容。
Ios seek flag | 含义 |
---|---|
beg | 偏移量相对于文件的开头(默认) |
cur | 偏移量相对于文件指针的当前位置 |
end | 偏移量相对于文件末尾 |
正偏移意味着将文件指针移向文件末尾,而负偏移意味着将文件指针移向文件的开头。
这里有些例子:
inf.seekg(14, ios::cur); //向前移动14个字节
inf.seekg(-18, ios::cur); //向后移动18个字节
inf.seekg(22, ios::beg); // 在文件中移动到第22个字节
inf.seekg(24); // 在文件中想移动到第24个字节
inf.seekg(-28, ios::end); // 移到文件结尾前的第28个字节
移动到文件的开头或结尾很容易:
inf.seekg(0, ios::beg); // 移到文件的开头
inf.seekg(0, ios::end); // 移到文件末尾
让我们使用seekg()和我们在上一课中创建的输入文件做一个例子。该输入文件如下所示:
This is line 1
This is line 2
This is line 3
This is line 4
这是一个例子:
int main()
{
using namespace std;
ifstream inf("Sample.dat");
//如果我们无法打开输入文件流进行读取
if (!inf)
{
// 打印错误并退出
cerr << "Uh oh, Sample.dat could not be opened for reading!" << endl;
exit(1);
}
string strData;
inf.seekg(5); // 移动到第5个字符
// 获取剩余的内容并打印出来
getline(inf, strData);
cout << strData << endl;
inf.seekg(8, ios::cur); // 将8个字节移动到文件中
// 获取剩余的内容并打印出来
getline(inf, strData);
cout << strData << endl;
inf.seekg(-15, ios::end); // 在文件末尾向前移动15个字节
// 获取剩余的内容并打印出来
getline(inf, strData);
cout << strData << endl;
return 0;
}
这会产生结果:
is line 1
line 2
his is line 4
注意:一些编译器在与文本文件结合使用时(由于缓冲)具有seekg()和tellg()的错误实现。如果您的编译器是其中之一(并且您将知道因为您的输出将与上面的不同),您可以尝试以二进制模式打开文件:
ifstream inf("Sample.dat", ifstream::binary);
另外两个有用的函数是tellg()和tellp(),它们返回文件指针的绝对位置。这可用于确定文件的大小:
ifstream inf("Sample.dat");
inf.seekg(0, ios::end); // 移到文件末尾
cout << inf.tellg();
这打印:
64
这是sample.dat以字节为单位的长度(假设在最后一行之后返回一个回车符)。
使用fstream同时读取和写入文件
fstream类能够同时读取和写入文件!这几乎是这里最大的警告,不可能在任意读写之间切换。一旦发生读或写,在两者之间切换的唯一方法是执行修改文件位置的操作(例如,搜索)。如果你实际上不想移动文件指针(因为它已经在你想要的位置),你可以随时寻找当前位置:
//假设iofile是fstream类型的对象
iofile.seekg(iofile.tellg(), ios::beg); // 寻求当前的文件位置
如果你不这样做,可能会发生任何奇怪和奇怪的事情。
(注意:虽然看起来似乎iofile.seekg(0, ios::cur)也可行,但似乎有些编译器可能会对此进行优化。
另一个棘手的问题:与ifstream不同,我们可以说while (inf)确定是否有更多要阅读,这对fstream不起作用。
让我们使用fstream做一个文件I / O示例。我们将编写一个程序来打开文件,读取其内容,并将它找到的任何元音更改为“#”符号。
int main()
{
using namespace std;
//注意我们必须同时指定in和out,因为我们正在使用fstream
fstream iofile("Sample.dat", ios::in | ios::out);
// 如果我们无法打开iofile,请输出错误
if (!iofile)
{
// 打印错误并退出
cerr << "Uh oh, Sample.dat could not be opened!" << endl;
exit(1);
}
char chChar; //我们将按字符分类
// 虽然仍有数据需要处理
while (iofile.get(chChar))
{
switch (chChar)
{
//如果我们找到一个元音
case 'a':
case 'e':
case 'i':
case 'o':
case 'u':
case 'A':
case 'E':
case 'I':
case 'O':
case 'U':
// 备份一个character
iofile.seekg(-1, ios::cur);
// 因为我们进行了搜索,所以我们现在可以安全地进行写操作了
//让我们在元音上写一个#
iofile << '#';
//现在我们想回到读取模式,以便下次调用
// get()将正确执行. 我们将seekg()用于当前位置,
// 因为我们不想移动文件指针。
iofile.seekg(iofile.tellg(), ios::beg);
break;
}
}
return 0;
}
其他有用的文件功能
要删除文件,只需使用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会导致您遇到麻烦。
规则:不要将地址写入文件。当您从磁盘读回其值时,最初位于这些地址的变量可能位于不同的地址,并且地址将无效。