系列文章目录
17.4.5 文件模式
程序清单17.18 append.cpp
程序清单17.19 binary.cpp
文章目录
17.4.5 文件模式
文件模式描述的是文件将被如何使用:读、写、追加等。将流与文件关联时(无论是使用文件名初始化文件流对象,还是使用open()方法),都可以提供指定文件模式的第二个参数:
ifstream fin("banjo",model);/constructor with mode arqument
ofstream fout();
fout.open("harp",mode2);//open()with mode arguments
ios base 类定义了一个 openmode 类型,用于表示模式;与 fimtflags 和 iostate 类型一样,它也是一种bitmask 类型(以前,其类型为 int)。可以选择 ios base 类中定义的多个常量来指定模式,表 17.7列出了这些常量及其含义。C++文件 I/O作了一些改动,以便与 ANSIC 文件 I/O 兼容。
如果 ifstream 和 ofstream 构造函数以及 open( )方法都接受两个参数,为什么前面的例子只使用一个参数就可以调用它们呢?您可能猜到了,这些类成员函数的原型为第二个参数(文件模式参数)提供了默认值。例如,ifstream open()方法和构造函数用ios base:in(打开文件以读取)作为模式参数的默认值,而ofstream open()方法和构造函数用ios base:out|ios base:trunc(打开文件,以读取并截短文件)作为默认值。位运算符 OR(1)用于将两个位值合并成一个可用于设置两个位的值。fstream 类不提供默认的模式值,因此在创建这种类的对象时,必须显式地提供模式。
注意,ios base::trunc标记意味着打开已有的文件,以接收程序输出时将被截短;也就是说,其以前的内容将被删除。虽然这种行为极大地降低了耗尽磁盘空间的危险,但您也许能够想象到这样的情形,即不希望打开文件时将其内容删除。当然,C++提供了其他的选择。例如,如果要保留文件内容,并在文件尾添加(追加)新信息,则可以使用ios base::app 模式:
ofstream fout("bagels",ios base::outios | base::app);
上述代码也使用|运算符来合并模式,因此ios base::out|ios base::app意味着启用模式out和 app(参见图 17.6)。
老式C++实现之间可能有一些差异。例如,有些实现允许省略前一例子中的iosbase::out,有些则不允许。如果不使用默认模式,则最安全的方法是显式地提供所有的模式元素。有些编译器不支持表17.6中的所有选项,有些则提供了表中没有列出的其他选项。这些差异导致的后果之一是,可能必须对后面的例子作一些修改,使之能够在所用的系统中运行。好在C++标准提供了更高的统一性。
标准 C++根据 ANSIC标准 IO 定义了部分文件 JO。实现像下面这样的 C++语句时:
ifstream fin(filename, c++mode);
就像它使用了C的fopen()函数一样:
fopen(filename, cmode);
其中,c++mode 是一个 openmode 值,如 ios base::in;而 cmode 是相应的℃模式字符串,如“r”表 17.8列出了 C++模式和C模式的对应关系。注意,ios base::out本身将导致文件被截短,但与ios base::in起使用时,不会导致文件被截短。没有列出的组合,如ios base:in[vn]ios base::trunc,将禁止文件被打开。is open()方法用于检测这种故障。
注意,ios_base:ate 和ios_base::app 都将文件指针指向打开的文件尾。二者的区别在于,ios base::app模式只允许将数据添加到文件尾,而iosbase::ate模式将指针放到文件尾。显然,各种模式的组合很多,我们将介绍几种有代表性的组合。
1.追加文件来看一个在文件尾追加数据的程序。
该程序维护一个存储来客清单的文件。该程序首先显示文件当前的内容(如果有话)。在尝试打开文件后,它使用isopen()方法来检查该文件是否存在。接下来,程序以ios_base:app 模式打开文件,进行输出。然后,它请求用户从键盘输入,并将其添加到文件中。最后,程序显示修订后的文件内容。程序清单17.18演示了如何实现这些目标。请注意程序是如何使用isopen()方法来检测文件是否被成功打开的。
注意:在早期,文件 I/0 可能是 C++最不标准的部分,很多老式编译器都不遵守当前的标准。例如,有些编译器使用诸如 nocreate 等模式,而这些模式不是当前标准的组成部分。另外,只有一部分编译器要求在第二次打开同一个文件进行读取之前调用fin.clear()。
程序清单17.18 append.cpp
// append.cpp -- appending information to a file
#include <iostream>
#include <fstream>
#include <string>
#include <cstdlib> // (or stdlib.h) for exit()
const char * file = "guests.txt";
int main()
{
using namespace std;
char ch;
// show initial contents
ifstream fin;
fin.open(file);
if (fin.is_open())
{
cout << "Here are the current contents of the "
<< file << " file:\n";
while (fin.get(ch))
cout << ch;
fin.close();
}
// add new names
ofstream fout(file, ios::out | ios::app);
if (!fout.is_open())
{
cerr << "Can't open " << file << " file for output.\n";
exit(EXIT_FAILURE);
}
cout << "Enter guest names (enter a blank line to quit):\n";
string name;
while (getline(cin,name) && name.size() > 0)
{
fout << name << endl;
}
fout.close();
// show revised file
fin.clear(); // not necessary for some compilers
fin.open(file);
if (fin.is_open())
{
cout << "Here are the new contents of the "
<< file << " file:\n";
while (fin.get(ch))
cout << ch;
fin.close();
}
cout << "Done.\n";
// cin.get();
return 0;
}
此时,guests.txt文件还没有创建,因此程序不能预览该文件。但第二次运行该程序时,guests.txt文件已经存在,因此程序将预览该文件。另外,新数据被追加到旧文件的后面,而不是取代它们。
可以用任何文本编辑器来读取 guest.txt的内容,包括用来编写源代码的编辑器。
2.二进制文件
将数据存储在文件中时,可以将其存储为文本格式或二进制格式。文本格式指的是将所有内容(甚至数字)都存储为文本。例如,以文本格式存储值-2.324216e+07时,将存储该数字包含的13个字符。这需要将浮点数的计算机内部表示转换为字符格式,这正是<<插入运算符完成的工作。另一方面,二进制格式指的是存储值的计算机内部表示。也就是说,计算机不是存储字符,而是存储这个值的 64位 double 表示。对于字符来说,二进制表示与文本表示是一样的,即字符的 ASCI 码的二进制表示。对于数字来说,二进制表示与文本表示有很大的差别(参见图17.7)。
每种格式都有自己的优点。文本格式便于读取,可以使用编辑器或字处理器来读取和编辑文本文件,可以很方便地将文本文件从一个计算机系统传输到另一个计算机系统。二进制格式对于数字来说比较精确,因为它存储的是值的内部表示,因此不会有转换误差或舍入误差。以二进制格式保存数据的速度更快,因为不需要转换,并可以大块地存储数据。二进制格式通常占用的空间较小,这取决于数据的特征。然而,如果另一个系统使用另一种内部表示,则可能无法将数据传输给该系统。同一系统上不同的编译器也可能使用不同的内部结构布局表示。在这种情况下,则必须编写一个将一种数据转换成另一种的程序。来看一个更具体的例子。考虑下面的结构定义和声明:
const int LIM=20;
struct planet
{
char name[LIM];// name of planet
double population;// its population
double g;// its acceleration of qravity
}
planet pl;
要将结构p的内容以文本格式保存,可以这样做:
ofstream fout("planets.dat",ios base::outios |base::app) ;
fout << pl.name<<""<< pl.population << " "<< pl.g << "\n"
必须使用成员运算符显式地提供每个结构成员,还必须将相邻的数据分隔开,以便区分。如果结构有30个成员,则这项工作将很乏味。
要用二进制格式存储相同的信息,可以这样做:
ofstream fout("planets.dat"
ios base::outios base::appios|base::binary);
fout.write((char *)&pl,sizeof pl);
上述代码使用计算机的内部数据表示,将整个结构作为一个整体保存。不能将该文件作为文本读取,但与文本相比,信息的保存更为紧凑、精确。它确实更便于键入代码。这种方法做了两个修改:
使用二进制文件模式:
使用成员函数write()。
下面更详细的介绍这两项修改。
有些系统(如Windows)支持两种文件格式:文本格式和二进制格式。如果要用二进制格式保存数据应使用二进制文件格式。在C++中,可以将文件模式设置为ios base::binary 常量来完成。要知道为什么在Windows系统上需要完成这样的任务,请参见后面的旁注“二进制文件和文本文件”。
二进制文件和文本文件
使用二进制文件模式时,程序将数据从内存传输给文件(反之亦然)时,将不会发生任何隐藏的转换而默认的文本模式并非如此。例如,对于 Windows文本文件,它们使用两个字符的组合(回车和换行)表示换行符;Macintosh 文本文件使用回车来表示换行符;而 UNIX和 Linux 文件使用换行(linefeed)来表示换行符。C++是从 UNIX系统上发展而来的,因此也使用换行(linefecd)来表示换行符。为增加可移植性,Windows C++程序在写文本模式文件时,自动将C++换行符转换为回车和换行;Macintosh C++程序在写文件时,将换行符转换为回车。在读取文本文件时,这些程序将本地换行符转换为C++格式。对于二进制数据,文本格式会引起问题,因此 double 值中间的字节可能与换行符的 ASCII 码有相同的位模式。另外在文件尾的检测方式也有区别。因此以二进制格式保存数据时,应使用二进制文件模式(UNIX 系统只有一种文件模式,因此对于它来说,二进制模式和文本模式是一样的)。
要以二进制格式(而不是文本格式)存储数据,可以使用write0)成员函数。前面说过,这种方法将内存中指定数目的字节复制到文件中。本章前面用它复制过文本,但它只逐字节地复制数据,而不进行任何转换。例如,如果将一个long 变量的地址传递给它,并命令它复制4个字节,它将复制 long值中的4个字节,而不会将它转换为文本。唯一不方便的地方是,必须将地址强制转换为指向char 的指针。也可以用同样的方式来复制整个planet结构。要获得字节数,可以使用sizeof运算符:
fout.write((char *)&pl,sizeof pl);
这条语句导致程序前往 pl结构的地址,并将开始的36个字节(sizeofpl表达式的值)复制到与 fout相关联的文件中。
要使用文件恢复信息,请通过一个ifstream对象使用相应的read()方法:
ifstream fin("planets.dat",ios base::in | ios base::binary);
fin.read((char *)&pl,sizeof pl);
这将从文件中复制 sizeofpl个字节到p1结构中。同样的方法也适用于不使用虚函数的类。在这种情况下,只有数据成员被保存,而方法不会被保存。如果类有虚方法,则也将复制隐藏指针(该指针指向虚函数的指针表)。由于下一次运行程序时,虚函数表可能在不同的位置,因此将文件中的旧指针信息复制到对象中,将可能造成混乱(请参见“编程练习6”中的注意)。
提示:read()和 write()成员函数的功能是相反的。请用read()来恢复用 write()写入的数据
程序清单17.19使用这些方法来创建和读取二进制文件。从形式上看,该程序与程序清单17.18相似,但它使用的是 write()和read(),而不是插入运算符和 get()方法。另外,它还使用控制符来格式化屏幕输出。注意:虽然二进制文件概念是 ANSIC的组成部分,但一些C和C++实现并没有提供对二进制文件模式的支持。原因在于:有些系统只有一种文件类型,因此可以将二进制操作(如read()和 write())用于标准文件格式。因此,如果实现认为 ios base:binary 是非法常量,只要删除它即可。如果实现不支持 fixed和 right 控制符,则可以使用 cout.setf(ios base::fixed、ios base::foatfield)和 cout.setf(ios base:.right.ios base::adiustfield)。另外,也可能必须用ios替换ios base。其他编译器(特别是老式编译器)可能还有其他特征。
程序清单17.19 binary.cpp
// binary.cpp -- binary file I/O
#include <iostream> // not required by most systems
#include <fstream>
#include <iomanip>
#include <cstdlib> // (or stdlib.h) for exit()
inline void eatline() { while (std::cin.get() != '\n') continue; }
struct planet
{
char name[20]; // name of planet
double population; // its population
double g; // its acceleration of gravity
};
const char * file = "planets.dat";
int main()
{
using namespace std;
planet pl;
cout << fixed << right;
// show initial contents
ifstream fin;
fin.open(file, ios_base::in |ios_base::binary); // binary file
//NOTE: some systems don't accept the ios_base::binary mode
if (fin.is_open())
{
cout << "Here are the current contents of the "
<< file << " file:\n";
while (fin.read((char *) &pl, sizeof pl))
{
cout << setw(20) << pl.name << ": "
<< setprecision(0) << setw(12) << pl.population
<< setprecision(2) << setw(6) << pl.g << endl;
}
fin.close();
}
// add new data
ofstream fout(file,
ios_base::out | ios_base::app | ios_base::binary);
//NOTE: some systems don't accept the ios::binary mode
if (!fout.is_open())
{
cerr << "Can't open " << file << " file for output:\n";
exit(EXIT_FAILURE);
}
cout << "Enter planet name (enter a blank line to quit):\n";
cin.get(pl.name, 20);
while (pl.name[0] != '\0')
{
eatline();
cout << "Enter planetary population: ";
cin >> pl.population;
cout << "Enter planet's acceleration of gravity: ";
cin >> pl.g;
eatline();
fout.write((char *) &pl, sizeof pl);
cout << "Enter planet name (enter a blank line "
"to quit):\n";
cin.get(pl.name, 20);
}
fout.close();
// show revised file
fin.clear(); // not required for some implementations, but won't hurt
fin.open(file, ios_base::in | ios_base::binary);
if (fin.is_open())
{
cout << "Here are the new contents of the "
<< file << " file:\n";
while (fin.read((char *) &pl, sizeof pl))
{
cout << setw(20) << pl.name << ": "
<< setprecision(0) << setw(12) << pl.population
<< setprecision(2) << setw(6) << pl.g << endl;
}
fin.close();
}
cout << "Done.\n";
// keeping output window open
// cin.clear();
// eatline();
// cin.get();
return 0;
}
看到该程序的主要特征后,下面再次讨论前面提到的几点。程序在读取行星的g值后,将使用下面的代码(以内嵌eatline()函数的形式):
while(std::cin.get()!=n')continue;
这将读取并丢弃输入中换行符之前的内容。考虑循环中的下一条输入语句:
cin.get(pl.name,20);
如果保留换行符,该语句将换行符作为空行读取,然后终止循环,
您可能会问,如果该程序是否可以使用string对象而不是字符数组来表示planet 结构的name 成员?答案是否定的,至少在不对设计做重大修改的情况下是否定的。问题在于,string对象本身实际上并没有包含字符串,而是包含一个指向其中存储了字符串的内存单元的指针。因此,将结构复制到文件中时,复制的将不是字符串数据,而是字符串的存储地址。当您再次运行该程序时,该地址将毫无意义。