在面向对象编程中,我们定义了类的概念并讲解了如何使用。下面我们通过标准输入输出的流类来进一步加深对于类和对象的理解。

本章的主要内容是流类,标准流对象,使用流操纵算子控制输出格式,调用cout的成员函数,以及cin的高级用法。在了解C++程序输入输出的规则的同时,我们也要进一步理解继承多态的概念。

流类

我们在程序中,常常使用cincout输入和输出数据,另外,我们的程序还应该可以在文件中读入数据,以及向文件中写入数据。数据输入和输出的过程也是数据传输的过程,数据像水流一样从一个地方流动到另一个地方,因此,在C++中将此过程称为“流”(stream),而在C++的标准模板库中,将用于进行数据输入输出的类统称为“流类”。例如cin就是istream类的对象,coutostream类的对象。图1展示出了流类中的派生关系。

图片2.png

图1:C++类库中的流类

C++是可以多继承的,所以在流类中,ios是抽象的基类,更为具体的说,ios_base是一个抽象类作为最基本的基类,从中派生出模板类basic_ios类,用来管理流缓冲区(streambuf)和异常状态(如failbiteofbit)。输入类istream和输出类ostream都是从basic_ios中派生而来的,然后istreamostream共同派生出iostream类。在从ios派生出istreamostream类时均使用了虚继承,避免多继承的二义性。

  • istream:用于输入的流类,cin就是该类的对象。
  • ostream:用于输出的流类,cout就是该类的对象。
  • ifstream:用于文件读取数据的类。
  • ofstream:用于文件写入数据的类。
  • iostream:是既能用于输入,又能用于输出的类。
  • fstream:即可以从文件中读取数据,又能像文件写入数据的类。

标准流对象

iostream头文件中定义了四个标准流对象,分别为:cincoutcerrclog

  • cin:对应于标准输入流,用于从键盘读取数据,也可以重定向为从文件中读取数据。
  • cout:对应于标准输出流,用于向键盘输出数据,也可以被重定向为像文件写入数据。
  • cerr:对应于标准错误输出流,用于像屏幕输出出错信息,不能被重定向。
  • clog:对应于标准错误输出流,用于像屏幕输出出错信息,不能被重定向。

cerrclog不同之处在于:cerr不适用缓冲区,直接向显示器输出信息;而输出到clog中的信息会先被存放到缓冲区,缓冲区满或者刷新时才输出到屏幕。

coutostream类的对象。ostream类的无参构造函数和复制构造函数都是私有的,因此一般情况下,程序中无法定义ostream对象,唯一可以进行使用的是ostream类的对象cout。当然,ostream是由共有的构造函数的,但是一般情况下没有必要去定义一个新的对象。

cout可以被重定向。所谓重定向,就是将输入的源或输出的目的地改变。例如,cout本应该输出到屏幕上,但是经过重定向,本该输出到屏幕的东西就可以被输出到文件中,例如如下的程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<iostream>
using namespace std;
int main(){
int x,y;
cin>>x>>y;
freopen("test.txt","w",stdout);//将标准输出重定向到test.txt中
if(y==0){//除数为0输出错误信息
cerr<<"error."<<endl;
}
else{
cout<<x/y;
}
return 0;
}

freopen是一个标准库函数,参数w代表写的模式,第三的参数代表标准输出。这个语句的作用就是将标准输出重定向到了test.txt文件中,在重定向之后,所有对cout的输出都不再出现再屏幕上了,而是会出现在重定向的位置,重定向仅对本程序有效,不会影响其它程序。在上述程序中,输入6 2可以看到屏幕上没有输出,而在test.txt中有一个字符3。而如果输入4 0,则在屏幕中输出error.。这说明cerr并不会被重定向。

另外,cin也是可以被重定向的,如果在程序中加入freopen("input.dat","r",stdin);,第二个参数r代表读入方式,第三个参数stdin代表标准输入。执行此语句后,cin就不再从键盘读入数据,而是从input.dat文件中读入数据。

使用流操纵算子控制输出格式

有时希望按照一定的格式进行输出,如按十六进制输出整数,输出浮点数是保留小数点后的几位,输出整数时按6个数字的宽度输出,宽度不足时在左边补0,等等。在C++中,用cout进行输出时,可以通过操纵算子进行格式控制。C++中常用的输出流操纵算子(也称格式控制符)如表1所示,在使用它们时,我们需要引用头文件iomanip

流操纵算子 作用
*dec 以十进制形式输出整数 常用
hex 以十六进制形式输出整数
oct 以八进制形式输出整数
fixed 以普通小数形式输出浮点数
scientific 以科学计数法形式输出浮点数
left 左对齐,即在宽度不足时将填充字符添加到右边
*right 右对齐,即在宽度不足时将填充字符添加到左边
setbase(b) 设置输出整数时的进制,b=8、10或16
setw(w) 指定输出宽度为w个字符,或输入字符串时读入w个字符
setfill(c) 在指定输出宽度的情况下,输出的宽度不足时用字符c填充(默认情况使用空格填充)
setprecision 设置输出浮点数的精度为n。在使用非fixed且非scientific方式输出的情况下,n即为有效数字最多的位数,如果有效数字位数超过n,则小数部分四舍五入,或自动变为科学计数法输出并保留一共n位有效数字留在使用fixed方式和scientific方式输出的情况下,n时小数点后面应保留的位数
setiosflags(标志) 将某个输出格式标志置为1
resetiosflags(标志) 将某个输出格式标志置为0
boolapha 把true和false输出为字符串 不常用
*noboolalpha 把true和false输出为0、1
showbase 输出表示数值的进制的前缀
*noshowbase 不输出表示数值的进制的前缀
showpoint 总是输出小数点
*noshowpoint 只有当小数部分存在时才显示小数点
showpos 在非负数值中显示+
*noshowpos 在非负数值中不显示+
*skipws 输入时跳过空白字符
noskipws 输入时不跳过空白字符
uppercase 十六进制数中使用'A'~'E'。若输出前缀,则前缀输出'0X',科学计数法中输出'E'
*nouppercase 十六进制数中使用'a'~'e'。若输出前缀,则前缀输出'0x',科学计数法中输出'e'
internal 数值的符号(正负号)在指定宽度内左对齐,数值右对齐,中间由字符填充
表1:C++中的流操纵算子

注意,“流操纵算子”栏中的*不是算子的一部分,星号表示在没有使用任何算子的情况下,就等效于使用了该算子,即默认情况。使用这些算子的方法就是将算子用<<cout连用,例如:cout<<hex<<12<<","<<24;这条语句的意思就是以16进制的形式输出后面的两个数,因此输出结果应该为c,18

setiosflags算子实际上是一个库函数,它以一些标志作为参数,这些标志可以是在iostream头文件中定义的以下几种取值,他们的含义与同名算子一样:

ios::left ios::right ios::internal
ios::showbase ios::showpoint ios::unppercase
ios::showpos ios::scientific ios::fixed

这些标志实际上都是仅有某比特位为1,而其他比特位都为0的整数。多个标志可以使用|运算符连接,表示同时设置,例如:cout<<setiosflags(ios::scientific|ios::showpos)<<12.34,输出结果应该为+1.234000e+001。如果两个相互矛盾的标志同时被设置,如先设置setiosflags(ios::fixed),然后有设置setiosflags(ios::scientific),那么结果可能导致两个标志都不起作用,所以在设置某个对立的标志时,这时应该使用resetiosflag清除原先的标志,例如:

1
2
3
cout<<setiosflags(ios::fixed)<<12.34<<endl;
cout<<resetiosflags(ios::fixed)<<setiosflags(ios::scientific|ios::showpos)<<12.34<<endl;
cout<<resetflags(ios::showpos)<<12.34<<endl;

这样的输出结果应该为:

1
2
3
12.340000
+12.34000e+001
1.234000e+001

另外需要注意的是,setw算子所起的作用是一次性的,即只影响下一次的输出。每次需要指定输出宽度时都要使用setw。我们看一个比较全面的示例程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include<iostream>
#include<iomanip>
using namespace std;
int main(){
int n=141;
//读者可以按照要求自己实现一下这些功能
//1. 分别以十六进制、十进制、八进制先后输出n
cout<<"(1)"<<hex<<n<<" "<<dec<<n<<" "<<oct<<n<<endl;

double x=1234567.89,y=12.34567;
//2. 保留5位有效数字
cout<<"(2)"<<setprecision(5)<<x<<" "<<y<<endl;
//3. 保留小数点后5位
cout<<"(3)"<<fixed<<setprecision(5)<<x<<" "<<y<<endl;
//4. 使用科学计数法输出,且保留小数点后5位
cout<<"(4)"<<scientific<<setprecision(5)<<x<<" "<<y<<endl;

double number=12.1;
//5. 非负数显示正号,输出宽度为12字符,宽度不足用'*'填充
cout<<"(5)"<<showpos<<fixed<<setw(12)<<setfill('*')<<number<<endl;
//6. 非负数不显示正号,输出宽度为12字符,宽度不足则右边用填充字符填充
cout<<"(6)"<<noshowpos<<setw(12)<<left<<number<<endl;
//7. 输出宽度为12字符,宽度不足则左边用填充字符填充
cout<<"(7)"<<setw(12)<<right<<number<<endl;
//8. 宽度不足时,负号和数值分列左右,中间用填充字符填充
cout<<"(8)"<<setw(12)<<internal<<-number<<endl;
cout<<"(9)"<<number<<endl;
return 0;
}

输出结果为:

1
2
3
4
5
6
7
8
9
(1)8d 141 215
(2)1.2346e+06 12.346
(3)1234567.89000 12.34567
(4)1.23457e+06 1.23457e+01
(5)***+12.10000
(6)12.10000****
(7)****12.10000
(8)-***12.10000
(9)12.10000

另外,需要说明的是,setw算子还可以影响cin的的行为,例如:

1
2
3
4
5
6
7
8
9
#include<iostream>
#include<iomanip>
using namespace std;
int main(){
string s1,s2;
cin>>setw(4)>>s1>>setw(3)>>s2;
cout<<s1<<","<<s2<<endl;
return 0;
}

当我们输入1234567890时,输出结果为1234,567,这说明setw(4)使得读入s1时,只读入4个字符。setw作用于cin时,同样只影响下一次的输入。

调用cout的成员函数

ostream类有一些成员函数,通过cout可以调用它们,也能用于控制输出格式,起作用和流操纵算子相同,如表2所示:

成员函数 作用相同的流操纵算子
precision(n) setprecision(n)
width(w) setw(w)
fill(c) setfill(c)
setf(标志) setiosflags(标志)
unsetf(标志) resetiosflags(标志)
表2:ostream类的成员函数

setfunsetf函数用到的“标志”和setiosflagsresetiosflags用到的完全相同,这些成员函数的用法十分简单,例如:

1
2
3
cout.setf(ios::scientific);
cout.precision(8);
cout<<12.23<<endl;

输出的结果为1.22300000e+001cout有一个成员函数put,可以用来输出一个字符。其参数类型为int,代表要输出字符的ASCII码,其返回值是cout的引用,例如:

1
2
cout.put('a');
cout.put(98).put('c').put('z');

输出结果是abcz,输入的'a'会自动转化为ASCII码值。

cin的高级用法

判断输入结束

cin可以用来从键盘输入数据。在输入数据的多少不确定时,且没有结束标志的情况下,该如何判断写入数据已经读完了呢?例如:输入若干个正整数,输出其中的最大值,程序该如何编写呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<iostream>
using namespace std;
int main(){
int n;
int maxN=0;
while(cin>>n){
if(maxN<n){
maxN=n;
}
}
cout<<maxN;
return 0;
}

在windows系统中,通过键盘输入时,在单独的一行按ctrl+z键后再按回车键,就代表输入结束。因此程序运行时,输入若干个正整数后换行,再按ctrl+z键和回车键,程序就会输出最大值并结束。即表达式cin>>n在碰到ctrl+z时就会返回falseUNIX/Linux系统中,ctrl+d代表结束。如果将标准输入重定向为某个文件,如在程序开始添加freopen("text.txt","r",stdin);语句,或者不添加上述语句,在windows的“命令提示符”窗口中输入:mycin<text.txt(注意上述文件编译为mycin.exe),这样也可以重定向输入位置。在这种情况下,test.txt文件中并不需要包含ctrl+z,只要有用空格或回车隔开的若干个正整数即可。cin读到文件末尾时,cin>>n就会返回false,从而导致程序结束。

在前面,“运算符重载”中提到istream类中将>>算符重载为成员函数,而且这些函数的返回值是cin的引用。准确的是,cin>>n的返回值是istream&类型的,而while语句中的条件表达式的返回值是bool类型、整数类型或其他和整数类型兼容的类型,istream&显然和整数类型不兼容,为什么while(cin>>n)还能成立呢?这是因为istream类对强制类型转换运算符bool进行重载,这使得cin对象可以被自动转换成bool类型。所谓自动类型转换,就是调用cinoperator bool()函数,而该成员函数可以返回某个标志值,该标志值在cin没有读到结尾时为true,读到结尾时就会变为false

istream成员函数

istream类作为输入流类,有一些很有用的成员函数。

get

istream类中有很多名为get的成员函数,这里介绍一个原型为int get();的函数。它的作用是从输入流中读入一个字符,返回其ASCII码值。如果碰到输入末尾,则返回值为EOFEOF是指End of Flie ,在istream类中从输入流中读取数据的成员函数,在把输入数据都读取完后再进行读取,就会返回EOFEOF是在iostream类中定义的一个整形常量,值为-1

注意,get函数不会跳过空格、制表符、回车等特殊字符,所有的字符都能被读入,例如:

1
2
3
4
5
6
7
8
9
#include <iostream>
using namespace std;
int main(){
int c;
while((c=cin.get())!=EOF){
cout.put(c);
}
return 0;
}

当我们输入Hello World!并回车时,会输出Hello world!并回车,只有输入^z,即ctrl+z时可以停止这个程序。

getline

getline成员函数有两个版本:

1
2
istream& getline(char* buf,int bufSize);
istream& getline(char* buf,int bufSize,char delim);

第一个版本是从输入流中读取bufSize-1个字符到缓冲区buf,或者读到\n为止,函数自动会在buf中读入数据的结尾添加\0。第二个版本与第一个版本的区别是,不再是读到\n为止,而是读到delim字符为止。\ndelim都不会读入buf中,但是会被输入流取走。这两个函数的返回值就是函数所作用的对象的引用。如果输入流中\ndelim之前的字符个数达到或超过dufSize,就会导致读入出错,其结果是:虽然本次读入已经完成,但是之后的读入都会失败。

从输入流中读入一行,可以使用第一个版本,用cin>>s(假设sstring对象或char*指针)则不行,因为这种写法在读到空格、制表符时就会停止,因此就不能保证s中读入的是整行。这里给出一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
using namespace std;
int main(){
char szBuf[20];
int n=120;
if(!(cin.getline(szBuf,6))){//输入字符超过5个就会报错
cout<<"error"<<endl;
}
cout<<szBuf<<endl;
cin>>n;
cout<<n<<endl;
cin.clear();//clear函数能够清除cin内部的错误标记,使之恢复正常
cin>>n;
cout<<n<<endl;
return 0;
}

我们谈一谈如果第6行输入的字符超过5个,例如输入ab cd123456k,则输出为:

1
2
3
4
5
ab cd123456k
error
ab cd
120
123456

第11行中没有读入,因为cin内部存在错误标记,不能继续输入,而后面清楚标记后,输入还是从输入流中输入,因此123456被读入n中。

eof

bool eof();此函数可以用来判断输入流是否已经结束。返回true表示已经结束了。

ignore

istream& ignore(int n=1,int delim=EOF);此函数的作用为跳过输入流中的n个字符,或跳过delim及其之前的所有字符,哪个条件先满足就按哪个条件执行。两个参数都是默认值,因此cin.ignore()就等效于cin.ignore(1,EOF),即跳过一个字符。该函数常用于删掉输入的无效部分,例如Tel:63652823Tel:就是无效内容。给出一个程序示例:

1
2
3
4
5
6
7
8
9
#include <iostream>
using namespace std;
int main(){
int n;
cin.ignore(5,'A');
cin>>n;
cout<<n;
return 0;
}

当我们输入abcde34时,输出为34。当我们输入abA34时,输出为34。分别对应上面的两种情况。

peek

int peek();此函数返回输入流的下一个字符,但是并不将该字符从输入流中取走,只是看一眼下一个字符是什么。cin.peek()不会跳过输入流中的空格、回车符。在输入已经结束的情况下,cin.peek()会返回EOF

putback

istream& putback(char c)作用为将一个字符插入到输入流的最前面。