C++:IO流

C++:IO流


在C语言中,有printfscanf进行基础的IO,也有fprintfsprintf等文件操作,字符流操作。C++作为面向对象语言,通过面向对象思想,将C语言的整个IO体系进行了重构,建立了一套IO体系。本博客将讲解C++中的IO类基本结构,部分重要的机制,以及文件,字符流的IO方法。

IO类框架

  • ios:定义了基本的IO框架,比如IO的错误标志,其中io_base是所有IO类的基类
  • iostream:包含了标准输入输出,最常用的头文件
  • fstream:用于文件输入输出
  • sstream:用于操作字符串,可以非常方便的进行字符串格式化与解析

IO流机制

在C++中,额外增加了许多IO的特殊机制,这些机制都在ios头文件中。

C/C++缓冲区同步

C++是在C语言基础上改进的语言,要兼容C语言的绝大部分内容,自然包括C语言本身的IO机制。对于标准输入而言,C++使用cincout,而C语言使用printfscanf。它们的功能相同,挑选一个使用即可,但是免不了会有用户两个版本交替使用,此时就可能带来同步问题。

因此C++中cout没有自己的缓冲区,而是与printf共用缓冲区,这样就算混用两者,也不会造成输出错误。

但是正因为兼容了printfcout就要去做很多额外的工作,那么cout的效率就会下降,这也就是为什么很多竞赛不使用coutcin这一套的原因。

为此,C++也提供了接口sync_with_stdio用于处理C/C++之间的同步问题。默认sync = true,也就是C/C++之间要进行同步。此时可以通过该函数手动调整为false,接触同步关系:

cpp 复制代码
sync_with_stdio(false);

此时C/C++就不兼容了,如果再混用coutprintf,就可能导致输出顺序错误。但是相应的,coutcin的效率会提高。


错误状态标志 iostate

每一个IO对象,内部都存储了字节的错误状态标志,其为四个比特位,分别是goodbiteofbitfailbitbadbit,四者含义如下:

iostate 描述 good eof fail bad
goodbit 没有错误 1 0 0 0
eopbit 文件读取结束 0 1 0 0
failbit 逻辑错误 0 0 1 0
badbit 读写错误 0 0 1 1

后面的比特位,表示对应错误发生时,每个位的状态。

比如说将一个char变量输入到int中,此时就会发生逻辑错误。如果想要查看对应的比特位,可以通过函数goodeoffailbad查看,比如:

cpp 复制代码
cout << "good = " << cin.good() << endl;
cout << "eof  = " << cin.eof() << endl;
cout << "fail = " << cin.fail() << endl;
cout << "bad  = " << cin.bad() << endl;

一般情况下输出结果为:

cpp 复制代码
good = 1
eof  = 0
fail = 0
bad  = 0

执行如下代码:

cpp 复制代码
int i;
cout << "请输入数字:";
cin >> i;

cout << "good = " << cin.good() << endl;
cout << "eof = " << cin.eof() << endl;
cout << "fail = " << cin.fail() << endl;
cout << "bad = " << cin.bad() << endl;

次数输入一个不是数字的内容,比如字符x

cpp 复制代码
请输入数字:s
good = 0
eof = 0
fail = 1
bad = 0

此时就出现了逻辑错误,也就是把一个char赋值给了int

一旦出现错误,这个cin对象就无法再使用了,必须通过clear来重置错误状态标志。

当IO对象内部的错误状态标志不正确,那么再次进行IO时就无法正常进行。clear用于将错误状态标志重置为goodbit = 1,其余位变为0,这样IO对象就可以再次使用了。

示例:

cpp 复制代码
int i;
cout << "请输入数字:";
cin >> i;

cout << "good = " << cin.good() << endl;
cout << "eof = " << cin.eof() << endl;
cout << "fail = " << cin.fail() << endl;
cout << "bad = " << cin.bad() << endl;

cin.clear();
cin.get();

cout << "请输入数字:";
cin >> i;
cout << "good = " << cin.good() << endl;
cout << "eof = " << cin.eof() << endl;
cout << "fail = " << cin.fail() << endl;
cout << "bad = " << cin.bad() << endl;

简单改动了刚才的代码,这一次第一次输入字母x,在clear之后再输入数字1

此处有一个小问题,我在clear之后紧跟了一个get,这是因为之前输入的字符x没有被读取走,发生了错误后,字符依然留在缓冲区中,如果clear后直接cin >>,那么cin还会读取到留在缓冲区中的x,导致一样的错误!所以要用get先把缓冲区中的字符读走,再让用户输入。

输出结果:

cpp 复制代码
请输入数字:x
good = 0
eof = 0
fail = 1
bad = 0
请输入数字:1
good = 1
eof = 0
fail = 0
bad = 0

第二次输入数字就可以正常读取了。


tie

tie是一种特殊的绑定机制,其将一个istream对象绑定到另一个ostream对象上。当istream收到数据时,会刷新对应的ostream的缓冲区。

这有啥用呢?默认情况下cin会绑定到cout上,而很多用户用完cin接收数据后,很有可能会用cout把处理好的数据输出出去。而cout的缓冲区中可能存有之前没有刷新的数据,为了保证这次输出的数据不受影响,在cin读取数据时,就把cout的缓冲区提前刷新一次。

也就是说,通过tid进行绑定的机制,其实就是将未来有可能要使用的缓冲区提前刷新,为后续数据预留空间的机制。就像一个家庭出生了一个婴儿,会提前空出地盘放一张婴儿床一样。

通过函数tie可以发现,其有两个重载,对于无参的tie,其返回该对象绑定到的ostream对象的指针;对于有参的tie,传入一个ostream*指针,将该对象重新绑定到其它ostream对象上,如果传入nullptr,那么相当于解绑,不绑定任何一个ostream对象。

比如可以这样:

cpp 复制代码
*cin.tie() << "hello world!" << endl;

无参的cin.tie()返回其绑定的对象指针,也就是默认的cout,在对指针解引用,就相当于cout <<。当然这多此一举了,此处不过想证明cin默认绑定到cout

解除cin的绑定:

cpp 复制代码
cin.tie(nullptr);

由于cin自动绑定到cout,那么每次cin读取数据都会刷新cout的缓冲区,这会影响到cin的效率,所以竞赛中会进行一个这样的解绑操作,让cin读取时不再刷新cout缓冲区,提高IO效率。


输入输出

标准流对象

C++的标准输入输出定义在头文件<iostream>中,其中包含istreamostream这两个类,分别用于输入输出。其中istream定义了基本的cin对象,用于读取stdin流,ostream定义了coutcerrclog对象,其中cout输出到stdout流,cerrclog输出到stderr流。

在效果上coutcerrclog没有区别,都输出到显示器。但是设计者的意图是用户在编码时在语义上将输出,错误,日志三者区分开,便于代码维护。

使用上,我们常用<<>>这样的操作符来操作标准流对象,这是基于操作符重载,以ostream重载<<为例:

ostream重载了<<,常见的内置类型都已经完成了重载,完成对应类型的输出。由于函数重载后会自动推导类型,所以不用像printf一样考虑变量的类型,再使用%s%d这样的占位符。

此处operator的返回值ostream&,是为了作用于连续输出:

cpp 复制代码
cout << "hello" << " world!";

这个代码本质是operator<<(operator<<(cout, "hello"), " world!"),其中operator<<(cout, "hello")返回了cout,所以输出完hello后代码变成operator<<(cout, " world!"),所以cout可以这样进行连续输出。

另外的,如果想要输出自定义类型,也可以自己重载对应的操作符:

cpp 复制代码
struct date
{
    int _year;
    int _month;
    int _day;

    date(int year, int month, int day)
        : _year(year)
        , _month(month)
        , _day(day)
    {}
};

ostream& operator<<(ostream& out, const date& d)
{
    cout << d._year << "-" << d._month << "-" << d._day;

    return out;
}

以上就是给date类重载了ostream的输出,注意函数最后要返回ostream对象的引用。

现在就可以输出date类了:

cpp 复制代码
date d(1949, 10, 1);
cout << d << endl;

输出结果:

cpp 复制代码
1949-10-1

常见输入策略

在写题时,常需要进行循环输入,一般来说会有以下常用格式:

  • 单变量循环输入:
cpp 复制代码
while (cin >> str)
{
	// ...
}

先前我说过,operator>>返回的是一个istream对象,所以也可以连续输入:

  • 多变量循环输入:
cpp 复制代码
while (cin >> str1 >> str2 >> str3)
{
	// ...
}

那么问题来了,while内部需要一个bool类型的值判断真假,此处不论输入几个变量都返回istream对象,为什么可以把这个输入语句直接放到while内部做判断条件?

其实ios类重载了operator bool,也就是到bool的隐式类型转化,因此istream可以隐式转化为bool类型,满足while的判断要求。


文件IO

在C++中,读写一个文件的基本套路如下:

  1. 定义一个文件流对象
  2. 打开指定文件
  3. 进行IO
  4. 关闭文件

而流文件对象有ifstreamofstream,分别作用于输入输出。需要头文件<fstream>

ifstream

ifstream的构造函数如上,最常用的是第二个初始化的构造:

cpp 复制代码
explicit ifstream (const char* filename, ios_base::openmode mode = ios_base::in);
  • filename:文件名
  • mode:打开模式

打开模式如下:

方式 功能
in 读取文件内容
out 输出内容到文件
binary 以二进制形式打开
ate 打开后,文件指针初始指向文件末尾
app 以追加的形式打开文件
trunc 打开时清空文件内容

此处要区分一下appate,两者打开文件后,文件指针都在文件末尾,那不都是追加吗?其实还是略有差别的。

  • ate:只是初始的时候文件指针在末尾,后续可以随意移动文件指针,自由读写。
  • app:每次进行写入操作,文件指针都会强制跳转到文件末尾,也就是说只能操作文件的末尾。

以上模式在ios_base类域中,可以混用,每个模式占一个位,传参时以按位或的形式传入:

cpp 复制代码
ifstream ifs ("test.cpp", ios_base::in | ios_base::binary);

以上就是定义了一个ifs对象,打开文件test.cpp,以读取+二进制模式。

由于istream重载了operator>>ifstream可以继承到,自然也可以通过>>读取文件。

示例:

cpp 复制代码
#include <iostream>
#include <fstream>
#include <windows.h>

using namespace std;

int main()
{
    ifstream ifs("test.cpp", ios_base::in);

    string buf;

    while (ifs >> buf)
    {
        cout << buf;
    }
    
	ifs.close();
	
    return 0;
}

这个代码,读取了test.cpp文件,其实就是这个代码本身,读取结果:

cpp 复制代码
#include<iostream>#include<fstream>#include<windows.h>usingnamespacestd;intmain(){ifstreamifs("test.cpp",ios_base::in);stringbuf;while(ifs>>buf){cout<<buf;}ifs.close();return0;}

奇怪的事情发生了,为什么所有内容都被压缩为了一行?

重载operator>>在实现时,会自动忽略空格与换行。对于cincout来说,这可以很方便的读取到变量,而对于文件来说,就不怎么适合了,所以一般不使用这个重载读取文件。

get

为了控制文件读取,一般会使用istream::get方法:

这个方法定义在istream中,ifstream继承到该方法,所以可以用于读取文件,可以看到其可以以字符为单位读取,也可以指定要读取的字符数量。

示例:

cpp 复制代码
#include <iostream>
#include <fstream>
#include <windows.h>

using namespace std;

int main()
{
    ifstream ifs("test.cpp", ios_base::in);

    char ch;

    while (ifs.get(ch))
    {
        cout << ch;
    }
    
	ifs.close();

    return 0;
}

此时按照字符读取内容,空格与换行也会被读取,最后输出的内容也就是格式的内容了。

read

对于二进制文件来说,一般会使用istream::read读取:

当然也可以用read读取字符文件,用get读取二进制文件,只是习惯问题。


ofstream

在打开方式上,ofstreamifstream差不多,都是传入一个文件名以及一个打开模式。此处两者一致,就不再谈了。

ofstream也继承了operator<<,一般对于文件写入,直接用<<即可。

示例:

cpp 复制代码
ofstream ofs("test.txt", ios_base::out);

ofs << "hello world!" << endl;
ofs << "------------" << endl;
ofs << "hello   C++!" << endl;

ofs.close();

直接当作cout用即可。

write

对于二进制文件写入,则使用write多:


字符流转换

在字符串处理方面,C++ 提供了两个非常有用的流类:istringstreamostringstream。这两个类分别用于字符串到数据的解析,以及将数据转换为字符串。需要头文件<sstream>

istringstream

istringstream 是 C++ 标准库中的一个类,属于 std::istringstream。它继承自 std::istream,可以将一个字符串视为输入流,从中提取各种数据类型。

在构造istringstream对象时,可以直接传入要解析的字符串作为输入流。

示例:

cpp 复制代码
string input = "123 45.67 Hello";
istringstream iss(input);

int integer;
double floating;
string word;

// 从字符串中提取数据
iss >> integer >> floating >> word;

在这段代码中,首先创建了一个 std::istringstream 对象 iss,并将 input 作为输入流。通过重载的 >> 操作符,我们可以依次从流中提取整数、浮点数和字符串,并将它们存储到相应的变量中。

我之前讲解过operator>>会自动忽略空格换行符,所以此处可以按照要求把数据一个个读取出来,并且复制到对应的变量中。这种使用方式非常简洁,适合从格式化的字符串中提取数据。


ostringstream

ostringstream 是 C++ 标准库中的另一个重要流类,属于 std::ostringstream。它继承自 std::ostream,主要用于将各种数据类型格式化为字符串。

格式化好的字符串存在ostringstream对象的缓冲区中,如果想要得到格式化后的字符串,则通过str函数。

示例:

cpp 复制代码
int integer = 123;
double floating = 45.67;
string word = "Hello";

ostringstream oss;
oss << "Integer: " << integer << ", Double: " << floating << ", String: " << word;

string output = oss.str();
cout << output << endl;

输出结果:

cpp 复制代码
Integer: 123, Double: 45.67, String: Hello

可以看到,ostringstream可以很方便的生成格式化字符串。


相关推荐
hacker70742 分钟前
探索数据的艺术:R语言与Origin的完美结合
开发语言·r语言·origin
炸鸡配泡面1 小时前
Qt 12.28 day3
java·开发语言
get_money_1 小时前
代码随想录38 322. 零钱兑换,279.完全平方数,本周小结动态规划,139.单词拆分,动态规划:关于多重背包,你该了解这些!背包问题总结篇。
java·开发语言·笔记·算法·动态规划
mahuifa3 小时前
C++(Qt)软件调试---VS性能探查器(27)
c++·qt·内存泄漏·vs性能探查器·cpu性能分析
不听话的好孩子3 小时前
基于深度学习(HyperLPR3框架)的中文车牌识别系统-Qt开发UI
开发语言·qt·ui
天马行空_3 小时前
周记-唐纳德的《计算机程序设计艺术》
c++
SomeB1oody4 小时前
【Rust自学】7.6. 将模块拆分为不同文件
开发语言·后端·rust
向宇it4 小时前
【从零开始入门unity游戏开发之——C#篇36】C#的out协变和in逆变如何解决泛型委托的类型转换问题
java·开发语言·unity·c#·游戏引擎
冀晓武5 小时前
C++ 设计模式:观察者模式(Observer Pattern)
c++·观察者模式·设计模式
捕鲸叉5 小时前
C++设计模式之行为型模式概述,它们的目的与特点
c++·设计模式