第17章 输入、输出和文件
1.使用cout进行输出
ostream类提供了put()方法和write()方法,前者接受一个参数,表示要显示的字符,后者接受两个参数,第一个参数表示要显示的字符串,第二个参数表示显示字符串的长度。两者都是成员函数,要有调用对象:
cout.put(‘W’);
cout.wirte(“Kansas”, 6);
注意,write()方法并不会在遇到空字符时自动停止打印字符,而只是打印指定数目的字符,即使超出了字符串的边界。
ostream类对cout对象处理的输出进行缓冲,输出不会立即发送到目标地址,而是被存储在缓冲区中,直到缓冲区填满。然后,程序将刷新缓冲区,把内容发送出去,并清空缓冲区,以存储新的数据。通常,缓冲区为512字节或其整数倍。每次填满缓冲区时将自动刷新缓冲区。程序期待输入时也将刷新缓冲区。也可以使用flush和endl控制符强制刷新缓冲区。控制符flush刷新缓冲区,控制符endl刷新缓冲区,并插入一个换行符:
cout << “Hello, good-looking! ” << flush;
cout << “Wait just a moment, please.” << endl;
事实上,控制符也是函数。能使用上述语句是因为ostream类对<<插入运算符进行了重载。
flush(cout);
endl(cout);
ostream插入运算符将值转换为文本格式。在默认情况下,格式化值的方式如下:
-
对于char值,如果它代表的是可打印字符,则将被作为一个字符显示在宽度为一个字符的字段中。
-
对于数值整型,将以十进制方式显示在一个刚好容纳该数字及负号(如果有的话)的字段中。
-
字符串被显示在宽度等于该字符串长度的字段中。
-
浮点类型被显示为6位,末尾的0不显示。当指数大于等于6或小于等于-5时将使用科学计数法表示。字段宽度恰好容纳数字和负号(如果有的话)。
要控制整数以十进制、十六进制还是八进制显示,可以使用dec、hex和oct控制符:
dec(cout); //十进制
hex(cout); //十六进制
oct(cout); //八进制
完成上述设置后,程序将以十六进制形式打印整数值,直到将格式状态被覆盖为止。注意,控制符不是成员函数,因此不必通过对象来调用。
虽然控制符实际上是函数,但它们通常的使用方式为:
cout << hex;
可以使用width成员函数调整输出字段的宽度,该方法的原型为:
int width();
int width(int i);
第一种格式返回字段宽度的当前设置。第二种格式将字段宽度设置为i个空格,并返回以前的字段宽度值。这使得能够保存以前的值,以便以后回复宽度值使用。默认的的字段宽度为0。
width()方法只影响将显示的下一个项目,然后字段宽度将恢复为默认值。
默认情况下,cout用空格填充字段中位被使用的部分,可以用fill()成员函数来改变填充字符。例如,下面的函数调用将填充字符改为星号:
cout.fill(‘*’);
浮点数进度的含义取决于输出模式。在默认模式下,它指的是显式的总位数。在定点模式和科学模式下,精度指的是小数点后面的位数。C++的默认精度为6位。precision()成员函数使得能够改变精度。例如,下面的语句将cout的精度设置为2:
cout.presision(2);
新的精度将一直有效,直到被覆盖。
ios_base类有一个受保护的数据成员,其中的各位(这里叫做标记)分别控制这格式化的各个方面,例如计数系统、是否显示末尾的0等。打开一个标记称为设置标记,并意味着相应的位被设置为1。setf()成员函数用于调整标记。
seft()函数有两个原型。第一个为:
fmtflags setf(fmtflags);
其中,fmtflags是bitmask类型。bitmask类型是一种用来存储各个位值的类型。fmtflags用于存储格式标记。ios_base类定义了代表位值的常量,表17.1列出了其中的一些定义。
常量 | 含义 |
---|---|
ios_base::boolalpha | 输入和输出bool值,可以为true或false |
ios_base::showbase | 对于输出,使用C++基数前缀(0,0x) |
ios_base::showpoint | 显示末尾的小数点 |
ios_base::uppercase | 对于16进制输出,使用大写字母,E表示法 |
ios_base::showpos | 在正数前加上+ |
修改将一直保持有效,直到被覆盖为止。
第二个setf()原型接受两个参数,并返回以前的设置:
fmtflags setf(fmtflags, fmtflags);
第一个参数和之前一样,包含了所需设置的fmtflags值。第二个参数指出要清除第一个参数中的那些位。ios_base类定义了可按这种方式处理的3组格式标记。每组标记都由一个可用作第二参数的常量和两三个可用作第一参数的常量组成。第二个参数清除一批相关的位,然后第一参数将其中以为设置为1。表17.2列出了用作setf()这些相关的常量以及它们的含义。
第二个参数 | 第一个参数 | 含义 |
---|---|---|
ios_base::basefield | ios_base::dec | 使用基数10 |
ios_base::basefield | ios_base::oct | 使用基数8 |
ios_base::basefield | ios_base::hex | 使用基数16 |
ios_base::floatfield | ios_base::fixed | 使用定点计数法 |
ios_base::floatfield | ios_base::scientific | 使用科学计数法 |
ios_base::adjustfield | ios_base::left | 使用左对齐 |
ios_base::adjustfield | ios_base::right | 使用右对齐 |
ios_base::adjustfield | ios_base::internal | 使用内部对齐 |
例如,要使用左对齐,可使用下面的调用:
ios_base::fmtflags old = cout.setf(ios_base::left, ios_base::adjustfield);
要恢复以前的设置,可以这样做:
cout.setf(old, ios_adjustfield);
调用setf()的效果可以通过unsetf()消除,后者的原型如下:
void unsetf(fmtflags mask);
unsetf()将mask中的位恢复为0。例如:
cout.setf(ios_base::showpoint);
cout.unsetf(ios_base::showpoint);
没有专门指示浮点数默认显示模式的标记。启用默认模式的方法有:
cout.setf(0, ios_base::floatfield);
cout.unsetf(ios_base::floatfield);
C++提供了多个标准控制符,能够调用setf()并自动提供正确的参数。表17.3列出了这些控制符。
控制符 | 调用 |
---|---|
boolalpha | setf(ios_base::boolalpha) |
noboolalpha | unsetf(ios_base::noboolalpha) |
showbase | setf(ios_base::showbase) |
noshowbase | unsetf(ios_base::showbase) |
showpoint | setf(ios_base::showpoint) |
noshowpoint | unsetf(ios_base::noshowpoint) |
showpos | setf(ios_base::showpos) |
noshowpos | unsetf(ios_base::noshowpos) |
uppercase | setf(ios_base::uppercase) |
nouppercase | unsetf(ios_base::nouppercase) |
internal | setf(ios_base::internal, ios_base::adjustfield) |
left | setf(ios_base::left, ios_base::adjustfield) |
right | setf(ios_base::right, ios_base::adjustfield) |
dec | setf(ios_base::dec, ios_base::basefield) |
hex | setf(ios_base::hex, ios_base::basefield) |
oct | setf(ios_base::oct, ios_base::basefield) |
oct | setf(ios_base::oct, ios_base::basefield) |
scientific | setf(ios_base::scientific, ios_base::basefield) |
例如,下面的语句打开左对齐和定点选项:
cout << left << fixed;
C++在头文件iomanip提供了其他一些控制符,它们能提供前面讨论过的服务,但表示起来更方便。3个最常用的控制符是setprecision()、setfill()和setw(),它们分别用来设置精度、填充字符和字段宽度。
2.使用cin进行输入
cin或cout对象包含一个描述流状态的数据成员。流状态(被定义为iostate类型,而iostate是一种bitmask类型)有3个ios_base元素组成:eofbit、badbit和failbit,其中每个元素都是一位,可以是1(设置)或0(清除)。当cin操作到达文件尾时,它将设置eofbit;当cin操作未能读取到预期字符时,它将设置failbit;I/O失败(如试图读取不可访问的文件或试图写入写保护的磁盘),也可能将failbit值设置为1。在一些无法诊断的失败破坏流时,badbit元素将被设置。当全部3个状态都设置为0时,说明一切顺利。表17.4列出了这些位和一些报告或改变流状态的ios_base方法。
成员 | 描述 |
---|---|
eofbit | 如果到达文件尾,则设置为1 |
badbit | 如果流被破坏,则设置为1 |
failbit | 如果输入操作未能读取预期的字符或输出操作没有写入预期的字符,则设置为1 |
goodbit | 另一种表示0的方法 |
good() | 如果流可以使用(所有的位都被清除),则返回true |
eof() | 如果eofbit被设置,则返回true |
bad() | 如果badbit被设置,将返回true |
fail() | 如果badbit或failbit被设置,则返回true |
rdstate() | 返回流状态 |
exceptions() | 返回一个位掩码,指出哪些标记导致异常被引发 |
exceptions(iostate ex) | 设置哪些状态将导致clear()引发异常 |
clear(iostate s) | 将流状态设置为s;s的默认值为0;如果(rdstate() & exceptions()) != 0,则引发异常basic_ios::failure |
setstate(iostate s) | 调用clear(rdstate() | s)。这将设置与s中设置的位对应的流状态位,其他流状态位保持不变 |
表中两种方法clear()和setstate()很相似。但采取的方式不同,clear()方式将状态设置为它的参数。而setstate()方法只影响其参数中已设置的位,而不会影响其他位。
exceptions()方法返回一个位字段,它包含3位,分别位于eofbit、failbit和badbit。修改流状态涉及clear()或setstate(),这都将使用clear()。当某个输入函数修改流状态后,clear()方法将当前的流状态与exceptions()返回的值进行比较。如果在返回值中某一位被设置,而当前状态中的对应位也被设置,则clear()将引发ios_base::failure异常。
exceptions()的默认设置为goodbit。重载的exceptions(iostate)函数可以控制其行为:
cin.exceptions(badbit);
上面的语句将使exceptions()方法返回badbit。
位运算符|使得能够指定多位:
cin.exceptions(badbit | eofbit);
设置流状态位会导致流对后面的输入或输出关闭,直到位被清除。例如:
while(cin >> input)
{
sum += input;
}
cin >> input; //如果上面的cin导致流状态位改变,则这个语句将不能输入
如果希望程序在流状态位被设置后能够读取后面的输入,就必须将流状态重置为良好。这可以通过调用clear()方法来实现。导致输入循环终止的不匹配输入仍然会留在输入队列中,程序必须跳过它。
istream类中有get()和getline()方法。其中get()方法有两种原型:
istream & get(char &);
int get(void);
它们与使用cin>>输入的区别是,cin>>将跳过空白,而get()方法将读取空白字符。
表17.5列出了get()方法两种原型的区别。
特征 | cin.get(ch) | cin.get() |
---|---|---|
传输输入字符的方法 | 赋给参数ch | 将函数返回值赋给ch |
字符输入时函数的返回值 | 指向istream的引用 | 字符编码(int值) |
达到文件尾时函数的返回值 | 转换为false | EOF |
getline()成员函数和get()的字符串读取版本都读取字符串,它们的函数特征标相同:
istream & get(char *, int, char);
istream & get(char *, int);
istream & getline(char *, int, char);
istream & geelinr(char *, int);
第一个参数用于防止输入字符串的内存单元的地址。第二个参数比要读取的最大字符数大1。第三个参数指定用作分界符的字符,只有两个参数的版本将换行符作为用作分界符。
get()和getline()之间的主要区别在于,get()将换行符留在输入流中,而getline()抽取并丢弃输入流中的换行符。
表17.6列出了两种方法改变流状态的行为。
方法 | 行为 |
---|---|
getline(char *, int) | 如果没有读取任何字符(换行符被视为读取了一个字符),则设置failbit。如果读取了最大数目的字符,且行中还有其他字符,则设置failbit |
get(char *, int) | 如果没有读取任何字符,则设置failbit |
成员函数ignore()接受两个参数:一个是数字,指定要读取的最大字符数;另一个是字符,用作分界符。下面是该函数的原型:
istream & ignore(int = 1, int = EOF);
istream类还提供了其他方法,包括read()、peek()、gcount()和putback()。
read()函数读取指定数目的字节,并将它们存储在指定的位置中。read()方法不会在输入后面加上空值字符,因此不能将输入转换为字符串。例如,下面的语句从标准输入中读取144个字符,并将它们存储在gross数组中:
char gross[144];
cin.read(gross, 144);
peek()函数返回输入中的下一个字符,但不抽取输入流中的字符。也就是说,它能够查看下一个字符。例如,下面的语句查看输入流中的下一个字符并把它赋给ch,但这个字符不会从输入流中被抽取出来,而是留在输入流中:
ch = cin.peek();
gcount()方法返回最后一个非格式化抽取方法读取的字符。这意味着字符是由get()、getline()、ignore()或read()方法读取的,而不是由抽取运算符>>读取的。
putback()函数将一个字符插入到输入字符串中,被插入的字符将是下一条输入语句的第一个字符。putback()方法接受一个char参数,表示要插入的字符。其返回类型为istream &。
3.文件输入和输出
C++在头文件fstream中定义了多个新类,其中包括用于文件输入的ifstream和用于文件输出的ofstream类。C++还定义了一个fstream类,用于同步文件I/O。这些类都是从头文件iostream中的类派生而来的,可以使用这些类的方法。
要让程序写入文件,必须完成下面几个步骤:
-
创建一个ofstream对象来管理输出流;
-
将该对象与特定的文件关联起来;
-
以使用cout的方式使用该对象,唯一的区别是输出将进入文件,而不是屏幕。
要完成上述任务,首先应包含头文件fstream。对于大多数实现来说,包含该文件变自动包含iostream文件,因此不必显式包含iostream。然后声明一个ofstream对象:
ofstream fout;
接下来要将这个对象与特定的文件关联起来。可以使用open()方法。例如,要打开文件jar.txt进行输出,可这样做:
fout.open(“jar.txt”);
然后,以使用cout的方式使用fout。
以这种方式打开文件来进行输出时,如果没有这样的文件,将创建一个文件;如果有这样的文件,则打开文件将清空文件,输出将进入到一个空文件中。
读取文件的要求与写入文件相似:
-
创建一个ifstream对象来管理输入流;
-
将该对象与特定的文件相关联;
-
以使用cin的方式使用该对象。
当驶入和输出流对象过期时,到文件的连接将自动关闭。另外,也可以使用close()方法来显式地关闭到文件的连接:
fout.close();
可以使用is_open()方法来检查文件是否被打开:
if(!fin.is_open())
{
//...
}
C++有一种让在命令行环境中运行的程序能够访问命令行参数的机制,方法是使用下面的main函数:
int main(int argc, char * argv[])
argc为命令行中参数的个数,其中包括命令本身。argv变量为一个指针,它指向一个指向char的指针。可以将argv看作一个指针数组,其中的指针指向命令行参数。例如,假设有下面的命令行:
wc report1 report2 report3
则argc为4, argv[0]为wc,argv[1]为report1,argv[2]为report2,argv[3]为report3。
文件模式描述的是文件将被如何使用:读、写、追加等。将流与文件关联时,都可以提供指定文件模式的第二个参数:
ifstream fin(“banjo”, mode1);
ofstream fout();
fout.open(“harp”, mode2);
ios_base类定义了一个openmode类型,用于表示模式。它也是一种bitmask类型。可以选择ios_base类中定义的多个常量来指定模式,表17.7列出了这些常量及其含义。
常量 | 含义 |
---|---|
ios_base::in | 打开文件,以便读取 |
ios_base::out | 打开文件,以便写入 |
ios_base::ate | 打开文件并移到文件尾 |
ios_base::app | 追加到文件尾 |
ios_base::trunc | 如果文件存在,则截短文件 |
ios_base::binary | 二进制文件 |
ios_base::ate和ios_base::app都将文件指针指向打开的文件尾。二者的区别在于,ios_base::app模式只允许将数据添加到文件尾,而ios_base::ate模式将指针放到文件尾。
ifstream open()方法和构造函数用ios_base::in作为模式参数的默认值,而ostream open()方法和构造函数用ios_base::out|ios_base::trunc作为默认值。位运算OR(|)用于将两个位值合并成一个可用于设置两个位的值。fstream类不提供默认的模式值,因此在创建这种类的对象时,必须显式地提供模式。
将数据存储在文件中时,可以将其存储为文本格式或二进制格式。文本格式指的是将所有内容都存储为文本。二进制格式指的是存储值的计算机内部表示。每种格式都有自己的优点。文本格式便于读取,可以使用编辑器或字处理器来读取和编辑文本文件,可以很方便地将文本文件从一个计算机系统传输到另一个计算机系统。二进制格式对于数字来说比较精确,因为它存储的是值的内部表示,因此不会有转换误差或舍入误差。以二进制格式保存数据的速度更快,因为不需要转换,并可以大块地存储数据。二进制格式通常占用的空间较小,这取决于数据的特征。
const int LIM = 20;
struct planet
{
char name[LIM];
double population;
double g;
};
planet pl;
要将结构pl的内容以文本格式保存,可以这样做:
ofstream fout(“planets.dat”, ios_base::out | ios_base::app);
fout << pl.name << “ ” << pl.population << “ ” << pl.g << “\n”;
必须使用成员运算符显式地提供每个结构成员,还必须将相邻的数据分隔开,以便区分。这样在结构成员较多的情况下将很麻烦。
要用二进制格式存储相同的信息,可以这样做:
ofstream fout(“planets.dat”,ios_base::out | ios_base::app | ios_base::binary);
fout.write( (char *) &p1, sizeof p1);
上述代码使用计算机的内部数据表示,将整个结构作为一个整体保存。用write()方法将信息写入文件中。不能将该文件作为文本读取,但与文本相比,信息的保存更为紧凑、精确。唯一不方便的地方是,必须将地址强制转换为指向char的指针。
要从文件中读取信息,可以通过一个ifstream对象使用相应的read()方法:
ifstream fin(“planets.dat”, ios_base::in | ios_base::binary);
fin.read( (char *) &pl, sizeof pl);
这将从文件中复制sizeof pl个字节到pl结构中。在这种情况下,只有数据成员被保存,而方法不会被保存。如果类有虚方法,则也将复制隐藏指针。由于下一次运行程序时,虚函数表可能在不同的位置,因此将文件中的旧指针信息复制到对象中,将可能造成混乱。
fsteam类为文件的随机存取继承了两个方法:seekg()和seekp(),前者将输入指针移到指定的文件位置,后者将输出指针移到指定的文件位置。也可以将seekg()用于ifstream对象,将seekp()用于ofstream对象。seekg()是一个模板函数,下面是seekg()的char类型模板具体化原型:
istream & seekg(streamoff, ios_base::seekdir);
istream & seekg(streampos);
第一个原型定位到离第二个参数指定的文件位置特定的距离(单位为字节)的位置;第二个原型定位到离文件开头特定距离的位置。
seekg()第一个原型中的第一个参数streamoff值被用来度量相对于文件特定位置的偏移量(单位为字节)。streamoff参数表示相对于三个位置之一的偏移量为特定值的文件位置。seek_dir参数是ios_base类中定义的另一种整型,有3个可能的值。常量ios_base::beg指相对于文件开始处的偏移量。常量ios_base::cur指相对于当前位置的偏移量。常量ios_base::end指相对于文件尾的偏移量。下面是一些示例:
fin.seekg(30, ios_base::beg); //相对文件开始处偏移30字节
fin.seekg(-1, ios_base::cur); //相对与当前位置向前偏移1字节
fin.seekg(0, ios_base::end); //文件尾
在第二个原型中streampos类型的值定位到文件中的一个位置。表示文件中的绝对位置(从文件开始处算起)。可以将streampos位置看作是相对于文件开始处的位置(以字节为单位,第一个字节编号为0)。
如果要检查文件指针的当前位置,则对于输入流,可以使用tellg()方法,对于输出流,可以使用tellp()方法。它们都返回一个表示当前位置的streampos值。
4.内核格式化
C++库还提供了sstream族,它们使用相同的接口提供程序和string之间的I/O。也就是说,可以使用于cout的ostream方法将格式化信息写入到string对象中,并使用istream()方法来读取string 对象中的信息。读取string对象中的格式化信息或将格式化信息写入string对象中被称为内核格式化。
头文件sstream定义了一个从ostream类派生而来的ostringstream类。如果创建了一个ostringstream对象,则可以将信息写入其中,它将存储这些信息。可以将可用于cout的方法用于ostringstream对象:
ostringstream outstr;
double price = 380.0;
char * ps = “ for a copy of the IOS/EIC C++ standard!”;
outstr.precision(2);
outstr << fixed;
outstr << “Pay only CHF” << price << ps << endl;
ostringstream类有一个名为str()的成员函数,该函数范湖一一个被初始化为缓冲区内容的字符串对象:
string mesg = outstr.str();