目录
7.1 流类
程序中常用的cin和cout,分别用于从键盘输入数据和向屏幕输出数据(简称为标准 I/O)。除此之外,程序还可以从文件中读入数据,以及向文件中写入数据(简称为文件 I/O)。数据输入和输出的过程也是数据传输的过程,数据像水一样从一个地方流动到另一个地方,因此,在C++中将此过程称为 “流” 。在C++的标准类库中,将用于进行数据输入输出的类统称为 “流类” 。cin 是流类 istream 的对象,cout 是流类 ostream 的对象。要使用流类,需要在程序中包含 iostream 头文件。C++中常用的几个流类及其相互关系如图7.1所示。
图7.1中的箭头代表派生关系。例如,ios 是抽象的基类,它派生出 istream 和 ostream 。istream 和 ostream 又同时派生了 iostream 类。为了避免多继承的二义性,从 ios 派生出 istream 和 ostream 时,均使用了 virtual 关键字。
istream 是用于输入的流类,cin 就是该类的对象。
ostream 是用于输出的流类,cout 就是该类的对象。
ifstream 是用于从文件读取数据的类。
ofstream 是用于向文件写入数据的类。
iostream 是既能用于输入,又能用于输出的类。
fstream 是既能从文件读取数据,又能向文件写入数据的类。
7.2 标准流对象
iostream 头文件中定义了四个标准流对象:cin、cout、cerr 和 clog。
cin 对应于标准输入流,用于从键盘读取数据,也可以被重定向为从文件中读取数据。
cout 对应于标准输出流,用于向屏幕输出数据,也可以被重定向为从文件中读取数据。
cerr 对应于标准错误输出流,用于向屏幕输出出错信息,不能被重定向。
clog 对应于标准错误输出流,用于向屏幕输出出错信息,不能被重定向。
cerr 和 clog 的区别在于:cerr 不使用缓冲区,直接向显示器输出信息;而输出到 clog 中的信息会先存放到缓冲区,缓冲区满或者刷新时才输出到屏幕。
cout 是 ostream 类的对象,定义如下:
_PURE_APPDOMAIN_GLOBAL static filebuf fout(_cpp_stdout);
_PURE_APPDOMAIN_GLOBAL extern_CRTDATA2 ostream cout(&fout);
简单的看,就是:
ostream cout(&fout);
ostream 类的无参构造函数和复制构造函数都是私有的,因此在程序中一般无法定义 ostream 类的对象,唯一能用的 ostream 类的对象就是 cout。当然,上面关于 cout 对象的定义语句说明 ostream 类还是有一个公有构造函数的,如果知道这个构造函数是什么样的,那么也能自己定义 ostream 类的对象——但这么做并无必要。
cout 可以被重定向,而 cerr 不能。所谓重定向,就是将输入的源或输出的目的地改变。例如,cout 本来是输出到屏幕上的,但是经过重定向,本该输出到屏幕上的东西就可以被输出到文件中。例如下面的程序:
#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;
}
第7行的 freopen 是一个标准库函数,第二个参数 w 代表写模式,第三个参数代表标准输出。该语句的作用是将标准输出重定向为 test.txt 文件中。重定向之后,所有对 cout 的输出都不再出现在屏幕上,而是出现在 test.txt 文件中。test.txt 文件会和本程序的可执行文件出现在同一个文件夹中(目前测试下来,需要自建 test.txt)。重定向仅对本程序有效,不影响其他程序。运行本程序,输入
6(空格)2(回车)
程序没有输出,但是打开 test.txt 文件,可以看到文件中有
3
输入
4(空格)0(回车)
则程序屏幕上输出
error.
说明cerr不会被重定向。
cin 也是可以被重定向的。如果在程序中加入
freopen("input. dat", "r", stdin);
第二个参数 r 代表读入方式,第三个参数 stdin 代表标准输入。执行此语句后,cin 就不再从键盘读入数据,而是从 input. dat 文件中读入数据,input. dat 文件中有什么,就相当于从键盘输入了什么。
7.3 使用流操纵算子控制输出格式
有时希望按照一定的格式进行输出,如按十六进制输出整数,输出浮点数时保留小数点后面两位,输出整数时按6个数字的宽度输出,宽度不足时左边补0,等等。在C++中,用 cout 进行输出时,可以通过流操纵算子进行格式控制。
C++中常用的输出流操纵算子(也叫格式控制符)如表7.1所示。它们都是在头文件 iomanip中定义的。要使用这些流操纵算子,必须包含该头文件。
流操纵算子 | 作用 | |
*dec | 以十进制形式输出整数 | 常用 |
hex | 以十六进制形式输出整数 | |
oct | 以八进制形式输出整数 | |
fixed | 以普通小数形式输出浮点数 | |
scientific | 以科学计数法形式输出浮点数 | |
left | 左对齐,即在宽度不足时将填充字符添加到右边 | |
*right | 右对齐,即在宽度不足时将填充字符添加到左边 | |
setbase (b) | 设置输出整数时的进制,b = 8、10或16 | |
setw (w) | 指定输出宽度为w个字符,或输入字符串时读入w个字符 | |
setfill (c) | 在指定输出宽度的情况下,输出的宽度不足时用字符c填充(默认情况是用空格填充) | |
setprecision (n) | 设置输出浮点数的精度为n。在使用非 fixed 且非 scientific 方式输出的情况下,n即为有效数字最多的位数,如果有效数字位数超过n,则小数部分四舍五入,或自动变为科学计数法输出并保留一共n位有效数字;在使用 fixed 方式和 scientific 方式输出的情况下,n是小数点后面应保留的位数。 | |
setiosflags (标志) | 将某个输出格式标志置为1 | |
resetioflags (标志) | 将某个输出格式标志置为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 | 数值的符号(正负号)在指定宽度内左对齐,数值右对齐,中间由填充字符填充 |
表7.1中,“流操纵算子” 栏中的星号 “*” 不是算子的一部分,星号表示在没有使用任何算子的情况下,就等效于使用了该算子。例如,在默认情况下,整数是用十进制形式输出的,等效于使用了 dec 算子。
使用这些算子的方法是将算子用 “<<” 和 cout 连用。例如:
cout << hex << 12 << "," << 24;
这条语句的作用是指定以十六进制形式输出后面两个数,因此输出结果是:
c,18
setiosflags 算子实际上是一个库函数,它以一些标志作为参数,这些标志可以是在 iostream 头文件中定义的以下几种取值,它们的含义和同名算子一样。
ios::left | ios::right | ios::internal |
ios::showbase | ios::showpoint | ios::uppercase |
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),那么结果可能就是两个标志都不起作用。因此,在设置了某标志,又要设置其他与之矛盾的标志时,就应该用 resetiosflags 清楚原先的标志。例如下面三条语句:
cout << setiosflags(ios::fixed) << 12.34 << endl;
cout << resetiosflags(ios::fixed) << setiosflags(ios::scientific | ios::showpos)
<< 12.34 << endl;
cout << resetiosflags(ios::showpos) << 12.34 << endl;//清除要输出正号的标志
输出结果是:
12.340000
+1.234000e + 001
1.234000e + 001
关于流操纵算子的使用,来看下面的程序。
#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;
//5)非负数显示正号,输出宽度为12字符,宽度不足则用'*'填补
cout << "5) " << showpos << fixed << setw(12) << setfill('*') << 12.1 << endl;
//6)非负数不显示正号,输出宽度为12字符,宽度不足则右边用填充字符填充
cout << "6) " << noshowpos << setw(12) << left << 12.1 << endl;
//7)输出宽度为12字符,宽度不足则左边用填充字符填充
cout << "7) " << setw(12) << right << 12.1 << endl;
//8)宽度不足时,负号和数值分列左右,中间用填充字符填充
cout << "8) " << setw(12) << internal << -12.1 << endl;
cout << "9) " << 12.1 << endl;
return 0;
}
程序的输出结果是:
1) 8d 141 215
2) 1.2346e+006 12.346
3) 1234567.89000 12.34567
4) 1.23457e+006 1.23457e+001
5) ***+12.10000
6) 12.10000****
7) ****12.10000
8) -***12.10000
9) 12.10000
需要注意的是,setw 算子所起的作用是一次性的,即只影响下一次输出。每次需要指定输出宽度时都要使用 setw。因此可以看到,第9)行的输出因为没有使用setw,输出的宽度就不再是前面指定的12个字符。
在读入字符串时,setw还能影响 cin 的行为。例如下面的程序:
#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(3)” 使得读入 s2 时只读入3个字符。
setw 用于 cin 时,同样只影响下一次的输入。
7.4 调用 cout 的成员函数
ostream 类有一些成员函数,通过 cout 调用它们也能用于控制输出格式,其作用和流操纵算子相同,如表7.2所示。
成 员 函 数 | 作用相同的流操纵算子 |
precision(n) | setprecision(n) |
width(w) | setw(w) |
fill(c) | setfill(c) |
setf(标志) | setiosflags(标志) |
unsetf(标志) | resetiosflags(标志) |
setf 和 unsetf 函数用到的 “标志” ,与 setiosflags 和 resetiosflags 用到的完全相同。
这些成员函数的用法十分简单。例如下面的三行程序:
cout.setf(ios::scientfic);
cout.precision(8);
cout << 12.23 << endl;
输出结果是:
1.22300000e + 001
cout 有一个成员函数 put,可以用来输出一个字符。其参数类型为 int,代表要输出的字符的 ASCII 码,其返回值是 cout 的引用。例如下面的两条语句:
cout.put('a');
cout.put(98).put('c').put('z');
输出结果是:
abcz
98是 'b' 的 ASCII 码,'a' 和 'z' 都能自动转换成证书,即字符对应的 ASCII 码。
ostream 类还有以下成员函数:put 函数、write 函数、flush 函数、tellp 函数、seekp 函数。由于关于 cout 的大部分操作可以用流操纵算子完成,故这里不做详解,具体可参考:
c++中ostream类的超详细说明_c++ ostream_cpp加油站的博客-CSDN博客
7.5 cin 的高级用法
7.5.1 判断输入结束
cin 可以用来从键盘输入数据;将标准输入重定向为文件后,cin 也可以用来从文件中读入数据。在输入数据的多少不确定,且没有结束标志的情况下,该如何判断输入数据已经读完了呢?例如,输入若干个正整数,输出其中的最大值,程序该如何编写?代码如下:
#include <iostream>
using namespace std;
int main()
{
int n;
int maxN = 0;
while(cin >> n){ //输入没有结束,条件就为真
if(maxN < n)
maxN = n;
}
cout << maxN;
return 0;
}
假设该程序编译得到的可执行程序是 mycin.exe。在Windows 系统中,通过键盘输入时,在单独的一行按 Ctrl+Z 键后再按回车键就代表输入结束。因此程序运行时,输入若干个正整数后换行,再按 Ctrl+Z 键和回车键,程序就会输出最大值并结束。即 “cin >> n” 这个表达式在碰到 Ctrl+Z时,会返回 false。在 UNIX/Linux 系统中,Ctrl+D 代表输入结束。
如果将标准输入重定向为某个文件,如在程序开始添加 “freopen("test.txt", "r", stdin);” 语句,或者不添加上述语句,但是在 Windows 的 “命令提示符” 窗口输入:
mycin < test. txt
则都能使得本程序不再从键盘输入数据,而是从 test. txt 文件输入数据(前提是 test. txt 文件和 mycin. exe 在同一个文件夹中)。在这种情况下,test. txt文件中并不需要包含 Ctrl+Z,只要有用空格或回车隔开若干个正整数即可。cin 读到文件末尾时,“cin > n” 就会返回 false 而导致程序结束。例如,嘉定 test .txt 文件中的内容如下所示:
112
23123
34 444 55
44
则程序的输出是;
23123
在第4章 “运算符重载” 中提到过 istream 类将 “ >>” 重载为成员函数,而且这些成员函数的返回值是 cin 的引用。准确地说,“cin >> n” 的返回值的确是 istream & 类型的,而 while 语句中的条件表达式的返回值应该是 bool 类型、整数类型或其他和整数类型兼容的类型,istream & 显然和整数类型不兼容,为什么 “while(cin >> n)” 还能成立呢?这是因为,istream 类对强制类型转换运算符bool进行了重载,这使得 cin 对象可以被自动转换成 bool 类型。所谓自动转换的过程,就是调用 cin 的 operator bool 这个成员函数,而该成员函数可以返回某个标志值,该标志值在 cin 没有读到输入结尾时为 true,读到输入结尾后变为 false。对该标志值的设置,在 operator << 成员函数中进行。
如果 cin 在读取过程中发生了错误,“cin >> n” 这样的表达式也会返回 false。例如下面的程序:
#include <iostream>
using namespace std;
int main()
{
int n;
while(cin >> n)
cout << n << endl;
return 0;
}
程序本该输入整数,如果输入了一个字母,则程序就会结束。因为,应该读入整数时却读入了字母也算读入出错。
7.5.2 istream 类的成员函数
istream 类有一些成员函数,这里介绍最常用的几个。
1. get
istream 类有多个名为 get 的成员函数,这里只介绍其中的一个。其原型是:
int get();
此函数从输入流中读入一个字符,返回值就是该字符的 ASCII码。如果碰到输入的末尾,则返回值为 EOF。EOF 是 End of File 的缩写。istream 类中从输入流(包括文件)中读取数据的成员函数,在把输入数据都读取完后再进行读取,就会返回 EOF。EOF 是在 iostream中定义的一个整型常量,值为-1;
get 函数不会跳过空格、制表符、回车等特殊字符,所有的字符都能被读入。例如下面的程序:
#include <iostream>
using namespace std;
int main()
{
int c;
while((c = cin.get()) != EOF)
cout.put(c);
return 0;
}
程序运行情况如下:
输入:
Hello world!(回车)
输出:
Hello world!(回车)
再输入:
^Z(回车)
输入^Z(回车)后程序结束。
程序中的变量 c 应为 int 类型,而不能是 char 型。在输入流中碰到 ASCII码等于 0xFF 的字符时,cin.get() 返回 0xFF,0xFF 赋值给 c,此时如果 c 是 char 类型的,那么其值就是 -1(因为符号位为1代表负数),即等于 EOF,于是程序就错误的认为输入已经结束。而在 c 为 int 类型的情况下,将 0xFF 赋值给 c,c 的值就是255(因为符号位为0,是正数),而非-1,即除非读到输入末尾,c 的值都不可能是-1.
要将文本文件 test.txt 中的全部内容原样显示出来,程序可以如下编写:
#include <iostream>
using namespace std;
int main()
{
int c;
freopen("test.txt", "r", stdin);
while((c = cin.get()) != EOF)
cout.put(c);
return 0;
}
2. getline
getline 成员函数有如下两个版本:
istream & getline(char* buf, int bufSize);
istream & getline(char* buf, int bufSize, char delim);
第一个版本从输入流中读取 bufSize-1 个字符到缓冲区 buf,或读到 “\n” 为止(哪个条件先满足就按哪个执行)。函数会自动在 buf 中读入数据的结尾添加 “\0”。第二个版本和第一个版本的区别在于,第一个版本是读到 “\n” 为止,第二个版本是读到 delim 字符为止。“\n” 和 delim 都不会被读入 buf,但会被从输入流中取走。这两个函数的返回值就是函数所作用的对象的引用。如果输入流中 “\n” 或 delim 之前的字符个数达到或超过 bufSize,就会导致读入出错,其结果是:虽然本次读入已经完成,但是之后的读入都会失败。
从输入流中读入一行,可以用第一个版本。用 cin >> s(假设 s 是 string 对象或 char* 指针)则不行,因为此种读法在碰到行中的空格或制表符时就会停止,因此就不能保证 s 中读入的是整行。第一个版本的 getline 函数的用法示例如下:
#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;
}
程序的运行过程如下:
ab cd(回车)
ab cd
33(回车)
33
44(回车)
44
在上面的输入情况下,程序是正常的。程序运行过程中还可能出现如下情况:
ab cd123456k(回车)
error
ab cd
120
123456
第7行,读入时因字符串超长导致出错,于是第11行并没有从输入流读入 n,n 维持原来的值120。第12行,调用 istream 的成员函数 clear 清除 cin 内部的错误标记,此后 cin 又能正常读入了。因此,123456在第13行被读入 n。
可以用 getline 函数的返回值(为 false 则输入结束)来判断输入是否结束。例如,要将文件 test.txt 中的全部内容(假设文件中一行最长有10 000个字符)原样显示,程序可以如下编写:
#include <iostream>
using namespace std;
const int MAX_LINE_LEN = 10000;
int main()
{
char szBuf[MAX_LINE_LEN+10];
freopen("test.txt", "r", stdin);
while(cin.getline(szBuf, MAX_LINE_LEN+5))
cout << szBuf << endl;
return 0;
}
程序每次读入文件中的一行到 szBuf 并输出。szBuf 中不会读入回车符,因此输出 szBuf 后要在输出 endl 以换行。
3. eof
bool eof();
此函数用于判断输入流是否已经结束。返回值为 true 表示输入结束。
4. ignore
istream & ignore(int n = 1, int delim = EOF);
此函数的作用是跳过输入流中的 n 个字符,或跳过 delim 及其之前的所有字符,哪个条件先满足就按哪个执行。两个参数都有默认值,因此 cin.ignore() 就等效于 cin.ignore(1, EOF),即跳过一个字符。该函数常用于跳过输入中的无用部分,以便提取有用的部分。例如,输入的电话号码形式是 “Tel:63652823”,“Tel:” 就是无用的内容。例如下面的程序:
#include <iostream>
using namespace std;
int main()
{
int n;
cin.ignore(5, 'A');
cin >> n;
cout << n;
return 0;
}
程序的运行过程可能如下:
abcde34(回车)
34
cin.ignore() 跳过了输入中的前5个字符,其余内容被当做整数输入 n 中。
该程序的运行过程也可能如下:
abA34(回车)
34
cin.ignore() 跳过了输入中的 'A' 及其前面的字符,其余内容被当做整数输入 n 中。
5. peek
int peek();
此函数返回输入流中的下一个字符,但是并不将该字符从输入流中取走——相当于只是看了一眼下一个字符,因此叫 peek。cin.peek() 不会跳过输入流中的空格、回车符。在输入流已经结束的情况下,cin.peek() 返回 EOF。
在输入数据的格式不同,需要预先判断格式再决定如何输入时,peek 就能起到作用。
例题:编写一个日期格式转换程序,输入若干个日期,每行一个,要求全部转换为 “mm-dd- yyyy” 格式输出。输入的日期格式可以是 “2011.12.24”(中式格式),也可以是 “Dec 24 2011”(西式格式)。要求该程序对于以下输入数据:
Dec 3 1990
2011.2.3
458.12.1
Nov 4 1998
Feb 12 2011
输出结果应为:
12-03-1990
02-03-2011
12-01-0458
11-04-1998
02-12-2011
输入数据中的 Ctrl+Z 略去不写,因为输入数据也可能来自于文件。
编写这个程序时,如果输入的是中式格式,就用 cin >> year(假设 year 是 int 类型变量)读取年份,然后再读取后面的内容;如果输入是西式格式,就用 cin >> sMonth(假设 sMonth 是 string 类型对象)读取月份,然后读取后面的内容。可是,如果没有将数据从输入流中读取出来,就无法判断输入到底是哪种格式。即便用 cin.get() 读取一个字符后再做判断,也很不方便。例如,在输入为 “2011.12.24” 的情况下,读取第一个字符 ‘2’ 后就知道是格式一,问题是输入流中的 ‘2’ 已经被读取了,剩下的表示年份的部分只有 “011”,如何将这个 “011” 和前面读取的 ‘2’ 凑成一个整数2011,也是颇费周折的事情。使用 peek 函数很容易解决这个问题。示例程序如下:
#include <iostream>
#include <iomanip>
#include <string>
using namespace std;
string Months[12] = { "Jan","Feb","Mar","Apr","May","Jun","Jul","Aug",
"Sep","Oct","Nov","Dec" };
int main()
{
int c;
while((c = cin.peek()) != EOF) { //取输入流中的第一个字符进行查看
int year,month,day;
if(c >= 'A' && c <= 'Z') { //美国日期格式
string sMonth;
cin >> sMonth >> day >> year;
for(int i = 0;i < 12; ++i) //查找月份
if(sMonth == Months[i]) {
month = i + 1;
break;
}
}
else { //中国日期格式
cin >> year ;
cin.ignore() >> month ; //用ignore跳过 "2011.12.3"中的'.'
cin.ignore() >> day;
}
cin.ignore(); //跳过行末 '\n'
cout<< setfill('0') << setw(2) << month ;//设置填充字符'\0',输出宽度2
cout << "-" << setw(2) << day << "-" << setw(4) << year << endl;
}
return 0;
}
istream 还有一个成员函数 istream & putback(char c) ,可以将一个字符插入输入流的最前面。对于上面的例题,也可以用 get 函数读取一个字符并判断是中式格式还是西式格式时,将刚刚读取的字符再用 putback 成员函数放回流中,然后再根据判断结果进行不同方式的读入。
注:istream 类还有以下成员函数:read 和 readsome 函数、putback函数、unget函数、sync函数、tellg 和 seekg函数。由于使用不是很常见,故不做详解,具体可以参考: