首页 > 代码库 > C++的那些事:流与IO类

C++的那些事:流与IO类

1、流的概念

"流"就是"流动",是物质从一处向另一处流动的过程,比如我们能感知到的水流。C++的流是指信息从外部输入设备(如键盘和磁盘)向计算机内部(即内存)输入和从内存向外部输出设备(如显示器和磁盘)输出的过程,这种输入输出过程被形象地比喻为"流"。

为了实现信息的内外流动,C++系统定义了I/O类库,其中的每一个类都称作相应的流或流类,用以完成某一方面的功能。根据一个流类定义的对象也时常被称为流。

通常标准输入或标准输出设备显示器称为标准流;外存磁盘上文件的输入输出称为文件流;对于内存中指定的字符串存储空间称为字符串流。

那么流的内容通常是什么呢?

流里的基本单位是字节,所以又称为字节流。字节流可以是ASCII字符、二进制数据、图形图像、音频视频等信息。文件和字符串也可以看成是有序的字节流,又称为文件流和字符串流。

2、IO类

C++的IO类库属于STL的一部分,在STL中定义了一个庞大的类库,它们的继承关系为下图:

image

管理标准的输入/输出流的类为:istream(输入)、ostream(输出)、iostream(输入输出),其中istream和ostream直接从ios中继承,iostream多重继承了istream和otream。而cin是STL中定位的一个用于输入的istream对象,cout、cerr、clog是三个用于输出的ostream对象。其中cout对象也被称为标准输出,用于正常的输出,cerr用来输出警告和错误信息,因为被称为标准错误,而clog用来输出程序运行时的一般性信息。cerr和clog之间的不同之处在于cerr是不经过缓冲区直接向显示器输出有关信息,而clog则是先把信息放在缓冲区,缓冲区满后或遇上endl时向显示器输出。

管理文件流的类为:ifstream(文件输入)、ofstream(文件输出)和fstream(文件的输入/输出)。其中ifstream是从istream中继承的类,ofstream是从ostream中继承的类,fstream是从iostream继承的类。

管理字符串流的类为:istringstream(字符串输入)、ostringstream(字符串输出)和stringstream(字符串的输入/输出)。其中istringstream是从istream中继承的类,ostringstream是从ostream中继承的类,stringstream是从iostream继承的类。

3,<<和>>操作符

3.1 <<的用法

在istream输入流类中定义有对右移操作符>>重载的一组公用成员函数,函数的具体声明格式为:

istream& operator>> (istream& is, char& c);
istream& operator>> (istream& is, signed char& c);
istream& operator>> (istream& is, unsigned char& c);
istream& operator>> (istream& is, char* s);
istream& operator>> (istream& is, signed char* s);
istream& operator>> (istream& is, unsigned char* s);

由于右移操作符重载用于给变量输入数据的操作,所以又称为提取操作符,即从流中提取出数据赋给变量。

当系统执行cin>>variable操作时,将根据实参x的类型调用相应的提取操作符重载函数,把variable引用传送给对应的形参,接着从键盘的输入中读入一个值并赋给variable后,返回cin流,以便继续使用提取操作符为下一个变量输入数据。

当从键盘上输入数据时,只有当输入完数据并按下回车键后,系统才把该行数据存入到键盘缓冲区,供cin流顺序读取给变量。还有,从键盘上输入的每个数据之间必须用空格或回车符分开,因为cin为一个变量读入数据时是以空格或回车符作为其结束标志的。

当cin>>str_ptr操作中的str_ptr为字符指针类型时,则要求从键盘的输入中读取一个字符串,并把它赋值给str_ptr所指向的存储空间中,若str_ptr没有事先指向一个允许写入信息的存储空间,则无法完成输入操作。另外从键盘上输入的字符串,其两边不能带有双引号定界符,若带有只作为双引号字符看待。对于输入的字符也是如此,不能带有单引号定界符。

3.2 >>的用法

在ostream输出流类中定义有对左移操作符<<重载的一组公用成员函数,函数的具体声明格式为:

istream& operator>> (bool& val);
istream& operator>> (short& val);
istream& operator>> (unsigned short& val);
istream& operator>> (int& val);
istream& operator>> (unsigned int& val);
istream& operator>> (long& val);
istream& operator>> (unsigned long& val);
istream& operator>> (long long& val);
istream& operator>> (unsigned long long& val);
istream& operator>> (float& val);
istream& operator>> (double& val);
istream& operator>> (long double& val);
istream& operator>> (void*& val);

除了与在istream流类中声明右移操作符重载函数给出的所有内置类型以外,还增加一个void* 类型,用于输出任何指针(但不能是字符指针,因为它将被作为字符串处理,即输出所指向存储空间中保存的一个字符串)的值。

由于左移操作符重载用于向流中输出表达式的值,所以又称为插入操作符。如当输出流是cout时,则就把表达式的值插入到显示器上,即输出到显示器显示出来。

当系统执行cout<<variable操作时,首先根据x值的类型调用相应的插入操作符重载函数,把variable的值按值传送给对应的形参,接着执行函数体,把variable的值(亦即形参的值)输出到显示器屏幕上,从当前屏幕光标位置起显示出来,然后返回cout流,以便继续使用插入操作符输出下一个表达式的值。

4,IO条件状态

4.1 查询流的状态

IO操作都有可能发生错误,一些错误是可恢复的,而其他错误发生在系统深处,已经超出了应用程序可以修正的范围。

IO类定义了一些函数和标志,可以帮助我们访问和操纵流的条件状态。

首先表示一个流当前的状态的变量的类型为strm::iostate,其中strm是一种流类型,可以是iostream、fstream等。比如,我们定义一个标准IO流状态:

iostream::iostate strm_state=iostream::goodbit;

IO库存定义了4个iostate类型的contexpr值,表示特定的位模式。这些值用来表示特定类型的IO条件,可以与位运算一起使用来一次性检测或设置多个标志位。

1)strm::badbit用来指定流已崩溃。它表示系统级的错误,如不可恢复的读写错误。通常情况下,一旦badbit被置位,流就无法再使用了。

2)strm::failbit用来指出一个IO操作失败了。

3)strm::eofbit用来指出流达了文件的结束。

在发生可恢复错误后,failbit被置位,如期望读取数值却读出一个字符错误。这种问题通常可以修正,流还可以继续使用。如果到达文件结束位置,eofbit和failbit都会被置位。

4)strm::goodbit用来指出流未处于错误状态。此值保证为零。

goodbit的值为0,表示流未发生错误。如果badbit、failbit和eofbit任一个置位,则检测流状态的条件会失败

标准库还定义了一组函数来查询这些标志位的状态,假如s是一个流,那么:

s.eof() // 若流s的eofbit置位,则返回true
s.fail() // 若流s的failbit或badbit置位,则返回true
s.bad() // 若流s的badbit被置位,则返回true
s.good() // 若流s处于有效状态,则返回true

在实际我们在循环中判断流的状态是否有效时,都直接使用流对象本身,比如:while(cin>>variable){cout<<variable},在实际中都转换为了while((cin>>variable).good()){cout<<variable}。

4.2 管理条件状态

IO类库提供了3个函数来管理和设置流的状态:

s.clear(); // 将流s中所有条件状态复位,将流的状态设置为有效,调用good会返回true
s.clear(flags); // 根据给定的flags标志位,将流s中对应的条件状态复位
s.setstate(flags); // 根据给定的flags标志位,将流s中对应的条件状态置位。
s.rdstate(); // 返回一个iostate值,对应流当前的状态。

我们可以这样使用上面的这些成员函数。

iostream::iostate old_state = cin.rdstate(); // 记住cin当前的状态 
cin.clear(); // 使用cin有效 
process_input(cin); // 使用cin 
cin.setstate(old_state); // 将cin置为原有状态 
cin.clear(cin.rdstate() & ~cin.failbit & ~cin.badbit); // 下failbit和badbit复位,保持eofbit不变。

5,IO缓冲区

5.1 输入缓冲

我们先看一个简单的输入输出程序:

int main() 
{ 
    char ch; 
    while (cin >> ch && ch!=‘#‘) 
    { 
        cout << ch; 
    } 
    return 0; 
}

程序的功能是,循环输入字符,然后把输入的字符显式出来,遇到#或cin流失败时结束,按照程序的表面来看,我们想要的效果是输入一个,显示一个,像这样rroonnyy#,红色代表的是显示的结果。而实际中我们的输出与输出却是这样的:

ronny#abc [Enter]

ronny

输入字符立即回显是非缓冲或直接输入的一个形式,它表示你所键入的字符对正在等待的程序立即变为可用。相反,延迟回显是缓冲输入的例子,这种情况下你所键入的字符块被收集并存储在一个被称为缓冲区的临时存储区域中。按下回车键可使你输入的字符段对程序起作用。

缓冲输入一般常用在文本程序内,当你输入有错误时,就可以使用你的键盘更正修正错误。当最终按下回车键时,你就可以发送正确的输入。

而在一些交互性的游戏里需要非缓冲输入,如:游戏里你按下一个键时就要执行某个命令。

缓冲分为两类:

1)完全缓冲:缓冲区被充满时被清空(内容发送到其目的地)。这种类型的缓冲通常出现在文件输入中。

2)行缓冲:遇到一个换行字符时被清空缓冲区。键盘的输入是标准的行缓冲,因此按下回车键将清空缓冲区。

5.2 输出缓冲

上面讲的是输入的缓冲,而C++中的输出也是存在缓冲的。

每个输出流都管理一个缓冲区,用来保存程序读写的数据。例如,如果执行下面的代码

os<<”please enter a value:”;

文本串可能立即打印出来,但也有可能被操作系统保存在缓冲区中,随后再打印。有了缓冲机制,操作系统就可以将程序的多个输出操作组合成单一的系统级写操作。由于设备的写操作可能很耗时,允许操作系统将多个输出操作组合为单一的设备写操作可以带来很大的性能提升。

导致缓冲刷新(即,数据真正写到输出设备或文件)的原因有很多:

<1> 程序正常结束,作为main函数的return操作的一部分,缓冲刷新被执行。

<2> 缓冲区满时,需要刷新缓冲,而后新的数据才能继续写入缓冲区。

<3> 我们可以使用操纵符endl来显式刷新缓冲区。

<4> 在每个输出之后,我们可以用操纵符unitbuf设置流的内部状态,来清空缓冲区。默认情况下,对cerr是设置unitbuf的,因此写到cerr的内容都是立即刷新的。

<5> 一个输出流被关联到另一个流。在这种情况下,当读写被关联的流时,关联到的流的缓冲区会被刷新,cin和cerr都关联到cout。因此读cin或写cerr会导致cout的缓冲区被刷新。

除了endl可以完成换行并刷新缓冲区外,IO库中还有两个类似的操纵符:flush和ends。flush刷新缓冲区,但不输出,任何额外的字符;ends向缓冲区插入一个空字符,然后刷新缓冲区。

cout << "hi!" << endl; // 输出 hi 和一个换行符,然后刷新缓冲区 
cout << "hi!" << flush; // 输出hi,然后刷新缓冲区,不附加任何额外字符 
cout << "hi!" << ends; // 输出hi和一个空字符。然后刷新缓冲区

如果想每次输出操作后都刷新缓冲区,我们可以使用unitbuf操纵符。它告诉流在接下来的每次写操作后都进行一次flush操作。而nounitbuf操作符则重置流使其恢复使用正常的系统管理的缓冲区刷新机制。

cout << unitbuf; // 所有输出操作后都立即刷新缓冲区 
// 任何输出都立即刷新,无缓冲 
cout << nounitbuf; // 回到正常的缓冲方式

注意:如要程序异常终止,输出缓冲区是不会被刷新的。当一个程序崩溃后,它所输出的数据很可能停留在输出缓冲区中等待打印。

我们可以将一个istream流关联到另一个ostream,也可以将一个ostream流关联到另一个ostream。

cin.tie(&cout); // 标准库已经将cin与cout关联在一起 
// s.tie如果s关联到一个输出流,则返回指向这个流的指针,如果对象未关联到流,则返回空指针 
ostream *old_tie = cin.tie(nullptr); // 将cin不再与其他流关联,同时old_tie指向cout 
cin.tie(&cerr);  // 读取cin会刷新cerr而不是cout 
cin.tie(old_tie);  // 重建cin和cout的正常关联 

6,使用文件流

6.1 使用文件流对象

创建一个文件流对象时,我们可以提供文件名,也可不提供文件名,后面用open成员函数来打开文件。

string infile="../input.txt"; 
string outfile = "../output.txt"; 
ifstream in(infile); // 定义时打开文件 
ofstream out; 
out.open(outfile); // 用open打开文件

如果调用open失败,failbit会被置位,所以调用open时进行检测通常是一个好习惯。

如果用一个读文件流ifstream去打开一个不存在的文件,将导致读取失败,而如果用一写文件流ofstream去打开一个文件,如果文件不存在,则会创建这个文件。

一旦一个文件流已经被打开,它就保持与对应文件的关联。实际上,对一个已经打开的文件流调用open会失败,并会导致failbit被置位,随后的试图使用文件流的操作都会失败。为了将文件流关联到另外一个文件,必须首先关闭已经关联的文件。关闭一个流的关联文件可以用close成员函数来完成。

6.2 文件模式

每个流都有一个关联的文件模式,用来指出如何使用文件,下面列出了文件模式和它们的含义:

in

以读方式打开

out

以写方式打开

app

每次写操作均定位到文件末尾

ate

打开文件后立即定位到文件末尾

trunc

截断文件

binary

以二进制方式进行IO

用文件名初始化一个流时或用open打开文件时都可以指定文件模式,但要注意下面几种限制:

<1>只可以对ofstream或fstream对象设定out模式。

<2>只可以对ifstream或fstream对象设定in模式。

<3>只有out设定时才可以设定trunc模式。

<4>只要trunc没有被设定,就可以设定app模式。在app模式下,即使没有显式指定out模式 ,文件也总是以输出方式被打开。

<5>默认情况下,即使我们没有指定trunc,以out模式打开的文件也会被截断。为了保留以out模式打开的文件的内容,我们必须同时指定app模式,这样只会将数据追加写到文件末尾;或者同时指定in模式,即打开文件同时进行读写操作。

<6>ate和binary模式可以用于任何类型的文件流对象,且可以与其他任何文件模式组合使用。

以out模式打开文件会丢弃已有数据,所以阻止一个ofstream清空给定文件内容的方法是同时指定app模式。

// 在这几条语句中,file1都被截断 
ofstream out("file1"); // 隐含以输出模式打开文件并截断文件 
ofstream out2("file1", ofstream::out); // 隐含地截断文件 
ofstream out3("file1", ofstream::out | ofstream::trunc); // 显式截断文件 
// 为了保留文件内容,我们必须显式指定app 
ofstream app("file2", ofstream::app); // 隐含为输出模式 
ofstream app("file2", ofstream::app | ofstream::out);

7,使用字符流

sstream头文件定义了三个类型来支持内存IO,这些类型可以向string写入数据,从string读取数据,就像string是一个IO流一样。

7.1 使用istringstream

很多时候我们需要逐行处理文本,而且需要对行内的单词进行单独分析,这时候使用istringstream是很方便的。

比如,我们程序需要一次读取一行文本,然后将其中的单词分别取出保存在一个vector中。

string line,word; 
while (getline(cin, line)) 
{ 
    vector<string> wordList; 
    istringstream lineText(line); 
    while (lineText >> word) 
    { 
        wordList.push_back(word); 
    } 
}

7.2 使用ostringstream

当我们逐步构造输出时,希望最后一起打印时,ostringstream是很有用的,它可以帮我们完成类似于itoa,ftoa这种数字转字符串的功能。

int num1 = 42; 
double pi = 3.1415926; 
string str = "some numbers";

ostringstream formatted; 
formatted << str << pi << num1; 
cout << formatted.str() << endl;

其中str成员函数是stringstream有几个特有操作之一。

string s;
stringstream strm(s);// 保存s的一个拷贝,此构造函数是explicit的。
strm.str(); // 返回strm所保存的string对象的拷贝。
strm.str(s); // 将s拷贝到strm中,返回void。

8,格式化输入输出

8.1 操纵符

标准库定义了一组操纵符用来修改流的状态,一个操纵符是一个函数或是一个对象,会影响流的状态,并能用作输入或输出运算符的运算对象,比如我们熟悉的endl,就是一个操纵符。

操纵符用于两大类输出控制:控制数值的输出形式以及控制补白的数量和位置,大多数改变格式状态的操纵符都是设置/复原成对的:一个操纵符用来将格式状态设置为一个新值,而另一个用来将其复原,恢复为正常默认格式。

8.2 控制布尔值的格式

通过设置boolalpha可以将bool型变量的true输出为true或将false输出为false。可以设置noboolalpha来将内部状态恢复为默认格式。

// modify boolalpha flag
#include <iostream>     // std::cout, std::boolalpha, std::noboolalpha

int main () {
  bool b = true;
  std::cout << std::boolalpha << b << ‘\n‘;
  std::cout << std::noboolalpha << b << ‘\n‘;
  return 0;
}

8.3 指定整形值的进制

默认情况是以十进制格式输出,我们可以设置不同的格式操纵符来改变输出整型值的进制。

oct:以八进制显示

hex:以十六进制显示

dec:以十进制显示

另外可以使用showbase操纵符来显式格式的前缀,8进制前有前导0,十六进制有前导0x。操纵符noshowbase恢复cout的状态,从而不再显示整型值的进制。有时候我们需要将16进制输出为大写如0X FF,可以用操纵符uppercase和nouppercase来控制流输出的大小写状态。

cout << uppercase << showbase << hex << 20 << 1024 
        << nouppercase << noshowbase << dec << endl;

8.4 控制浮点数格式

打印精度是通过precision成员或使用setprecision操纵符来改变。其中precision是一个重载函数,一个版本接受int参数,将精度设置为此值,并返回旧精度值。另外一个版本不接受参数,返回当前精度值。setprecision操纵符接受一个参数,用来设置精度。

用scientific用来指定科学记数法,fixed指定为定点十进制,hexfloat指定为十六进制的浮点数。defaultfloat将流恢复到默认的状态。

设置showpoint可以用来强制打印小数。

8.5 输出补白

setw:指定下一个数字或字符串的最小空间

left:表示左对齐输出。

right:表示右对齐输出,右对齐是默认格式。

internal:控制负数的符号的位置,它左对齐符号,右对齐值,用空格填满所有中间空间。

setfill:允许指定一个字符代替默认的空格来补白输出。

int i = -16; 
double d = 3.14159; 
cout << "i:" << setw(12) << i << ‘\n‘ 
<< "d:" << setw(12) << d << ‘\n‘; 
cout << left 
<< "i:" << setw(12) << i << ‘\n‘ 
<< "d:" << setw(12) << d << ‘\n‘; 
cout << right 
<< "i:" << setw(12) << i << ‘\n‘ 
<< "d:" << setw(12) << d << ‘\n‘; 
cout << internal 
<< "i:" << setw(12) << i << ‘\n‘ 
<< "d:" << setw(12) << d << ‘\n‘; 
cout << setfill(‘#‘) 
<< "i:" << setw(12) << i << ‘\n‘ 
<< "d:" << setw(12) << d << ‘\n‘ 
<< setfill(‘ ‘);

image

9,流的随机访问

不同的流类型一般支持对相关流中数据的随机访问。可以重新定位流,以便环绕跳过,首先读最后一行,再读第一行,以此类推。标准库提供一对函数来定位(seek)给定位置并告诉(tell)相关流中的当前位置。

9.1 seek和tell函数

seekg:重新定位输入流中的标记

tellg:返回输入流中标记的当前位置

seekp:重新定位输出流中的标记

tellp:返回输出流中标记的当前位置

逻辑上,只能在istream或者ifstream或者istringstream上使用g版本,并且只能在ostream类型或其派生类性ofstream或者ostringstream之上使用p版本。iostream对象,fstream或者stringstream对象对相关流既可以读也可以写,可以使用两个版本的任意版本。9.

9.2 只有一个标记

虽然标准库区分输入和输入而有两个版本,但它只在文件中维持一个标记——没有可区分的读标记和写标记。

只是试图在ifstream对象上调用tellp的时候,编译器将会给出错误提示。反之亦然。

使用既可以读又能写的fstream类型以及stringstream类型的时候,只有一个保存数据的缓冲区和一个表示缓冲器中当前位置的标记,标准库将g版本和p版本都映射到这个标记。

9.3 普通iostream对象一般不允许随机访问。

9.4 重新定位标记

seekg(new_position);
seekp (new_position);
seekg( offset, dir);
seekp( offset, dir);

第一个版本将当前位置切换到给定地点,第二个版本接受一个偏移量以及从何处计算偏移的指示器。

9.5 访问标记

tell函数返回的一个值,使用适当类的pos_type成员来保存。

10,一个实例

假定给定一个文件来读,我们将在文件的末尾写一个新行,改行包含了每一行开头的相对位置(程序不必写第一行的偏移量,因为它总是0)。例如给定下面的文件,

abcd

efg

hi

j

这段程序应产生修改过的文件如下:

abcd

efg

hi

j

5 9 12 14

#include <iostream>
#include <fstream>
#include <string>

using std::fstream;
using std::cerr;
using std::endl;
using std::ifstream;
using std::ofstream;
using std::string;
using std::getline;
using std::cout;
//using namespace std;

int main()
{
    fstream inOut("copyOut.txt",
        fstream::ate | fstream::in | fstream::out);            //用ate方式打开,会将文件的位置定位到文件末尾。

    if( !inOut )        {
        cerr << "unable to open file" <<endl;
        return EXIT_FAILURE;
    }

    inOut.seekg(-1,fstream::end);            //go to the last char
    if( inOut.peek() != 10)                    //if the last char of the file is not a newline,add it.
    {
        inOut.seekg(0,fstream::end);
        inOut.put(‘\n‘);
    }

    inOut.seekg(0,fstream::end);
    ifstream::pos_type endMark = inOut.tellg();        //record the last position .

    inOut.seekg(0,fstream::beg);

    int cnt = 0;                        //accumulator for byte count
    string line;                        //hold each line of input

    while( inOut && inOut.tellg() != endMark
        && getline(inOut , line)
        )
    {
        cnt += line.size() + 1;            // add 1 to acount for the newline

        ifstream::pos_type mark = inOut.tellg();
        inOut.seekp( 0, fstream::end);    //set write marker to end
        inOut << cnt;

        if( mark != endMark) inOut << " ";
        inOut.seekg(mark);                //restore read position
    }
    inOut.clear();                        //clear flags in case we hit an error
    inOut.seekp(0 , fstream::end);        //seek to end
    inOut << endl;                        //write a newline at end of file

    return 0;
}