第8章 I/O流

学习目标

★ 了解I/O流类库

★ 掌握标准I/O流

★ 掌握文件流及其使用

★ 了解字符串流及其使用

输入/输出(I/O)用于完成数据传输。C++语言支持两种I/O,一种是C语言中的I/O函数,另一种是面向对象的I/O流类库。本章将针对C++中I/O流类库及其使用进行详细讲解。

8.1 I/O流类库

I/O流类库是C++标准库的重要组成部分,它主要包括ios类库和streambuf类库。其中,ios类库提供流的高级I/O操作,streambuf类库主要负责缓冲区的处理,下面将分别介绍ios类库和streambuf类库。

8.1.1 ios类库

ios类库以ios类为基类,ios类是一个抽象类,提供了输入/输出所需的公共接口,如设置数据流格式、错误状态恢复、设置文件的输入/输出模式等。ios类库的层次结构如图8-1所示。

图8-1 ios类库的层次结构

由图8-1可知,抽象基类ios类派生了2个类,分别是istream类和ostream类,其中istream类是输入流类,ostream类是输出流类,它们定义了输入流和输出流的基本特性。istream类和ostream类又派生了多个类,具体介绍如下。

(1)ifstream类:文件输入流类,支持文件的读操作。

(2)istringstream类:字符串输入流类,支持字符串的输入操作。

(3)ofstream类:文件输出流类,支持文件的写操作。

(4)ostringstream类:字符串输出流类,支持字符串的输出操作。

(5)fstream类:文件输入/输出流类,支持文件的读写操作。

(6)stringstream类:字符串输入/输出流类,支持字符串的输入和输出操作。

8.1.2 streambuf类库

streambuf类库以streambuf类为基类,streambuf类是一个抽象类,提供了缓冲区操作接口,如设置缓冲区、从缓冲区提取字节、向缓冲区插入字节等。streambuf类库的层次结构如图8-2所示。

由图8-2可知,streambuf类派生了3个类,分别是stdiobuf类、filebuf类、stringstreambuf类。其中,stdiobuf类用于标准I/O缓冲区管理,filebuf类用于文件缓冲区管理,stringstreambuf类用于内存缓冲区管理。

图8-2 streambuf类库的层次结构

8.2 标准I/O流

标准输入流和标准输出流的一系列操作方法都是由istream和ostream两个类提供的,这两个类继承自抽象基类ios,它们预定义了标准输入流对象和标准输出流对象,并且提供了多种输入/输出方法。本节就针对标准输入流和标准输出流进行详细的讲解。

8.2.1 预定义流对象

C++提供了四个预定义流对象,包括cin、cout、cerr和clog。cin是istream类的对象,用于处理标准输入(键盘输入)。cout、cerr和clog是ostream类的对象,其中,cout用于处理标准输出(屏幕输出),cerr和clog用于处理标准错误信息。

cin与cout对象在1.3.2节已经详细讲解,这里不再赘述。cerr与clog用法相同,默认设备都是显示器。clog有缓冲区,而cerr没有缓冲,意味着cerr输出的信息会直接发送给屏幕,不会等到缓冲区填满或遇到换行符才输出错误信息。

关于四个预定义流对象的信息如表8-1所示。

表8-1 预定义流对象的信息对应设备

8.2.2 标准输出流

ostream类重载了“<<”运算符,输出流对象与“<<”运算符结合使用,可以输出各种类型的数据。此外,ostream类还提供了成员函数用于输出数据,比较常用的两个成员函数为put()函数和write()函数,下面分别进行讲解。

1.put()函数

put()函数用于输出单个字符。put()函数将字符插入输出流对象,通过输出流对象将字符输出到指定位置。其函数声明如下所示:

ostream& put(char ch); 

上述函数声明中,参数ch表示要输出的字符,函数返回值为ostream类对象引用。由于put()函数返回的是输出流对象,因此put()函数与输出运算符“<<”一样,可以连续调用。

下面调用put()函数输出单个字符,示例代码如下所示:

cout.put('a');      //输出字符'a' 
cout.put('\n');      //输出换行符 
cout.put('d').put('h');     //连续调用put()函数输出字符

上述代码中,前两行代码调用put()函数输出字符'a'和换行符,最后一行代码连续调用put()函数输出字符'd'和'h'。

2.write()函数

write()函数用于输出一个字符串。write()函数将指定个数的字符串插入输出流对象,通过输出流对象将字符串输出到指定位置。其函数声明如下所示:

ostream& write(const char* str, streamsize count); 

上述函数声明中,第一个参数str表示字符串;第二个参数count表示输出的字符个数。需要注意的是,stream size是long long类型的重定义。write()函数返回值为ostream类对象引用。与put()函数一样,write()函数也可以连续调用。

下面调用write()函数输出字符串,示例代码如下所示:

cout.write("I love China",6); 
cout.write("I love China",6).write("I love China",5

上述代码中,第一行代码调用write()函数输出字符串“I love China”的前6个字符;第二行代码连续调用write()函数输出字符串“I love China”的前6个和前5个字符。

8.2.3 标准输入流

istream类预定义了输入流对象cin,并且重载了“>>”运算符,输入流对象与“>>”运算符结合使用,可以输入各种类型的数据。此外,istream类还提供了成员函数用于输入数据,如get()函数、getline()函数、read()函数等,下面分别介绍这些函数。

1.get()函数

get()函数用于从输入流中读取单个字符或多个字符,istream类重载了多个get()函数。常用的重载形式有以下三种。

(1)第一种形式:

int get();

第一种重载形式的get()函数没有参数,返回值为int类型。get()函数的作用是从输入流读取一个字符,返回该字符的ASCII码值。

(2)第二种形式:

istream& get(char& ch); 

第二种重载形式的get()函数有一个char类型的引用作为参数,返回值为istream类对象引用。get()函数的作用是从输入流读取一个字符存储到字符ch中。

(3)第三种形式:

istream& get(char* dst, streamsize count,char delimiter);

第三种重载形式的get()函数有三个参数,其中dst为char类型的指针,指向一块内存空间;count表示读取的字符个数;delim iter表示结束符,默认是'\0'。get()函数的作用是从输入流中读取count−1个字符(最后一个字符要留给'\0'),存储到dst指向的内存空间。在读取过程中,遇到结束符就结束读取,即使没有读取够count−1个字符,遇到结束符之后也会结束读取,结束符不包含在读取的字符串内。如果读取了count−1个字符也没有遇到结束符,则在结束读取时,系统自动在字符串末尾添加'\0'。

下面通过案例演示get()函数的用法,如例8-1所示。

例8-1 get.cpp

 1  #include<iostream> 
 2  using namespace std; 
 3  int main() 
 4  { 
 5       char ch; 
 6       cout<<"请输入一个字符串:"<<endl; 
 7       cout<<"第一种形式:"<<cin.get()<<endl; 
 8       cin.get(ch); 
 9       cout<<"第二种形式:"<<ch<<endl; 
 10      char buf[20]; 
 11      cin.get(buf,6,' '); 
 12      cout<<"第三种形式:"<<buf<<endl; 
 13      return 0; 
 14 }

例8-1运行结果如图8-3所示。

在例8-1中,第7行代码通过cin调用第一种形式的get()函数(即无参数的get()函数),从输入流cin中读取一个字符,并将结果输出。第8~9行代码调用第二种形式的get()函数(即带有一个char类型参数的get()函数),从输入流cin中读取一个字符,存储到字符ch中,并输出ch的值。第10~12行代码调用第三种形式的get()函数(即带有三个参数的get()函数),从输入流cin中读取5(6−1)个字符存储到buf数组中,遇到空格结束读取。

图8-3 例8-1运行结果

由图8-3可知,当输入字符串“I LOVE CH INA!!!”时,第一种形式的get()函数调用输出结果为73,为字符'I'的ASCII码值;第二种形式的get()函数调用时,读取第二个字符,由于第二个字符是空格,因此输出结果为空;第三种形式的get()函数调用时,从第三个字符开始读取5个字符,遇到空格结束读取,因此第三种形式的get()函数调用读取到了4个字符。

2.getline()函数

getline()函数用于从输入流中读取字符,直到读取到指定长度的字符或遇到终止字符时结束读取。getline()有两种重载形式,具体如下。

(1)第一种形式:

istream& getline(char* dst, streamsize count); 

第一种重载形式的getline()函数有两个参数,第一个参数dst指向一块内存空间;第二个参数count表示读取的字符个数。getline()函数的作用是从输入流中读取count−1个字符存储到dst指向的内存空间。

(2)第二种形式:

istream& getline(char* dst, streamsize count, char delimiter);

第二种重载形式的getline()函数有三个参数,前两个参数与第一种形式的参数含义相同,第三个参数delim iter表示结束符。getline()函数的作用是从输入流中读取count−1个字符存储到dst指向的内存空间,遇到结束符就结束读取。

下面调用getline()函数读取一个字符串,示例代码如下所示:

char buf1[20],buf2[20]; 
cin.getline(buf1,20); 
cin.getline(buf2,20,’d’);   //从输入流中读取19个字符,遇到字符'd'结束读取

上述代码中,第一次调用getline()函数,表示从输入流中读取19个字符存储到buf1数组中。第二次调用getline()函数,表示从输入流中读取19个字符,在读取过程中,如果遇到字符'd'就结束读取。

3.read()函数

read()函数用于从输入流中读取指定字符个数的字符串,函数声明如下所示:

istream& read(char* dst, streamsize count); 

上述函数声明中,read()函数的参数与getline()函数的参数含义相同,只是read()函数没有结束符,直到读取count−1个字符才会结束读取。

read()函数在读取数据时,对读取到的字节序列不作任何处理。read()函数不会识别换行符、空格等特殊字符,遇到换行符'\n'也不会结束读取。

下面通过案例演示read()函数的用法,如例8-2所示。

例8-2 read.cpp

 1  #include<iostream> 
 2  using namespace std; 
 3  int main() 
 4  { 
 5       char buf[50]={0}; 
 6       cout<<"请输入一个字符串:"<<endl; 
 7       cin.read(buf,25); 
 8       cout<<"输出:"<<endl<<buf<<endl; 
 9       return 0; 
 10 }

例8-2运行结果如图8-4所示。

图8-4 例8-2运行结果

在例8-2中,第7行代码调用read()函数读取24个字符存储到buf数组中。第8行代码输出buf数组中的数据。

由图8-4可知,当换行输入两行字符串时,read()函数成功读取了两行字符串,并存储到buf数组中,在输出时成功输出了buf数组中的数据。在读取过程中,遇到换行符'\n',read()函数并没有结束读取。

多学一招:instream类的其他成员函数

除了get()函数、getline()函数和read()函数,istream类还提供了其他的成员函数,下面进行简单介绍。

1.ignore()函数

ignore()函数的作用是跳过输入流中的n个字符,函数声明如下所示:

istream& ignore(streamsize count, int delimeter); 

在上述函数声明中,参数count表示要跳过的字符个数,默认值是1;参数delimeter表示结束符,在跳跃过程中,如果遇到结束符就结束跳跃。ignore()函数不识别换行符、空格、制表符等特殊字符。

ignore()函数用法示例代码如下所示:

char buf[10]; 
cin.ignore(6, 'T');   //跳过前面6个字符,遇到字符'T'终止跳跃 
cin.getline(buf, 8);   //跳跃结束,读取7个字符并存储到buf数组中 
cout << buf << endl;   //输出buf数组中的数据 

2.gcount()函数

gcount()函数的作用是计算上一次读取到的字符个数,函数声明如下所示:

streamsize gcount() const; 

gcount()函数的声明与用法很简单,其用法示例代码如下所示:

char buf[50]={0}; 
cin.getline(buf,20);   //读取字符串并存储到buf数组中 
int count=cin.gcount();   //统计上次读取的字符个数 

3.peek()函数

peek()函数的作用是检测输入流中待读取的字符,函数声明如下所示:

int peek();

在上述函数声明中,peek()函数没有参数,返回值为int类型,即返回检测字符的ASCII码值。peek()函数只是检测待读取的字符,但并不会真正读取它。

peek()函数用法示例代码如下所示:

char ch=cin.peek();   //检测字符 
cout<<ch<<endl; 
cin.get(ch);     //读取字符 
cout<<ch<<endl; 

运行上述代码,两次输出的ch值是一样的,表明peek()函数并没有真正去读取检测到的字符。如果peek()函数读取了检测的字符,输入流自动向后移动一个位置(字符),则调用get()函数读取时会读取到输入流中下一个字符,两次输出的ch会不同。

4.putback()函数

putback()函数的作用是将上一次读取的字符放回输入流中,使之可被下一次读取,函数声明如下所示:

istream& putback(char ch);

在上述函数声明中,参数ch是上一次通过get()函数或getline()函数读取的字符。putback()函数是将字符ch重新放回输入流中。

putback()函数用法示例代码如下:

char ch=cin.get();    //读取字符 
cout<<ch<<endl; 
cin.putback(ch);    //将字符重新放回输入流 
cout<<cin.get()<<endl;   //再次读取 

8.3 文件流

文件流是以磁盘中的文件作为输入、输出对象的数据流。输出文件流将数据从内存输出到文件中,这个过程通常称为写文件;输入文件流将数据从磁盘中的文件读入内存,这个过程通常称为读文件。本节将针对文件流进行详细讲解。

8.3.1 文件流对象的创建

在C++中,要进行文件的读写操作,首先必须建立一个文件流对象,然后把文件流对象与文件关联起来(打开文件)。文件流对象与文件关联之后,程序就可以调用文件流类的各种方法对文件进行操作了。

C++提供了三个类支持文件流的输入、输出,这三个类都包含在fstream头文件中。

文件流不像标准I/O流预定义了输入流对象和输出流对象,使用文件流时,需要创建文件流对象。创建文件流对象时,可以调用文件流类的无参构造函数,也可以调用文件流类的有参构造函数,具体如下所示。

(1)调用无参构造函数创建文件流对象。

ifstream类、ofstream类和fstream类都提供了默认无参构造函数,可以创建不带参数的文件流对象,示例代码如下所示:

ifstream ifs;    //定义一个文件输入流对象 
ofstream ofs;    //定义一个文件输出流对象 
fstream fs;      //定义一个文件输入、输出流对象

(2)调用有参构造函数创建文件流对象。

ifstream类、ofstream类和fstream类也提供了有参构造函数,在创建文件流对象时可以指定文件名和文件打开模式,示例代码如下所示:

ifstream ifs("filename",ios::in);  
ofstream ofs("filename",ios::out);  
fstream fs("filename",ios::in|ios::out

ifstream类默认文件打开模式为ios::in,ofstream类默认文件打开模式为ios::out,fstream类默认文件打开模式为ios::in|ios::out。

8.3.2 文件的打开与关闭

文件最基本的操作就是打开和关闭,在对文件进行读写之前,需要先打开文件;读写结束之后,要及时关闭文件。下面将针对文件的打开与关闭进行讲解。

1. 打开文件

C++提供了两种打开文件的方式:第一种方式是调用文件流类的构造函数;第二种方式是调用文件流类的成员函数open()。第一种调用文件流类的构造函数打开文件方式就是在创建文件流对象时传入文件名和文件打开模式,这种方式在8.3.1节已经讲解。下面主要讲解第二种调用open()函数打开文件的方式。

ifstream类、ostream类和fstream类都提供了成员函数open()用于打开文件,open()函数声明如下所示:

void open(const char* filename, int mode); 

在上述函数声明中,参数filenam e表示要打开的文件;参数m ode表示文件打开模式。如果文件打开失败,则文件流对象的值为0。

文件打开模式就是指以什么方式打开文件,如只读模式、只写模式等。C++常用的文件打开模式及含义如表8-2所示。

表8-2  C++常用的文件打开模式及含义含义

文件打开模式可以通过位或运算符“|”组合使用,示例代码如下所示:

ofstream ofs;           //创建文件流对象 
ofs.open("Hello.txt", ios::in|ios::out|ios::binary);  //多种打开模式组合使用

2. 关闭文件

文件使用完毕之后,要及时关闭。关闭文件就是解除文件与文件流的关联,释放缓冲区和其他资源的过程。ifstream类、ostream类和fstream类都提供了成员函数close()用于关闭文件,close()函数声明如下所示:

void close();

close()函数没有参数和返回值,用法也很简单,直接通过文件流对象调用close()函数就可以关闭文件。为了加深读者的理解,下面通过案例演示文件的打开与关闭。首先在项目根目录下创建文本文件hello.txt,然后编写代码,调用open()函数打开hello.txt文件,再调用close()函数关闭hello.txt文件,代码如例8-3所示。

例8-3 open_close.cpp

 1  #include<iostream> 
 2 #include<fstream>       //包含fstream文件 
 3  using namespace std; 
 4  int main() 
 5  { 
 6       ifstream ifs;        //创建输入流对象 
 7       ifs.open("hello.txt", ios::in);   //以只读方式打开hello.txt 
 8      if(!ifs)        //判断文件打开是否成功 
 9            cout << "文件打开失败" << endl; 
 10      else 
 11           cout << "文件打开成功" << endl; 
 12      ifs.close();        //关闭文件 
 13      return 0; 
 14 }

例8-3运行结果如图8-5所示。

图8-5 例8-3运行结果

在例8-3中,第6行代码创建ifstream类对象ifs。第7行代码通过对象ifs调用open()函数打开hello.txt文件。第8~11行代码判断文件打开是否成功,如果文件打开失败,就输出“文件打开失败”;如果文件打开成功,就输出“文件打开成功”。第12行代码通过对象ifs调用close()函数关闭文件。由图8-5可知,hello.txt文件打开成功。

8.3.3 文本文件的读写

对文本文件的读写,C++提供了两种方式,第一种方式是使用提取运算符“>>”和插入运算符“<<”,第二种方式是调用文件流类的成员函数。下面分别介绍这两种读写方式。

1. 使用提取运算符“>>”和插入运算符“<<”读写文件

istream类重载了“>>”运算符,ifstream类继承了istream类,也继承了“>>”运算符;ostream类重载了“<<”运算符,ofstream继承了ostream类,也继承了“>>”运算符。istream类和ostream类共同派生了iostream类,而fstream类又继承自iostream类,因此fstream类同时继承了“>>”运算符和“<<”运算符。

由上述继承关系可知,文件流对象也可以使用“>>”运算符和“<<”运算符传输数据,实现文本文件的读写。

下面通过案例演示使用“>>”运算符和“<<”运算符读写文本文件,如例8-4所示。

例8-4 operator.cpp

 1  #include<iostream> 
 2  #include<fstream> 
 3  using namespace std; 
 4  int main() 
 5  { 
 6       //创建文件流对象,并以只写模式打开hello.txt文件 
 7       ofstream ofs("hello.txt",ios::out);  
 8      if(!ofs)        //判断文件打开是否成功 
 9       { 
 10           cout<<"写文件时,文件打开失败"<<endl; 
 11           exit(0)
 12      } 
 13      cout<<"请输入要写入文件的数据:"<<endl; 
 14      char str[1024]={0};      //定义数组str 
 15    cin>>str;        //从键盘输入数据并存储到str数组 
 16    ofs<<str;        //将str数组中数据写入文件 
 17      cout<<"文件写入成功"<<endl; 
 18      ofs.close();        //关闭文件 
 19      //创建文件流对象,并以只读模式打开hello.txt文件 
 20      ifstream ifs("hello.txt",ios::in); 
 21      if (!ifs)        //判断文件是否打开成功 
 22      { 
 23           cout<<"读文件时,文件打开失败"<<endl; 
 24           exit(0); 
 25      } 
 26      char buf[1024]={0};      //定义数组buf 
 27    ifs>>buf;        //将文件内容读入buf 
 28      cout<<"文件读取成功,内容如下:"<<endl; 
 29      cout<<buf<<endl;      //输出buf数组中的数据 
 30      ifs.close();        //关闭文件 
 31      return 0; 
 32 }

例8-4运行结果如图8-6所示。

图8-6 例8-4运行结果

在例8-4中,第7行代码创建了ofstream类对象ofs,并以只写模式打开项目根目录下的hello.txt文件。第14~15行代码定义字符数组str,并通过预定义流对象cin从键盘输入数据并存储到str数组中。第16行代码使用对象ofs和“<<”运算符将str数组中的数据写入文件中。第18行代码调用close()函数关闭文件。

第20行代码创建了ifstream类对象ifs,并以只读模式打开hello.txt文件。第26行代码定义了字符数组buf。第27行代码使用对象ifs和“>>”运算符将文件中的数据读取到buf数组中。第29~30行代码输出buf数组中的数据,并关闭文件。

由图8-6可知,当从键盘输入字符串“nihao”时,程序将数据成功写入文件中;从文件中读取数据时,也成功读取到了“nihao”字符串。

2. 调用文件流类的成员函数读写文件

文件流类继承了istream类和ostream类的成员函数get()、getline()和put(),通过调用这些成员函数,文件流对象也可以完成文件的读写。下面就通过案例演示调用文件流类的成员函数实现文件的读写,如例8-5所示。

例8-5 memfunc.cpp

 1  #include<iostream> 
 2  #include<fstream> 
 3  using namespace std; 
 4  int main() 
 5  { 
 6       //创建文件流对象,并以只写模式打开hello.txt文件 
 7       ofstream ofs("hello.txt",ios::out);  
 8       if(!ofs)        //判断文件打开是否成功 
 9       { 
 10           cout<<"写文件时,文件打开失败"<<endl; 
 11           exit(0); 
 12      } 
 13      cout<<"将26个字母写入文件"<<endl; 
 14      char ch1='a'; 
 15      for(int i=0;i<26;i++) 
 16      { 
 17           ofs.put(ch1+i);     //调用put()函数将字母写入文件 
 18      } 
 19      cout<<"文件写入成功"<<endl; 
 20      ofs.close();       //关闭文件 
 21      //创建文件流对象,并以只读模式打开hello.txt文件 
 22      ifstream ifs("hello.txt",ios::in); 
 23      if (!ifs)        //判断文件是否打开成功 
 24      { 
 25           cout<<"读文件时,文件打开失败"<<endl; 
 26           exit(0); 
 27      } 
 28      cout<<"文件读取成功,内容如下:"<<endl; 
 29      char ch2; 
 30      ifs.get(ch2);       //调用get()函数将字母读取到ch2变量中 
 31      while(!ifs.eof())      //循环读取剩余字母 
 32      { 
 33           cout<<ch2;      //输出ch2的值 
 34           ifs.get(ch2); 
 35      } 
 36      ifs.close();       //关闭文件 
 37      return 0; 
 38 }

例8-5运行结果如图8-7所示。

图8-7 例8-5运行结果

在例8-5中,第7行代码创建了ofstream类对象ofs,并以只写模式打开项目根目录下的hello.txt文件。第14~18行代码通过for循环调用put()函数将26个小写字母写入文件。第20行代码调用close()函数关闭文件。

第22行代码创建了ifstream类对象ifs,并以只读模式打开hello.txt文件。第29~35行代码通过while循环调用get()函数读取文件中的数据。第36行代码调用close()函数关闭文件。由图8-7可知,程序成功将26个小写字母写入了文件,并成功读取了文件中的数据。

在例8-5中的程序中调用get()函数从文件中读取数据,读者也可以调用get()函数的其他重载形式或调用getline()函数读取文件中的数据。

多学一招:文件流类的其他成员函数

除了读写函数,文件流类还提供了其他成员函数,如eof()、bad()等,这些函数一般用于处理文件读写过程中的错误。下面介绍几种常用的错误处理函数。

1.eof()函数

eof()函数用于检测文件是否到达末尾,函数声明如下所示:

bool eof() const;

在读文件时,如果文件到达末尾,eof()函数会返回true。例8-5中的第31行代码就调用了eof()函数判断文件读取是否到达末尾,这里不再针对其用法举例。

2.bad()函数

bad()函数用于检测文件在读写过程中是否出错,函数声明如下所示:

bool bad() const;

如果文件读写出错,bad()函数会返回true。例如,对一个打开模式为ios::in的文件进行写入操作,或者写入的设备没有足够空间,调用bad()函数会返回true。

bad()函数的用法示例代码如下:

fstream fs("a.txt",ios::in);  //创建fstream类对象,以只读模式打开a.txt 
fs.put('a');     //向文件中写入字符'a' 
if(fs.bad())     //调用bad()函数检测,写入过程是否出错 
 cout<<"文件写入失败"<<endl; 

3.fail()函数

fail()函数也用于检测在读写过程中是否出错,函数声明如下所示:

bool fail() const;

fail()函数比bad()函数的检错范围更广泛。文件到达末尾,或者读写过程没有达到预期条件,例如,想要读取一个整数却获得了一个字母时,fail()函数都能检测到并返回true。

fail()函数的用法示例代码如下:

fstream fs("a.txt",ios::in);  //创建fstream类对象,以只读模式打开a.txt 
cout<<fs.get()<<endl;   //读取字符 
if(fs.fail())     //如果文件不存在或到达末尾,则读取失败 
 cout<<"文件读取失败"<<endl; 

4.good()函数

good()函数用于检测文件流状态和文件读写过程是否正常,函数声明如下所示:

bool good() const; 

如果文件流状态、文件读写过程没有错误,good函数就返回true。它的作用与bad()函数、fail()函数相反,对于bad()函数、fail()函数返回true的情况,good()函数会返回false。

good()函数的用法示例代码如下:

fstream fs("a.txt",ios::in);  //创建fstream类对象,以只读模式打开a.txt 
cout<<fs.get()<<endl;   //读取字符 
if(fs.good())     //如果没有错误,good()函数返回true 
 cout<<"文件读取成功"<<endl;

5.clear()函数

clear()函数用于清除文件流的错误状态,即重置文件流的状态标志位,函数声明如下所示:

void clear(iostate state= goodbit);

在上述函数声明中,参数state表示流的状态,默认值为goodbit(值为0)。在文件读写过程中,调用close()关闭文件之后,如果再使用文件流对象打开其他文件,一般先调用clear()函数将文件流对象的标志位重新初始化。

clear()函数的用法示例代码如下:

ofstream ofs; 
ofs.open("file1");    //打开file1文件 
ofs.close();     //关闭file1文件 
ofs.clear();     //调用clear()函数重置文件流的状态标志位 
ofs.open("file2");    //关闭file1文件后,再打开file2文件 
ofs.close();

8.3.4 二进制文件的读写

文件流类从istream类和ostream类分别继承了write()函数和read()函数,这两个函数可以用来读写二进制文件。write()函数与read()函数的声明与用法在8.2.2节和8.2.3节已经讲解,这里不再赘述。

下面通过案例演示二进制文件的读写,如例8-6所示。

例8-6 binary.cpp

 1  #include<iostream> 
 2  #include<fstream> 
 3  using namespace std; 
 4  struct Student     //定义学生结构体Student 
 5  { 
 6       char name[20];    //姓名 
 7       int age;      //年龄 
 8       char sex;      //性别 
 9  }; 
 10 int main() 
 11 { 
 12      Student stus[3];   //定义学生结构体数组,大小为3 
 13      cout << "请输入3个学生的信息:\n(姓名 年龄 性别)" << endl; 
 14      for(int i = 0; i < 3; i++) //通过for循环从键盘输入学生信息 
 15      { 
 16           cin >> stus[i].name>>stus[i].age>>stus[i].sex; 
 17      } 
 18      //创建输出文件流对象,以二进制、写入模式打开student.dat二进制文件 
 19      ofstream ofs("student.dat", ios::out | ios_base::binary); 
 20      if(!ofs) 
 21      { 
 22           cerr << "写入时,文件打开失败" << endl; 
 23           exit(0); 
 24      } 
 25      //通过for循环将3个学生的信息写入文件  
 26      for(int i = 0; i < 3; i++) 
 27      { 
 28           //调用write()函数写入数据,每次写入一个学生信息 
 29           ofs.write(reinterpret_cast<char*>(&stus[i]), sizeof(stus[i])); 
 30           ofs.flush(); 
 31      } 
 32      ofs.close();     //关闭文件 
 33      cout << "写入成功" << endl << "读取文件:" << endl; 
 34      //创建输入文件流对象,以只读和二进制模式打开student.txt文件 
 35      ifstream ifs("student.dat", ios::in | ios_base::binary); 
 36      if(!ifs) 
 37      { 
 38           cout << "读取时,文件打开失败" << endl; 
 39           exit(0); 
 40      } 
 41      Student stus1[3];    //定义学生结构体数组存储读取的文件内容 
 42      for(int i = 0; i < 3; i++) 
 43      { 
 44           //调用read()函数读取数据,每次读取一个学生信息 
 45           ifs.read(reinterpret_cast<char*>(&stus1[i]), sizeof(stus1[i])); 
 46           cout << stus1[i].name << " " << stus1[i].age  
 47                << " " << stus1[i].sex << endl; 
 48      } 
 49      ifs.close();     //关闭文件 
 50      return 0; 
 51 }

例8-6运行结果如图8-8所示。

在例8-6中,第4~9行代码定义了学生结构体Student,该结构体包含姓名、年龄、性别3个成员。第12~17行代码定义了Student结构体数组stus,大小为3,并通过键盘输入3个学生信息。第19~32行代码创建ofstream类对象ofs,以ios::out|ios::binary模式打开项目根目录下的student.dat文件。在for循环中通过调用write()函数将stus数组中3个学生的信息写入文件,并关闭文件。

第35~49行代码创建ifstream类对象ifs,以ios::in|ios::binary模式打开student.dat文件。在for循环中通过调用read()函数将文件中数据读取到学生结构体数组stus1中,并关闭文件。

由图8-8可知,从键盘输入3个学生的信息后,程序通过调用write()函数成功将3个学生的信息写入student.dat文件中,然后通过调用read()函数成功读取了文件中的数据。

图8-8 例8-6运行结果

需要注意的是,在例8-6中,每次读写一个结构体变量,由于read()函数和write()函数的第一个参数为char*类型,因此在传递参数时,需要调用reinterpret_cast<>运算符将Student结构体变量的地址转换成char*类型。

8.3.5 文件随机读写

在C语言中实现文件的随机读写要依靠文件位置指针,在C++中文件的随机读写也是通过移动文件位置指针完成的。

C++文件流类提供了设置文件位置指针的函数。ifstream类提供了tellg()函数、seekg()函数,这两种函数声明分别如下所示:

streampos tellg();   
istream& seekg(streampos);   
istream& seekg(streamoff, ios::seek_dir)

在上述函数声明中,tellg()函数用于返回文件位置指针的位置。seekg()函数用于设置文件位置指针的位置,即移动文件位置指针,它有两种重载形式。第一种重载形式有一个参数stream pos,表示文件位置指针从文件开头移动stream pos长度的距离;第二种重载形式有两个参数,第一个参数stream off表示文件位置指针的移动距离,第二个参数ios::seek_dir表示参照位置。

ios::seek_dir有以下三个取值。

注意:

streampos和streamoff是C++标准库重定义的数据类型。

tellg()函数与seekg()函数的用法示例代码如下:

ifstream ifs("a.txt");  //创建输入文件流对象ifs 
ifs.tellg();    //调用tellg()函数获取文件位置指针 
ifs.seekg(20);   //将文件位置指针移动20个字节 
ifs.seekg(-20,ios::end); //将文件位置指针从文件末尾处向前移动20个字节 

ofstream类提供了tellp()函数、seekp()函数用于移动文件位置指针,这两种函数声明分别如下所示:

streampos tellp();   
ostream& seekp(streampos);   
ostream& seekp(streamoff, ios::seek_dir

tellp()函数与tellg()函数的含义与用法相同,seekp()函数与seekg()函数的含义与用法相同。fstream类拥有上述所有函数,即拥有tellg()函数、seekg()函数、tellp()函数和seekp()函数。为了加深读者对这两组函数的理解,下面通过案例演示文件的随机读写,如例8-7所示。

例8-7 random.cpp

 1  #include<iostream> 
 2  #include<fstream> 
 3  using namespace std; 
 4  int main( )  
 5  { 
 6       //创建输出文件流对象ofs 
 7       ofstream ofs("random.dat",ios::out|ios::binary); 
 8       if(!ofs) 
 9       { 
 10           cout<<"写入文件时,文件打开失败"<<endl; 
 11           exit(0); 
 12      } 
 13      //输出文件位置指针的位置 
 14      cout<<"文件打开时,文件位置指针位置:"<<ofs.tellp()<<endl; 
 15      cout<<"请输入数据:"<<endl; 
 16      char buf[1024]={0};    //定义字符数组buf 
 17      cin.getline(buf,1024,'/');  //将从键盘输入的数据存储到buf中 
 18      ofs.write(buf,30);     //调用write()函数将buf中的数据写入文件 
 19      //写入完成之后,输出文件位置指针的位置 
 20      cout<<"写入完成后,文件位置指针位置:"<<ofs.tellp()<<endl; 
 21      ofs.seekp(-10,ios::end);   //移动文件位置指针 
 22      //移动之后,输出文件位置指针的位置 
 23      cout<<"移动之后,文件位置指针位置:"<<ofs.tellp()<<endl; 
 24      return 0; 
 25 }

例8-7运行结果如图8-9所示。

图8-9 例8-7运行结果

在例8-7中,第7行代码创建了输入文件流对象ofs,并以ios::out|ios::binary模式打开文件random.dat。第14行代码调用tellp()函数获取文件位置指针的位置。第16~18行代码定义字符数组buf,将从键盘输入的数据存储到buf中,并调用write()函数将buf中的数据写入文件中。第20行代码调用tellp()函数再次输出文件位置指针的位置。第21~23行代码调用seekp()函数将文件位置指针从文件末尾向前移动10个字节,然后再次调用tellg()函数输出文件位置指针的位置。

由图8-9可知,刚打开文件时,文件位置指针的位置为0,即文件位置指针在文件开头;向文件写入30个字符之后,文件位置指针的位置为30,表明文件位置指针指向文件末尾;调用seekp()函数移动文件位置指针之后,文件位置指针的位置为20(相对于文件开头),表明seekp()函数调用成功。

8.4 字符串流

字符串流是以string对象为输入/输出对象的数据流,这些数据流的传输在内存中完成,因此字符串流也称为内存流。C++提供了istringstream、ostringstream和stringstream这三个类支持string对象的输入/输出。这三个类都由istream类和ostream类派生而来,因此它们都可以使用“>>”运算符和“<<”运算符,以及istream类和ostream类的成员函数。下面简单介绍这三个类。

1.istringstream类

istringstream是输入字符串流类,用于实现字符串对象的输入。istringstream类的构造函数有三个,具体如下所示:

istringstream(openmode=ios_base::in);  
istringstream(const string& str, openmode=ios_base::in);  
istringstream(istringstream&& x)

第一个构造函数带有一个默认参数openm ode,表示流的打开方式,默认值为ios_base::in。第二个构造函数带有两个参数:第一个参数str为string对象的常引用;第二个参数openm ode是流的打开模式,默认值为ios_base::in。第三个构造函数为移动构造函数。

istringstream类的一个典型用法就是将一个数字字符串转换成对应的数值。相比于C语言的数字和字符串转换函数,istringstream类具有模板亲和性,转换效率更高而且更安全。

下面通过案例演示如何将数字字符串转换成对应的数值,如例8-8所示。

例8-8 istringstream.cpp

 1  #include<iostream> 
 2  #include<sstream> 
 3  using namespace std; 
 4  //定义模板函数:将一个数字字符串转换成对应的数值 
 5  template<class T> 
 6  inline T swapString(const string &str) 
 7  { 
 8       istringstream istr(str);     //创建istringstream类对象istr 
 9    T t;         //定义变量t 
 10     istr>>t;        //将对象istr中的数据输入t中 
 11    return t;        //返回t 
 12 } 
 13 int main( )  
 14 { 
 15      int num=swapString<int>("10");   //将字符串"10"转换成数值10 
 16      cout<<"num="<<num<<endl; 
 17      double d=swapString<double>("3.14");  //将字符串"3.14"转换成数值3.14 
 18      cout<<"d="<<d<<endl; 
 19      float f=swapString<float>("abc");    //将字符串"abc"转换成float类型 
 20      cout<<"f="<<f<<endl; 
 21      return 0; 
 22 }

例8-8运行结果如图8-10所示。

图8-10 例8-8运行结果

在例8-8中,第5~12行代码定义了模板函数swapString(),用于将字符串转换为对应的数值。第15~16行代码,调用swapString()函数将字符串“10”转换为int类型数据10,并输出。第17~18行代码,调用swapString()函数将字符串“3.14”转换为double类型数据,并输出。第19~20行代码,调用swapString()函数将字符串“abc”转换为float类型数据,并输出。由图8-10可知,前两次转换均成功,最后一次将字符串“abc”转换为float类型数据失败,转换失败结果为0。

需要注意的是,istringstream类、ostringstream类和stringstream类都包含在sstream头文件中,使用这三个类时要包含sstream头文件。

2.ostringstream类

ostringstream是输出字符串流类,用于实现字符串对象的输出。ostringstream类也提供了三个构造函数,分别如下所示:

ostringstream(openmode=ios_base::out); 
ostringstream(const string& str,openmode=ios_base::out); 
ostringstream(ostringstream&& x)

ostringstream类构造函数的参数含义与istringstream类构造函数的参数含义相同。除了构造函数,ostringstream类还提供了一个比较常用的成员函数str()。str()函数用于获取ostringstream流缓冲区中的内容副本,函数声明如下所示:

string str() const;

str()函数获取ostringstream流缓冲区的数据后,并不对数据进行修改,获取的数据可以存储到一个string对象中。

下面通过案例演示ostringstream类和str()函数用法,如例8-9所示。

例8-9 ostringstream.cpp

 1  #include<iostream> 
 2  #include<sstream> 
 3  using namespace std; 
 4  int main( )  
 5  { 
 6       ostringstream ostr;   //创建ostringstream类对象ostr 
 7       string str;    //创建string类对象str 
 8       cout<<"请输入一个字符串"<<endl;  
 9       getline(cin,str);    //调用getline()函数从键盘为str输入内容 
 10      ostr<<str;     //将str内容插入到ostr类对象中 
 11      string result=ostr.str(); //调用str()成员函数获取ostr对象缓冲区内容 
 12      cout<<result<<endl;   //输出获取的内容 
 13      return 0; 
 14 } 

例8-9运行结果如图8-11所示。

图8-11 例8-9运行结果

在例8-9中,第6行代码创建了ostringstream类对象ostr。第7~9行代码创建了string类对象str,并从键盘输入数据存储到str中。第10行代码使用插入运算符“<<”将str中的数据插入类对象ostr中。第11~12行代码,通过对象ostr调用str()函数获取对象ostr的缓冲区内容并输出。由图8-11可知,程序成功获取了对象str中的内容并输出。

3.stringstream类

stringstream类是输入/输出字符串流类,可同时实现字符串对象的输入/输出。stringstream类也提供了三种形式的构造函数,分别如下所示:

stringstream(openmode=ios_base::in|ios_base::out); 
stringstream(const string& str,openmode=ios_base::in|ios_base::out); 
stringstream(stringstream&& x)

stringstream类构造函数的参数含义与istringstream类构造函数的参数含义相同。stringstream类包括了istringstream类与ostringstream类的功能,这里不再举例演示。

小提示:字符串流类

在C++98标准之前,C++使用istrstream、ostrstream和strstream三个类完成string对象的输入/输出,但从C++98标准开始,这三个类被弃用了,取而代之的是istringstream类、ostringstream类和stringstream类。

8.5 本章小结

本章主要讲解了C++中I/O流的相关知识。首先介绍了I/O流类库;其次讲解了标准I/O流,包括预定义流对象、标准输出流和标准输入流;然后讲解了文件流,包括文件流对象的创建、文件的打开与关闭、文本文件的读写、二进制文件的读写和文件的随机读写;最后介绍了字符串流。熟练掌握C++中I/O流的使用,对编程非常重要。

8.6 本章习题

一、填空题

1. 在ios类库中,基类ios直接派生了两个类,分别是___和___。

2.C++预定义的四个流对象中,不带缓冲的流对象是___。

3.C++中的输入/输出流可以分为标准I/O流、___和___三类。

4.ifstream类默认的文件打开模式为___。

二、判断题

1. 在C++流类库中,ios根基类是一个抽象类。( )

2.ostream类提供的成员函数,如put()、write()函数等,无法连续调用。( )

3.istream类提供的成员函数getline(),默认以‘\0’作为结束符。( )

4. 文件流对象无法使用“>>”“<<”运算符传输数据。( )

5.C++不支持文件的随机读写。( )

三、选择题

1. 下列选项中,不属于刷新缓冲区的方式的是( )。

A.执行flush()函数

B.执行endl语句

C.关闭文件

D.等上5 s时间编译器自动刷新

2. 关于标准输入/输出流,下列说法中错误的是( )。

A.输出流提供的put()函数用于单个字符的输出

B.write()函数一次可以输出一个字符串

C.输入流提供的get()函数在遇到'\n'时会结束读取

D.getline()一次可以读取一个字符串

3. 下列选项中,只能用于检测文件读取操作的函数是( )。

A.fail()

B.eof()

C.bad()

D.good()

4. 下列选项中,可以清除输入流错误状态的函数是( )。

A.clear()

B.fail()

C.put()

D.eof()

5. 下列选项中,构建的文件流对象正确的是( )(多选)。

A.ifstream ifs;

B.ifstream ifs("filenam e");

C.ofstream ofs;

D.fstream fs("filenam e");

6. 关于文件的打开与关闭,下列说法中错误的是( )。

A.ifstream类、ostream类和fstream类都提供了成员函数open()用于打开文件

B.ifstream类打开文件的默认方式是ios::in,ostream类打开文件的默认方式是ios::out

C.文件流使用完毕后,析构函数会自动释放资源,不用手动调用close()函数关闭文件

D.文件的打开方式可以组合使用

四、简答题

1. 简述一下C++预定义的四个流对象之间的区别。

2. 简述一下getline()函数与read()函数的区别。

五、编程题

1. 请编写程序实现以下功能:从键盘输入若干个字符串,统计长度最长的字符串,并输出最长字符串的内容和长度。

2. 有一个文本文件data.txt,它记录了几个学生的信息,内容如下:

姓名 学号 性别 年龄

张三 1001 男 22

李明 1002 男 21

李红 1003 女 22

李明 1004 女 20

请编写一个程序,从文本文件data.txt中找出所有叫“李明”的学生,并输出他们的信息。