一、C++输入和输出概述
1.1、流和缓冲区
C++程序把输入和输出看作字节流。输入时,程序从输入流中抽取字节;输出时,程序将字节插入到输出流中。对于面相文本的程序,每个字节代表一个字符,更通俗地说,字节可以构成字符或数值数据的二进制表示。输入流中的字节可能来自键盘,也可能来自存储设备(如硬盘)或其他程序。输出流中的字节可以流向屏幕、打印机、存储设备或其他程序。流充当了程序和流源或流目标之间的桥梁。这使得C++程序可以以相同的方式对待来自键盘的输入和来自文件的输入。C++程序只检查字节流,而不需要知道字节来自何方。同理,通过使用流,C++程序处理输出的方式将独立于其去向。因此管理输入将包含两步:
*将流与输入去向的程序关联起来。
*将流与文件连接起来。
换句话说,输入流需要两个连接,每端各一个。文件端部连接提供了流的来源,程序端连接将流的流出部分转储到程序中(文件端连接可以是文件,也可以是设备)。同样,对输出的管理包括将输出流连接到程序以及将输出目标与流关联起来。
通常,通过使用缓冲区可以更高效地处理输入和输出。缓冲区是用作中介的内存块,它是将信息从设备传输到程序或从程序传输给设备的临时存储工具。
1.2、流、缓冲区和iostream文件
管理流和缓冲区的工作有点复杂,但iostream(以前为iostream.h)文件中包含一些专门设计用来实现、管理流和缓冲区的类。C++98版本C++I/O定义了一些类模板,以支持char和wchar_t数据;C++11添加了char16_t和char32_t具体化。通过使用typedef工具,C++使得这些模板char具体化能够模仿传统的非模板I/O实现。下面是其中的一些类:
*streambuf类为缓冲区提供了内存,并提供了用于填充缓冲区、访问缓冲区内容、刷新缓冲区和管理缓冲区内存的类方法;
*ios_base类表示流的一般特征,如是否可读取、是二进制流还是文本流等;
*ios类基于ios_base,其中包含了一个指向streambuf对象的指针成员;
*ostream类是从ios类派生而来的,提供了输出方法;
*istream类也是从ios类派生而来的,提供了输入方法;
*iostream类是基于istream和ostream类的,因此继承了输入方法和输出方法。
要使用这些工具,必须使用适当的类对象。例如,使用ostream对象(如cout)来处理输出。创建这样的对象将打开一个流,自动创建缓冲区,并将其与流关联起来,同时使得能够使用类成员函数。
补充:重定义I/O
ISO/ANSI标准C++98对I/O作了两方面的修订。首相是从ostream.h到ostream的变化,用ostream将类放到了std名称空间中。其次,I/O类被重新编写。为成为国际语言,C++必须能够处理需要16位的国际字符集或更宽的字符类型。因此,该语言在传统的8位char(“窄”)类型的基础上添加了wchar_t(“宽”)字符类型;而C++11添加了类型char16_t和char32_t。每种类型都需要有自己的I/O工具。标准委员会被没有开发两套(现在为4套)独立的类,而是开发了一套I/O类模板,其中包括basic_istream<charT, traits<charT>>和basic_ostream<charT, traits<charT>>。traits<charT>模板是一个模板类,为字符类型定义了具体特性,如如何比较字符是否相等以及字符的EOF值等。该C++11标准提供了I/O的char和wchar_t具体化。例如,istream和ostream都是char具体化的typedef。同样,wistream和wostream都是wchar_t的具体化。例如,wcout对象用于输出宽字符流。头文件ostream中包含了这些定义。
ios基类中的一些独立于类型的信息被移动到了新的ios_base类中,这包括各种格式化常量,例如ios::fixed(现在为ios_base::fixed)。另外,ios_base还包含了一些老式ios中没有的选项。
C++的iostream类库管理了很多细节。例如,在程序中包含iostream文件将自动创建8个流对象(4个用于窄字符流,4个用于宽字符流)。
*cin对象对应于标准输入流。在默认情况下,这个流被关联到标准输入设备(通常为键盘)。wcin对象于此类似,但处理的是wchar_t类型。
*cout对象与标准输出流相对应。在默认情况下,这个流被关联到标准输出设备(通常为显示器)。wcout对象与此类似,但处理的事wchar_t类型。
*cerr对象与标准错误流相对应,可用于显示错误消息。在默认情况下,这个流被关联到标准输出设备(通常为显示器)。这个流没有被缓冲,这意味着信息将被直接发送给屏幕,而不会等到缓冲区填满或新的换行符。wcerr对象与此类似,但处理的是wchar_t类型。
*clog对象也对应着标准错误流。在默认情况下,这个流被关联到标准输出设备(通常为显示器)。这个流被缓冲。wclog对象于此类似,但处理的是wchar_t类型
*对象代表流——这意味着什么呢?当iostream文件为程序声明一个cout对象时,该对象将包含存储了与输出有关的信息的数据成员,如显示数据时使用的字段宽度、小数位数、显示整数时采用的计数方法以及描述用来处理输出流的缓冲区的streambuf对象的地址。下面的语句通过指向的streambuf对象将字符串"Hello world!"中的字符放到cout管理的缓冲区中:cout << "Hello world!";
总之,流的一端与程序相连,另一端与标准输出相连,cout对象凭借streambuf对象的帮助,管理者流中的字节流。
1.3、重定向
标准输入和输出流通常连接着键盘和屏幕。但很多操作系统(包括UNIX,Linux和Windows)都支持重定向,这个工具使得能够改变标准输入和标准输出。
二、使用cout进行输出
C++将输出看作字节流(根据平台的不同,可能是8位、16位或32位的字节,但都是字节),但在程序中,很多数据被组织成比字节更大的单位。例如,ing类型由16位或32位的二进制表示;double值由64位的二进制数表示。但在将字节流发送给屏幕时,希望每个字节表示一个字符值。因此,ostream类最重要的任务之一是将数值类型(如int或float)转换为以文本形式表示的字符流。也就是说,ostream类将数据内部表示(二进制位模式)转换为由字符字节组成的输出流(以后会有仿生移植物,使得能够直接翻译二进制数据)。为执行这些转换任务,ostream类提供了多个类方法。
2.1、重载的<<运算符
在C++中,与C一样,<<运算符的默认含义是按位左移运算符。但ostream类重新定义了<<运算符,方法是将其重载为输出。在这种情况下,<<叫做插入运算符,而不是左移运算符。插入运算符被重载,使之能够识别C++中所有的基本类型:
* unsigned char;
* signed char;
* char;
* short;
* unsigned short;
* int;
* unsigned int;
* long;
* unsigned long;
* long long(C++11);
* unsigned long long (C++11);
* float;
* double;
* long double;
对于上述每种类型,ostream类都提供了operator<<()函数的定义。因此,如果使用下面这样一条语句,而value是前面列出的类型之一,则C++程序将其对应于有相应的特征标的运算符函数:
cout << value;
2.1.1 输出和指针
ostream类还为下面的指针类型定义了插入运算符函数:
* const signed char *;
* const unsigned char *;
* const char *;
* void *。
C++使用指向字符串存储位置的指针来表示字符串。指针的形式可以是char数组、显式的char指针或用引号扩起来的字符串;并使用字符串中的终止空字符来确定何时停止显示字符。
对于其他类型的指针,C++将其对应于void *,并打印地址的数值表示。如果要获取字符串的地址,则必须将其强制转换为其他类型,例如:
int num = 12;
char * name = "Tom";
cout << # // 打印变量num的地址
cout << name;//将会打印字符串“Tom”
cout << (void *) name;//将会打印字符串“Tom”的地址
2.1.2 拼接输出
插入运算符的所有化身的返回类型都是ostream & 。也就是说,原型格式如下:
ostream & operator<<(type);
(其中,type是要显示的数据的类型)返回类型ostream &意味着使用该运算符将返回一个指向ostream对象的引用;函数定义指出,该引用指向用于调用该运算符的对象。换句话说,运算符函数的返回值为调用运算符的对象。这种特性使得能够通过插入来连接输出。
2.2 其他的ostream方法
除了各种operator<<()函数外,ostream类还提供了put()方法和write()方法,前者用于显示字符,后者用于显示字符串。
最初,put()的原型如下:
ostream & put(char);
当前标准与此相同,但被模板化,以适用于wchar_t。可以用类方法表示法调用它:
cout.put('W');
其中,cout是调用方法的对象,put()是成员函数。和<<运算符函数一样,该函数也返回一个指向调用对象的引用,因此可以用它拼接输出:
cout.put(''A).put('B');
在原型合适的情况下,可以将数值型参数(如int)用于put(),让函数原型自动将参数转换为正确的char值。例如:
cout.put(65);//显示A
cout.put(66.3);//显示B
write()方法显示整个字符串,其模板原型如下:
basic_ostream<charT, traits> & write (const char_type* s, streamsize n);
write()的第一个参数提供了要显示的字符串的地址,第二个参数指出要显示多少个字符。使用cout调用write()时,将调用char具体化,因此返回类型为ostream &。
1 #include <iostream> 2 3 int main() 4 { 5 using std::cout; 6 using std::endl; 7 const char * state1 = "Florida"; 8 const char * state2 = "Kansas"; 9 const char * state3 = "Euphoria"; 10 std::size_t len = std::strlen(state2); 11 cout << "Increasing loop index:\n"; 12 int i = 0; 13 for (i = 1; i < len; i ++) { 14 cout.write(state2, i); 15 cout << endl; 16 } 17 18 cout << "Decreasing loop index:\n"; 19 for (i = (int)len; i > 0; i--) { 20 cout.write(state2, i) << endl; 21 } 22 cout << "Exceeding string length:\n"; 23 cout.write(state2, len + 5) << endl; 24 return 0; 25 } 26 27 输出结果: 28 Increasing loop index: 29 K 30 Ka 31 Kan 32 Kans 33 Kansa 34 Decreasing loop index: 35 Kansas 36 Kansa 37 Kans 38 Kan 39 Ka 40 K 41 Exceeding string length: 42 KansasEuph
从上面的程序可以看出,write()方法并不会在遇到空字符时停止打印字符,而只是打印指定数目的字符,即使超出了字符串的边界。
write()方法也可用于数值数据,可以将数字的地址强制转换为char *,然后传递给它:
#include <iostream> int main() { long val = 560031841; std::cout.write((char *) & val, sizeof(val)); return 0; } 输出结果: aha!
这不会将数值转换为相应的字符,而是传输内存中存储的位表示。例如,4字节的long值将作为4个独立的字节被传输。输出设备将把每个字节作为ASCII码进行解释。
2.3 刷新输出缓冲区
如果程序使用cout将字节发送给标准输出,情况将如何?由于ostream类对cout对象处理的输出进行缓冲,所以输出不会立即发送到目标地址,而是被存储在缓冲区中,直到缓冲区填满。然后,程序将刷新(flush)缓冲区,把内容发送出去,并清空缓冲,以存储新的数据。通常,缓冲区为512字节或其整数倍。当标准输出连接的是硬盘上的文件时,缓冲可以节省大量的时间。
如果实现不能在所希望时刷新输出,可以使用两个控制符中的一个来强行进行刷新。空字符flush刷新缓冲区,而控制符endl刷新缓冲区并插入一个换行符。这两个控制符的使用方式与变量名相同:
cout << "Hello world!" << flush;
cout << "Wait for me." << endl;
事实上,控制符也是函数。例如,可以直接调用flush()来刷新cout缓冲区:
flush(cout);
然而,ostream类对<<插入运算符进行了重载,使得下述表达式将被替换为函数调用flush(cout):
cout << flush;
因此,可以用更为方便的插入表示法来成功地进行刷新。
2.4 使用cout进行格式化
ostream插入运算符将值转换为文本格式。在默认情况下,格式化值的方式如下。
* 对于char值,如果它代表的是可打印字符,则将被作为一个字符显示在宽度为一个字符的字段中。
* 对于数值整型,将以十进制方式显示在一个刚好容纳该数字及负号(如果有的话)的字段中;
* 字符串被显示在宽度等于该字符串长度的字段中。
浮点数的默认行为有变化。下面详细说明了老式实现和新式实现之间的区别。
*新式:浮点类型被显示为6位,末尾的0不显示(注意,显示的数字位数与数字被存储时精度设置没有任何关系)。数字以定点表示法显示还是科学计数法表示,取决于它的值。具体来说,当指数大于等于6或小于等于-5时,将使用科学计数法表示。另外,字段宽度恰好容纳数字和负号(如果有的话)。默认的行为对应于带%g说明符的标准C库函数fprintf()。
*老式:浮点类型显示为带6位小数,末尾的0不显示(注意,显示的数字位数与数字被存储时的精度没有任何关系)。数字以定点表示法显示还是以科学计数法表示,取决于他的值。另外,字段宽度恰好容纳数字和负号(如果有的话)。
因为每个值的显示宽度等于它的长度,因此必须显式地在值之间提供空格;否则,相邻的值将不会被分开。
注意:并非所有的编译器都能生成符合当前C++标准格式的输出。另外,当前标准允许区域性变化。例如,欧洲实现可能遵循欧洲人的风格:使用逗号而不是句点来表示小数点。也就是说,2.54将被写成2,54 。区域库(头文件locale)提供了用特定的风格影响(imbuing)输入或输出流的机制,所以同一个编译器能够提供多个区域选项。本章使用美式格式。
(1)修改显示时使用的计数系统
ostream类是从ios类派生来的,而后者是从ios_base派生来的。ios_base类存储了描述格式状态的信息。例如,一个类成员中某些位决定了使用的计数系统,而另一个成员则决定了字段宽度。通过使用控制符(manipulator),可以控制显示整数时使用的技术系统。通过使用ios_base的成员函数,可以控制字段宽度和小数位数。由于ios_base类是ostream的间接基类,因此可以将其方法用于ostream对象(或子代),如cout。
注意:ios_base类中的成员和方法以前位于ios中。现在,ios_base是ios的基类。在新系统中,ios是包含char和wchar_t具体化的模板,而ios_base包含了非模板特性。
要控制整数以十进制、十六进制还是八进制显示,可以使用dec、hex和oct控制符。例如,下面的函数调用将cout对象的计数系统格式状态设置为十六进制:
hex(cout);
完成上述设置后,程序将以十六进制形式打印整数值,直到将格式状态设置为其他选项为止。注意,控制符不是成员函数,因此不必通过对象来调用。
虽然控制符实际上是函数,但它们通常的使用方式为:
cout << hex;
ostream类重载了<<运算符,这使得上述用法与函数调用hex(cout)等价。控制符位于名称空间std中。下面的代码演示了控制符的使用方法(注意,可以单独使用控制符,也可以将其作为一系列插入的部分):
#include <iostream> int main() { using namespace std; cout << "输入一个数字:"; int n; cin >> n; std::string str(5,' '); cout << "n" << str << "n*n\n"; cout << n << str << n*n << "(decimal)\n"; //设置成十六进制系统 cout << hex; cout << n << str; cout << n*n << " (hexadecimal)\n"; //设置为八进制系统 cout << oct << n << str << n * n << " (octal)\n"; dec(cout); cout << n << str << n * n << " (decimal)\n"; return 0; } 输出结果: 输入一个数字:12 n n*n 12 144(decimal) c 90 (hexadecimal) 14 220 (octal) 12 144 (decimal)
(2)调整字段宽度
可以使用width成员函数将长度不同的数字放到宽度相同的字段中,该方法的原型为:
int width();
int width(int i);
第一种格式返回字段宽度的当前设置;第二种格式将字段宽度设置为i个空格,并返回以前的字段宽度值。这使得能够保存以前的值,以便以后恢复宽度值时使用。
width()方法之影响显示的下一个项目,然后字段宽度将恢复为默认值。由于width()是成员函数,因此必须使用对象来调用它。
C++永远不会截短数据,因此如果试图在宽度为2的字段中打印一个7位值,C++将增宽字段,以容纳该数据。C/C++的原则是:显示所有的数据比保持列的整洁更重要;C++视内容重于形式。
1 #include <iostream> 2 3 int main() 4 { 5 using namespace std; 6 size_t w = cout.width(30); 7 cout << "Default field width = " << w << ":\n"; 8 cout.width(5); 9 cout << "N" << ':'; 10 cout.width(8); 11 cout << "N * N" << ":\n"; 12 13 for (long i = 1; i <= 100; i *= 10) { 14 cout.width(5); 15 cout << i << ':'; 16 cout.width(8); 17 cout << i * i << ":\n"; 18 } 19 return 0; 20 } 21 22 输出结果: 23 Default field width = 0: 24 N: N * N: 25 1: 1: 26 10: 100: 27 100: 10000:
在上面的输出中,值在字段中右对齐。输出中包含空格,也就是说,cout通过加入空格来填满整个字段。右对齐时,空格被插入到值的左侧。用来填充的字符叫做填充字符(fill character)。右对齐是默认的。
(3)填充字符
在默认情况下,cout使用空格填充字段中未被使用的部分,可以使用fill()成员函数来改变填充字符。例如,下面的函数调用将填充字符改为星号:
cout.fill('*');
这对于检查打印结果,防止接收方添加数字很有用。
1 #include <iostream> 2 3 int main() 4 { 5 using namespace std; 6 cout.fill('*'); 7 const char * staff[2] = {"iPhone6s", "iPad2"}; 8 long bonus[2] = {900,1350}; 9 for (int i = 0; i < 2; i ++) { 10 cout.width(10); 11 cout << staff[i] << ": $"; 12 cout.width(7); 13 cout << bonus[i] << "\n"; 14 } 15 return 0; 16 } 17 18 输出结果: 19 **iPhone6s: $****900 20 *****iPad2: $***1350
(4)设置浮点数的显示精度
浮点数精度的含义取决于输出模式。在默认情况下,它指的是显示的总位数。在定点模式和科学模式下,精度指的是小数点后面的位数。已经知道,C++的默认精度为6位(但末尾的0将不显示)。precision成员函数使得能够选择其他值。例如,下面的函数调用将cout的精度设置为2:
cout.precision(2);
和width()的情况不同,但与fill()相似,新的精度设置将一直有效,直到被重新设置。下面的程序说明了这一点:
1 #include <iostream> 2 3 int main() 4 { 5 using namespace std; 6 float price1 = 25.5; 7 float price2 = 1.9 + 8.0/9.0; 8 cout.width(10); 9 cout << "\"Apple\"" << ": $" << price1 << "\n"; 10 cout.width(10); 11 cout << "\"Flower\"" << ": $" << price2 << "\n"; 12 13 cout.precision(2); 14 cout.width(10); 15 cout << "\"Apple\"" << ": $" << price1 << "\n"; 16 cout.width(10); 17 cout << "\"Flower\"" << ": $" << price2 << "\n"; 18 19 return 0; 20 } 21 22 输出结果: 23 "Apple": $25.5 24 "Flower": $2.78889 25 "Apple": $26 26 "Flower": $2.8
注意,第3行没有打印小数点及其后面的内容。另外,第4行显示的总位数为2位。
(5)打印末尾的0和小数点
对于有些输出,希望保留末尾的0,iostream系列类没有提供专门用于完成这项任务的函数,但ios_base类提供了一个setf()函数(用于set标记),能够控制多种格式化特性。这个类还定义了多个常量,可以作为函数的参数。例如,下面的函数调用使cout显示末尾的小数点:
cout.setf(ios_base::showpoint);
使用默认的浮点格式时,上述语句还将导致末尾的0被显示出来。
showpoint是ios_base类声明中定义的类级静态常量。类级意味着如果在成员函数定义的外面使用它,则必须在常量名前加上作用域运算符(::)。因此,ios_base::showpoint指的是在ios_base类中定义的一个常量。
1 #include <iostream> 2 3 int main() 4 { 5 using namespace std; 6 float price1 = 25.5; 7 float price2 = 1.9 + 8.0/9.0; 8 cout.setf(ios_base::showpoint); 9 cout.width(10); 10 cout << "\"Apple\"" << ": $" << price1 << "\n"; 11 cout.width(10); 12 cout << "\"Flower\"" << ": $" << price2 << "\n"; 13 14 cout.precision(2); 15 16 cout.width(10); 17 cout << "\"Apple\"" << ": $" << price1 << "\n"; 18 cout.width(10); 19 cout << "\"Flower\"" << ": $" << price2 << "\n"; 20 21 return 0; 22 } 23 24 输出结果: 25 "Apple": $25.5000 26 "Flower": $2.78889 27 "Apple": $26. 28 "Flower": $2.8
(6)再谈setf()
ios_base类有一个受保护的数据成员,其中的各位(这里叫做标记)分别控制着格式化的各个方面,例如计数系统、是否显示末尾的0等。打开一个标记称为设置标记(或位),并意味着相应的位被设置为1 。位标记是编程开关,相当于DIP开关以配置计算机硬件。例如,hex、dec和oct控制符调整控制计数系统的3个标记位。setf()函数提供了另一个调整标记位的途径。
setf()函数有两个原型。第一个为:
fmtflags setf(fmtflags);
其中,fmtflags是bitmask类型的typedef名,用于存储格式标记。该名称是在ios_base类中定义的。这个版本的setf()用来设置单个位控制的格式信息。参数是一个fmtflags值,指出要设置哪一位。返回值的类型是fmtflags的数字,指出所有标记以前的设置。如果打算以后恢复原始设置,则可以保存这个值。ios_base类定义了代表位值得常量,下表列出了其中的一些定义:
常量 | 含义 |
ios_base::boolalpha | 输入和输出bool值,可以为true和false |
ios_base::showbase | 对于输出,使用C++基数前缀(0,0x) |
ios_base::showpoint | 显示末尾的小数点 |
ios_base::uppercase | 对于16进制输出,使用大写字母,E表示法 |
ios_base::showpos | 在正数前面加上+ |
注意:bitmask类型是一种用来存储各个位值的类型。它可以是整型、枚举,也可以是STL bitset容器。这里的主要思想是,每一位都是可以单独访问的,都有自己的含义。iostream软件包使用bitmask来存储状态信息。
由于这些格式常量都是在ios_base类中定义的,因此使用它们时,必须加上作用域解析运算符。并且,修改将一直有效,直到被覆盖为止。注意,仅当基数为10才使用加号。C++将十六进制和八进制都是为无符号的,因此对于它们,无需使用符号。
第二个setf()原型接受两个参数,并返回以前的设置:
fmtflags setf (fmtflags, fmtflags);
函数的这种重载格式用于设置由多位控制的格式选项。第一个参数和以前一样,也是一个包含了所需设置的fmtflags值。第二个参数指出要清除第一个参数中的哪些位。例如,将第3位设置为1表示以10为基数,将第4位设置为1表示以8为基数,将第5位设置为1表示以16为基数。假设输出是以10为基数的,而要将它设置为16位基数,则不仅需要将第5位设置为1,还需要将第3位设置为0——这叫做清除位。
ios_base类定义了可按这种方式处理的3组格式标记。每组标记都由一个可用作第二参数的常量和两三个可用作第一参数的常量组成。第二个参数清除一批相关的位,然后第一参数将其中一位设置为1 。上面的表列出了用作setf()的第二参数的常量名称、可用作第一参数的相关常量以及它们的含义。左对齐意味着将值放在字段的左端,右对齐表示将值放在字段的右端。内部对齐表示将符号或基数前缀放在字段左侧,余下的数字放在字段的右侧。
在C++标准中,定点表示法和科学表示法都有下面的特征:
*精度值得是小数位数,而不是总位数;
*显示末尾的0;
setf()函数是ios_base类的一个成员函数。由于这个类是ostream的基类,因此可以使用cout对象来调用该函数。例如:
std::cout.setf(ios_base::hex, ios_base::basefield);
(7)标准控制符
使用setf()不是进行格式化的、对用户最为友好的方法,C++提供了多个控制符,能够调用setf(),并且自动提供正确的参数。例如,下面的语句打开左对齐和定点选项:
cout << left << fixed;
下面的表列出来这些控制符:
控制符 | 调用 |
boolalpha | setf(ios_base::boolalpha) |
noboolalpha | unsetf(ios_base::boolalpha) |
showbase | setf(ios_base::showbase) |
noshowbase | unsetf(ios_base::showbase) |
showpoint | setf(ios_base::showpoint) |
noshowpoint | unsetf(ios_base::showpoint) |
showpos | setf(ios_base::showpos) |
noshowpos | unsetf(ios_base::showpos) |
uppercase | setf(ios_base::uppercase) |
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) |
oct | setf(ios_base::oct, ios_base::basefield) |
hex | setf(ios_base::hex, ios_base::basefield) |
fixed | setf(ios_base::fixed, ios_base::floatfield) |
scientific | setf(ios_base::scientific, ios_base::floatfield) |
(8)头文件iomanip
使用iostream工具来设置一些格式值不太方便。位简化工作,C++在头文件iomanip中提供了一些控制符,它们能够提供前面讨论过的服务,但表示起来更方便。3个最常用的控制符分别是setprecision()、setfill()、setw(),它们分别用来设置精度、填充字符和字段宽度。与前面讨论的控制符不同,这3个控制符带参数。setprecision()接受一个指定精度的整数参数;setfill()空字符接受一个指定填充字符的char参数;setw()控制符接受一个指定字段宽度的整数参数。由于它们都是控制符,因此可以用cout语句连接起来。
1 #include <iostream> 2 #include <iomanip> 3 #include <cmath> 4 5 int main() 6 { 7 using namespace std; 8 cout << right << fixed; 9 cout << setw(6) << "N" << setw(14) << "square root" 10 << setw(15) << "fourth root\n"; 11 double root; 12 for (int n = 10; n <= 100; n += 10) { 13 root = sqrt(double(n)); 14 cout << setw(6) << setfill('.') << n << setfill(' ') 15 << setw(12) << setprecision(3) << root 16 << setw(14) << setprecision(4) << sqrt(root) 17 << endl; 18 } 19 return 0; 20 } 21 22 输出结果: 23 N square root fourth root 24 ....10 3.162 1.7783 25 ....20 4.472 2.1147 26 ....30 5.477 2.3403 27 ....40 6.325 2.5149 28 ....50 7.071 2.6591 29 ....60 7.746 2.7832 30 ....70 8.367 2.8925 31 ....80 8.944 2.9907 32 ....90 9.487 3.0801 33 ...100 10.000 3.1623
三、使用cin进行输入
cin对象将标准输入表示为字节流。通常情况下,通过键盘来生成这种字符流。cin对象根据接收值的变量的类型,使用其方法将字符序列转换为所需的类型。
通常,可以这样使用cin:
cin >> value_holder;
其中,value_holder为存储输入的内存单元,它可以是变量、引用、被解除引用的指针,也可以是类或结构的成员。cin解释输入的方式取决于value_holder的数据类型。istream类(在iostream头文件中定义)重载了抽取运算符>>,使之能够识别下面这些基本类型:
* signed char &
* unsigned char &
* char &
* short &
* unsigned short &
* int &
* unsigned int &
* long &
* unsigned long &
* long long & (C++11)
* unsigned long long & (C++11)
* float &
* double &
* long double &
这些运算符函数被称为格式化输入函数(formatted input functiongs),因为它们可以将输入转换为目标指定的格式。
典型的运算符函数的原型如下:
istream & operator>>(int &);
参数和返回值都是引用。引用参数意味着下面的语句将导致operator>>()函数处理变量staff_size本身,而不是像常规参数那样处理它的副本:
cin >> staff_size;
由于参数类型是引用,因此cin能够直接修改用作参数的变量的值。抽取运算符将字符输入转换为指定类型的值。例如,假设staff_size的类型为int,则编译器将:
cin >> staff_size;
与下面的原型匹配:
istream & operator>>(int &);
并且,可以将hex、dec和oct控制符与cin一起使用,来制定将整数输入解释为十六进制、十进制和八进制格式。
istream类还为下列字符指针类型重载了>>抽取运算符:
* signed char *;
* char *;
* unsigned char * 。
对于这种类型的参数,抽取运算符将读取输入中的下一个单词,将它放置到指定的地址,并加上一个空值字符,使之成为一个字符串。
每个抽取运算符都返回调用对象的引用,这使得能够将输入拼接起来,就像拼接输出那样。
3.1 cin>>如何检查输入
不同版本的抽取运算符查看输入流的方法是相通的。它们跳过空白(空格、换行符和制表符),直到遇到非空白字符。即使对于单字符模式(参数类型为char、unsigned char或signed char),情况也是如此,但对于C语言的字符输入函数,情况并非如此。在单字符模式下,>>运算符将读取该字符,将它放置到指定的位置。在其它模式下,>>运算符将读取一个指定类型的数据。也就是说,它读取从非空白字符开始,到与目标类型不匹配的第一个字符之间的全部内容。
3.2 流状态
cin或cout对象包含一个描述流状态(stream state)的数据成员(从ios_base类那里继承来的)。流状态(被定义为iostate类型,而iostate是一种bitmask类型)由3个ios_base元素组成:eofbit、badbit或failbit,其中每个元素都是一位,可以是1(设置)或0(清除)。当cin操作到达文件末尾时,它将设置eofbit;当cin操作未能读取到预期的字符时,它将设置failbit。I/O失败(如试图读取不可访问的文件或试图写入写保护的磁盘),也可能将failbit设置为1 。在一些无法诊断的失败破坏流时,badbit元素将被设置(实现没有必要就哪些情况下设置failbit,哪些情况下设置badbit达成一致)。当全部3个状态位设置为0时,说明一切顺利。程序可以检查流状态,并使用这种信息决定下一步做什么。
成员 | 描述 |
eofbit | 如果到达文件尾,则设置为1 |
badbit | 如果流被破坏,则设置1;例如,文件读取错误 |
failbit | 如果输入操作未能读取预期的字符或输出操作没有写入预期的字符,则设置为1 |
goodbit | 另一种表示0的方法 |
good() | 如果流可以使用(所有的位都被清除),则返回true |
eof() | 如果eofbit被设置,则返回true |
bad() | 如果badbit被设置,则返回true |
fail() | 如果badbit和failbit被设置,则返回true |
rdstate() | 返回流状态 |
exceptions() | 返回一个位掩码,指出哪些标记号导致异常被引发 |
exceptions(isostate ex) | 设置哪些状态将导致clear()引发异常;例如,如果ex是eofbit,则如果eofbit被设置,clear()将引发异常 |
clear(iostate ex) | 将流状态设置为s;s的默认值为0(goodbit);如果(restate() & exceptions())!= 0,则引发异常basic_ios::failure |
setstate(iostate s) | 调用clear(rdstate() | s)。这将设置与s中设置的位对应的流状态位,其他流状态位保持不变 |
(1)设置状态
clear()和setstate()很相似。它们都充值状态,但采取的方式不同。clear()方法将状态设置为它的参数。因此,下面的调用将使用默认参数0,这将清除全部3个状态位(eofbit,badbit和failbit):
clear ();
同样,下面的调用将设置eofbit;也就是说,eofbit被设置,另外两个状态位被清除:
clear(eofbit);
而setstate()方法只影响其参数中已设置的位。因此,下面的调用将设置eofbit,而不会影响其他位:
setstate(eofbit);
因此,如果failbit被设置,则仍将被设置;
setstate()的主要用途是为输入和输出函数提供一种修改状态的途径。
(2)I/O异常
exceptions()方法返回一个字段,它包含3位,分别对应eofbit、failbit和badbit。修改流状态涉及clear()或setstate(),这都将使用clear()。修改流状态后,clear()方法将当前的流状态与exceptions()返回的值进行比较。如果在返回值中某一位被设置,而当前状态中的对应位也被设置,则clear()将引发ios_base::failure异常。如果两个值都设置了badbit,将发生这种情况。如果exceptions()返回goodbit,则不会引发任何异常。ios_base::failure异常类是从std::exception类派生而来的,因此包含一个what()方法。
exceptions()的默认设置为goodbit,也就是说,没有引发异常。但重载的exceptions(iostate)函数使得能够控制其行为:
cin.exceptions(badbit); //设置badbit位,使其能够抛出异常
为运算符OR使得能够指定多位。例如,如果badbit或eofbit随后被设置,下面的语句将引发异常:
cin.exceptions(badbit | eofbit);
1 #include <iostream> 2 3 int main() 4 { 5 using namespace std; 6 cin.exceptions(ios_base::badbit | ios_base::failbit); 7 try { 8 int num; 9 while (cin >> num) { 10 cout << num << endl; 11 } 12 } catch (ios_base::failure & ex) { 13 cout << ex.what(); 14 } 15 16 return 0; 17 } 18 19 输出结果: 20 12 21 12 22 q 23 ios_base::clear: unspecified iostream_category error
(3)流状态的影响
只有在流状态良好(所有的位都被清除)的情况下,下面的测试才返回true:
while(cin >> input);
如果测试失败,可以使用诸如eof()、bad()和fail()等成员函数来判断可能的原因。
1 #include <iostream> 2 3 int main() 4 { 5 using namespace std; 6 int num; 7 while (1) { 8 cin >> num; 9 if (cin.eof()) { 10 cin.clear(); 11 cout << "输入结束\n"; 12 break; 13 } 14 if (cin.bad()) { 15 cin.clear(); 16 cout << "不好的输入\n"; 17 break; 18 } 19 if (cin.fail()) { 20 cin.clear(); 21 cout << "糟糕的输入\n"; 22 break; 23 } 24 cout << num << endl; 25 } 26 27 return 0; 28 } 29 30 输出结果: 31 21 32 21 33 33 34 33 35 23523 36 23523 37 q 38 糟糕的输入
设置流状态位有一个非常重要的后果:流将对后面的输入或输出关闭,直到位被清除。
如果希望程序在流状态位被设置以后能够正确读取后面的输入,则必须讲流状态设置为良好。这可以通过调用clear()方法来实现。但是,这还不足以重新设置流状态。导致输入循环终止的不匹配输入仍留在输入队列中,程序必须跳过它。一种方法是一直读取字符,直到到达空白为止。isspace()函数是一个cctype函数,它在参数是空白字符时返回true。另一种方法是,丢弃行中的剩余部分,而不仅仅是下一个单词:
while(cin.get() != '\n')
continue;
假设循环是由于到达文件尾或者由于硬件故障而终止的,可以使用fail()方法来检查假设是否正确,来修复问题。由于历史原因,fail()在failbit或eofbit被设置时返回true,因此代码必须排除后一种情况。下面是一个排除这种情况的例子:
1 while (cin >> input) { 2 sum += input; 3 } 4 cout << "最后一个输入的值 = " << input << endl; 5 cout << "Sum = " << sum << endl; 6 if (cin.fail() && !cin.eof()) { 7 cin.clear(); 8 while (!isspace(cin.get())) { 9 continue; 10 } 11 } 12 else{ 13 cout << "不能继续\n"; 14 exit(1); 15 } 16 cout << "请重新输入数据:\n"; 17 cin >> input;
3.3 其他istream 类方法
get()和getline()方法提供下面的输入功能:
*方法get(char &)和get(void)提供不跳过空白的单字符输入功能;
*函数get(char *, int, char)和getline(char *, int, char)在默认情况下读取整行而不是一个单词。
它们被称为非格式化输入函数,因为它们只读取字符输入,而不会跳过空白,也不进行数据转换。
来看一下istream类的这两组成员函数。
(1)单字符输入
在使用char参数或没有参数的情况下,get()方法读取下一个输入字符,即使该字符是空格、制表符或换行符。get(char & ch)版本将输入字符赋给其参数,而get(void)版本将输入字符转换为整型(通常是int),并将其返回。
1)成员函数get(char &)
先来看get(char &)。假设程序中包含如下循环:
1 #include <iostream> 2 3 int main() 4 { 5 using namespace std; 6 int ct = 0; 7 char ch; 8 cin.get(ch); 9 while (ch != '\n') { 10 cout << ch; 11 ct ++; 12 cin.get(ch); 13 } 14 cout << ct << endl; 15 16 return 0; 17 } 18 19 输出结果: 20 I C++ clearly //键盘输入 21 I C++ clearly13
get(char &)成员函数返回一个指向用于调用它的istream对象的引用,这意味着可以拼接get(char &)后面的其他抽取。
如果cin.get(char &)到达文件尾,它都不会给其参数赋值。另外,该方法还调用setstate(failbit),导致cin的测试结果为false:
char ch;
while(cin.get(ch)){
//。。。。
}
只要存在有效输入,cin.get(ch)的返回值都将是cin,此时的判定结果为true,因此循环将继续。到达文件结尾时,返回值判定为false,循环终止。
2)成员函数get(void)
get(void)成员函数还读取空白,但使用返回值来将输入传递给程序。
get(void)成员函数的返回类型为int(或某种更大的整型,这取决于字符集和区域)。
到达文件尾后,cin.get(void)都将返回值EOF——头文件iostream提供的一个符号常量。这种设计特性使得可以这样来读取输入:
int ch;
while((ch = cin.get()) != EOF){
//...
}
这里应该将ch的类型声明为int,而不是char,因为值EOF可能无法使用char来表示。
下面的表对单字符输入函数的特性进行了总结:
特征 | cin.get(ch) | ch=cin.get() |
传输输入字符的方法 | 传给参数ch | 将函数返回值赋给ch |
字符输入时函数的返回值 | 指向istream对象的引用 | 字符编码(int值) |
到达文件尾时函数的返回值 | 转换为false | EOF |
(2)采用哪种单字符输入形式
假设可以选择使用>>、get(char &)或get(void),应使用哪一个呢?
首先,应确定是否希望跳过空白。如果跳过空白更方便,则使用抽取运算符>>。
如果希望程序检查每个字符,请使用get()方法。在get()方法中,get(char &)的接口更佳。get(void)的主要优点是,它与标准C语言中的getchar()函数极其类似,这意味着可以通过包含iostream(而不是stdio.h),并用cin.get()替换所有的getchar(),用cout.put(ch)替换所有的putchar(ch),来将C程序转换为C++程序。
(3)字符串输入:getline(),get()和ignore()
getline()成员函数和get()的字符串读取版本都读取字符串,它们的函数特征标相同(这是从更为通用的模板声明简化而来的):
istream & get(char *, int, char);
istream & get(char *, int);
istream & getline(char *, int, char);
istream & getline(char *, int);
第一个参数是用于放置输入字符串的内存单元的地址。第二个参数比要读取的最大字符数大1(额外的一个字符用于存储结尾的空字符,以便将输入存储为一个字符串)。第三个参数制定用作分节符的字符,只有两个参数的版本将换行符用作分界符。上述函数都在读取最大数目的字符或遇到换行符后为止。
例如,下面的代码将字符输入读取到字符数组line中:
char line[50];
cin.get(line, 50);
cin.get()函数将在到达第49个字符或者遇到换行符(默认情况)后停止将输入读取到数组中。get()和getline()之间的主要区别在于,get()将换行符留在输入流中,这样接下来的输入操作首先看到是将是换行符,而getline()抽取并丢弃输入流中的换行符。
1 #include <iostream> 2 3 int main() 4 { 5 using namespace std; 6 char line[50]; 7 cin.get(line,50); 8 if (cin.get() == '\n') { 9 cout << "\"\\n\"\n"; 10 } 11 cin.getline(line, 50); 12 if (cin.get() == '\n') { 13 cout << "\"\\n\"\n"; 14 } 15 16 return 0; 17 } 18 19 输出结果: 20 Good morning! //输入 21 "\n" 22 Good evening! //输入 23 1 //输入
接下来看接受三个参数的版本,第三个参数用于制定分界符。遇到分节符后,输入将停止,即使还未读取最大数目的字符。因此,在默认情况下,如果在读取指定数目的字符之前到达行尾,这两种方法都将停止读取输入。和默认情况一样,get()将分界字符留在输入队列中,而getline()不保留。
getline():
1 #include <iostream> 2 3 int main() 4 { 5 using namespace std; 6 char line[50]; 7 char ch; 8 cin.getline(line, 50, 'a'); 9 cout << line << endl; 10 while (cin.get(ch)) { 11 cout << ch; 12 } 13 14 return 0; 15 } 16 17 输出结果: 18 heart //键盘输入 19 he 20 rt
get():
1 #include <iostream> 2 3 int main() 4 { 5 using namespace std; 6 char line[50]; 7 char ch; 8 cin.get(line, 50, 'a'); 9 cout << line << endl; 10 while (cin.get(ch)) { 11 cout << ch; 12 } 13 14 return 0; 15 } 16 输出结果: 17 heart //键盘输入 18 he 19 art
ignore()成员函数接受两个参数:一个是数字,指定读取的最大字符数;另一个是字符,用作输入分节符。例如下面的函数调用读取并丢弃接下来的255个字符或直到到达第一个换行符:
cin.ignore(255,'\n');
原型为两个参数的提供的默认值为1和EOF,该函数的返回类型为istream &:
istream & ignore(int = 1, int = EOF);
默认参数值EOF导致ignore()读取指定数目的字符或读取到文件尾。
该函数返回调用对象,这使得能够拼接函数调用。
(4)意外字符串输入
get(char *, int)和getline()的某些输入形式的某些输入将影响流状态。与其他输入函数一样,这两个函数在遇到文件尾时将设置eofbit,遇到流被破坏时将设置badbit。另外两种特殊情况是无输入以及输入到达或超过函数调用指定的最大字符数。下面来看这些情况。
对于上述两个方法,如果不能抽取字符,它们将把空字符放置到输入字符串中,并使setstate()设置failbit。方法在什么时候无法抽取字符呢?一种可能情况是输入方法立刻到达文件尾。对于get(char *, int)来说,另一种可能是输入了一个空行:
char temp[80];
while(cin.get(temp, 80))
...
有意思的是,空行并不会导致getline()设置failbit。这是因为getline()仍将抽取换行符,虽然不会存储它。如果希望getline()在遇到空行时终止循环,则可以这样编写:
char temp[80];
while(cin.getline(temp, 80) && temp[0] != '\0')
现在假设输入队列中的字符数等于或超过了输入方法指定的最大字符数。首先来看getline()和下面的代码:
char temp[30];
while(cin.getline(temp, 30))
getline()方法将从输入队列中读取字符,将它们放到temp数组的元素中,直到(按测试顺序)到达文件尾、将要读取的字符是换行符或存储了29个字符为止。如果到达文件尾,则设置eofbit;如果将要读取的字符是换行符,则该字符被读取并丢弃;如果读取了29个字符,并且下一个字符不是换行符,则设置failbit。因此,包含30个或更多字符的输入行将终止输入。
现在来看get(char *, int)方法。它首先测试字符数,然后测试是否为文件尾以及下一个字符是否是换行符。如果他读取了最大数目的字符,则不设置failbit标记。然而,由此可以知道终止读取是否是由于输入字符过多引起的。可以用peek()来查看下一个输入字符。如果它是换行符,则说明get()已读取了整行;如果不是换行符,则说明get()是在到达行尾前停止的。这种技术对getline()不适用,因为getline()读取并丢弃换行符,因此查看下一个字符无法知道任何情况。然而,如果使用的是get(),则可以知道是否读取了整个一行。下面的表总结了这些行为:
方法 | 行为 |
getline(char *, int) | 如果没有读取任何字符(但换行符被视为读取了一个字符),则设置failbit; 如果读取了最大数目的字符,且行中还有其他字符,则设置failbit; |
get(char *, int) | 如果没有读取任何字符,则设置failbit; |
3.4 其他istream方法
除了前面讨论过的外,其他istream方法包括read()、peek()、gcount()和putback()。read()函数读取指定数目的字节,并将它们存储在指定的位置中。例如,下面的语句从标准输入中读取144个字符,并将他们存储在gross数组中:
char gross[144];
cin.read(gross, 144);
与get()和getline()不同的是,read()不会在输入后加上空字符,因此不能将输入转换为字符串。read()方法不是专为键盘输入设计的,它最常与ostream::write()函数结合使用,来完成文件输入和输出。该函数的返回类型为istream &,因此可以像下面这样将它们拼接起来:
char gross[144];
char score[20];
cin.read(gross, 144).read(score, 20);
peek()函数返回输入中的下一个字符,但不抽取输入流中的字符。也就是说,它是的能够查看下一个字符。假设要读取输入,直到遇到换行符或句点,则可以使用peek()来查看输入流中的下一个字符,以此来判断是否继续读取:
char great_input[80];
char ch;
int i = 0;
while((ch = cin.peek()) != '.' && ch != '\n')
cin.get(great_input[i++]);
great_input[i] = '\0';
gcount()方法返回最后一个非格式化抽取方法读取的字符数。这意味着字符是由get()、getline()、ignore()或read()方法读取的,不是由抽取运算符(>>)读取的,抽取运算符对输入进行格式化,使之与特定的数据类型匹配。例如,假设使用cin.get(myarray, 80)将一行读入myarray数组中,并想知道读取了多少个字符,则可以使用strlen()函数来计算数组中的字符数,这种方法比使用cin.gcount()计算从输入流中读取了多少字符的速度要快。
putback()函数将一个字符插入到输入字符串中,被插入的字符将是下一条输入语句读取的第一个字符。putback()方法接受一个char参数——要插入的字符,其返回类型为istream &,这使得可以将该函数调用与其他istream方法拼接起来。使用peek()的效果相当于先使用get()读取一个字符,然后使用putback()将该字符放回到输入流中。然而,putback()允许将字符放到不是刚才读取的位置。
四、文件输入和输出
大多数计算机程序都使用文件。文件本身是存储在某种设备(磁带、光盘、软盘或硬盘)上的一系列字节。通常,操作系统管理文件,跟踪它们的位置、大小、创建时间等。除非在操作系统级别上编程,否则通常不必担心这些事情。需要的只是将程序与文件相连的途径、让程序读取文件内容的途径以及让程序创建和写入文件的途径。重定向可以提供一些文件支持,但它比显式程序中的文件I/O的局限性更大。 另外重定向来自操作系统,而非C++,因此并非所有系统都有这样的功能。
C++I/O类软件包处理文件输入和输出的方式与处理标准输入和输出的方式非常相似。要写入文件,需要创建一个ofstream对象,并使用ostream方法,如<<插入运算符或 write()。要读取文件,需要创建一个ifstream对象,并使用istream方法,如>>抽取运算符或get()。
4.1 简单的文件I/O
要让程序写入文件,必须这样做:
1)创建一个ofstream对象来管理输出流;
2)将该对象与特定的文件关联起来;
3)以使用cout的方式使用该对象,唯一的区别是输出将写入文件,而不是屏幕。
要完成上述任务,首先应包含文件fstream文件。对于大多数实现来说,包含该文件便自动包括iostream文件,因此不必显式包含iostream文件。然后声明一个ofstream对象:
ofstream fout;
对象名可以是任意有效的C++名称。
接下来,必须将这个对象与特定的文件关联起来。为此,可以使用open()方法。例如,假设要打开jar.txt进行输出,则可以这样做:
fout.open("jar.txt");
可以使用另一个构造函数完成这两步(创建对象和关联到文件)合并成一条语句:
ofstream fout("jar.txt");
然后,以使用cout的方式使用ofstream对象。
由于ostream是ofstream类的基类,因此可以使用所有的ostream方法,包括各种插入运算符定义、格式化方法和控制符。ofstream类使用被缓冲的输出,因此程序在创建ofstream对象时,将为输出缓冲区分配空间。如果创建两个ofstream对象,程序就创建两个缓冲区,每个对象一个。ofstream对象从程序那里逐字节地收集输出,当缓冲区填满后,它便将缓冲区内容一同传输给目标文件。由于磁盘驱动器被设计称以大块的方式传输数据,而不是逐字节地传输,因此通过缓冲区可以大大提高从程序到文件传输数据的速度。
以这种方式打开文件来进行输出时,如果没有这样的文件,将创建一个新文件;如果有这样的文件,则打开文件将清空文件,输出将进入到一个空文件中。
警告:以默认房事打开文件进行输出将自动把文件的长度截短为零,这相当于删除已有的内容。
读取文件的要求与写入文件相似:
1)创建一个ifstream对象来管理输入流;
2)将该对象与特定的文件关联起来;
3)以使用cin的方式使用该对象。
上述读文件的步骤类似于写文件。首先,要包含fstream。然后声明一个ifstream对象,将它与文件名关联起来。
可以像使用cin那样使用ifstream对象。
输入和输出一样,也是被缓冲的;与输出一样,通过缓冲,传输数据的速度比逐字节传输要快的多。
当输入和输出流对象过期(如程序终止)时,到文件的连接将自动关闭。另外,也可以使用close()方法来显式地关闭到文件的连接:
fout.close();
关闭这样的连接并不会删除流,而只是断开流到文件的连接。然而,流管理装置仍被保留。例如,ifstream的对象与它管理的输入缓冲区仍然存在。可以将流连接到同一个文件或另一个文件。
下面的例子要求输入文件名,然后创建一个名称为输入名的文件,将一些信息写入到高文件中,然后关闭该文件。关闭文件将刷新缓冲区,从而确保文件被刷新。然后程序打开该文件,读取并显示其内容。注意,该程序以使用cin和cout的方式使用fin和fout。另外,该程序将文件名读取到一个string对象中,然后使用方法c_str()来给ofstream和ifstream的构造函数提供一个C- 风格字符串参数。
1 #include <iostream> 2 #include <fstream> 3 4 int main() 5 { 6 using namespace std; 7 string filename; 8 cout << "Enter name for new file:\n"; 9 cin >> filename; 10 filename = "/Users/xheart/Desktop/" + filename; 11 12 ofstream fout(filename.c_str()); 13 fout << "For your eyes only!\n"; 14 cout << "Enter your secret number:\n"; 15 float secret; 16 cin >> secret; 17 fout << "Your secret number is " << secret << endl; 18 fout.close(); 19 20 ifstream fin(filename.c_str()); 21 cout << "Here are the contents of " << filename << ": \n"; 22 char ch; 23 while (fin.get(ch)) { 24 cout << ch; 25 } 26 cout << "Done\n"; 27 fin.close(); 28 return 0; 29 } 30 31 输出结果: 32 Enter name for new file: 33 myNewFile //键盘输入 34 Enter your secret number: 35 1991 //键盘输入 36 Here are the contents of /Users/xheart/Desktop/myNewFile: 37 For your eyes only! 38 Your secret number is 1991 39 Done
4.2 流状态检查和is_open()
C++文件流类从ios_base类那里继承了一个流状态成员。正如前面指出的,该成员存储了指出流状态的信息:一切顺利、已到达文件、I/O操作失败等。如果一切顺利,则流状态为零(没有消息就是好消息)。其他状态都是通过将指定位设置为1来记录的。文件流还继承了ios_base类中报告流状态的方法。可以通过检查流状态来判断最后一个流操作是否成功。对于流文件,这包括检查试图打开文件时是否成功。
C++实现提供了一种更好的检查文件是否被打开的方法——is_open()方法:
if(!fin.is_open()){
//...
}
警告:以前,检查文件是否成功打开的常见方式如下:
if(fin.fail()) ... //failed to open
if(!fin.good()) ... //failed to open
if(!fin) ... // failed to open
fin对象被用于测试条件中时,如果fin.good()为false,将被转换为false;否则将被转换为true。因此上面的三种方式等价。然而,这些测试无法检查到这样一种情形:试图以不合适的文件模式打开文件时失败。方法is_open()能够检测到这种错误以及good()能够检测到的错误。然而,老式C++实现没有is_open()。
4.3 打开多个文件
程序可能需要打开多个文件,打开多个文件的策略取决于他们被如何使用。如果需要同时打开两个文件,则必须为每个文件创建一个流。可以同时打开的文件数取决于操作系统。
然而,可能要依次处理一组文件。在这种情况下,可以打开一个流,并将它依次关联到各个文件。这在节省计算机资源方面,比为每个文件打开一个流的效率高。使用这种方法,需要声明一个ifstream对象(不必对他进行初始化),然后使用open()方法将这个流与文件关联起来。例如,下面依次读取两个文件的代码:
ifstream fin;
fin.open("fat.txt");
...
fin.close();
fin.clear();
fin.open("rat.txt");
...
fin.close();
4.4 命令行处理技术
文件处理程序通常使用命令行参数来指定文件。命令行参数是用户在输入命令时,在命令行中输入的参数。例如,在UNIX或Linux系统中计算文件包含的字数,可以在命令行提示符下输入下面的命令:
wc report1 report2 report3
其中,wc是程序名,report1,report2和report3是作为命令行参数传递给程序的文件名。
C++有一种让在命令环境中运行的程序能够访问命令行参数的机制,方法是使用下面的main()函数:
int main(int argc, char *argv[])
argc为命令行中的参数个数,其中包括命令名本身。argv变量为一个指针,它指向一个指向char的指针。这过于抽象,但可以将argv看作一个指针数组,其中的指针指向命令行参数,argv[0]z是一个指针,指向存储第一个命令行参数的字符串的第一个字符,依此类推。也就是说,argv[0]是命令行中的第一个字符串,依此类推。例如,假设有下面的命令行:
wc report1 report2 report3
则argc为4,argv[0]为wc,argv[1]为report1,依此类推。下面的循环将把每个命令行参数分别打印在单独一行上:
for(int i = 1; i < argc; i++ )
cout << argv[i] << endl;
以i = 1开头将只打印命令行参数;以i=0开头同时打印命令名。
当然,命令行参数与命令行操作系统(Windows命令提示模式、UNIX和Linux)紧密相关。其他程序也可能允许使用命令行参数。
*很多Windows IDE(继承开发环境)都有一个提供命令行参数的选项。通常,必须选择一系列菜单,才能打开一个可以输入命令行参数的对话框。具体的步骤随厂商和升级版本而异,因此请查看文档。
*很多Windows IDE都可以生成可执行文件,这些文件能够在Windows命令提示符模式下进行。
4.5 文件模式
文件模式描述的是文件将被如何使用:读、写、追加等。将流与文件关联时(无论是使用文件名初始化文件流对象,还是使用open()方法),都可以提供指定文件模式的第二个参数:
ifstream fin("banjo", mode1);
ofstream fout();
fout.open("harp", mode2);
ios_base类定义了一个openmodel类型,用于表示模式;与fmtflags和iostate类型一样,它也是一种bitmask类型(以前,其类型为int)。可以选择ios_base类中定义的多个常量来指定模式,下面的表列出了这些常量及其含义。C++文件I/O做了一些改动,以便与ANSI C文件I/O兼容。
常量 | 含义 |
ios_base::in | 打开文件,以便读取 |
ios_base::out | 打开文件,以便写入 |
ios_base::ate | 打开文件,并移到文件尾 |
ios_base::app | 追加到文件尾 |
ios_base::trunc | 如果文件存在,则截短文件 |
ios_base::binary | 二进制文件 |
ifstream和ofstream的构造函数以及open()方法都接受两个参数,这些类成员函数的原型为第二个参数提供了默认值。ifstream::open()方法和构造函数用ios_base::in作为模式参数的默认值,而ofstream::open()方法和构造函数用ios_base::out|ios_base::trunc作为默认值。位运算符OR(|)用于将两个位值合并成一个可用于设置两个位的值。fstream不提供默认的模式值,因此在创建这种类的对象时,必须显式地提供模式。
注意,ios_base::trunc标记意味着打开已有的文件,以接收程序输出时将被截短;也就是说,其以前内容被删除。
老式C++时间之间可能有一些差异。例如,有些实现允许省略ios_base::out,有些则不允许。如果不是用默认模式,则最安全的方法是显式地提供所有的模式元素。有些编译器不支持上面表中的所有选项,有些则提供了表中没有列出的其他选项。但是C++标准提供了更高的统一性。
标准C++根据ANSI C标准I/O定义了部分文件I/O。实现像下面这样的C++语句:
ifstream fin(filename, c++mode);
就像它使用C的fopen()函数一样:
fopen(filename, cmode);
其中,c++mode是一个openmode值,如ios_base::in;而cmode是相应的C模式字符串,如“r”。下面的表列出了C++模式和C模式的对应关系。注意,ios_base::out本身将导致文件被截短,但与ios_base::in一起使用时,不会导致文件被截短。没有列出的组合,例如ios_base::in|ios_base::trunc,将禁止文件被打开。is_open()方法用于检查这种故障。
C++模式 | C模式 | 含义 |
ios_base::in | "r" | 打开以读取 |
ios_base::out | "w" | 等价于ios_base::out|ios_base::trunc |
ios_base::out | ios_base::trunc | "w" | 打开以写入,如果已存在,则截短文件 |
ios_base::out | ios_base::app | "a" | 打开以写入,只追加 |
ios_base::in | ios_base::out | "r+" | 打开以读写,在文件允许的位置写入 |
ios_base::in | ios_base::out | ios_base::trunc | "w+" | 打开以读写,如果已存在,则首先截短文件 |
c++mode | ios_base::binary | "cmodeb" | 以C++mode(或相应的cmode)和二进制模式打开,例如:ios_base::in | ios_base::binary或“rb” |
c++mode | ios_base::ate | "cmode" | 以指定的模式打开,并移到文件尾。C使用一个独立的函数调用,而不是编码模式。例如:ios_base::in | ios_base::ate被转换为"r"模式和C函数调用fseek(file, 0, SEEK_END) |
注意,ios_base::ate和ios_base::app都将文件指针指向打开的文件尾。二者的区别在于,ios_base::app模式只允许将数据添加到文件尾,而ios_base::ate模式将指针放到文件尾。
显然,各种模式的组合很多,我们将介绍几种有代表性的组合。
(1)追加文件
来看一个在文件尾追加数据的程序。该程序维护一个存储来客清单的文件。该程序显示文件当前的内容(如果有的话)。尝试打开文件后,它使用is_open()方法检查该文件是否存在。接下来,程序以ios_base::app模式代开文件,进行输入。然后,它请求用户从键盘输入,并将其添加到文件中。最后,程序显示修订后的文件内容。下面的程序清单演示了如何实现这些目标,请注意程序是如何使用is_open()方法检查文件是否被成功打开的。
1 #include <iostream> 2 #include <fstream> 3 #include <string> 4 #include <cstdlib> 5 6 const char * file = "/Users/xheart/Desktop/guests.txt"; 7 int main(int argc, char * argv[]) 8 { 9 using namespace std; 10 char ch; 11 //show initial contents 12 ifstream fin; 13 fin.open(file); 14 15 if(fin.is_open()){ 16 cout << "Here are the current contents of the " << file << " file: \n"; 17 while (fin.get(ch)) { 18 cout << ch; 19 } 20 fin.close(); 21 } 22 23 //add new names 24 ofstream fout(file,ios_base::out | ios_base::app); 25 if (!fout.is_open()) { 26 cerr << "Can't open " << file << " file for output.\n"; 27 exit(EXIT_FAILURE); 28 } 29 cout << "Enter guest names (enter a blank line to quit):\n"; 30 string name; 31 while (getline(cin, name) && name.size() > 0) { 32 fout << name.c_str() << endl; 33 } 34 fout.close(); 35 36 //show revised file 37 fin.clear(); 38 fin.open(file); 39 if (fin.is_open()) { 40 cout << "Here are the new contents of the " << file << " file:\n"; 41 while (fin.get(ch)) { 42 cout << ch; 43 } 44 fin.close(); 45 } 46 cout << "Done.\n"; 47 return 0; 48 } 49 50 输出结果: 51 Here are the current contents of the /Users/xheart/Desktop/guests.txt file: 52 卡卡西 53 鸣人 54 佐助 55 宁次 56 Enter guest names (enter a blank line to quit): 57 Jum 58 Tom 59 60 Here are the new contents of the /Users/xheart/Desktop/guests.txt file: 61 卡卡西 62 鸣人 63 佐助 64 宁次 65 Jum 66 Tom 67 Done.
(2)二进制文件
将数据存储在文件中时,可以将其存储为文本格式或二进制格式。文本格式指的是将所有内容(甚至数字)都存为文本。例如,以文本格式存储值-2.324216e+07时,将存储该数字包含的13个字符。这需要将浮点数的计算机内部表示转换为字符格式,这正是<<插入运算符完成的工作。另一方面,二进制格式指的是存储值的计算机内部表示。也就是说,计算机不是存储字符,而是存储这个值的64位double表示。对于字符来说,二进制表示与文本表示是一样的,即字符的ASCII码的二进制表示。对于数字来说,二进制表示与文本表示有很大的差别。
每种格式都有自己的优点。文本格式便于读取,可以使用编辑器或字处理器来读取和编辑文本文件,可以很方便地将文本文件从一个计算机系统传输到另一个计算机系统。二进制格式对于数字来说比较精确,因为他存储的是值的内部表示,因此不会有转换误差或舍入误差。以二进制格式保存数据的速度更快,因为不需要转换,并可以大块地存储数据。二进制格式通常占用的空间较小,这取决于数据的特征。然而,如果另一个系统使用另一种内部表示,则可能无法将数据传输给该系统。同一系统上不同的编译器也可能使用不同的内部结构布局表示。在这种情况下,必须编写一个将一种数据转换成另一种的程序。
来看一个例子。考虑下面的结构定义和声明:
const int LIM = 20;
struct planet{
char name[LIM]; //name of planet
double population; //its population
double g; //its acceleration of gravity
};
planet p1;
要将结构p1的内容以文本格式保存,可以这样做:
ofstream fout("Planets.dat",ios_base::out | ios_base::app);
fout << p1.name << " " << p1.population << " " << p1.g << "\n";
必须使用成员运算符显式地提供每个结构成员,还必须将相邻的数据分隔开,以便区分。如果结构有30个成员,则这项工作将很乏味。
要使用二进制格式存储相同的信息,可以这样做:
ofstream fout("Planets.dat", ios_base::out | ios_base::app | ios_base::binary);
fout.write((char *) &p1, sizeof(p1));
上述代码使用计算机的内部数据表示,将整个结构作为一个整体保存。不能将该文件作为文本读取,但与文本相比,信息的保存更为紧凑、精确。它确实便于键入代码,。这种方法做了两个修改:
*使用二进制文件格式;
*使用成员函数write().
下面来详细的介绍这两项修改。
有些系统(如Windows)支持两种文件格式:文本格式和二进制格式。如果要用二进制格式保存数据,应使用二进制文件格式。在C++中,可以将文件模式设置为ios_base::binary常量来完成。
补充:二进制文件和文本文件
使用二进制文件模式时,程序将数据从内存传输给文件(反之亦然)时,将不会发生任何隐藏的转换,二默认的文本模式并非如此。例如,对于Windows文本文件,它们使用两个字符的组合(回车和换行)表示换行符;Macintosh文本文件使用回车来表示换行符;而UNIX和Linux文件使用换行(linefeed)来表示换行符。C++是从UNIX系统上发展而来的,因此也使用换行(linefeed)来表示换行符。为增加可移植性,Windows C++程序在写文本模式文件时,自动将C++换行符转换为回车和换行;Macintosh C++程序在写文件时,将换行符转换为回车。在读取文本文件时,这些程序将本地换行符转换为C++格式。对于二进制数据,文本格式会引起问题,因此double值中间的字节可能与换行符的ASCII码有相同的位模式。另外,在文件尾的检测方式也有区别。因此以二进制格式保存数据时,应使用二进制文件模式(UNIX系统只有一种文件模式,因此对于他来说,二进制模式和文本模式是一样的)。
要以二进制格式(而不是文本格式)存储数据,可以使用write()成员函数。这种方法将内存中指定书目的字节复制到文件中。但它只逐字节地复制数据,而不进行任何转换。例如,如果将一个long变量的地址传给它,并命令它复制4个字节,它将复制long值中的4个字节,而不会将他转换为文本。唯一不方便的地方是,必须将地址转换为指向char的指针。也可以使用同样的方式来复制整个planet结构。要获得字节数,可以使用sizeof运算符:
fout.write((char *)&p1, sizeof(p1));
这条语句导致程序前往p1的地址,并将开始的36个字节(sizeof(p1)表达式的值)复制到与fout关联的文件中。
要使用文件恢复信息,请通过一个ifstream对象使用相应的read()方法:
ifstream fin("planets.dat", ios_base::in | ios_base::binary);
fin.read((char *)&p1, sizeof(p1));
这将从文件复制sizeof(p1)个字节到p1结构中。同样的方法也适用于不使用虚函数的类。在这种情况下,只有数据成员被保存,而方法不会被保存。如果类有虚方法,则也将复制隐藏指针(该指针指向虚函数的指针表)。由于下一次运行程序时,虚函数表可能在不同的位置,因此将文件中的旧指针信息复制到对象中,将可能造成混乱。
提示:read()和write()成员函数的功能是相反的。请使用read()来恢复用write()写入的数据。
下面的程序清单使用这些方法来创建和读取二进制文件。
1 #include <iostream> 2 #include <fstream> 3 #include <string> 4 #include <cstdlib> 5 #include <iomanip> 6 7 inline void eatline(){while (std::cin.get() != '\n')continue;} 8 struct planet{ 9 char name[20]; 10 double population; 11 double g; 12 }; 13 14 const char * file = "/Users/xheart/Desktop/planets.dat"; 15 16 int main(int argc, char * argv[]) 17 { 18 using namespace std; 19 planet p1; 20 cout << fixed << right; 21 22 //show initial contents 23 ifstream fin; 24 fin.open(file,ios_base::in | ios_base::binary); 25 if (fin.is_open()) { 26 cout << "Here are the current contents of the " << file << " file:\n"; 27 while (fin.read((char *)&p1, sizeof(p1))) { 28 cout << setw(20) << p1.name << ": " << setprecision(0) << setw(12) << p1.population << setprecision(2) << setw(6) << p1.g << endl; 29 30 } 31 fin.close(); 32 } 33 34 //add new data 35 ofstream fout(file, ios_base::out | ios_base::app | ios_base::binary); 36 if (!fout.is_open()) { 37 cerr << "Can't open " << file << " file for output:\n"; 38 exit(EXIT_FAILURE); 39 } 40 cout << "Enter planet name (enter a blank line to quit):\n"; 41 cin.get(p1.name, 20); 42 while (p1.name[0] != '\0') { 43 eatline(); 44 cout << "Enter planetary population:"; 45 cin >> p1.population; 46 cout << "Enter planet's acceleration of gravity:"; 47 cin >> p1.g; 48 eatline(); 49 fout.write((char *)&p1, sizeof(p1)); 50 cout << "Enter planet name (enter a blank line to quit):\n"; 51 cin.get(p1.name, 20); 52 } 53 fout.close(); 54 55 //show revised file 56 fin.clear(); 57 fin.open(file, ios_base::in | ios_base::binary); 58 if (fin.is_open()) { 59 cout << "Here are the new contents of the " << file << " file:\n"; 60 while (fin.read((char *)&p1, sizeof(p1))) { 61 cout << setw(20) << p1.name << ": " << setprecision(0) << setw(12) << p1.population 62 << setprecision(2) << setw(6) << p1.g << endl; 63 } 64 fin.close(); 65 } 66 cout << "Done.\n"; 67 return 0; 68 } 69 70 输出结果: 71 Here are the current contents of the /Users/xheart/Desktop/planets.dat file: 72 Earth: 6928198253 9.81 73 Enter planet name (enter a blank line to quit): 74 Jenny's World 75 Enter planetary population:32155648 76 Enter planet's acceleration of gravity:8.93 77 Enter planet name (enter a blank line to quit): 78 79 Here are the new contents of the /Users/xheart/Desktop/planets.dat file: 80 Earth: 6928198253 9.81 81 Jenny's World: 32155648 8.93 82 Done.
4.6 随机存取
随机存取指的是直接移动(不是依次移动)到文件的任何位置。随机存取常被用于数据库文件,程序维护一个独立的索引文件,该文件指出数据在主数据文件中的位置。这样,程序便可以直接跳到这个位置,读取(还可能修改)其中的数据。如果文件有长度形同的记录组成,这种方法实现起来最简单。
要实现随机存取,需要创建一个fstream对象。fstream类是从iostream类派生而来的,而后者基于istream喝ostream两个类,因此它继承了它们的方法。它还继承了两个缓冲区,一个用于输入,一个用于输出,并能同步化这两个缓冲区的处理。也就是说,当程序读写文件时,它将协调地移动输入缓冲区的输入指针和输出缓冲区中的输出指针。
要实现随机存取,首先要回答的是使用哪种文件模式。为读取文件,要使用ios_base::in模式。为执行二进制I/O,需要使用ios_base::binary模式(在某些非标准系统上,可以省略这种模式,事实上,可能必须省略这种模式)。为写入文件,需要ios_base::out或ios_base::app模式。然而,追加模式只允许程序将数据添加到文件尾,文件的其他部分是只读的;也就是说,可以读取原始数据,但不能修改它;要修改数据必须使用ios_base::out。同时使用in模式和out模式将得到读/写模式,因此只需要添加二进制元素即可。如前所述,要使用|运算符来组合模式。因此,需要使用下面的语句:
finout.open(file, ios_base::in | ios_base::out | ios_base::binary);
接下来,需要一个在文件中移动的方式。fstream类为此继承了两个方法:seekg()和seekp(),前者将输入指针移动到指定的文件位置,后者将输出指针移动到指定的文件位置(实际上,由于fstream类使用缓冲区来存储中间数据,因此指针指向的是缓冲区中的位置,而不是实际的文件)。也可以将seekg()用于ifstream对象,将seekp()用于ofstream对象。下面是seekg()的原型:
basic_istream<charT, traits> & seekg(off_type, ios_base::seekdir);
basic_istream<charT, traits> & seekg(pos_type);
它们都是模板,本章使用char类型的模板具体化。对于char具体化,上面的两个原型等同于下面的代码:
istream & seekg(streamoff, ios_base::seekdir);
istream & seekg(streampos);
第一个原型定位到离第二个参数指定的文件位置特定距离(单位为字节)的位置;第二个原型定位到离文件开头特定距离(单位为字节)的位置。
来看seekg()的第一个原型的参数。streamoff值被用来度量相对于文件特定位置的位移量(单位为字节)。streamoff参数表示相对于三个位置之一的偏移量为特定值(以字节为单位)的文件位置(类型可定义为整型或类)。seekdir参数是ios_base类中定义的另一种整型,有三个可能的值。常量ios_base::beg指相对于文件开始处的偏移量。常量ios_base::cur指相对于当前位置的偏移量;常量ios_base::end指相对于文件尾的偏移量。下面是一些调用示例,这里假设fin是一个ifstream对象:
fin.seekg(30,ios_base::beg); //从开始位置往后30个字节
fin.seekg(-1, ios_base::cur); //后退一个字节
fin.seekg(0, ios_base::end); //移动到文件尾
下面来看第二个原型。streampos类型的值定位到文件中的一个位置。它可以是类,但如果是这样的话,这个类将包含一个接受streamoff参数的构造函数和一个接受整型参数的构造函数,以便将两种类型转换为streampos值。streampos值表示文件中的绝对位置(从文件开始出算起)。可以将streampos位置看作相对于文件开始处的位置(以字节为单位,第一个字节的编号为0)。因此下面的语句将文件指针指向第112个字节,这是文件中的113个字节:
fin.seekg(112);
如果要检查文件指针的当前位置,对于输入流,可以使用tellg()方法,对于输出流,可以使用tellp()方法。它们都返回一个表示当前位置的streampos值(以字节为单位,从文件开始处算起)。创建fstream对象时,输入指针和输出指针将一前一后地移动,因此tellg()和tellp()返回的值相同。然而,如果使用istream对象管理输入流,而使用ostream对象来管理输出流,则输入指针和输出指针将彼此独立地移动,因此tellg()和tellp()将返回不同的值。