[1] I/O基础
大多数计算机语言的输入输出的实现都是以语言本身为基础的,但是C/C++没有这样做。C语言最初把I/O留给了编译器实现人员。这样做的一个原因是可以提供足够的自由度,使之最适合目标机器的硬件条件。但是大多数实现人员都将I/O建立在了Unix库函数中,之后C才将该库引入了C标准中,被C++继承了下来。
但是C++也有自己的I/O解决方案,将其定义于iostream和fstream中。这两个库不是语言的组成部分,只是定义了一些类而已。
C++把输入和输出看作字节流。对于面向文本的程序,一个字节代表一个字符。流充当了源于目标之间的桥梁,因此对流的管理包含两个方面:从哪里来,到哪里去。其中,缓冲区作为临时存储空间,可以提高流处理的速度。
C++中专门定义了一些类来管理流和缓冲区:
C++98定义了一些模板类,用以支持char和wchar_t类型;
C++ 11添加了char16_t和char32_t类型(实则就是一些typedef来模拟不同的类型)。
C++程序中若包含了iostream类库,则自动创建8个流对象:4个窄的,4个宽的:
cin: 标准输入流,默认关联到键盘[与之对应的宽型为wcin(处理wchar_t)];
cout: 标准输出流,默认关联到显示器[与之对应的宽型为wcout(处理wchar_t)];
cerr: 标准错误输出流,默认关联到显示器,这个流没有缓冲区,意味着信息会直接发送到显示器[与之对应的宽型为wcerr(处理wchar_t)];
clog: 标准错误输出流,默认也关联到显示器,但是这个流对应着缓冲区[与之对应的宽型为wclog(处理wchar_t)]。
[1.1] cout 的使用
C++将输出看作是字节流(可能是8位/16位/32位),但在将字节流发送给屏幕时,希望每个字节表示一个字符值。比如,要显示-2.34,需要5个字符表示,分别为-/2/./3/4,并不是这个值的浮点数表示形式。因此,ostream类的最重要的任务之一就是将数值类型转换为文本形式表示的字符流。为了达到这个目标,C++使用了如下方法:
重载的<<运算符:C++的重载操作除了基本的内置类型,还有如下几种:const signed char*/const unsigned char*/const char*/void*。
对于其他类型的指针,C++将其看作void*,然后打印地址的数值。如果要想获得字符串的地址,必须将其强转为void*类型。
ostream类的put()和write()方法,前者用于显示字符,后者用于显示字符串。
ostream& put(char);
// cout.put(‘B’).put(‘T’);
basic_ostream<charT, traits>& write(const char_type* s, streamsize n);
// write()的第一个参数是要显示的字符串的地址,第二个参数是要显示字符个数。
1 const char* name = "benxintuzi"; 2 for(int i = 0; i <= strlen(name); i++) 3 { 4 cout.write(name, i); 5 cout << endl; 6 } 7 8 /** output */ 9 b 10 be 11 ben 12 benx 13 benxi 14 benxin 15 benxint 16 benxintu 17 benxintuz 18 benxintuzi
说明:
write()方法不会在遇到空白字符就停止打印,而是打印指定数目的字符,即使超出了字符串的边界。write()也可将数字转换为相应的形式打印,此时需要将数字的地址转换为char*,但是不会打印字符,而是内存中表示数值的位,可能是乱码,这个再正常不过了。
由于ostream类对数据进行了缓冲,因此利用cout也许并不会立即得到输出,而是被存储在缓冲区中,直至缓冲区填满为止。通常,缓冲区大小为512字节及其整数倍。如果需要将尚未填满的缓冲区进行刷新操作,需要执行如下代码:
cout << flush; // 立即刷新
或者:
cout << endl; // 立即刷新并插入换行符
控制数值显示的格式:
dec/hex/oct,虽然这些都是函数,但是其通常的使用方式是:
cout << hex;
cout << value << endl;
使用width成员函数将不同的字段放到宽度相同的字段中:有两个函数版本:
int width()/int width(int i);
第一个返回当前字段所占的空格数;
第二个函数将宽度设置为i个空格后,返回旧的空格数。但是注意:该函数只影响紧挨着它的一项显示输出,然后将字段宽度恢复为默认值。
说明:
C++不会截断数据,比如如果在宽度为2的字段中打印7位的值,那么该字段将被增大,C++的原则是:内容比形式更重要。
默认情况下,C++的对其方式为右对齐,并且默认的填充字符是空格。可以使用fill()成员函数来更改填充字符,例如:cout.fill(‘*’);
1 string birth("19890608"); 2 cout.width(20); 3 cout << birth << endl; 4 cout.fill(‘*‘); 5 cout.width(20); 6 cout << birth << endl; 7 8 /** output */ 9 19890608 10 ************19890608
默认情况下,浮点数的显示精度指的是总位数;
在定点模式和科学模式下,指的是小数点后的位数;
C++默认的精度为6位,并且末尾的0不显示。可以使用cout.precision(int)设置精度的位数,一旦设置,直到第二次设置前永远有效。
对于有些输出,必须保留末尾的0,此时需要借助于ios_base类中的setf()函数:cout.setf(ios_base::showpoint);
为了简化格式设置工作,C++专门提供了一个工具(头文件<iomanip>),3个最常用的格式控制符分别为:setprecision()、setfill()、setw(),分别用来设置精度、填充字符、字段宽度。
1 for(int i = 1; i <= 10; i++) 2 { 3 cout << setw(5) << i << setw(10) << i * 10 << endl; 4 } 5 6 /** output */ 7 1 10 8 2 20 9 3 30 10 4 40 11 5 50 12 6 60 13 7 70 14 8 80 15 9 90 16 10 100
[1.2] cin 的使用
cin对象将标准输入表示为字节流,通常情况下,键盘生成的是字符流,cin根据接受值的变量的类型,将字符序列转换为所需的类型。
再次强调:C++解释输入的方式取决于变量的数据类型。istream类重载了>>,可以识别如下基本类型:
signed char&/unsigned char&/char&/short&/unsigned short&/int&/unsigned int&/long&/unsigned&/long long&/unsigned long long&/float&/double&/long double&。
除此之外,还支持:
signed char*/char*/unsigned char*。
[1.3] 流状态
cin和cout对象各自包含一个描述流状态的数据成员(从ios_base中继承的)。流状态被定义为iostate类型,实则是bitmask类型,由3个ios_base元素组成:eofbit/badbit/failbit。其中每个元素都是一位:
当cin到达文件末尾时,置位eofbit;
当cin未能读取到预期的字符时,置位badbit;
当I/O故障失败时,置位failbit。
当全部三个状态位设置为0时,说明一切顺利。程序可以根据当前的流状态决定下一步的动作。
[1.4] 字符的输入
首先应该确定是否希望跳过空白符,希望跳过空白则选择>>;如果希望检查输入的每个字符,则使用get(void)或者get(char&)。如果要将标准C程序改写为C++程序,可以使用cin.get()替换getchar(), 使用cin.put(ch)替换putchar(ch)。
字符串输入函数getline()、get()、ignore():
istream& get(char*, int, char); istream& get(char*, int); istream& getline(char*, int, char); istream& getline(char*, int); 其中: char*: 存放字符串的内存单元地址; int: 内存单元大小(比字符串长度大1,用于存放’\0’); char: 指定分界符。 例如,将字符串读入字符数组line中: char line[50]; cin.get(line, 50); 读取时,get将在到达第49个字符或者遇到换行符后停止从流中读取字符,其在中途若遇到流中的换行符,那么将其留在流中不予处理,下一次读取时,首先看到的仍然是换行符;而getline则会将遇到的换行符读取出来并丢弃掉,这样流中就没有换行符了。 cin.ignore(255, ‘\n’)表示即将读取接下来的255个字符并且丢弃掉或者遇到分界符\n后停止读取操作,其默认形式为:istream& ignore(int = 1, int = EOF)。 |
1 #include <iostream> 2 #include <cstring> 3 #include <iomanip> 4 using namespace std; 5 6 const int Limit = 255; 7 8 int main() 9 { 10 char input[Limit]; 11 12 cout << "Enter a string for getline() processing: "; 13 cin.getline(input, Limit, ‘#‘); // 丢弃流中的# 14 15 cout << "Here is your input: "; 16 cout << input << "\nDone with phase 1" << endl; 17 18 char ch; 19 cin.get(ch); // 读取到的字符为#后边的第一个字符 20 cout << "The next input character is: " << ch << endl; 21 if(ch != ‘\n‘) 22 cin.ignore(Limit, ‘\n‘); 23 24 cout << "Enter a string for get() processing: "; 25 cin.get(input, Limit, ‘#‘); // 保存流中的# 26 27 cout << "Here is your input: "; 28 cout << input << "\nDone with phase 2" << endl; 29 30 cin.get(ch); // 读取到的字符为# 31 cout << "The next input character is: " << ch << endl; 32 33 return 0; 34 } 35 36 /** output */ 37 Enter a string for getline() processing: I love tu#zi ! 38 Here is your input: I love tu 39 Done with phase 1 40 The next input character is: z 41 Enter a string for get() processing: I love tu#zi ! 42 Here is your input: I love tu 43 Done with phase 2 44 The next input character is: #
除了前面介绍的方法外,其他的istream方法还包括read()/peek()/gcount()/putback()等:
read()读取指定数目的字节并存储到指定的位置,如: char gross[100]; cin.read(gross, 100); 说明: 与get和getline不同,read不会在输入后加上’\0’,因此,不能将输入转换为字符串。Read常与ostream write()函数结合使用,完成文件的输入和输出。返回类型为istream&。 peek()函数返回输入中的下一个字符,但是不去读下一个字符,也就是说可以用来作为检查流的作用,以此来判断是否还要继续读取: char gross[80]; char ch; int i = 0; while((ch = cin.peek()) != ‘\n’) cin.get(gross[i++]); gross[i] = ‘\0’; gcount()返回最后一个非格式化方法读取的字符个数,意味着get()/ getline()/ ignore()/ read()方法读取的字符个数。实际中用的不多,可以用strlen()代替。 putback()方法将一个字符退回到流中,以便下一次读取。该方法返回一个istream&。使用peek()的效果相当于先使用一个get()读取一个字符,然后使用putback()将该字符退回到流中去。当然,putback()也可将字符放置在流中的其他位置。 |
1 #include <iostream> 2 using namespace std; 3 4 int main() 5 { 6 char ch; 7 8 while(cin.get(ch)) 9 { 10 if(ch != ‘#‘) 11 cout << ch; 12 else 13 { 14 cin.putback(ch); 15 break; 16 } 17 } 18 if(!cin.eof()) 19 { 20 cin.get(ch); 21 cout << endl << ch << "is the next input character." << endl; 22 } 23 else 24 { 25 cout << "End of file reached." << endl; 26 } 27 28 while(cin.peek() != ‘#‘) 29 { 30 cin.get(ch); 31 cout << ch; 32 } 33 if(!cin.eof()) 34 { 35 cin.get(ch); 36 cout << endl << ch << "is the next input character." << endl; 37 } 38 else 39 { 40 cout << "End of file reached." << endl; 41 } 42 43 return 0; 44 } 45 46 /** output */ 47 I love #tuzi and I am #benxin. 48 I love 49 #is the next input character. 50 tuzi and I am 51 #is the next input character.
[2] 文件I/O
文件类的I/O与基本I/O非常相似,其相关类派生自iostream类,只是要操作文件,还必须将文件与流进行关联起来。
[2.1] 文件I/O基础
要让程序写文件,必须进行如下操作:
1 创建一个ofstream对象管理输出流:ofstream fout;
2 将该对象与特定文件关联起来:fout.open(“jar.txt”)【如果jar.txt不存在,那么会自动创建它】;
3 将内容写入文件中去:fout << “benxintuzi test”;
要完成上述任务,首先包含头文件fstream, 由于fstream中的类大多派生自iostream中的类,因此,fstream必然包含了iostream,因此,包含头文件fstream后不必再显式包含iostream了。
说明:
可以在创建流的同时关联文件:ofstream fout(“jar.txt”);
以上述这种默认模式打开文件时,如果文件不存在,则自动创建该文件;若文件存在,则自动将文件清空,然后再写入新内容。
程序读文件流程类似:
ifstream fin;
fin.open("jayne.txt");
char buf[50];
fin.getline(buf, 50); // 读入一行内容
cout << buf << endl;
虽然程序结束时会自动关闭流,但是最好显式关闭,如下:
fout.close();
fin.close();
关闭流并不是删除流,只是将断开流与文件的关联性。这样就可以重新将该流关联到另一个文件中。
[2.2] 流状态检查
C++文件流类从ios_base类继承了一个表示流状态的成员以及报告流状态的方法。可以通过这些方法判断流操作是否成功。比如检查打开文件是否成功,可以如下:
fin.open(“***”);
if(fin.fail())
或者:
fin.open(“***”);
if(!fin)
或者利用C++提供的新方法:
if(!fin.is_open())
说明:
if(fin.fail())、if(!fin.good())、if(!fin)...是等价的。但是如果以不合适的文件模式打开文件失败时,这些方法将不会检测出来。相比之下,is_open()却可以检测到这种错误。
在打开多个文件时,常理来说需要为每个文件关联一个流对象,然而为了节约系统资源,可以创建一个流,然后分别关联到不同的文件中。
文件模式描述的是打开的文件将被如何使用:读、写、追加等。
ifstream fin(“jar.txt”, model);
ofstream fout;
fout.open(“jar.txt”, model);
ios_base类定义了一个openmode类型,用来表示模式。与fmtflags和iostate类型一样,它也是一个bitmask类型。可以选择ios_base类中定义的多个常量来指定模式:
说明:
ios_base::ate和ios_base::app都将文件指针指向打开的文件尾,二者的区别在于:ios_base::app模式只允许将数据添加到文件尾,而ios_base::ate模式将指针放到文件尾。
追加文件示例:
1 #include <iostream> 2 #include <fstream> 3 #include <string> 4 #include <cstdlib> 5 6 using namespace std; 7 8 const char* file = "append.txt"; 9 10 int main() 11 { 12 // show the initial contents 13 ifstream fin; 14 fin.open(file); 15 if(fin.is_open()) 16 { 17 cout << "The contents of " << file << " is: " << endl; 18 char ch; 19 while(fin.get(ch)) 20 cout << ch; 21 fin.close(); 22 } 23 24 // add new contents to the end of file 25 ofstream fout(file, ios::out | ios::app); 26 if(!fout.is_open()) 27 { 28 exit(EXIT_FAILURE); 29 } 30 else 31 { 32 cout << "Enter your contents to be added: "; 33 string contents; 34 while(getline(cin, contents) && contents.size() > 0) 35 fout << contents << endl; 36 fout.close(); 37 } 38 39 // show revised file 40 fin.clear(); 41 fin.open(file); 42 if(fin.is_open()) 43 { 44 cout << "The contents of " << file << " is: " << endl; 45 char ch; 46 while(fin.get(ch)) 47 cout << ch; 48 fin.close(); 49 } 50 cout << endl << "Done !" << endl; 51 52 return 0; 53 } 54 55 /** output */ 56 The contents of append.txt is: 57 This is line 1 58 This is line 2 59 This is line 3 60 Enter your contents to be added: I love tuzi 61 So you can guess 62 63 The contents of append.txt is: 64 This is line 1 65 This is line 2 66 This is line 3 67 I love tuzi 68 So you can guess 69 70 Done !
说明:
以二进制读写文件时,请使用write和read方式。
[2.3] 随机存取文件
随机存取常被用于数据库文件中,可以根据索引操作数据项。
在文件中移动指针:
seekg(): 将输入指针移到指定的位置;
seekp(): 将输出指针移到指定的位置。
说明:
其实由于fstream类使用缓冲区来存储中间数据,因此指针指向的是缓冲区的位置,而不是真正的文件中的位置。
检查文件指针的当前位置:
对于输入流,使用tellg();
对于输出流,使用tellp();
它们都返回一个表示当前位置距离起始位置的偏移量,单位为字节。
在系统中需要创建临时文件时,可以使用cstdio中的char* tmpnam(char* pszName)函数,该函数可以生成TMP_NAM个不同的临时文件名,其中每个文件名包含的字符不超过L_tmpnam个。
1 #include <cstdio> 2 #include <iostream> 3 4 using namespace std; 5 6 int main() 7 { 8 char pszName[L_tmpnam] = {‘\0‘}; 9 cout << "10 temp file names are follows: " << endl; 10 for(int i = 0; i < 10; i++) 11 { 12 tmpnam(pszName); 13 cout << pszName << " "; 14 } 15 cout << endl; 16 17 return 0; 18 } 19 20 /** output */ 21 10 temp file names are follows: 22 \s4ic. \s4ic.1 \s4ic.2 \s4ic.3 \s4ic.4 \s4ic.5 \s4ic.6 \s4ic.7 \s4ic.8 \s4ic.9
[2.4] 内核格式化
iostream提供程序与终端之间的I/O;
fstream提供程序和文件之间的I/O;
sstream提供程序和string对象之间的I/O。
读取string对象中的格式化信息或者将格式化的信息写入到string对象中被称为内核的格式化(incore formatting):
sstream类定义了一个从ostream类派生而来的ostringstream类,当创建了一个ostringstream对象时,就可以将信息写入其中。该对象使用动态内存分配来增大缓冲区。ostringstream类有一个名为str()的成员函数,该函数返回一个被初始化为缓冲区内容的string对象。
string mesg = outstr.str();
注意:使用str()方法后不能再对ostringstream对象进行写操作。
与之相对的有istringstream类。
1 #include <iostream> 2 #include <sstream> 3 #include <string> 4 using namespace std; 5 6 int main() 7 { 8 ostringstream outstr; // 管理一个string流 9 string name; 10 cout << "What‘s the name of your hard disk ? : "; 11 getline(cin, name); 12 cout << "What‘s the capacity in GB ? : "; 13 int cap; 14 cin >> cap; 15 16 outstr << "The name is : " << name << ", and the capacity is " << cap << " GB." << endl; 17 string str = outstr.str(); 18 19 // <11111> 20 cout << "<11111>" << endl; 21 cout << str << endl; 22 23 // <22222> 24 cout << "<22222>" << endl; 25 istringstream instr(str); 26 string word; 27 while(instr >> word) // read a word every a time 28 cout << word << endl; 29 30 return 0; 31 } 32 33 /** output */ 34 What‘s the name of your hard disk ? : TOSHIBA 35 What‘s the capacity in GB ? : 320 36 <11111> 37 The name is : TOSHIBA, and the capacity is 320 GB. 38 39 <22222> 40 The 41 name 42 is 43 : 44 TOSHIBA, 45 and 46 the 47 capacity 48 is 49 320 50 GB.