第十七章:输入,输出和文件
显而易见,本章的主要内容是和文件相关的数据操作;实际上关于文件系统,操作系统提供了相应的接口,并且很多本质上不是文件的系统也被抽象为文件,比如鼠标键盘打印机等,这种“万物皆文件”的封装方法实际上已经有一段历史了,不过本章的研究中心并不在这个方向,我们关心的是如何操作成品文件,如何读写,如何在屏幕上显示信息,如何从键盘获得输入等;
1.C++输入和输出概述
C++中有两个头文件:<\iostream\>
和\<fstream\>
;在这门语言中,输入和输出都被抽象为依靠“流”这种东西,因此管理输入分为两步:
- 将流和输入去向的程序关联起来
- 将流与文件关联起来
正如同现实中的水流那样,每个流都有两端,分别是数据的来源和目的地;对于从文件中读取数据来说,一端是文件,一端是程序,数据从文件流向程序;对于从程序写数据到文件来说,一端是程序,一端是文件,数据从程序流向文件;
缓冲区是一种精巧的设计,它衔接了高速存储器和低速存储器,使得访问数据的效率得到了很大的提升;
可以这样理解:你有一个钱包,你时不时从银行中取几张红色钞票放进钱包;你的钱包是贴身的,存取(对于程序来说则是读写)都比直接从银行中取钱快得多;有了钱包,买东西时可以很快完成支付,等钱不够的时候再重新取钱;如果你没有钱包,每次支付都要打车去银行取钱,这就很费时间;有了钱包(缓冲区),你支付(读写数据)的速度就比直接从银行(低速存储器)取钱(读写数据)就快了很多;
缓冲区满了之后会刷新,不过有时也会因为其它原因刷新;比如在C++中输入时会等用户按下回车或输入换行符后刷新缓冲区;另外当输入和输出切换时,也会刷新缓冲区;缓冲区一般等于或大于512字节,这和磁盘的扇区大小有关;
头文件<iostream>中定义了用于输入输出的类,这是它们的关系:
这些类具体是:
- streambuf类为缓冲区提供了内存,并提供了用于填充缓冲区,访问缓冲区内容,刷新缓冲区和管理缓冲区内存的类方法
- ios_base类表示流的一般特征,比如是否可以读取,是二进制流还是文本流等
- ios类基于ios_base,其中包括了一个指向streambuf的类方法;
- ostream类从ios类派生而来,提供了输出方法
- istream类从ios类派生而来,提供了输入方法
- iostream类是从istream和ostream类派生的,因此继承了输入方法和输出方法
cin和cout是适用于一般的char的,而wcin和wcout是适用于wchar_t的,也就是宽字符;
包含C++库iostream时,具体自动创建这8个类:
- cin:处理窄字符char,从标准输入流获取数据,默认来自键盘可重定向
- cout:处理窄字符char,将数据插入标准输出流,默认指向屏幕可重定向
- wcin:处理宽字符wchar_t,从标准输入流获取数据,默认来自键盘可重定向
- wcout:处理宽字符wchar_t,将数据插入标准输出流,默认指向屏幕可重定向
- cerr:处理窄字符char,将错误信息插入标准错误流,不进行缓冲
- clog:处理窄字符char,将错误信息插入标准错误流,进行缓冲
- wcerr:处理宽字符wchar_t,将错误信息插入标准错误流,不进行缓冲
- wclog:处理宽字符wchar_t,将错误信息插入标准错误流,进行缓冲
重定向借助于符号>,你可以将它理解为一个漏斗,比如在WINDOWS10中的命令提示符系统中:
cow_cnt file:
C>counter<oklahoma>cow_cnt
C>
//对程序counter.exe执行操作:将标准输入和oklahoma文件关系起来,将标准输出和文件cow_cnt关联起来
这种重定向实际上是在不同操作系统中得到支持的,比如DOS,Windows,Linux,UNIX等;注意重定向的只是标准输入和标准输出,标准错误不受影响,也就是说下列代码:
if (success)
std::cout << "Here come the goodies!\n";
else
{
std::cerr << "Something horrible has happened.\n";
exit(1);
}
在进行输出重定向时,屏幕上仍然会显示标准错误信息;
2.使用cout进行输出
C++C将输入视为字节流,具有多个类方法实现这个事情;首先是cout,它有四个特点:
- 自动识别数据类型
- 可以连续使用
- 输入endl时打印换行符并刷新缓冲区
- 输入flush时只刷新缓冲区
另外还有一种特殊情况,当cout接受一个指向字符串的指针时,它会打印这个字符串,但当接受指向其它类型的指针时,会以十六进制显示这个指针;如果要打印字符串的地址,可以先将字符串的地址强制转换为其它类型;
另外还有cout.put()和cout.write()这两个方法,前者打印一个字符后者打印一个字符串;注意当向cput.put()输入一个字符之外的值时,它会尝试将这个数据转换成整型,然后再转换为字符;二者都可以拼接使用;cout.write()的模板原型是:
basic_ostream<charT, traits>& write(const char_type* s, streamsize n);
//也就是说,第一个参数是要打印的字符串,第二个参数是要打印的字符数
注意:对cout.write()来说,即使n大于字符串的字符总数,函数也会尝试打印足够多的字符数;这会导致意想不到的结果;
具体而言,cout格式化的策略是:
- 对于char值,如果它代表的是可打印字符,则将被作为一个字符显示在一个宽度为一个字符的字段中;
- 对于数值整型,将以十进制方式显示在一个刚好容纳该数字及符号的字段中;
- 字符串被显示在宽度等于该字符串长度的字段中
- 浮点类型被显示为6位,末尾的0不显示;数字以定点表示还是科学计数法表示取决于它的值,具体来说当指数大于等于6或小于等于-5时使用科学计数法,否则使用定点表示法;
另外,显示时的技术系统功能也是十分强大的,下面我们会具体讨论计数系统的显示设置;ios_base类中定义了很多控制符,因为ostream是继承来的,所以这些控制符都可以直接使用在ostream对象上;控制符有两种使用方式,函数式和控制符式,下面我们就会见识到这一点;
比如:分别以十进制,八进制,十六进制来显示数据:
dec(cout);//十进制
oct(cout);//八进制
hex(cout);//十六进制
另外,可以把这三个函数当控制符使用:
cout << hex;
字段的宽度是可以调整的,借助函数width():
int width();//返回当前字段宽度
int width(int i);//将字段宽度设置为i
每次重新设置只能影响下一个操作,之后恢复到默认字段长度;不过要注意这个函数是类方法,因此调用的时候要使用对象;另外,C++永远不会主动截断数据,因为输出完整的数据比输出好看的数据重要;
还可以设置字段中空白字符的填充字符:
cout.fill('*');
C++的默认精度是六位,但这是可以改变的:
cout.precision(2);//将精度设置为2位
新设置的精度一直有效;
ios_base类还提供了一个名为setf()的函数,还有一种专用的数据格式:fmtflags,这个数据格式实际上是bitmask的一个typeef;比如这样使用这个函数:
cout.setf(ios_base::showpoint);//显式显示末尾的小数点
这是所有单参数的操作符:
常量 | 含义 |
---|---|
ios_base::boolalpha | 输入和输出bool值,可以为true或false值 |
ios_base::showbase | 对于输出使用C++基数前缀 |
ios_base::showpoint | 显示末尾的小数点 |
ios_base::uppercase | 对于十六进制输出,使用大写字母,E表示法 |
ios_base::showpos | 在正数前面加上+ |
注意最后一个加上+,只针对十进制有效,因为其它进制都是默认是无符号整型的;第二种setf()函数使用两个参数并返回原来的设置:
fmtflags setf(fmtflags, fmtflags);
//返回更改前的设置情况
两个参数的工作模式是:第一参数指出要设置哪些位,第二参数指出要清除哪些位;下面的表格提供了实例:
第二个参数 | 第一个参数 | 含义 |
---|---|---|
ios_base::basefield | ios_base::dec | 使用基数10 |
ios_base::oct | 使用基数8 | |
ios_base::hex | 使用基数16 | |
ios_base::floatfield | ios_base::fixed | 使用定点计数法 |
ios_base::scientific | 使用科学计数法 | |
ios_base::adjustfield | ios_base::left | 使用左对齐 |
ios_base::right | 使用右对齐 | |
ios_base::internal | 符号或基数前缀左对齐,值右对齐 |
定点表示法和科学计数法都有两个特征:
- 精度指的是小数位数,而不是总位数
- 显示末尾的0
比如可以这样完成一次修改:
ios_base::fmtflags old = cout.setf(ios_base::left, ios_base::adjusted);//使用左对齐,并保存原来的设置
cout.setf(old, ios_base::adjusted);//恢复原来的设置
另外实际上,这些参数并不是非要在setf()函数中使用的,也可以像控制符flush,endl那样,这是关于控制符的表格:
控制符 | 调用 |
---|---|
nouppercase | unsetf(ios_base::uppercase) |
internal | setf(ios_base::internal, ios_base::adjustfield) |
left | setf(ios_base::left, ios_base::adjustfield) |
right | setf(ios_base::right, ios_base::adjustfield) |
dec | setf(ios_base::dec, ios_base::basefield) |
hex | setf(ios_base::hex, ios_base::basefield) |
oct | setf(ios_base::oct, ios_base::basefield) |
fixed | setf(ios_base::fixed, ios_base::floatfield) |
scientific | setf(ios_base::fixed, ios_base::floatfield) |
C++有一个名为iomanip的头文件,提供了相应的控制符;分别是setprecision(),和setfill(),和setw(),分别设置精度,填充字符和字段宽度;另外,setf()函数调用后的效果可以用函数usetf()来取消;下面的表格列出了标准控制符:
控制符 | 调用 |
---|---|
boolalpha | setf(ios_base::boolalpha) |
noboolalpha | unsetf(ios_base::noboolalpha) |
showbase | setf(ios_base::showbase) |
noshowbase | unsetf(ios_base::noshowbase) |
showpoint | setf(ios_base::showpoint) |
noshowpoint | unsetf(ios_base::noshowpoint) |
showpos | setf(ios_base::showpos) |
noshowpos | unsetf(ios_base::noshowpos) |
uppercase | setf(ios_base::uppercase) |
nouppercase | unsetf(ios_base::nouppercase) |
使用cin进行输入
cin是istream类的一个实例,也支持拼接使用;同样的,dec,hex,oct控制符也可以在cin上使用:
cin>>oct;
运算符>>实际上还可以被称为抽取运算符,在对一个字符串进行输入操作时,cin会尝试从输入流中抽取一个单词,并加上一个空值字符组成一个C风格字符串;当输入没有得到满足时,它会写一个0进去;
流状态指的是cin在进行输入时,数据来源是否正常的表征;在ios_base中有三个表示流状态的量:eofbit,badbit,failbit;当cin到达文件尾时,eofbit被设置;当cin没能读取到预期的字符时,设置failbit();当出现了莫名其妙的故障时,设置badbit;这是具体情况的表格汇总:
成员 | 描述 |
---|---|
eofbit | 如果达到文件尾,被设置为1 |
badbit | 如果流被破坏,则设置为1 |
failbit | 如果没能获得预期的字符,则设置为1 |
goodbit | 另一种表示0的方法 |
good() | |
eof() | |
bad() | |
fail() | |
rdstate() | 返回流状态 |
exceptions() | 返回一个位掩码,指出哪些标记导致异常被引发 |
exceptions(iostate ex) | 设置哪些状态将导致clear()引发异常; |
clear(iostate s) | 将流状态设置为s,默认参数是0,也就是清楚异常状态 |
setstate(iostate s) | 调用clear(rdstate()|s) |
这里我实际上并不想涉及太多,因为后面几个复杂的函数的工作实际上都可以由一组功能简单的函数;
cin本身只有在所有输入状态都良好的情况下才会返回true;设置流状态位后,cin实际上就不再接受输入了,除非加一些代码清除故障并且恢复被重置的错误位;
这里还有其它的istream方法:
- 方法get(char&)和get(void)不提供跳过空白字符串的功能;
- 函数get(char* int, char)和getline(char *, int, char)在默认情况下读取整行而不是一个单词;
这两种函数被称为非格式化输入函数;另外,cin.get(char &)是可以嵌套使用的;但cin.get()是不可以嵌套使用的,因为它的返回值是一个int类型数;下面的表格完成了总结:
特征 | cin.get(ch) | ch = cin.get() |
---|---|---|
传输输入字符的方式 | 赋值给参数ch | 将函数返回值赋值给ch |
字符输入时函数的返回值 | 指向istream对象的引用 | 字符编码 |
到达文件为时函数的返回值 | 转换为false | EOF |
注意>>抽取运算符是有跳过空白的属性的,但是其它形式没有
下面是字符串输入函数:
istream & get(char *, int, char);//将分隔符保留在流中不保留在输入中,重新指定了分隔符
istream & get(char *, int);//同上,但没指定分隔符
istream & getline(char *, int, char);//分隔符被丢弃,重新指定了分隔符
istream & getline(char *, int);//同上,但没指定分隔符
//上面四个都指定了读取的字符数,在读满字符数或者遇到分隔符时终止读入
还有一个特殊的函数:
cin.ignore(255,'\n');//读够255个字符,或在遇到换行符后停止;可以嵌套使用
下面的表格总结了cin.get()和cin.getline()的行为:
方法 | 行为 |
---|---|
cin.getline(char *,int) | 如果没有读取任何字符(但换行符被视为读取了一个字符),则设置fainbit;如果读取了最大数目的字符并且行中还有其它字符,设置fainbit; |
cin.get(char *,int) | 如果没有读取任何字符,则设置failbit; |
下面还有几个其它的istream方法:
- read()函数读取指定的字节数,存放在指定位置中
- peek()函数返回流中的下一个字符但是不抽取它
- gcount()函数返回最后一个以非格式化抽取方法读取的字符数,抽取指的是<<读取
- putback()将一个字符插入输入字符串中,这样读取时的第一个字符就是之前插入的字符
文件输入和输出
对于针对屏幕的输入输出,我们使用的是头文件iostream中的组件,对于文件读写,我们需要用到fstream中的组件;要写一个文件必须做到:
- 创建一个ofstream对象来管理输出流
- 将该对象与特定的文件关联起来
- 已使用cout的方式使用该对象,唯一的区别是输出将进入文件而不是屏幕
要读一个文件必须做到:
- 创建一个ifstream对象来管理输入流
- 将该对象与特定的文件关联起来
- 以使用cin的方法使用该对象
当输入和输出流对象过期时,到文件的连接自动关闭;另外还可以使用close()来手动关闭到文件的链接;但是这样做不会删除流本身,依然可以将流关联到另外一个文件;
当打开一个流之后,可以使用函数is_open()来检查是否成功打开;当打开失败时,会设置failbit位,这时fail()函数会返回1;但前者是更好的办法,可以检测出很多莫名其妙的问题;
操作系统对一个程序能同时打开的文件数是有限制的,所以我们可以把同时打开多个文件的操作转换为一个一个打开;
命令行处理基数是指在启动程序时,系统会传递给被启动的程序一个指针数组,里面是参数的信息,比如:
int main(int argc, char *argv[]){}
//其中argc是数组的大小,数组的每个元素是指向参数的指针,对这个指针解引用就能得到一个字符串;
文件模式指的是对文件进行操作的方式,有很多种;下面的表格解释了文件模式常量和它们的功能:
常量 | 含义 |
---|---|
ios_base::in | 打开文件以便读取 |
ios_base::out | 打开文件以便写入 |
ios_base::ate | 打开文件并移动到文件尾 |
ios_base::app | 写入的数据追加到文件尾 |
ios_base::trunc | 如果文件存在,截断文件 |
ios_base::binary | 二进制文件 |
运算符|可以合并打开模式;比如下面的模式就是写文件并且在文件的尾部写:
ofstream fout("bagels", ios_base::out | ios_base::app);
要注意的是,ios_base::out本身会导致文件被截断,但是当ios_base::out和ios_base::in使用|运算符连接的时候,不会截断文件;
二进制文件的意思是,数据是以二进制而不是以字符的形式存储的;字符形式存储的文件,可以方便的用任何一种文本编辑器打开;但是在写入二进制文件时,我们会面临如何将高复杂度的数据类型写入文件的问题,实现方法实际上仅仅是将那个内存块的内容搬进文件中:
const int LIM = 20;
struct planet
{
char name[LIM];
double population;
double g;
};
planet pl;
ofstream fout("planets.dat", ios_base::out | ios_base::app | ios_base::binary);
fout.write((char*) &pl, sizeof pl);
总之写入二进制文件的要点是:
- 使用二进制文件模式
- 使用成员函数write()
有些系统把文本模式和二进制模式合二为一了,不过这不是我们的研究范围;具体而言,为了以二进制格式写文件必须这么做:
fout.write((char *) &pl, sizeof pl);
//将指针&pl强制转换为指向字符串的指针,这实际上把内存块直接搬入文件中
//第二个参数sizeof pl的意思是将(sizeof pl)长度的内存单元写入文件
要从文件中恢复变量数据,只需要执行相反的操作:
ifstream fin("planet.dat", ios_base::in | ios_base::binary);
fin.read((char *) &pl, sizeof pl);//从文件中读取指定个数的字节到某个指针地址中,以字节的形式;
read()和write()函数的功能是相反的,一般来说由后者写入的数据都要靠前者恢复;一定要注意搭配使用,因为后者写入的数据实际上只能由前者恢复;
如果同时有对文件读取和写入的需求,可以将控制流的实例初始化为fstream的类对象,同时需要使用ios_base::in,ios_base::out,ios_base::binary;具体而言,需要这样一条语句:
finout.open(file, ios_base::in, ios_base::out, ios_base::binary);
接下来为了实现随机访问,需要一种在文件中移动的方式;这里有两个方法:seekg()
和seekp()
;前者是写文件的指针,后者是读文件的指针;下面是seekg()的原型:
basic_istream<charT, traits>& seekg(off_type, ios_base::seekdir);//移动到距离第二个参数第一个参数的位置
basic_istream<charT, traits>& seekg(pos_type);//定位到距离开头特定距离的位置
这两个原型看起来有些令人费解,实际代码中它们是这样的:
istream & seekg(streamoff, ios_base::seekdir);//移动到距离第二个参数第一个参数的位置
istream & seekg(streampos);//定位到距离开头特定距离的位置
注意两个参数的seekg()的第二个参数实际上只有表格中的三种类型:
关键字 | 含义 |
---|---|
ios_base::beg | 以开头位置为参照 |
ios_base::cur | 以当前位置为参照 |
ios_base::end | 以文件尾为参照物 |
和数组下标类似,文件的第一个字节编号为0;要注意的是,一般程序读取到文件尾时会设置eofbit,要继续操作文件时应当使用clear()来重置流状态;如果想获取写入指针和读取指针的位置,可以使用函数tellg()
和tellp()
;
有些时候我们可能会需要使用临时文件来存储一些数据,可以使用一个函数来生成随机的文件名:
char* tmpnam(char* pszName);//将生成的文件名存储到参数的字符串中