输入、输出和文件

1.流和缓冲区 p593

c++把输入和输出看作字节流。输入时,程序从输入流中抽取字节;输出时,程序将字节插入到输出流中。

管理输入包含两步:

  • 将流与输入去向的程序关联起来
  • 将流与文件连接起来

通常,通过缓冲区可以高效的处理输入和输出,缓冲区是用作中介的内存块。

键盘输入每次提供一个字符,在这种情况下,程序无需缓冲区来帮助匹配不同的数据传输速率。然而,对键盘输入进行缓冲可以让用户在将输入传输给程序之前返回并更正;c++程序通常在用户按下回车键时刷新输入缓冲区。对于屏幕输出,c++程序通常在用户发送换行符时刷新输出缓冲区

2.流、缓冲区和 iostream 文件 p594

iostream 文件中包含用来管理流和缓冲区的类:

  • streambuf 类为缓冲区提供了内存,并提供了用于填充缓冲区、访问缓冲区内容、刷新缓冲区和管理缓冲区内存的类方法;
  • ios_base 类表示流的一般特征,如是否可读取、是二进制流还是文本流等;
  • ios 类基于 ios_base,其中包含了一个指向 streambuf 对象的指针成员;
  • ostream 类是从 ios 类派生而来的,提供了输出方法;(ostream 类对象 cout)
  • istream 类是从 ios 类派生而来的,提供了输入方法;(istream 类对象 cin)
  • iostream 类是基于 istream 和 ostream 类的,继承了输入方法和输出方法。

要使用这些工具,必须使用适当的类对象。如,使用 ostream 对象(如 cout)来处理输出。创建这样的对象将打开一个流,自动创建缓冲区,并将其与流关联起来,同时使得能够使用类成员函数。

c++ 的 iostream 类库管理了很多细节。例如,在程序中包含 iostream 文件将自动创建 8 个流对象(4 个用于窄字符流,4 个用于宽字符流)。

  • cin 对象对应于标准输入流。在默认情况下,这个流被关联到标准输入设备(通常为键盘)。
  • cout 对象与标准输出流相对应。在默认情况下,这个流被关联到标准输出设备(通常为显示器)。
  • cerr 对象与标准错误流相对应,可用于显示错误消息。在默认情况下,这个流被关联到标准输出设备(通常为显示器)。这个流没有被缓冲,这意味着信息将被直接发送给屏幕,而不会等到缓冲区填满或新的换行符
  • clog 对象也对应着标准错误流。在默认情况下,这个流被关联到标准输出设备(通常为显示器)。这个流被缓冲。

对象代表流。当 iostream 文件为程序声明一个 cout 对象时,该对象将包含存储了与输出有关的信息的数据成员,如显示数据时使用的字段宽度、小数位数、显示整数时采用的计数方法以及描述用来处理输出流的缓冲区的 streambuf 对象的地址。下面的语句通过指向的 streambuf 对象将字符串"Bjarna free" 中的字符放到 cout 管理的缓冲区中:

cout << "Bjarne free";

ostream 类定义了上述语句中使用的 operator<<() 函数。

在上述代码中,来自缓冲区的输出被引导到标准输出(通常是显示器,由操作系统提供)。总之,cout 代表的流的一端与程序相连,另一端与标准输出相连,cout 对象凭借 streambuf 对象的帮助,管理着流中的字节流。

3.重定向 p596

4.使用 cout 进行输出 p597

c++ 将输出看作字节流,但在程序中,很多数据被组织成比字节更大的单位。如,int 类型由 16 位或 32 位的二进制值表示;double 值由 64 位的二进制数据表示。但在字节流发送给屏幕时,希望每个字节表示一个字符值,即,要在屏幕上显示数字 -2.34,需要将 5 个字符(-、2、.、3、和4)而不是这个值的 64 位内部浮点表示发送到屏幕上。因此,ostream 类最重要的任务之一是将数值类型(如 int 或 float)转换为以文本形式表示的字符流,即,ostream 类将数据内部表示(二进制位模式)转换为由字符字节组成的输出流

  • 插入运算符 <<

<< 运算符的默认含义是按位左移运算符。ostream 类重新定义了 << 运算符,方法是将其重载为输出。在这种情况下,<< 叫作插入运算符

如,表达式 cout << 88 对应于下面的方法原型:

ostream & operator<<(int);

该原型表明,operator<<() 函数接受一个 int 参数,这与上述语句中的 88 匹配。该原型还表明,函数返回一个指向 ostream 对象的引用,这使得可以将输出连接起来。

  • 插入运算符 << 和指针

ostream 类还为下面的指针类型定义了插入运算符函数:

1)const signed char *;

2)const unsigned char *;

3)const char *;

4)void *。

c++ 使用指向字符串存储位置的指针来表示字符串。指针的形式可以是 char 数组名、显示的 char 指针或用引号括起的字符串。

对于其他类型的指针,c++ 将其对应于 void *,并打印地址的数值表示。如果要获得字符串的地址,则必须将其强制转换为其他类型,如下面的代码片段所示:

int eggs = 12;
char * amount = "dozen";
cout << &eggs; // prints address of eggs variable
cout << amount; // prints the string "dozen"
cout << (void *) amount; // prints the address of the "dozen" string

除了各种 operator<<() 函数外,ostream 类还提供了 put() 方法write() 方法(注意,这两个方法是类方法),前者用于显示字符,后者用于显示字符串。

最初,put() 方法的原型如下:

ostream & put(char);

当前标准于此相同,但被模板化,以适用于 wchar_t。

可以用类方法表示来调用它:

cout.put('W'); // display the  W character

和 << 运算符函数一样,该函数也返回一个指向调用对象的引用,因此可以用它拼接输出:

cout.put('I').put('t'); // displaying It with two put() calls

write() 方法显示整个字符串,其模板原型如下:

basic_ostream<charT, traits>& write(const char_type* s, streamsize n);

write() 的第一个参数提供了要显示的字符串的地址,第二个参数指出要显示多少个字符。使用 cout 调用 write() 时,将调用 char 具体化,因此返回类型为 ostream &。

write() 方法并不会在遇到空字符时自动停止打印字符,而只是打印指定数组的字符,即使超出了字符串的边界。

  • 刷新输出缓冲区

当程序使用 cout 将字节发送给标准输出时,ostream 类对 cout 对象处理的输出进行缓冲,所以输出不会立即发送到目标地址,而是被存储在缓冲区中,直到缓冲区填满。然后,程序将刷新缓冲区,将内容发送出去,并清空缓冲区,以存储新的数据。

在屏幕输出时,程序不必等到缓冲区被填满。例如,换行符发送到缓冲区后,将刷新缓冲区。此外,多数 c++ 实现都会在输入即将发生时刷新缓冲区。如果有如下代码:

cout << "Enter a number: ";
float num;
cin >> num;

程序期待输入这一事实,将导致它立刻显示 cout 消息(即刷新 "Enter a number: " 消息),即使输出字符串中没有换行符。如果没有这种特性,程序将等待输入,而无法通过 cout 消息来提示用户。

如果实现不能在所希望时刷新输出,可以使用两个控制符中的一个来强行进行刷新。控制符 flush 刷新缓冲区,而控制符 endl 刷新缓冲区,并插入一个换行符。这两个控制符的使用方式与变量名相同:

cout << "Hello, good-looking!" << flush;
cout << "Wait just a moment, please." << endl;

事实上,控制符也是函数。例如,可以直接调用 flush() 来刷新 cout 缓冲区:

flush(cout);

并且,ostream 类对 << 插入运算符进行了重载,使得下述表达式将被替换为函数调用 flush(cout):

cout << flush;
  • 使用 cout 进行格式化 p601

ostream 插入运算符将值转换为文本格式。

1)修改显示时使用的计数系统(进制)

ostream 类是从 ios 类派生而来的,而后者是从 ios_base 类派生而来的。 ios_base 类存储了描述格式状态的信息。例如,一个类成员中某些位决定了使用的计数系统,另一个成员决定了字段宽度。通过使用控制符(manipulator),可以控制显示整数时使用的计数系统。通过使用 ios_base 的成员函数,可以控制字段宽度和小数位数。由于 ios_base 类是 ostream 的间接基类,因此可以将其方法直接用于 ostream 对象,如 cout。

要控制整数以十进制、十六进制还是八进制显示,可以使用 dec、hex 和 oct 控制符。例如,下面的函数调用将 cout 对象的计数系统格式状态设置为十六进制:

hex(cout);

完成上述设置后,程序将以十六进制形式打印整数值,直到将格式状态设置为其他选项为止。注意,控制符不是成员函数,因此不必通过对象来调用。

虽然控制符实际上是函数,但它们通常的使用方式为:

cout << hex;

ostream 类重载了 << 运算符,使得上述用法与函数调用 hex(cout) 等价。控制符位于名称空间 std 中。

2)调整字段宽度 p603

width 成员函数将长度不同的数字放到宽度相同的字段中,该方法的原型为:

int width();
int width(int i);

第一种格式返回字段宽度的当前设置;第二种格式将字段宽度设置为 i 个空格,并返回以前的字段宽度值。这使得能够保存以前的值,以便以后恢复宽度值时使用。

width() 方法只影响将显式的下一个项目,然后将字段宽度恢复为默认值。

c++永远不会截短数据,因此如果试图在宽度为 2 的字段中打印一个 7 位值,c++ 将增宽字段,以容纳该数据。

3)填充字符

cout 默认以空格填充字符,且右对齐(右对齐时,空格被插入到值的左侧)。用来填充的字符叫填充字符。默认的字段宽度为 0(由于 c++ 总会增长字段,以容纳数据,因此这种值适用于所有的数据)。

可以用 fill() 成员函数来改变填充字符,例如,下面的函数调用将填充字符改为星号:

cout.fill('*');

新的填充字符将一直有效,直到更改它为止。

4)设置浮点数的显示精度 p604

浮点数精度的含义取决于输出模式。在默认模式下,它指的是显式的总位数。在定点模式和科学模式下,精度指的是小数点后面的位数。c++的默认精度为 6 位(末尾的 0 将不显示)。precision() 成员函数使得能够选择其他值。

cout.precision(2);

与 fill() 类似,新的精度设置将一直有效,直到被重新设置。

5)打印末尾的 0 和小数点 p605

ios_base 类提供了一个 setf() 成员函数(用于 set 标记),能够控制多种格式化特性。这个类还定义了多个常量,可用作该函数的参数。例如,下面的函数调用使 cout 显式末尾小数点:

cout.setf(ios_base::showpoint);

其中,showpoint 是 ios_base 类声明中定义的类级静态常量。

6)setf()

ios_base 类有一个受保护的数据成员,其中的各位(这里叫作标记)分别控制着格式化的各个方面,如技术系统、是否显式末尾的 0 等。打开一个标记成为设置标记(或位),并意味着相应的位被设置为 1。

setf() 函数有两个原型,第一个为:

fmtflags setf(fmtflags);

其中,fmtflags 是 bitmask 类型的 typedef 名,该名称是在 ios_base 类中定义的。这个版本的 setf() 用来设置单个位控制的格式信息。参数是一个 fmtflags 值,指出要设置哪一位。返回值是类型为 fmtflags 的数字,指出所有标记以前的设置,可以用来恢复原始设置。ios_base 类定义了代表位值的常量,如:

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 类为此定义了常量。p607

例如,要修改基数,可以将常量 ios_base::basefield 用作第二参数,将 ios_base::hex 用作第一参数:

cout.setf(ios_base::hex, ios_base::basefield)

其中,定点表示法和科学表示法都有下面两个特征:

  • 精度指的是小数位数,而不是总位数
  • 显式末尾的 0

 

调用 setf() 的效果可通过 unsetf() 消除,后者原型如下:

void unsetf(fmtflags mask);

其中,mask 是位模式。mask 中所有的位都设置位 1,将使得对应的位被复位。级,setf() 将位设置为1,unsetf() 将位恢复为 0。例如:

cout.setf(ios_base::showpoint); // show trailing decimal point
cout.unsetf(ios_base::showpoint); // don't show trailing decimal point
cout.setf(ios_base::boolalpha); // display true, false
cout.unsetf(ios_base::boolalpha); // display 1, 0

7)标准控制符 p609

使用 setf() 不是进行格式化的、对用户最为友好的方法,c++ 提供了多个控制符,能够调用 setf(),并自动提供正确的参数。

 

可以这样方便的使用这些标准控制符:

cout << left << fixed; // 打开左对齐和定点选项

8)头文件 iomanip

使用 iostream 工具来设置一些格式值(如字段宽度)不太方便。c++ 在头文件 iomanip 中提供了其他一些控制符,最常用的 3 个有:

setprecision()、setfill()、setw()

它们分别用来设置精度、填充字符和字段宽度。与前面讨论的控制符不同的是,这 3 个控制符带参数。setprecision() 控制符接受一个指定精度的证书参数;setfill() 控制符接受一个指定填充字符的 char 参数;setw() 控制符接受一个指定字段宽度的整数参数。由于它们都是控制符,因此可以使用 cout 语句连接起来。

例如:

cout << setw(6) << "N" << setw(14) << "square root" << endl;

 5. 使用 cin 进行输入 p611

cin 对象将标准输入表示为字节流。

抽取运算符 >> 将字符输入转换为指定类型的值。

例如,假设 staff_size 的类型为 int,则编译器将:

cin >> staff_size;

与下面的原型匹配:

istream & operator>>(int &);

对应于上述原型的函数将读取输入流发送给程序的字符流(假设字符为 2,3,1,8 和 4)。对于使用 2 字节 int 的系统来说,函数将把这些字符转换为整数 23184 的 2 字节二进制表示。如果 staff_size 的类型为 double,则 cin 将使用 operator>>(double &) 将上述输入转换为值 23184.0 的8字节浮点表示。

istream 类还为下列字符指针类型重载了 >> 抽取运算符:

  • signed char *;
  • char *;
  • unsigned char *;

对于这种类型的参数,抽取运算符将读取输入中的下一个单词,将它放置到指定的地址,并加上一个空值字符,使之成为一个字符串。

1)cin >> 如何检查输入 p612

不同版本的抽取运算符查看输入流的方法是相同的。它们跳过空白(空格、换行符和制表符),直到遇到非空白字符。即使对于单字符模式(参数类型为 char, unsigned char 或 signed char),情况也是如此。

2)流状态 p613

cin 或 cout 对象包含一个描述流状态(stream state)的数据成员(从 ios_bae 类继承而来)。流状态(被定义为 iostate 类型,而 iostate 是一种 bitmask 类型)由 3 个 ios_base 元素组成:eofbit, badbit 或 failbit,其中每个元素都是一位,可以是 1(设置)或 0(清除)。当 cin 操作到达文件末尾时,它将设置 eofbit;当 cin 操作未能读取到预期的字符时,它将设置 failbit。I/O 失败(如试图读取不可访问的文件或试图写入开启写保护的磁盘),也可能将 failbit 设置为1。在一些无法诊断的失败破坏流时,badbit 元素将被设置。当全部 3 个状态位都被设置为 0 时,说明一切顺利。

 

 

上表中的两种方法——clear() 和 setstate() 很相似。它们都重置状态,但采取的方式不同。

clear() 方法将状态设置为它的参数。下面的调用将使用默认参数 0,这将清楚全部 3 个状态位(eofbit,badbit 和 failbit):

clear();

同样,下面的调用将状态设置为 eofbit;即,eofbit 将被设置,另外两个状态位被清除:

clear(eofbit);

而 setdtate() 方法只影响其参数中已经设置的位。因此i,下面的调用将设置 eofbit,而不会影响其他位:

setstate(eofbit);

因此,如果 failbit 位被设置,则仍将被设置。

在输入不匹配或到达文件尾时,需要使用不带参数的 clear() 重新打开输入。

2)I/O 和异常 p614

假设某个输入函数设置了 eofbit,这是否会导致异常被引发呢?在默认情况下,答案是否定的。但可以使用 exceptions() 方法来控制异常如何被处理。

exceptions() 方法返回一个位字段,它包含 3 位,分别对应于 eofbit, failbit 和 badbit。

3)流状态的影响

只有在流状态良好的(所有的位都被清除)的情况下,下面的测试才返回 true:

while(cin >> input)

可以使用上表中的成员函数来判断流失败的可能的原因:

while (cin >> input)
{
    sum += input;
}
if (cin.eof())
    cout << "Loop terminated because EOF encountered\n";

设置流状态位有一个非常重要的后果:流将对后面的输入或输出关闭,直到位被清除

如果希望程序在流状态位被设置后能够读取后面的输入,就必须将流状态重置为良好。这可以通过 clear() 方法来实现:

while (cin >> input)
{
    sum += input;
}
cout << "Last value entered = " << input << endl;
cout << "Sum = " << sum << endl;
cout << "Now enter a new number: ";
cin.clear(); // reset stream state
while (!isspace(cin.get()))
    continue; // get rid of bad input
cin >> input; // will work now

注意,这还不足以重新设置流状态。导致输入循环终止的不匹配输入仍留在输入队列中,程序必须跳过它。

上述例子假设循环由于不恰当的输入而终止。现在假设循环是由于到达文件尾或者由于硬件故障而终止的。可以使用 fail() 方法检测假设是否正确,来修复问题。fail() 在 failbit 或 eofbit 被设置时返回 true。

while ( cin >> input)
{
sum += input;
}
cout << "Last value entered = " << input << endl;
cout << "Sum = " << sum << endl;
if (cin.fail() && !cin.eof()) // failed because of mismatched input
{
    cin.clear(); // reset stream state
    while (!isspace(cin.get()))
          continue; // get rid of bad input
}
else // else bail out
{
cout << "I cannot go on!\n";
exit(1);
}
cout << "Now enter a new number: ";
cin >> input; // will work now

4)其他 istream 类方法 p616

  • 方法 get(char&) 和 get(void) 提供不跳过空白的单字符输入功能;
  • 函数 get(char*, int, char) 和 getline(char*, int, char) 在默认情况下读取整行而不是一个单词。

它们被称为非格式化输入函数,因为它们只是读取字符输入,而不会跳过空白,也不进行数据转换。

get(char&) 成员函数返回一个指向用于调用它的 istream 对象的引用,因此可以拼接 get(char&) 后面的其他抽取:

char c1, c2, c3;
cin.get(c1).get(c2) >> c3;

get(void) 成员函数的返回类型为 int,因此不能够拼接:

char c1, c2, c3;
cin.get().get() >> c3; // not valid

到达文件尾后(不管是真正的文件尾还是模拟的文件尾),cin.get(void) 都将返回值 EOF——头文件 iostream 提供的一个符号常量。这种设计特性可以这样来读取输入:

int ch;
while ((ch = cin.get() ) != EOF)
{
    // process input
}

getline() 成员函数和 get() 的字符串读取版本都读取字符串,它们的函数特征标相同: p618

istream & get(char *, int, char);
istream & get(char *, int);
istream & getline(char *, int, char);
istream & getline(char *, int);

第一个参数适用于放置输入字符串的内存单元的地址。第二个参数比要读取的最大字符数大 1(额外一个字符用于存储结尾的空字符,以便将输入存储为一个字符串)。第三个参数指定用作分界符的字符,只有两个参数的版本将换行符用作分界符。上述函数都在读取最大数目的字符或遇到换行符后为止。

get() 和 getline() 之间的主要区别在于,get() 将换行符留在输入流中,这样接下来的输入操作首先看到的将是换行符,而 getline() 抽取并丢弃输入流中的换行符。

ignore() 成员函数接受两个参数:一个是数字,指定要读取的最大字符数;另一个是字符,用作输入分界符,例如,下面的函数调用读取并丢弃接下来的 255 个字符或直到到达的第一个换行符:

cin.ignore(255, '\n');

原型为两个参数提供的默认值为 1 和 EOF,该函数的返回类型为 istream &:

istream & ignore(int = 1, int = EOF);

该函数返回调用对象,因此能够拼接函数调用:

cin.ignore(255, '\n').ignore(255, '\n');

其中,第一个 ignore() 方法读取并丢弃一行,第二个调用读取并丢弃另一行,因此一共读取了两行。

意外字符串输入:

get(char *, int) 和 getline() 的某些输入形式将影响流状态。与其他输入函数一样,这两个函数在遇到文件尾时将设置 eofbit,遇到流被破坏(如设备故障)时将设置 badbit。另外两种特殊情况是无输入以及输入到达或超过函数调用指定的最大字符数。

对于上述两个方法,如果不能抽取字符,它们将把空值字符放置到输入字符串中,并使用 setstate() 设置 failbit。方法在什么时候无法抽取字符呢?一种可能的情况是输入方法立刻到达了文件尾。对于 get(char *, int) 来说,另一种可能是输入了一个空行:

char temp[80];
while (cin.get(temp, 80)) // terminates on empty line
    ...

空行并不会导致 getline() 设置 failbit。因为 getline() 仍将抽取换行符,虽然不会存储它。如果希望 getline() 在遇到空行时终止循环,则可以这样编写:

char temp[30];
while (cin.getline(temp, 80) && temp[0] != '\0') // terminates on empty line

现在假设输入队列中的字符数等于或超过了输入方法指定的最大字符数。首先来看 getline() 和下面的代码:

char temp[30];
while (cin.getline(temp, 30)

getline() 方法将从输入队列中读取字符,将它们放到 temp 数组的元素中,直到到达文件尾、将要读取的字符是换行符或存储了 29 个字符为止。如果遇到文件尾,则设置 eofbit;如果将要读取的字符是换行符,则该字符将被读取并丢弃;如果读取了 29 个字符,并且下一个字符不是换行符,则设置 failbit。因此,包含 30 个或更多字符的输入行将终止输入。

对于 get(char *, int) 方法,它首先测试字符数,然后测试是否为文件尾以及下一个字符是否是换行符。如果它读取了最大数目的字符,则不设置 failbit 标记。然而,由此可以知道终止读取是否是由于输入字符过多引起的。可以用 peek() 来查看下一个输入字符。如果它是换行符,则说明 get() 已经读取了整行;如果不是换行符,则说明 get() 是在到达行尾前停止的。这种技术对 getline() 不适用,因为 getline() 读取并丢弃换行符,因此查看下一个字符无法知道任何情况。然而,如果使用的是 get(),则可以知道是否读取了整个一行。

5)其他 istream 方法

read()

read() 函数读取指定数目的字节,并将它们存储在指定的位置中。例如,下面的语句从标准输入中读取 144 个字符,并将它们存储在 gross 数组中:

char gross[144];
cin.read(gross, 144);

与 getline() 和 get() 不同的是,read() 不会再输入后加上空值字符,因此不能将输入转换为字符串。read() 方法不是专为键盘输入设计的,它最常与 ostream write() 函数结合使用,来完成文件输入和输出。该方法的返回类型为 istream &,因此可以拼接。

peek()

peek() 函数返回输入中的下一个字符,但不抽取输入流中的字符。即,它能够查看下一个字符。假设要读取输入,直到遇到换行符或句点,则可以使用 peek() 查看输入流中的下一个字符,以此来判断是否继续读取。

char ch = cin.peek();

上述语句中,cin.peek() 查看下一个输入字符,并将它赋给 ch。

gcount()

gcount() 方法返回最后一个非格式化抽取方法读取的字符数。这意味着字符是由 get()、getline()、ignore() 或 read() 方法读取的,不是由抽取运算符(>>)读取的。抽取运算符对输入进行格式化,使之与特定的数据类型匹配。

putback() 函数将一个字符插入到输入字符串(输入流)中,被插入的字符将是下一条输入语句读取的第一个字符。putback() 方法接受一个 char 参数——要插入的字符,其返回类型为 istream &,这使得可以将该函数调用与其他 istream 方法拼接起来。使用 peek() 的效果相当于先使用 get() 读取一个字符,然后使用 putback() 将该字符放回到输入流中。

6.文件输入和输出

与标准输入输出相比,文件的管理更为复杂。例如,必须将新打开的文件和流关联起来。可以以只读模式、只写模式或读写模式打开文件。

1)简单的文件 I/O

要让程序写入文件,必须:

a.创建一个 ofstream 对象来管理输出流

b.将该对象与特定的文件关联起来

c.以使用 cout 的方式使用该对象,唯一的区别是输出将进文件,而不是屏幕。

当输入和输出流对象过期(如程序终止)时,到文件的连接将自动关闭。另外,也可以使用 close() 方法来显式地关闭到文件的连接:

fout.close(); // close output connection to file
fin.close(); // close input connection to file

关闭这样的连接并不会删除流,而只是断开流到文件的连接。然而,流管理装置仍被保留,例如,fin 对象与它管理的输入缓冲区仍然存在。可以将该流重新连接到同一个文件或另一个文件。

2)流状态检查和 is_open()

c++ 文件流类从 ios_base 类继承了一个流状态成员。该成员存储了指出流状态的信息:一切顺利、已到达文件尾、I/O 操作失败等。如果一切顺利,则流状态为 0。可以通过检查流状态来判断最后一个流操作是否成功,对于文件流,这包括检查试图打开文件时是否成功,例如,试图打开一个不存在的文件进行输入时,将设置 failbit 位,因此可以这样进行检查:

fin.open(argv[file]);
if (fin.fail()) // open attempt failed
{
...
}

ifstream 对象和 istream 对象一样,被放在需要 bool 类型的地方时,将被转换为 bool 值,因此可以这样做:

fin.open(argv[file]);
if (!fin)
{
...
}

较新的 c++ 提供了一种更好的检查文件是否被打开的方法—— is_open() 方法:

if (!fin.is_open()) // open attempt failed
{
...
}

使用该方式可以检测出其他方式不能检测出的一些问题:

if (fin.fail()) ... //  failed to open
if (!fin.good()) ... // failed to open
if (!fin) ... // failed to open

上面三种方式等价;然而,这些测试无法检测到这样一种情形:试图以不合适的文件模式打开文件时失败。方法 is_open() 能够检测到这种错误以及 good() 能够检测到的错误。

3)打开多个文件

如果同时需要打开多个文件,必须为每个文件创建一个流;如果要依次处理一组文件,可以打开一个流,并将它一次关联到各个文件。

4)命令行处理技术 p626

c++ 有一种让在命令行环境中运行的程序能够访问命令行参数的机制,方法是使用下面的 main() 函数:

int main(int argc, char * argv[])

argc 为命令行中的参数个数,其中包括命令行名本身。argv 变量为一个指针,它指向一个指向 char 的指针,可以将 argv 看作一个指针数组,其中的指针指向命令行参数,argv[0] 是一个指针,指向存储第一个命令行参数的字符串的第一个字符,依次类推。

假设有下面的命令行:

wc report1 report2 report3

则 argc 为 4,argv[0] 为 wc,argv[1] 为 report1,...。

5)文件模式

文件模式描述的是文件将被如何使用:读、写、追加等。将流与文件关联时,可以指定文件模式的第二个参数:

ifstream fin("banjo", model); // constructor with mode argument
ofstream fout();
fout.open("harp", mode2); // open() with mode arguments

ios_base 类定义了一个 openmode 类型,用于表示模式;与 fmtflags 和 iostate 类型一样,它也是一种 bitmask 类型。可以选择 ios_base 类中定义的多个常量来指定模式:

 

 

 

 

  • 追加文件 p629
     
  • 二进制文件 p630

文本格式指的是将所有内容(包括数字)都存储为文本;例如,以文本格式存储值 -2.324216e+07 时,将存储该数字包含的 13 个字符。这需要将浮点数的计算机内部表示转换为字符格式,这正是 << 运算符完成的工作。另一方面,二进制格式指的是存储 值的 计算机内部表示,即,计算机不是存储字符串,而是存储这个值的 64 位 double 表示。对于字符来说,二进制表示与文本表示是一样的,即字符的 ASCII 码的二进制表示。

考虑下面的例子:

const int LIM = 20;
struct planet
{
    char name[LIM]; // name of planet
    double population; // its population
    double g; // its acceleration of gravity
};
planet pl;

要将结构 pl 的内容以文本格式保存,可以这样做:

ofstream fout("planets.dat", ios_base::out | ios_base::app);
fout << pl.name << " " << pl.population << " " << pl.g << "\n";

要用二进制格式存储相同的信息,可以这样做:

ostream fout("planets.dat", ios_base::out | ios_base::app | ios_base::binary);
fout.write((char*) &pl, sizeof(pl));

上述代码使用计算机的内部数据表示,将整个结构作为一个整体保存。不能将该文件作为文本读取,但与文本相比,信息的保存更为紧凑、精确。这种方法做了两个修改:

a. 使用二进制文件模式

b. 使用成员函数 write()

要以二进制格式(而不是文本格式)存储数据,可以使用 write() 成员函数。这种方法将内存中指定数组的字节复制到文件中。但它只逐字节地复制数据,而不进行任何转换。例如,如果将一个 long 变量的地址传递给它,并命令它复制 4 个字节,它将复制 long 值中的 4 个字节,而不会将它转换为文本。唯一需要注意的是,必须将地址强制转换为指向 char 的指针

fout.write( (char*) &pl, sizeof pl);

这条语句导致程序前往 pl 结构的地址,并将开始的 36 个字节(sizeof(pl) 表达式的值)复制到与 fout 相关联的文件中。要使用文件恢复信息,需通过一个 ifstream 对象使用相应的 read() 方法

ifstream fin("planets.dat", ios_base::in | ios_base::binary);
fin.read( (char*) &pl, sizeof(pl));

这将从文件中复制 sizeof pl 个字节到 pl 结构中。同样的方法也适用于不适用虚函数的类。如果类有虚方法,则也将复制隐藏指针(该指针指向虚函数的指针表)。

注意,上述例子中,是否可以使用 string 对象而不是字符数组来表示 planet 结构的 name 成员?不可以,因为 string 对象本身实际上并没有包含字符串,而是包含一个指向其中存储了字符串的内存单元的指针,因此,将结构复制到文件中时,复制的将不是字符串数据,而是字符串的存储地址。

6)随机存取

fstream 类继承了两个方法:

seekg() 和 seekp(),前者将输入指针移到指定的文件位置,后者将输出指针移到指定的文件位置(事实上,由于 fstream 类使用缓冲区来存储中间数据,因此指针指向的是缓冲区中的位置,而不是实际的文件)。也可以将 seekg() 用于 ifstream 对象,将 seekp() 用于 ofstream 对象。下面是 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);

第一个原型定位到离第二个参数指定的文件位置特定距离(单位为字节)的位置;第二个原型定位到离文件开头特定距离(单位为字节)的位置。

在 seekg() 的第一个原型的参数中,streamoff 值被用来度量相对于文件特定位置的偏移量(单位为字节)。streamoff 参数表示相对于三个位置之一的偏移量为特定值(以字节为单位)的文件位置(类型可定义为整形或类)。seek_dir 参数是 ios_base 类中定义的另一种整形,有 3 个可能的值。常量 ios_base::beg 指相对于文件开始处的偏移量;常量 ios_base::cur 指相对于当前位置的偏移量;常量 ios_base::end 指相对于文件尾的偏移量。

在 seekg() 的第二个原型中,streampos 类型的值定位到文件中的第一个位置。它可以是类,但如果是这样的话,这个类将包含一个接受 streamoff 参数的构造函数和一个接受整型参数的构造函数,以便将两种类型转换为 streampos 值。streampos 值表示文件中的绝对位置(从文件开始处算起)。可以将 streampos 位置看作是相对于文件开始处的位置(以字节为单位,第一个字节的编号为 0)。因此下面的语句将文件指针指向第 112 个字节,这是文件中的第 113 个字节:

fin.seekg(112);

 如果要检查文件指针当前的位置,对于输入流,可以使用 tellg() 方法;对于输出流,可以使用 tellp() 方法。它们都返回一个表示当前位置的 streampos 值(以字节为单位,从文件开始处算起)。创建 fstream 对象时,输入指针和输出指针将一前一后地移动,因此 tellg() 和  tellp() 返回的值相同。如果使用 istream 对象来管理输入流,而是用 ostream 对象来管理同一个文件地输出流,则输入指针和输出指针将彼此独立地移动,因此 tellg() 和 tellp() 将返回不同的值。

 

7.内核格式化 p638

posted @ 2022-06-21 12:36  SanFranciscoo  阅读(254)  评论(0编辑  收藏  举报