2.2 基本输入/输出流
听过HelloWorld.exe的自我介绍之后,大家已经知道了一个C++程序的任务就是描述数据和处理数据。这两大任务的对象都是数据,可现在的问题是,数据不可能无中生有地产生,C++程序也不可能凭空创造出来数据。那么,C++程序中的数据又从何而来呢?
在现实世界中,国与国之间的交流是通过外交官来完成的。在C++世界中,也有负责应用程序跟外界进行数据交流的外交官,它们的名字就是基本输入/输出流对象(iostream)。一个C++程序在工作的时候,负责输入的外交官(istream)会将现实世界中的数据(比如,来自键盘的用户输入数据)输入程序中,然后C++程序才能对这些数据进行处理。当C++程序得到结果数据之后,负责输出的外交官(ostream)又会将结果数据输出(比如,输出到屏幕或者文件)。在C++程序中,我们将这种数据在程序和外部对象(键盘、屏幕等)之间的流动称为流(stream),分别由istream和ostream这两位外交官负责。而正是这两位外交官的通力合作,才完成了C++程序与外界的数据交流。
2.2.1 标准的输入和输出对象
为了便于使用,C++标准库中已经预先定义了4个最基本的输入/输出流(I/O)对象,其中最常用的是负责键盘输入的cin对象和负责屏幕输出的cout对象。另外,标准库还定义了两个辅助的输出对象,分别是用于输出程序错误信息的cerr和用于输出日志信息的clog。这些对象都已经在标准库中预先定义,只要引入相应的头文件<iostream>,我们就可以在程序中直接使用它们来完成程序的基本输入/输出,就像我们在上面的程序中直接使用cout来向屏幕输出“Hello World!”字符串一样。
cin和cout的使用非常简单,我们可以通过提取(get-from)符“>>”从cin中提取用户通过键盘输入的数据,实现从键盘到程序的数据输入;也可以通过插入(put-to)符“<<”向cout中插入程序内的数据,实现从程序到屏幕的数据输出。这里,箭头的方向形象地代表了数据流动的方向。输入数据的时候,数据是从cin对象流出到程序,所以箭头指向远离cin对象的方向,而输出数据的时候,数据是从程序流入cout对象,所以箭头是指向靠近cout对象的方向。例如,可以使用“<<”插入符向cout对象中插入数字或者字符串,将其显示到屏幕上:
cout<<1; // 向cout对象中插入数字1,这个数据从程序流动到屏幕 cout<<"Hello World!"; // 向cout对象插入字符串“Hello World!” cout<<"1 + 2 = "<<1+2; // 向cout对象中插入字符串“1 + 2 =”以及1+2的计算结果
第一句中的插入符将数字“1”插入到cout对象中,这样就会在屏幕上显示数字1。同理,第二句会在屏幕显示一个字符串“Hello World!”。而在最后一条语句中,系统首先会计算得到“1+2”的结果数据3,然后第一个插入符会首先将“1 + 2 = ”这个字符串数据插入cout对象,接着第二个插入符会将计算结果数据3插入cout对象,这样,我们在屏幕上看到的最终输出就是“1 + 2 = 3”这样一个字符串。
对于输入流对象cin,可以使用提取符“>>”从cin输入流中获取用户通过键盘输入的数据并保存到程序内的变量中。例如:
// 用于保存用户输入数据的变量 string strName; // string类型的变量,用于保存用户输入的字符串 int nAge = 0; // int类型的变量,用于保存用户输入的整数 // 从cin对象中提取用户输入的字符串数据和整数数据, // 例如,输入"Liangqiao (空格)28(回车)" // cin会读取其中的"Liangqiao"和"28"这两个数据, // 并分别保存到strName和nAge这两个变量中 cin>>strName>>nAge;
在这里,我们首先定义了两个变量strName和nAge,分别用于保存用户输入的字符串数据和整数数据。然后,利用提取符“>>”从cin对象中提取用户通过键盘输入的数据,当程序执行到这里的时候会暂停下来等待用户输入,一旦用户完成输入并回车后,“>>”就会从cin对象中提取用户输入的数据并分别保存到相应的变量中,这样就完成了数据从键盘到应用程序的输入。
下面再来看一个输入和输出配合使用的实例。
// 引入定义输入/输出流对象的头文件 #include <iostream> // 使用std名字空间 using namespace std; int main() { // 在屏幕上输出提示信息 // 在字符串的后面,我们还输出了一个特殊的操纵符endl, // 它的做用是表示一行的结束(end of line),它会让输出换行 // 并刷新输出缓冲区,让用户可以立即看到输出 cout<< "请输入两个整数(例如,19 83):" <<endl; // 用于保存输入数据的变量 int n1, n2; // 从cin提取用户输入的两个整数,输入时,整数之间以空格间隔 cin>> n1 >> n2; // 对数据进行处理 // 计算两个加数的和,将结果保存到nRes变量 int nRes = n1 + n2; // 将两个加数和计算结果输出到屏幕 cout<< n1 << " + " << n2 // 两个加数 << " = " << nRes <<endl; // 计算结果 return 0; }
利用cin和cout,短短的几行代码,就实现了一个整数加法计算程序。它可以接受用户输入的数据(两个加数),然后对数据进行处理(加和运算),并最终将结果数据(nRes)输出,使得问题(计算两个整数的和)得到圆满解决。
2.2.2 输出格式控制
在输出数据的时候,除了简单地输出数据之外,在不同的应用场景之下,我们往往对数据输出的格式也有着不同的要求。比如,同样是输出一个double类型的小数,如果这个小数只是一个学生成绩,那么输出时保留一位小数就足够了,而如果它是一个人民币汇率,那么输出时至少要保留三位小数才行。为了对输出格式进行控制,C++提供了很多操纵符,比如我们在前面的代码中用到过的endl就是一个控制换行的操纵符。这些操纵符可以使用“<<”直接插入到输出流对象cout中以实现对输出格式的控制。这些操纵符大都定义在头文件<iomanip>中,所以如果想在代码中使用它们来控制输出格式,我们需要先使用预编译指令#include引入这个头文件才行。表2-1列出了C++中常用的格式操纵符。
表2-1 常用的输出流格式操纵符
操 纵 符 |
作 用 |
dec |
采用十进制显示数值数据,这是输出流对象的默认设置 |
hex |
采用十六进制显示数值数据,比如,十进制数值54321会被输出显示为十六进制的d431。如果同时在输出流中插入一个showbase操纵符,还可以同时输出十六进制数值的0x前缀 |
oct |
采用八进制显示数值数据,同样的十进制数值54321,在这种模式下会被输出显示为八进制的152061 |
endl |
插入换行符,并刷新输出流缓冲区 |
setprecision(n) |
设置浮点数的输出精度为n。默认情况下,精度指的是浮点数中小数点前后所有数字的个数。如果同时在输出流中插入了fixed操纵符,那么精度指的就是小数点后的数字的个数 |
setw(n) |
设置输出的每个数据的显示宽度 |
综合运用这些操纵符,可以满足一些特殊的输出格式要求。例如,要求以“保留小数点后两位有效数字”的格式输出小数1.23456,然后换行,可以用如下的代码实现:
cout<<fixed<<setprecision(2)<<1.23456<<endl;
在这里,首先向cout对象插入一个fixed操纵符,表示以固定的小数位数输出小数数值。然后,通过setprecision()设置需要保留的小数点后有效数字位数,这样就可以达到“保留小数点后两位有效数字”的输出格式要求了。
除了对数值数据输出格式的控制之外,很多时候,我们还需要对字符串的输出格式进行控制,从而让程序的输出更加美观。跟在输出流中插入操纵符控制数值数据的输出格式类似,我们也可以通过在字符串中加入一些用于格式控制的转义字符来实现对字符串输出格式的控制。这里所谓的转义字符,就是在某些字符前加上“\”符号构成的特殊字符,这些字符不再表达它原本的含义,转而表达的是对格式的控制或其他特殊意义,所以被称为转义字符。常用的格式控制转义字符有:“\n”表示换行;“\t”表示间隔整数个Tab的距离输出等。例如,下面的代码实现了换行显示:
cout<<"分多行\n显示一个字符串"<<endl;
程序执行后,将在屏幕上看到“\n”将一个字符串分成了两行显示:
分多行
显示一个字符串
综合使用C++语言所提供的这些输出流操纵符和格式控制转义字符,可以实现灵活多样的自定义格式化输出,从而满足对输出格式的个性化要求。
2.2.3 读/写文件
除了用硬件设备(键盘、屏幕等)与程序进行输入/输出之外,更多的时候,程序还需要对文件进行读写以实现跟文件的数据输入/输出:从文件读取数据到应用程序进行处理,实现数据输入;将处理得到的结果数据写入文件进行保存,实现数据输出。C++标准库提供了ifstream(input file stream)和ofstream(output file stream),分别用来从文件中读取数据和将数据写入文件。它们定义在<fstream>头文件中,如果想在程序中使用它们来读写文件,我们需要先引入这个头文件。(需要提醒的是,这一小节涉及到了很多C++中比较高阶的内容,比如,类、对象以及成员函数等。如果在阅读这一小节的时候有困难,可以先跳过这一小节,等到掌握后面的必要的知识后再来阅读,就可以轻松理解了。)
在使用的时候,我们首先需要创建它们的实例对象。如果是为了将数据输出到文件,则创建ofstream的对象,反之则创建ifstream的对象。通过在创建对象的时候提供文件名,这些对象就可以打开或者创建相应的文件,从而将对象跟某个具体的文件关联起来,接下来就可以操作这些对象,判断文件是否成功打开等。
在完成这些创建对象打开文件的准备工作之后,就可以直接通过这些对象对文件进行读写操作了。其方式跟通过cin或cout进行数据输入/输出非常类似,我们也同样是用插入符“<<”将数据插入到一个ofstream对象中,实现将程序中的数据输出到这个对象关联的文件;用提取符“>>”从一个ifstream对象中提取数据,实现从这个对象关联的文件中输入数据到程序。例如,我们首先读取一个文件中的内容,然后将新的内容写入这个文件:
#include <iostream>
#include <iostream> // 引入文件输入/输出需要的头文件 #include <fstream> using namespace std; // 主函数 int main() { // 定义变量,保存程序中的数据 int nYear, nMonth, nDate; // 创建输入文件流对象fin,并尝试打开Data.txt文件 // 这个文件应该位于exe文件相同的目录下, // 才能直接使用文件名打开,否则应该使用完整的文件路径 ifstream fin("Date.txt"); // 判断Date.txt文件是否成功打开 // 如果成功打开文件,则从文件中读取内容 if( fin.is_open() ) { // 用提取符“>>”从文件输入流对象fin中读取文件中的数据,并保存到相应的变量 // 文件中的内容应该是以空格间隔的三个整数,例如:1983 7 3 fin>>nYear>>nMonth>>nDate; // 将读取的数据显示到屏幕 cout<<"文件中记录的日期是" <<nYear<<"-"<<nMonth<<"-"<<nDate<<endl; // 读取完成后,关闭文件 fin.close(); } else { // 如果文件打开失败,则提示错误信息 cout<<"无法打开文件并进行读取"<<endl; } // 提示用户输入新的数据并将其写入文件 cout<<"请输入新日期(例如,1981 9 22):"<<endl; // 获取用户通过键盘输入的数据并保存到相应的变量中 cin>>nYear>>nMonth>>nDate; //创建输出文件流对象fout,并尝试打开Data.txt文件, // 如果这个文件不存在,则创建一个新文件并打开 ofstream fout("Date.txt"); // 如果成功打开Date.txt文件,则将用户输入的数据写入文件 if( fout.is_open() ) { // 利用插入符“<<”将数据插入文件输出流对象fout中, // 也就是将数据写入与之关联的Data.txt文件中 // 为了便于将来的读取,这里输出的数据以空格作为间隔 fout<<nYear<<" "<<nMonth<<" "<<nDate; // 写入完成后,关闭文件 fout.close(); } else { // 如果无法打开文件,则提示用户信息 cout<<"无法打开文件并进行写入"<<endl; } return 0; }
在这段程序中,首先创建了一个输入文件流ifstream的对象fin,并利用它的构造函数将其关联到一个文本文件Date.txt。所谓构造函数,就是这个对象创建的时所执行的函数。这里,使用“Date.txt”文件名作为构造函数的参数,实际上就是打开这个文件并使用这个文件创建fin对象。除此之外,还可以使用fin所提供的open()成员函数来打开一个文件。当我们创建fin对象之后,在进行读取操作之前,为了提高程序的健壮性,我们往往还需要使用它的is_open()成员函数对是否成功打开文件进行判断,只有文件被成功打开,才能进行下一步的读取操作。当利用fin成功打开一个文件之后,就可以利用提取符“>>”从fin中提取各种数据,实际上也就是从Data.txt文件中读取数据。例如,如果文件中的内容如下:
1982 10 3
而对应的,程序中用于读取的代码是:
fin>>nYear>>nMonth>>nDate;
默认情况下,提取符“>>”会以空格为分隔符,逐个从文件中读取数据并将其保存到相应的数据变量中。代码执行完毕后,文件中的“1983”、“7”和“3”这三个数值就分别被读取并保存到了程序中的nYear、nMonth和nDate 这三个变量中,实现了从文件到程序的数据输入。在文件读取完毕之后,需要用close()成员函数关闭文件。
同样,为了将数据写入文件,需要创建一个输出文件流ofstream的对象fout,同时通过它的构造函数或open()函数来打开一个文件,将这个文件和fout对象关联起来,然后通过插入符“<<”将需要输出的数据插入到fout对象,也就相当于将数据写入到了与之关联的文件,输出完成后用close()函数关闭文件,这样就实现了从程序到文件的数据输出。整个过程如图2-9所示。
图2-9 文件读/写
这里我们所介绍的只是文件输入/输出的最基本操作,但已经基本上能够满足我们的日常需要了。除此之外,C++还提供了更丰富的文件读写操作,比如读/写二进制文件,调整移动读写位置等,从而也满足我们对文件读/写的进一步需求。