一、IO库

  • IO库主要分为三大类:
    • iostream:标准输入输出流
    • fstream:文件的输入输出流
    • sstream:string对象流
  • 宽字符类型:
    • 为了支持宽字符的语言,标准库定义了一组类型和对象来操纵wchar_t类型的数据
    • 宽字符版本的类型和函数的名字以一个w开始。例如,wcin、wcout、wcerr是分别对应cin、cout、cerr的宽字符版对象
    • 宽字符版本的类型和对象与其对应的普通char版本的类型定义在同一头文件中。例如头文件fstream定义了ifstream和wifstream

IO类型间的继承关系

  • 概念上,设备类型和字符大小都不会影响我们要执行的IO操作。例如,我们可以用>>读取数据,而不用管是从一个控制台窗口,一个磁盘文件,还是一个string读取。类似的,我们也不用管读取的字符能存入一个char对象内,还是需要一个wchar_t对象来存储
  • 标准库使我们能忽略这些不同类型的流之间的差异,这是通过继承机制实现的。利用模板,我们可以使用具有继承关系的类,而不必了解继承机制如何工作的细节
  • 例如:
    • 类型ifstream和istringstream都继承自istream:因此我们可以像使用istream对象一样来使用ifstream和istringstream对象。也就是说,我们是如何使用cin的,就可以同样地使用这些类型的对象。例如,可以对一个ifstream或istringstream对象调用getline,也可以使用>>从一个ifstream或istringstream对象中读取数据
    • 类似的,ofstream和ostringstream都继承自ostream:因此我们是如何使用cout的,就可以同样地使用这些类型的对象
    • 类似的,fstream和stringstream都继承自iostream

二、IO对象无拷贝或赋值

  • 我们不能拷贝或对IO对象赋值。例如:
#include <fstream>

int main()
{
    ofstream out1, out2;
    out1 = out2;              //错误,不能对流对象赋值
    ofstream print(ofstream); //错误,不能初始化ofstream
    out2 = print(out2);       //错误,不能拷贝对象

    return 0;
}
  • IO对象的用法:
    • 由于不能拷贝IO对象,因此我们也不能将形参或返回值类型设置为流类型
    • 进行IO操作的函数通常以引用方式传递和返回流
    • 读写一个IO对象会改变其状态,因此传递和返回的引用不能是const的

三、条件状态

  • IO操作使用时也会发生错误:
    • 一些错误是可恢复的,而其他错误则发生在系统深处,已经超出了应用程序可以修正的范围
    • 上表列出了IO类所定义的一些函数和标志,可以帮我们访问和操纵流的条件状态

IO错误的案例

  • 下面的代码要求为int变量输入数据
#include <iostream>
using namespace std;

int main()
{
    int ival;
    //如果输入成功执行循环
	while (std::cin >> ival) {
        std::cout << "cin success..." << std::endl;
	}
    return 0;
}
  • 但是当程序运行时,我们尝试输入字符串(例如Boo),那么上面的cin语句就会出错,因此while循环会终止
  • 一旦流发生错误,其上后续的IO操作都会失败

流标志位(iostate、badbit、failbit、eofbit、goodbit)

  • 上面的演示案例将流作为条件使用,只能告诉我们流是否有效,而无法告诉我们具体发生了什么。有时我们需要知道流为什么失败,IO库定义了下面一些列相关的知识
  • iostate:
    • iostate提供了表达流状态的完整功能。这个类型应作为一个位集合来使用
    • IO库定义了4个iostate类型的constexor值,表示特定的位模式。这些值用来表示特定类型的IO条件,可以与位运算符一起使用来一次检测或设置多个标志位
    • 这些值用来表示特定类型的IO条件,可以与位运算符一起使用来一次性检测或设置多个标志位
  • badbit:
    • badbit表示系统级错误,如不可恢复的读写错误。通常情况下,一旦badbit被置位,流就无法使用了
    • 如果badbit被置位,则检测流状态的条件会失败
  • failbit:
    • 在发生可恢复错误后,failbit被置位,如期望读取数值却读出一个字符等错误。这种问题通常是可以修正的,流还可以继续使用
    • 如果文件达到尾端,failbit会被置位
    • 如果failbit被置位,则检测流状态的条件会失败
  • eofbit:
    • 如果文件达到尾端,eofbit也会被置位
    • 如果eofbit被置位,则检测流状态的条件会失败
  • goodbit:
    • goodbit的值为0,表示流未发生错误

查询流状态函数

  • 在上面的图片中还定义了一组函数用来查询上面标志位的状态
  • 规则如下:
    • 操作good()在所有错误位均未置位的情况下返回true
    • bad()、fail()、eof()则在对应错误位被置位时返回true。此外,badbit被置位时,fail()也会返回true。这意味着,使用good()和fail()是确定流的总体状态的正确方法
    • eof()和bad()操作只能表示特定的错误

管理条件状态函数

  • 相关函数如下:
    • rdstate()函数:返回一个iostate值,对应流的当前状态
    • setstate()函数:将给定条件为置位,表示发生了对应错误
    • clear()函数:有两个版本:一个不接受参数的版本,而另一个版本接受一个iostate类型的参数
  • 无参数的clear()函数清除(复位)所有错误标志位。执行clear()后,调用good会返回true。演示案例如下:
void process_unput(istream& is)
{
    //在其中对is操作
}

int main()
{
    auto old_state = cin.rdstate(); //记住cin的当前状态
    cin.clear();                    //使cin有效(清除(复位)所有错误标志位)
    process_unput(cin);             //使用cin
    cin.setstate(old_state);        //将cin置为原有状态
    
    return 0;
}
  • 带参数的clear()函数接受一个iostate值,表示流的新状态。演示案例如下:
    • 为了复位单一的条件状态位,首先调用rdstate读出当前条件状态,然后用位操作将所需位复位来生成新的状态
    • 例如,下面将failbit和badbit复位,但保持eofbit不变
//复位failbit和badbit,保持其他标志位不变
cin.clear(cin.rdstate() & ~cin.failbit & ~cin.badbit);

四、输出缓冲

缓冲区

  • 每个输出流都管理着一个缓冲区,用来保存程序读写的数据
  • 文本串可能立即被打印出来,但也有可能被操作系统保存在缓冲区中,随后再打印
  • 设计的目的:有了缓冲机制,操作系统就可以将程序的多个输出操作组合成单一的系统级写操作。由于设备的写操作可能很耗时,允许操作系统将多个输出操作组合成单一的设备写操作可以带来很大的性能提升
  • 导致缓冲刷新的原因又如何几种(即,数据真正写到输出设备或文件):
    • 程序正常结束。作为main函数的return操作的一部分,缓冲刷新被执行
    • 缓冲区满时,需要刷新缓冲,而后新的数据才能继续写入缓冲区
    • 使用endl、flush、ends操纵符显式刷新缓冲区
    • 在每个输出操作之后,我们可以使用unitbuf操纵符设置流的内部状态,来清空缓冲区。默认情况下,对cerr是设置ubitbuf的,因此写到cerr的内容都是立即刷新的
    • 一个输出流可能被关联到另一个流。在这种情况下,当读写被关联到的流时,关联到的流的缓冲区会刷新。例如,默认情况下,cin和cerr都关联到cout。因此,读cin或写cerr都会导致cout的缓冲区被刷新

刷新输出缓冲区(endl、flush、ends)

  • 用法如下:
    • endl:输出一个换行,然后刷新缓冲区
    • flush:刷新缓冲区,不附加任何额外字符
    • ends:输出一个空字符,然后刷新缓冲区
  • 演示案例:
std::cout << "hi!" << endl;  //输出hi和一个换行,然后刷新缓冲区
std::cout << "hi!" << flush; //输出hi,然后刷新缓冲区,不附加任何额外字符
std::cout << "hi!" << ends;  //输出hi,和一个空字符,然后刷新缓冲区

unitbuf、nounitbuf操纵符

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

//任何输出都立即刷新,无缓冲

std::cout << nounitbuf; //回到正常的缓冲方式

关联输入和输出流

  • 当一个输入流被关联到一个输出流时,任何视图从输入流读取数据的操作都会被先刷新关联的输出流
  • 标准库将cout和cin关联在一起,因此下面语句会导致cout的缓冲区被刷新:
int ival;
std::cin >> ival; //会刷新前面的输出缓冲区

  • tie有两个重载版本:
    • 一个版本不带参数,返回指向输出流的指针。如果本对象当前关联到一个输出流,则返回的就是指向这个流的指针,如果对象未关联流,则返回空指针
    • 一个版本接受一个指向ostream的指针,将自己关联到此ostream。即,x.tie(&o)将流x关联到输出流o
  • 每个流同时最多关联到一个流,但多个流可以同时关联到同一个ostream
  • 例如我们既可以将一个istream对象关联到另一个ostream,也可以将一个ostream关联到另一个ostream:
cin.tie(&cout);  //仅仅是用来展示:标准库将cin和cout关联在一起

ostream *old_tie = cin.tie(nullptr); //cin不再与其他六关联

cin.tie(&cerr);   //读取cin会刷新cerr而不是cout
cin.tie(old_tie); //重建cin和cout间的正常关联

五、标准输入输出流(iostream)

  • C++语言并未定义任何输入输出语句(IO)语句,取而代之,包含了一个全面的标准库来提供IO机制(以及很多其他设施)

iostream库

  • iostream头文件包含两个基本类型:
    • istream类:输入流
    • ostream类:输出流
  • 一个流就是一个字符序列,是从IO设备读出或写入IO设备的

标准输入输出对象(cin、cout、cerr、clog)

  • cin:标准输入(为istream类型的对象)
  • cout:标准输出(为ostream类型的对象)
  • cerr:错误标准,用来输出警告和错误信息(为ostream类型的对象)
  • clog:用来输出程序运行时的一般性信息(为ostream类型的对象)

使用标准库中的名字

  • 我们在使用输入输出对象时,一般在对象前加上命名空间限定符std,标准库定义的所有名字都在命名空间std中
  • 例如:
std::cout << "HelloWolrd" << std::endl;

IO运算符

  • 向流中写入数据,使用输出运算符(<<)
  • 从流中读取数据,使用输入运算符(>>)

六、文件流(fstream)

fstream库

  • fstream头文件包含三个基本类型:
    • ifstream类:从一个给定文件读取数据
    • ofstream:向一个给定文件写入数据
    • fstream:读写给定文件
  • 这些类和上面介绍的cin和cout的操作一样,我们也可以使用IO运算符(<<和>>)来读写文件,可以用getline从一个ifstream读取数据,包括在文章前面的那些内容都适用于这些类型
  • 除了继承iostream类型的行为之外,fstream头文件中还定义了一些新的成员,定义如下:

使用文件流对象

  • 创建文件流对象时,可以提供文件名(可选的)。如果提供了一个文件名,则open会自动被调用。例如:
ifstream in(ifile); //构造一个ifstream并打开(open)给定文件。ifile为一个文件名

ofstream out;       //输出文件流,未关联到任何文件
  • 在C++新标准之前,文件名只允许C风格的字符数组;C++新标准之后,文件名既可以是string对象,也可以是C风格字符数组

用fstream代替iostream&

  • 在文章的最开始我们提到过,文件流对象与string流对象都继承于标准输入输出流,因此我们可以将一个文件流对象或string流对象赋值给一个标准输入输出流对象(但是必须根据继承关系进行对应转换)
  • 例如有一个自定义的Sales_data类,还有两个read()、print()函数
struct Sales_data {
    std::string isbn()const { return bookNo; }
    Sales_data& conbine(const Sales_data&);
    double avg_price()const;

    std::string bookNo;     //图书编号
    unsigned units_sold = 0;//销量
    double revenue = 0.0;   //收入
};

istream &read(istream &is, Sales_data &item) 
{
    double price = 0; //单价
    //从istream对象中读取数据,将读取的内容输入到item对象中
    is >> item.bookNo >> item.units_sold >> price;
    item.revenue = price*item.units_sold;//设定item对象的收入
    return is;
}

ostream &print(ostream &os, const Sales_data &item)
{
    //将item对象的内容输出对ostream对象中
    os << item.isbn() << " " << item.units_sold << " "
        << item.revenue << " " << item.avg_price();
    return os;
}
  • 对于read()、print()函数,虽然参数分别为istream和ostream,但是由于ifstream继承于istream、ofstream继承于ostream,所以我们可使用ifstream和ofstream传递给两者:
int main(int argc,char *argv[])
{
    ifstream input(argv[1]);  //打开销售记录文件
    ofstream output(argv[2]); //打开输出文件,保存结果

    Sales_data total; //保存销售总额的变量

    if (read(input, total)) {        //如果有销售记录
        Sales_data trans;            //保存下一条销售记录的变量
        while (read(input, trans)) { //读取剩余记录
            if (total.isbn() == trans.isbn()) //如果两者编号相同,合并两者
                total.combine(trans);         //合并并更新
            else {
                print(output, total) << endl; //如果不一样,就打印结果
                total = trans;                //处理下一本书
            }
        }
        print(output, total) << endl;
    }
    else {                              //没有销售记录
        cerr << "No data?!" << endl;
    }
    return 0;
}

成员函数open和close

  • 我们定义一个空文件流对象时,这个对象没有关联任何文件,所以没有open(打开)任何文件。当我们在初始化一个文件流对象时指定了文件名,那么默认打开这个文件
string ifile("file_test"); //文件名

ifstream in(ifile);        //创建一个ifstream对象并打开给定文件

ofstream out;              //创建一个ofstream对象,未与任何文件关联
out.open(ifile + ".copy"); //显式打开一个文件
  • 如果open成功,则open会设置流的状态,使得good()为true。如果open失败,那么流的failbit会被置位。所以我们在调用该函数时最后进行一个判断
string ifile("1.txt");
ofstream out;
out.open(ifile + ".copy");
if(out){
    //...
}
  • 当我们打开一个文件并操作完成之后,可以使用close来关闭文件,关闭完成之后还可以将这个流与其他文件进行关联
string ifile("1.txt");
ofstream out;
out.open(ifile + ".copy");
if(out){
    //...
}

out.close();           //关闭
out.open(ifile + "2"); //打开别的文件

自动构造和析构

  • 查看下面的一段代码,它的main函数接受一个要处理的文件列表,代码如下:
//遍历参数列表
for (auto p = argv + 1; p != argv + argc; ++p)
{
    ifstream input(*p);  //创建一个输出流
    if (input)           //如果创建成功
        process(input);  //调用自定义函数对input进行操作
    else
        cerr << "couldn't open: " + string(*p);
}
  • 在上面我们创建了一个局部变量input,那么:
    • 当每次进行for循环时,input都会被创建
    • 当一次for循环结束后,input局部变量就会被销毁(input所关联的文件自动被关闭)

文件模式

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

  • 使用文件模式有如下的限制:
    • 只可以对ofstream或fstream对象设定out模式
    • 只可以对ifstream或fstream对象设定in模式
    • 只有当out也被设定时才可以设定trunc模式
    • 只要trunc没被设定,就可以设定app模式。在app模式下,即使没有显式指定out模式,文件也总是以输出方式被打开
    • 默认情况下,即使我们没有指定trunc,以out模式打开的文件也会被截断。为了保留以out模式打开的文件的内容,我们必须同时指定app模式,这样只会将数据追加写到文件末尾;或者同时指定in模式,即打开文件同时进行读写操作
    • ate和binary模式可用于任何类型的文件流对象,且可以与其他任何文件模式组合使用
  • 默认打开模式:
    • ifstream:以in模式打开
    • ofstream:以out模式打开
    • fstream:以in和out模式打开

七、string流(sstream)

sstream库

  • sstream头文件包含三个基本类型:
    • istringstream类:从string读取数据
    • ostringstream类:向string写入数据
    • stringstream类:读写string
  • 上面这些类型都继承于iostream头文件中相对应的类型,因此这些类可以使用iostream类型的行为动作。sstream头文件中还定义了一些新的成员,定义如下:

演示案例:使用istringstream

  • 假设有一个文件,列出了一些人和它们的电话号码(号码可有多个)

  • 创建一个结构体,用来保存个人信息
struct PersonInfo {
    std::string name;           //姓名
    vector<std::string> phones; //电话号码
};
#include <iostream>
#include <string>
#include <sstream>
#include <vector>
using namespace std;

struct PersonInfo {
    std::string name;
    std::vector<std::string> phones;
};

int main()
{
    std::string line, word;              //line保存一行数据,word保存单个字符串
    std::vector<PersonInfo> people;      //用来保存所有人员的信息

    //读取一行数据,并将整行数据保存在line中
    while (std::getline(std::cin, line))
    {
        PersonInfo info;                 //保存单条记录
        std::istringstream record(line);      //将记录绑定到刚才输入的整行数据上

        record >> info.name;             //根据空格,保存整行数据中的第一个字符串(人名)
        while (record >> word)           //循环遍历,根据空格来进行分割,保存后面的号码
            info.phones.push_back(word); //保存号码

        people.push_back(info);
    }

    return 0;
}

演示案例:使用ostringstream

  • 下面的程序是对上面的演示案例进行读取操作
  • valid和format假设是两个自定义的函数,用来完成电话号码验证和改变格式的功能
int main()
{
    //遍历所有的人员信息,entry为const vector<PersonInfo>&类型
    for (const auto &entry : people)
    {
        std::ostringstream formatted, badNums;

        //遍历所有的电话号码,nums类型为const string&
        for (const auto &nums : entry.phones)
        {
            if (!valid(nums))       //如果号码格式无效,将错误的号码保存在badNums中
                badNums << " " << nums;
            else                    //如果号码格式有效,将号码保存格式化之后保存在formatted中
                formatted << " " << format(nums);
        }

        //如果没有错误的号码,打印
        if (badNums.str().empty())
            std::cout << entry.name << " " << formatted << " " << std::endl;
        //打印名字和错误的号码
        else
            std::cerr << "input error: " << entry.name << " invalid number(s) " << badNums.str() << std::endl;
    }
    return 0;
}

 

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐