C++ STL 基础及应用(4) 输出输出流

在 C++ 的标准模板库中提供了一组模板类来支持面向对象的数据的输入输出功能,如基本的输入输出流 istream类/ostream类,文件输入输出流 ifstream类/ofstream类/fstream类,字符串输入输出流 stringstream类/istringstream类/ostringstream类等。C++ I/O 还可以对对象进行输入输出操作,这些都是 C 所不具备的。这些流都位于名词空间 std 内,一般都是以 stream为后缀。它们在数据流的处理上具有灵活、完善和强大的功能。

本章将介绍标准输入输出流、文件输入输出流和字符串输入输出流的相关使用。

各流类之间的继承关系

无需多说,上图就什么都清楚了。


图一:各流类之间的继承关系

以及给出必要的定义:

typedef basic_ios<char, char_traits<char> > ios;
typedef basic_streambuf<char, char_traits<char> > streambuf;
typedef basic_istream<char, char_traits<char> > istream;
typedef basic_ostream<char, char_traits<char> > ostream;
typedef basic_iostream<char, char_traits<char> > iostream;
typedef basic_stringbuf<char, char_traits<char>,allocator<char> > stringbuf;
typedef basic_istringstream<char, char_traits<char>,allocator<char> > istringstream;
typedef basic_ostringstream<char, char_traits<char>,allocator<char> > ostringstream;
typedef basic_stringstream<char, char_traits<char>,allocator<char> > stringstream;
typedef basic_filebuf<char, char_traits<char> > filebuf;
typedef basic_ifstream<char, char_traits<char> > ifstream;
typedef basic_ofstream<char, char_traits<char> > ofstream;
typedef basic_fstream<char, char_traits<char> > fstream;

对于流类间的关系有了个概况后,接下来进入正文。

标准输入输出流

标准输入流 cin (即键盘),标准输出流 cout (即显示器)。

插入符 << 与提取符 >>

在输入输出流库中(即<istream>与<ostream>中)重载了两种运算符 operator << 和 operator >>,用于简化输入输出流的使用。"<<" 常用作插入符,表明"输出到",如 cout<<"Hello" 即把"Hello"输出到屏幕上。">>"常用作提取符,表明"赋值给",例如 cin>>i 就是把键盘输入的信息赋值给 i 。

为什么能直接使用 cin、cout?

在 windows 上 VC++ 编译出来的控制台程序,实际上并不是从main开始执行,而是从mainCRTStartup()开始执行,执行一些初始化动作再调用main函数,这些初始化动作包括初始化 cin 和 cout,因此cin、cout 对象是在CRT初始化的时候创建的,在 main()之前,
使用 cin 获取输入和使用 cout 进行输出到屏幕上,实际上 cin 和 cout 的内部是调用了windowsAPI,如ReadConsole()和WriteConsole()之类来实现获取用户输入和输出到屏幕上,而这些API是如何操控设备(显示器)显示文字的,这个就是windows系统的事情了。当然,这是在windows上的情况,在Linux或其他系统上cin和cout的内部调用的是相应的Linux API等。

在<iostream>中有如下语句,可以发现 cin,cout 是全局变量。
extern istream cin;
extern ostream cout;

标准输入输出流代码示例:

#include <iostream>
using namespace std;

int main()
{
    int i;
    float f;
    char s[20];
    cin>>i;
    cin>>f;
    cin>>s;
    cout<<"i="<<i<<endl;
    cout<<"f="<<f<<endl;
    cout<<"s="<<s<<endl;
    return 0;
}
其中,插入符"<<"和提取符">>"可连续使用。对于 cin 而言,数据默认以空格分隔,上面程序中就可以使用 cin>>i>>f>>s 来赋值,可以使用 cout<<"i="<<i<<endl<<"f="<<f<<endl<<"s="<<s<<endl 来输出。

由于 cin 默认以空格分隔,那么如何一次性读取一行字符串如"How are you ?"呢?

get 系列函数很好地解决了这个问题。常用的 get 系列函数有三种,如下所示:

(1)get();
读取输入流第一个字符的ASCII值。
(2)istream& get(unsigned char * pszBuf,int nBufLen,char delim='\n');
pszBuf:指向字符串缓冲区的指针,用于保存读取的结果。
nBufLen:指定缓冲区大小。
delim:指定分隔符,可不填,此时默认会'\n'。
(3)istream& getline(unsigned char * pszBuf,int nBufLen,char delim='\n');
参数解释同(2),区别在于(2)遇到分隔符即停止执行,不读取分隔符,而(3)将会读取该分隔符,但是它不会将其存入结果缓存中。

仔细观察下面两段代码的区别,比较结果,体会(2)、(3)的区别。

#include <iostream>
using namespace std;
int main () {
	char s1[100],s2[100];
	cout<<"输入:?How are you? (Tab键) Fine,thank you!"<<endl;
	int a=cin.get();
	cin.get(s1,sizeof(s1),'\t');
	cin.getline(s2,sizeof(s2),'\n');
	cout<<"a:"<<a<<endl<<"s1:"<<s1<<endl<<"s2:"<<s2<<endl;
	return 0;
}
//注意!只有 get() 和 getline()位置发生变化。
#include <iostream>
using namespace std;
int main () {
	char s1[100],s2[100];
	cout<<"输入:?How are you? (Tab键) Fine,thank you!"<<endl;
	int a=cin.get();
	cin.getline(s1,sizeof(s1),'\t');
	cin.get(s2,sizeof(s2),'\n');
	cout<<"a:"<<a<<endl<<"s1:"<<s1<<endl<<"s2:"<<s2<<endl;
	return 0;
}
第一段代码执行结果为:
63
How are you?
Fine,thank you!
(注意,Fine 之前输出了制表符)

第二段代码执行结果为:
63
How are you?
Fine,thank you!

由于'?'的ASCII值为63,因此先输出63,第二段没有输出制表符的原因就是getline()提取了该制表符,但没有存入结果缓冲。

处理流错误

如何判断输入输出操作是否发生错误? C++ 对每次输入输出操作的结果都记录了其状态信息,存储于变量 int _Mystate 中,编程中通过这些状态信息即可判断此次输入输出操作正确与否。

获取状态信息的函数如下:
int rdstate();    //无参数,返回 _Mystate,通过下面四个函数来判断输入输出操作状态。

使用下面四个函数来检测相应状态:
bool good() const    //输入正确
{ // test if no state bits are set
return (rdstate() == goodbit);
}

bool eof() const    //已到达流的末尾
{ // test if eofbit is set in stream state
return ((int)rdstate() & (int)eofbit);
}

bool fail() const    //I/O失败,主要原因是非法数据,但流可以继续使用
{ // test if badbit or failbit is set in stream state
return (((int)rdstate() & ((int)badbit | (int)failbit)) != 0);
}

bool bad() const    //发生了致命性错误(或许是物理上的),流无法继续使用
{ // test if badbit is set in stream state
return (((int)rdstate() & (int)badbit) != 0);
}
其中标志位如下:
static const _Iostate goodbit = (_Iostate)0x0;
static const _Iostate eofbit = (_Iostate)0x1;
static const _Iostate failbit = (_Iostate)0x2;
static const _Iostate badbit = (_Iostate)0x4;

检测流错误的函数使用代码如下:

#include <iostream>
using namespace std;
int main () {
	int a;
	cin>>a;
	cout<<"输入状态信息码:"<<cin.rdstate()<<endl;
	if(cin.eof())
	{
		cout<<"已到达流尾!"<<endl;
	}
	else
	{
		if(cin.good())
		{
			cout<<"输入正确!"<<endl;
		}
		if(cin.fail())
		{
			cout<<"输入数据类型错误!"<<endl;
		}	
		if(cin.bad())
		{
			cout<<"致命错误!"<<endl;
		}
	}
	return 0;
}
输入:1
输出:输入状态信息码:0 输入正确!

输入:a
输出:输入状态信息码:2 输入数据类型错误!

输入:Ctrl键+z键(Linux下为Ctrl+d)
输出:输入状态信息码:3 已到达流尾!

注意,最后一种情况使用 Ctrl+z 来使控制台模拟文件结束符,一般eof()在读取文件时用到,用来判断是否已经读取完毕。

另外,当产生cin.fail()错误时,可以通过清除当前缓冲区来继续使用这个流,使用cin.clear()函数来清除状态标志位,并用get()读取这个错误的数据以抛弃即可重新使用这个流,具体使用如下所示。

#include <iostream>
using namespace std;
int main () {
	int d[5];
	int n=0;
	while(n<5)
	{		
		cin>>d[n];
		if(cin.fail())
		{
			cout<<"数据类型出错!将该数据抛弃并继续读取!"<<endl;
			cin.clear();    //清空状态标志位
			cin.get();    //将这个错误数据从流中读出以抛弃
		}
		else
		{
			cout<<"已正确读取前"<<n+1<<"个数字: ";
			for(int i=0;i<=n;i++)
			cout<<d[i];
			cout<<endl;
			n++;
		}	
	}
	return 0;
}
输入:
1 a 2 b 3 4 5
输出:
已正确读取前1个数字: 1
数据类型出错!将该数据抛弃并继续读取!
已正确读取前2个数字: 12
数据类型出错!将该数据抛弃并继续读取!
已正确读取前3个数字: 123
已正确读取前4个数字: 1234
已正确读取前5个数字: 12345

文件输入输出流

C++ 对于文件读写使用的是 ifstream、ofstream 和 fstream 流类,同样是依照"打开->读写->关闭"原语进行操作。文件读写所用到的很多常数都在基类 ios 中被定义出来,iftream 类只用于读取文件数据,ofstream 类只用于写入数据到文件,fstream类则可用于读取和写入文件数据。

文件打开

ifstream、ofstream、fstream 流类都使用构造函数或者 open() 函数打开文件,原型如下:
ifstream(const char* szName,int nMode=ios::in,int nProt=(int)ios_base::_Openprot)
ofstream(const char* szName,int nMode=ios::out,int nProt=(int)ios_base::_Openprot)
fstream(const char* szName,int nMode,int nProt=(int)ios_base::_Openprot)
void ifstream::open(const char* filename,int openmode=ios::in)
void ofstream::open(const char* filename,int openmode=ios::out|ios::trunc)
void fstream::open(const char* filename,int openmode=ios::in|ios::out)

szName:文件名
nMode:打开方式,打开方式有以下几种:
ios::in    以读取方式打开文件。
ios:out    以写入方式打开文件。
ios:app    每次写入数据时,先将文件指针移到文件尾,以追加数据到尾部。
ios:ate    仅初始时将文件指针移到文件尾,扔可在任意位置写入数据。
ios:trunc    写入数据前,先删除原有文件内容(清空文件),当文件不存在时则先创建一个文件。
ios:binary    以二进制形式打开文件。

nProt:指定文件保护规格说明,默认取 (int)ios_base::_Openprot,即操作系统默认值,指兼容共享模式。

文件关闭

ifstream、ofstream、fstream 流类都使用 close() 函数来释放文件资源。
close() 无参数,一般来说打开文件与关闭文件都是成对出现的。

文件读写

1.读写文本文件

文本文件是最常见的操作对象,关键是要解决如何按行读、如何按行写的问题。
执行程序后能在可执行文件当前目录下生成一个"test.txt"文件。

#include <iostream>
#include <fstream>
using namespace std;
int main () {
	char s[100];
	ofstream fout("test.txt");
	fout<<"Hello World!"<<endl<<"How are you?"<<endl<<"Fine,thank you!";
	fout.close();

	ifstream fin("test.txt");
	if(!fin)
	{
		cout<<"文件不存在!";
		return 0;
	}
	else
	{
		while(!fin.eof())
		{
		fin.getline(s,sizeof(s));
		cout<<s<<endl;
		}
	}
	return 0;
}

2.读写二进制文件

二进制文件读写也经常会用到,使用下面的函数来进行二进制文件读写。
ostream& write(const char*,int nSize)
istream& read(char*,int nSize)

读写二进制文件代码如下:
(会在可执行文件出产生一个"test.txt文件",右键查看属性即可发现该文件大小为24字节,即结构体 STUDENT 的大小)

#include <iostream>
#include <fstream>
using namespace std;
struct STUDENT
{
	char name[20];
	int ID;
};
int main () {	
	STUDENT t1={"张三",15};
	STUDENT t2;
	ofstream fout("test.txt");
	fout.write((const char*)&t1,sizeof(t1));
	fout.close();
	ifstream fin("test.txt");
	if(!fin)
	{
		cout<<"文件打开失败!";
		return 0;
	}
	else
	{
		fin.read((char*)&t2,sizeof(t2));
		cout<<t2.name<<endl<<t2.ID;
		fin.close();
	}
	return 0;
}

3.输入输出流缓冲

C++ 标准库封装了一个缓冲区类 steambuf,以供输入输出流对象使用。每个标准 C++ 输入输出流对象都包含一个纸箱 streambuf 的指针,用户可以通过调用 rdbuf() 成员函数来获得该指针,从而直接访问底层 streambuf 对象;可以直接对底层缓冲进行数据读写,从而跳过上层的格式化输入输出操作。但由于类似的功能均可由上层缓冲区实现,因此就不加以论述了。streambuf 最精彩的部分在于它重载了 operator<< 和 operator>>。对于 operator<< 来说,它以 streambuf 指针为参数,实现把 streambuf 对象中的所有字符输出到输出流中;对 operator>> 来说,可把输入流对象中的所有字符输出到 streambuf 对象中。

打开一个文件并把文件中的内容送到标准输出流中:

#include <iostream>
#include <fstream>
using namespace std;
int main () {
	ifstream fin("test.txt");
	if(fin)
	{
		cout<<fin.rdbuf();
	}
	fin.close();
	return 0;
}
同样是将文件内容输出,这个方法是很简洁的。当然,如果把 cout<<fin.rdbuf() 左侧的标准输出流改成文件输出流,则可实现文件的赋值功能等。

4.定位输入输出流

流的位置标识有三个:
ios::beg     流的开始位置
ios::cur     流的当前位置
ios::end     流的结束位置

定位函数主要有两个
istream& seekg(long relativepos,ios::seek_dir dir)
针对输入流,第一个参数是要移动的字符数目,可正可负,第二个参数是移动方向,即上面流的位置标识中的一个值。含义是字符指针相对于移动方向向前或者向后移动了多少个字符。
ostream& seekp(long relativepos,ios::seek_dir dir)
针对输出流,含义同 istream& seekg(long relativepos,ios::seek_dir dir)
另外,上述两个函数只传一个参数时,表示移动到流的绝对位置,如 seekg(4) 表明移动到流的第四个字符位置。

定位函数使用代码如下:

#include <iostream>
#include <fstream>
using namespace std;
int main () {
	fstream fout("test.txt",ios::in|ios::out|ios::trunc);
	fout.write("How are you?",12);
	
	fout.seekp(0,ios::beg);    //将指针移到文件头
	cout<<fout.rdbuf()<<endl;

	fout.seekp(4);    //将指针移到流的第四个位置
	cout<<fout.rdbuf()<<endl;
	
	fout.seekp(0,ios::end);    //将指针移到文件尾
	cout<<fout.rdbuf();

	fout.close();
	return 0;
}
输出:
How are you?
are you?

(注意!输出为三行,最后一行没有内容。)

字符串输入输出流

字符串输入输出流直接对内存而不是对文件和标准输出进行操作,它使用与 cin 及 cout 相同的读取和格式化函数来操纵内存中的数据,所有字符串流类的申明都包含在标准头文件 <stringstream> 中。标准库定义了三种类型的字符串流:
istringstream:  字符串输入流,提供读 string 功能。
ostringstream:  字符串输出流,提供写 string 功能。
stringstream:   字符串输入输出流,提供读写 string 功能。

利用字符串输入输出流,可以把多种基本数据类型组合成字符串,也可以反解字符串给各种变量赋值。

反解字符串给变量赋值、合并多种基本数据类型代码如下:
#include <iostream>
#include <sstream>
using namespace std;
int main () {
	int a;
	float b;
	string c;
	char d[20];
	string text="1 3.14 hello 你好";
	string temp;
	istringstream sin(text);
	//反解字符串
	sin>>a;
	sin>>b;
	sin>>c;
	sin>>d;
	cout<<a<<endl<<b<<endl<<c<<endl<<d;
	//合并基本类型
	ostringstream sout;
	sout<<a<<" "<<b<<" "<<c<<" "<<d<<endl;
	cout<<sout.str();
	return 0;
}

你可能感兴趣的:(C++,STL,标准模板库,流类,输出输出流)