22-随机存取
17.4.6 随机存取
在最后一个文件示例中,将探讨随机存取。随机存取指的是直接移动(不是依次移动)到文件的任何位置。随机存取常被用于数据库文件,程序维护一个独立的索引文件,该文件指出数据在主数据文件中的位置。这样,程序便可以直接跳到这个位置,读取(还可能修改)其中的数据。如果文件由长度相同的记录组成,这种方法实现起来最简单。每条记录表示一组相关的数据。例如,在程序清单17.19的示例中,每条文件记录将表示关于特定行星的全部数据。很自然,文件记录对应于程序结构或类。
我们将以程序清单17.19中的二进制文件程序为基础,充分利用planet结构为文件了记录模式,来创建这个例子。为使编程更具创造性,该示例将以读写模式打开文件,以便能够读取和修改记录。为此,可以创建一个fstream对象。fstream类是从iostream类派生而来的,而后者基于istream和ostream两个类,因此它继承了它们的方法。它还继承了两个缓冲区,一个用于输入,一个用于输出,并能同步化这两个缓冲区的处理。也就是说,当程序读写文件时,它将协调地移动输入缓冲区中的输入指针和输出缓冲区中的输出指针。
该示例将完成以下工作:
1.显示planets.dat文件当前的内容;
2.询问要修改哪条记录;
3.修改该记录;
4.显示修改后的文件。
更复杂的程序将使用菜单和循环,使得能在操作列表中不断地进行选择。但这里的版本只能执行每种操作一次。这种简化让您能够检验读写文件的多个方面,而不陷入程序设计事务之中。
警告: 这个程序假设planets.dat文件已经存在,该文件是由程序清单17.19中的binary.cpp程序创建的。
要回答的第一个问题是:应使用哪种文件模式。为读取文件,需要使用ios_base::in模式。为执行二进制I/O,需要使用ios_base::binary模式(在某些非标准系统上,可以省略这种模式,事实上,可能必须省略这种模式)。为写入文件,需要ios_base::out或ios_base::app模式。然而,追加模式只允许程序将数据添加到文件尾,文件的其他部分是只读的;也就是说,可以读取原始数据,但不能修改它;要修改数据,必须使用ios_base::out。表17.8表明,同时使用in模式和out模式将得到读/写模式,因此只需添加二进制元素即可。如前所述,要使用|运算符来组合模式。因此,需要使用下面的语句:
finout.open(file,ios_base::in | ios_base::out | ios_base::binary);
接下来,需要一种在文件中移动的方式。fstream类为此继承了两个方法:seekg()和seekp(),前者将输入指针移到指定的文件位置,后者将输出指针移到指定的文件位置(实际上,由于fstream类使用缓冲区来存储中间数据,因此指针指向的是缓冲区中的位置,而不是实际的文件)。也可以将seekg()用于ifstream对象,将seekp()用于oftream对象。下面是seekg()的原型:
basic_istream<charT,traits>& seekg(off_type, ios_base::seekdir);
basic_istream<charT,traits>& seekg(pos_type);
正如您看到的,它们都是模板。本章将使用char类型的模板具体化。对于char具体化,上面两个原型等同于下面的代码:
istream & seekg(streamoff, ios_base::seekdir);
istream & seekg(streampos);
第一个原型定位到离第二个参数指定的文件位置特定距离(单位为字节)的位置;第二个原型定位到离文件开头特定距离(单位为字节)的位置。
类型升级
在C++早期,seekg()方法比较简单。streamoff和streampos类型是一些标准整型(如long)的typedef。但为创建可移植标准,必须处理这样的现实情况:对于有些文件系统,整数参数无法提供足够的信息,因此streamoff和streampos允许是结构或类类型,条件是它们允许一些基本的操作,如使用整数值作为初始值等。随后,老版本的istream类被basic_istream模板取代,streampos和streamoff被basic_istream模板取代。然而,streampos和streamoff继续存在,作为pos_type和off_type的char的具体化。同样,如果将seekg()用于wistream对象,可以使用wstreampos和wstreamoff类型。
来看seekg()的第一个原型的参数。streamoff值被用来度量相对于文件特定位置的偏移量(单位为字节)。streamoff参数表示相对于三个位置之一的偏移量为特定值(以字节为单位)的文件位置(类型可定义为整型或类)。seek_dir参数是ios_base类中定义的另一种整型,有3个可能的值。常量ios_base::beg指相对于文件开始处的偏移量。常量ios_base::cur指相对于当前位置的偏移量;常量ios_base::end指相对于文件尾的偏移量。下面是一些调用示例,这里假设fin是一个ifstream对象:
fin.seekg(30, ios_base::beg); // 30 bytes beyond the beginning
fin.seekg(-1, ios_base::cur); // back up one byte
fin.seekg(0, ios_base::end); // go to the end of the file
下面来看第二个原型。streampos类型的值定位到文件中的一个位置。它可以是类,但如果是这样的话,这个类将包含一个接受streamoff参数的构造函数和一个接受整数参数的构造函数,以便将两种类型转换为streampos值。streampos值表示文件中的绝对位置(从文件开始处算起)。可以将streampos位置看作是相对于文件开始处的位置(以字节为单位,第一个字节的编号为0)。因此下面的语句将文件指针指向第112个字节,这是文件中的第113个字节:
fin.seekg(112);
如果要检查文件指针的当前位置,则对于输入流,可以使用tellg()方法,对于输出流,可以使用tellp()方法。它们都返回一个表示当前位置的streampos值(以字节为单位,从文件开始处算起)。创建fstream对象时,输入指针和输出指针将一前一后地移动,因此tellg()和tellp()返回的值相同。然而,如果使用istream对象来管理输入流,而使用ostream对象来管理同一个文件的输出流,则输入指针和输出指针将彼此独立地移动,因此tellg()和tellp()将返回不同的值。
然后,可以使用seekg()移到文件的开头。下面是打开文件、移到文件开头并显示文件内容的代码片段:
fstream finout; // read and write streams
finout.open(file,ios::in | ios::out |ios::binary);
//NOTE: Some Unix systems require omitting | ios::binary
int ct = 0;
if (finout.is_open())
{
finout.seekg(0); // go to beginning
cout << "Here are the current contents of the "
<< file << " file:\n";
while (finout.read((char *) &pl, sizeof pl))
{
cout << ct++ << ": " << setw(LIM) << pl.name << ": "
<< setprecision(0) << setw(12) << pl.population
<< setprecision(2) << setw(6) << pl.g << endl;
}
if (finout.eof())
finout.clear(); // clear eof flag
else
{
cerr << "Error in reading " << file << ".\n";
exit(EXIT_FAILURE);
}
}
else
{
cerr << file << " could not be opened -- bye.\n";
exit(EXIT_FAILURE);
}
这与程序清单17.19的开头很相似,但也修改和添加了一些内容。首先,程序以读/写模式使用一个fstream对象,并使用seekg()将文件指针放在文件开头(对于这个例子而言,这其实不是必须的,但它说明了如何使用seekg())。接下来,程序在给记录编号方面做了一些小的改动。然后添加了以下重要的代码:
if (finout.eof())
finout.clear(); // clear eof flag
else
{
cerr << "Error in reading " << file << ".\n";
exit(EXIT_FAILURE);
}
上述代码解决的问题是,程序读取并显示整个文件后,将设置eofbit元素。这使程序相信,它已经处理完文件,并禁止对文件做进一步的读写。使用clear()方法重置流状态,并打开eofbit后,程序便可以再次访问该文件。else部分处理程序因到达文件尾之外的其他原因(如硬件故障)而停止读取的情况。
接下来需要确定要修改的记录,并修改它。为此,程序让用户输入记录号。将该编号与记录包含的字节数相乘,得到该记录第一个字节的编号。如果record是记录号,则字节编号为record * sizeof pl:
long rec;
cin >> rec;
eatline(); // get rid of newline
if (rec < 0 || rec >= ct)
{
cerr << "Invalid record number -- bye\n";
exit(EXIT_FAILURE);
}
streampos place = rec * sizeof pl; // convert to streampos type
finout.seekg(place); // random access
变量ct表示记录号。如果试图超出文件尾,程序将退出。
接下来,程序显示当前的记录:
finout.read((char *) &pl, sizeof pl);
cout << "Your selection:\n";
cout << rec << ": " << setw(LIM) << pl.name << ": "
<< setprecision(0) << setw(12) << pl.population
<< setprecision(2) << setw(6) << pl.g << endl;
if (finout.eof())
finout.clear(); // clear eof flag
显示记录后,程序让您修改记录:
cout << "Enter planet name: ";
cin.get(pl.name, LIM);
eatline();
cout << "Enter planetary population: ";
cin >> pl.population;
cout << "Enter planet's acceleration of gravity: ";
cin >> pl.g;
finout.seekp(place); // go back
finout.write((char *) &pl, sizeof pl) << flush;
if (finout.fail())
{
cerr << "Error on attempted write\n";
exit(EXIT_FAILURE);
}
程序刷新输出,以确保进入下一步之前,文件被更新。
最后,为显示修改后的文件,程序使用seekg()将文件指针重新指向开头。程序清单17.20列出了完整的程序。不要忘了,该程序假设binary.cpp创建的planets.dat文件是可用的。
注意: 实现越旧,与C++标准相冲突的可能性越大。一些系统不能识别二进制标记、fixed和right控制符以及ios_base。
程序清单17.20 random.cpp
// random.cpp -- random access to a binary file
#include <iostream> // not required by most systems
#include <fstream>
#include <iomanip>
#include <cstdlib> // for exit()
const int LIM = 20;
struct planet
{
char name[LIM]; // name of planet
double population; // its population
double g; // its acceleration of gravity
};
const char * file = "planets.dat"; // ASSUMED TO EXIST (binary.cpp example)
inline void eatline() { while (std::cin.get() != '\n') continue; }
int main()
{
using namespace std;
planet pl;
cout << fixed;
// show initial contents
fstream finout; // read and write streams
finout.open(file,
ios_base::in | ios_base::out | ios_base::binary);
//NOTE: Some Unix systems require omitting | ios::binary
int ct = 0;
if (finout.is_open())
{
finout.seekg(0); // go to beginning
cout << "Here are the current contents of the "
<< file << " file:\n";
while (finout.read((char *) &pl, sizeof pl))
{
cout << ct++ << ": " << setw(LIM) << pl.name << ": "
<< setprecision(0) << setw(12) << pl.population
<< setprecision(2) << setw(6) << pl.g << endl;
}
if (finout.eof())
finout.clear(); // clear eof flag
else
{
cerr << "Error in reading " << file << ".\n";
exit(EXIT_FAILURE);
}
}
else
{
cerr << file << " could not be opened -- bye.\n";
exit(EXIT_FAILURE);
}
// change a record
cout << "Enter the record number you wish to change: ";
long rec;
cin >> rec;
eatline(); // get rid of newline
if (rec < 0 || rec >= ct)
{
cerr << "Invalid record number -- bye\n";
exit(EXIT_FAILURE);
}
streampos place = rec * sizeof pl; // convert to streampos type
finout.seekg(place); // random access
if (finout.fail())
{
cerr << "Error on attempted seek\n";
exit(EXIT_FAILURE);
}
finout.read((char *) &pl, sizeof pl);
cout << "Your selection:\n";
cout << rec << ": " << setw(LIM) << pl.name << ": "
<< setprecision(0) << setw(12) << pl.population
<< setprecision(2) << setw(6) << pl.g << endl;
if (finout.eof())
finout.clear(); // clear eof flag
cout << "Enter planet name: ";
cin.get(pl.name, LIM);
eatline();
cout << "Enter planetary population: ";
cin >> pl.population;
cout << "Enter planet's acceleration of gravity: ";
cin >> pl.g;
finout.seekp(place); // go back
finout.write((char *) &pl, sizeof pl) << flush;
if (finout.fail())
{
cerr << "Error on attempted write\n";
exit(EXIT_FAILURE);
}
// show revised file
ct = 0;
finout.seekg(0); // go to beginning of file
cout << "Here are the new contents of the " << file
<< " file:\n";
while (finout.read((char *) &pl, sizeof pl))
{
cout << ct++ << ": " << setw(LIM) << pl.name << ": "
<< setprecision(0) << setw(12) << pl.population
<< setprecision(2) << setw(6) << pl.g << endl;
}
finout.close();
cout << "Done.\n";
return 0;
}
下面是程序清单17.20中的程序基于planets.dat文件的运行情况,该文件比上次见到时多了一些条目:
Here are the current contents of the planets.dat file:
0: Earth: 6928198253 9.81
1: Jenny's World: 32155648 8.93
2: Tramtor: 89000000000 15.03
3: Trellan: 5214000 9.62
4: Freestone: 3945851000 8.68
5: Taanagoot: 361000004 10.23
6: Marin: 252409 9.79
Enter the record number you wish to change: 2
Your selection:
2: Tramtor: 89000000000 15.03
Enter planet name: Trantor
Enter planetary population: 89521844777
Enter planet's acceleration of gravity: 10.53
Here are the new contents of the planets.dat file:
0: Earth: 6928198253 9.81
1: Jenny's World: 32155648 8.93
2: Trantor: 89521844777 10.53
3: Trellan: 5214000 9.62
4: Freestone: 3945851000 8.68
5: Taanagoot: 361000004 10.23
6: Marin: 252409 9.79
Done.
通过使用该程序中的技术,对其进行扩展,使之能够让用户添加新信息和删除记录。如果打算扩展该程序,最好通过使用类和函数来重新组织它。例如,可以将planet结构转换为一个类定义,然后对<<插入运算符进行重载,使得cout<<pl按示例的格式显示类的数据成员。另外,该示例没有对输入进行检查,您可以添加代码来检查数值输入是否合适。
使用临时文件
开发应用程序时,经常需要使用临时文件,这种文件的存在是短暂的,必须受程序控制。您是否考虑过,在C++中如何使用临时文件呢?创建临时文件、复制另一个文件的内容并删除文件其实都很简单。首先,需要为临时文件制定一个命名方案,但如何确保每个文件都被指定了独一无二的文件名呢?cstdio中声明的tmpnam()标准函数可以帮助您。
char* tmpnam( char* pszName );
tmpnam()函数创建一个临时文件名,将它放在pszName指向的C-风格字符串中。常量L_tmpnam和TMP_MAX(二者都是在cstdio中定义的)限制了文件名包含的字符数以及在确保当前目录中不生成重复文件名的情况下tmpnam()可被调用的最多次数。下面是生成10个临时文件名的代码。
#include <cstdio>
#include <iostream>
int main()
{
using namespace std;
cout << "This system can generate up to " << TMP_MAX
<< " temporary names of up to " << L_tmpnam
<< " characters.\n";
char pszName[ L_tmpnam ] = {'\0'};
cout << "Here are ten names:\n";
for( int i=0; 10 > i; i++ )
{
tmpnam( pszName );
cout << pszName << endl;
}
return 0;
}
更具体地说,使用tmpnam()可以生成TMP_NAM个不同的文件名,其中每个文件名包含的字符不超过L_tmpnam个。生成什么样的文件名取决于实现,您可以运行该程序,来看看编译器给您生成的文件名。