C++修炼:IO流

Hello大家好!很高兴我们又见面啦!给生活添点passion,开始今天的编程之路!

我的博客: <但凡.

我的专栏: 《编程之路》《数据结构与算法之美》《C++修炼之路》《Linux修炼:终端之内 洞悉真理》

感谢你打开这篇博客!希望这篇博客能为你带来帮助,也欢迎一起交流探讨,共同成长。

目录

1、C++中的IO库

2、IO流状态

3、管理输出缓冲区

4、标准IO流

5、文件IO流

[6、string IO流](#6、string IO流)


1、C++中的IO库

C++不仅支持从控制台读取数据,还支持从设备中读取数据和向设备中写入数据的IO操作,设备可以是文件、控制台窗口等。

**C++中的IO库是通过一个庞大的继承体系来实现的。**这个套体系包含了标准IO流,文件IO流,stringIO流等等。也就是说,我们之前使用的都是标准IO流,从控制台读取字符。但实际上还支持从文件读取,对string字符串,对宽字符进行IO操作。

2、IO流状态

在IO的过程中可能会出现各种错误,IO流对象中给了四种状态标识错误。

默认都是good状态,eofbit表示读到了文件末尾,badbit表示系统级错误,如不可恢复的读写错误, 通常情况下,badbit⼀旦被设置了,流就无法再使用了**。failbit表示⼀个逻辑错误**,如期望读取⼀个整形,但是却读取到⼀个字符,failbit被设置了,流是可以恢复的,恢复以后可以继续使用。

我们在cin变量的时候,如果我们的变量是int类型,但是我们输入的是一个字符,那么这时候就会标识为failbit:

cpp 复制代码
#include<iostream>
using namespace std;

int main()
{
	int i = 0;
	cin >> i;//输入一个字符

	cout << cin.good() << endl;
	cout << cin.eof() << endl;
	cout << cin.fail() << endl;
	cout << cin.bad() << endl;
	return 0;
}

输出结果:

那么这个时候,我们就得想办法恢复状态:

cpp 复制代码
#include <iostream>
using namespace std;

int main() {
    int i = 0;
    cin >> i; // 输入一个字符(比如 'x1')

    cout << cin.good() << endl;  // 0(false)
    cout << cin.eof() << endl;   // 0(false)
    cout << cin.fail() << endl;  // 1(true)
    cout << cin.bad() << endl;   // 0(false)

    if (cin.fail()) {
        cin.clear(); // 必须先清除错误状态,否则后续操作无效
        // 清除所有非数字字符(包括换行符)
        char ch;
        while (cin.get(ch) && !(ch >= '0' && ch <= '9')) {
            cout << ch;
        }
        // 如果读取到数字,放回缓冲区供下一次 cin >> i 读取
        if (ch >= '0' && ch <= '9') {
            cin.putback(ch);//重新放回到输入流中
        }
        cout << endl;
    }
    //if (cin.fail())
//{
//    // clear可以恢复流状态位goodbitcin.clear();
//    // 我们还要把缓冲区中的多个字符都读出来,读到数字停下来,否则再去cin >> i还是会失败

//    char ch = cin.peek();
//    while (!(ch >= '0' && ch <= '9'))
//    {
//        ch = cin.get();
//        cout << ch;
//        ch = cin.peek();
//    }
//    cout << endl;
//}
    cout << cin.good() << endl;  // 1(true)
    cout << cin.eof() << endl;   // 0(false)
    cout << cin.bad() << endl;   // 0(false)
    cout << cin.fail() << endl;  // 0(false)

    cin >> i; // 现在可以正常读取数字
    cout << i << endl;

    return 0;
}

输出结果:

3、管理输出缓冲区

在输出的时候,我们的结果不是直接输出到控制台的,而是先输出到内存中的缓冲区。也就是说,操作系统可能会把多个输出操作合并成一个输出操作。不论是Windows系统还是Linux系统都是这样,我们可以验证一下:

cpp 复制代码
#include <iostream>
#include<windows.h>
using namespace std;

int main() {
    cout << "test1" << endl;
    cout << "test2" << endl;
    cout << "test3" << endl;
    cout << "test4" << endl;

    Sleep(5);//系统休眠五秒
    return 0;
}

我们执行这串代码,发现,在执行之后,控制台窗口光标闪烁大约五秒,之后一次性输出了所有的字符串。那么在这五秒中,这些字符串就在缓冲区中存放着。

那么为什么会这样设计呢?因为设备的IO操作通常是很耗时的,允许操作系统将多个输出操作组合为单一的设备写操作会带来很大的性能提升。

每次触发缓冲区刷新,我们缓冲区的内容就会输出到控制台或者文件中。 那么什么情况下会触发缓冲区刷新 呢?第一,程序正常结束;第二缓冲区满了;第三,当输出操作符endl或者flush会立即刷新缓冲区;第四,我们使用了操作符unitbuf设置流的内部状态没来清空缓冲区, cerr设置了unitbuf,所以cerr输出都是立即刷新的;第五,一个输出流关联到另一个流时,当这个流被读写时,输出流会立即刷新缓冲区。

我们详细说一下第五条。第五条是通过tie来实现的 ,比如,默认情况下cin和cout是绑定在一起的所以说当我们cin时,会刷新cout的缓冲区。我们也可以手动绑定cin和其他的东西。在竞赛的时候,我们有时会关闭C++和C在每次输入输出后同步,解绑cin和cout关联绑定的其他流。这些操作本质上都是为了提升效率。

cpp 复制代码
ios_base::sync_with_stdio(false);//关闭C流与C++流同步
//解绑cin和cout关联绑定的其他流
cin.tie(nullptr);
cout.tie(nullptr);

使用案例:

cpp 复制代码
#include<iostream>
#include<fstream>
using namespace std;
void func(ostream& os)
{
	os << "hello world";
	os << "hello C++";

	system("pause");
	//os << endl;
	//os << flush;
	//int i;
	//cin >> i;
	os << "hello cat";
	// "hello cat"是否输出不确定

	system("pause");
}
int main()
{
	ofstream ofs("test.txt");
	//func(cout);
	// unitbuf设置后,ofs每次写都直接刷新

	 //ofs << unitbuf;
	// cin绑定到ofs,cin进⾏读时,会刷新ofs的缓冲区

	// cin.tie(&ofs);
	func(ofs);
	return 0;
}

大家可以测试一下以上代码。

另外再说一下endl,endl其实是一个函数,endl相比\n的区别就是endl除了换行,还会强制刷新缓冲区。

4、标准IO流

这部分就比较熟悉了。C++标准IO流是默认关联到控制台窗口的。cin是istream类型全局对象,cout/cerr/clog是ostream类型的全局对象。内置类型都对这两个类进行了重载,自定义类型粗腰自己重载<<和>>运算符。

istream的cin对象支持转换为bool值,进行条件逻辑判断。如果读取失败(cin被标识eofbit或falibit)就会返回false。

cpp 复制代码
int a = 0, b = 0;
while (cin >> a >> b)//如果读取失败会返回false,跳出循环
{
	cout << a << " " << b;
}

5、文件IO流

文件IO流包含ofstream,ifstream,fstream,其中ofstream是输出文件流,我们可以理解为,现在的输出不是输出到控制台上了,而是输出到文件中。ifstream是输入文件流,可以从文件中读入数据。fstream是ifstream和ofstream的派生类,即可以读也可以写。

文件IO流提供了非常丰富的文件打开方式,这些文件打开方式通过 | 可以组合起来,搭配出来更多种方式。搭配的方式是比C语言的文件操作更丰富的。

文件打开方式是通过 std::ios_base 标志位组合实现的,常用的标志位包括:

  • std::ios_base::in:以读取方式打开文件。
  • std::ios_base::out:以写入方式打开文件。
  • std::ios_base::app:以追加模式打开文件,写入内容追加到文件末尾(总是在文件尾)。
  • std::ios_base::ate:打开文件后立即定位到文件末尾。
  • std::ios_base::trunc:如果文件已存在,清空文件内容。
  • std::ios_base::binary:以二进制模式打开文件。

这些值都是ios_base中定义的成员变量继承下来的,并且他们也是组合的独立二进制位值,需要组合时,可以或到一起。以下是他们可以搭配出的各种方式和C语言打开文件方式的对应。

对于图片这种非文本文件,我们需要使用二进制打开方式,暴力的把所有的内容都读取出来。

接下来我们通过代码分别看几个文件打开方式。

cpp 复制代码
#include<iostream>
#include<fstream>
using namespace std;
int main()
{
	ofstream ofs("test.txt");

	ofs.put('x');
	ofs.write("hello\nworld", 11);//一般使用\n

	ofs << "2222";

	int x = 3333;
	ofs << x;

	ofs.close();

	ofs.open("test.txt", ios_base::out);//写入文件(覆盖)
	//或ofs.open("test.txt", ios_base::trunc);
	ofs.close();


	ofs.open("test.txt", ios_base::out | ios_base::app);//写入文件(追加)
	ofs << "555" << endl;
    //需要注意,如果用out和ate其实会覆盖
	ofs.open("test.txt", ios_base::in | ios_base::out);//读写文件(不覆盖)
	ofs << "666" << endl;
	ofs.close();

	ofs.open("test.txt", ios_base::in | ios_base::out | ios_base::trunc);//读写文件(覆盖)
	ofs << "777" << endl;
	ofs.close();

	std::fstream file("example.bin", std::ios::in | std::ios::out | std::ios::binary);//二进制模式打开文件


	return 0;
}

此外,我们用C++的文件操作实现图片的复制操作。对于图片来说,我们只能使用二进制,暴力的复制。注意在二进制读写时我们不能读写string。因为这时候我们读的实际上是一个指向堆空间的指针,当string对象析构后,这块空间就被释放了。

cpp 复制代码
#include<iostream>
#include<fstream>
using namespace std;
int main()
{
    // 实现⼀个图⽚⽂件的复制,需要⽤⼆进制⽅式打开读写,第⼀个参数可以给⽂件的绝对路径

    ifstream ifs("C:\\Users\\nobody\\Desktop\\test.png",ios_base::in | ios_base::binary);
    ofstream ofs("C:\\Users\\nobody\\Desktop\\test-copy.png",ios_base::out | ios_base::binary);
    int n = 0;
    while (ifs && ofs)
    {
        char ch = ifs.get();
        ofs << ch;
        ++n;
    }
    cout << n << endl;
    return 0;
}

需要注意,使用文件IO流时,ifstream和cin的规则类似,都提取到空格或者换行就终止。所以说我们要注意,如果文件中的文本是连成一片的,此时我们使用ifstream提取一个string类型的对象,实际上是把这一片内容去哪都提取走了。

6、string IO流

ostringstream是string的写入流,ostringstream是ostream的派生类。istringstream是string的读出流,istringstream是istream的派生类。stringstream是ostringstream和istringstream的派生类,即可以读也可以写。

我们来看一下案例了解一下:

cpp 复制代码
#include <sstream>
#include <iostream>
#include <iomanip>
int main() {
    std::ostringstream oss;
    oss << "Hello, " << 42 << " world!";
    std::string result = oss.str();
    std::cout << result << std::endl;

    std::ostringstream oss1;
    oss1 << std::setprecision(3) << 3.14159;//设置精度,包含在头文件iomanip
    std::string result1 = oss1.str();
    std::cout << result1 << std::endl;

    return 0;
}

istringstream:

cpp 复制代码
#include <iostream>
#include <sstream>
#include <string>

int main() {
    std::string data = "42 3.14 hello";
    std::istringstream iss(data);

    int num;
    double pi;
    std::string str;

    iss >> num >> pi >> str;
    std::cout << num << ", " << pi << ", " << str << std::endl;

}

stringstream:

stringstream系列底层维护了⼀个string类型的对象用来保存结果,使用方法跟上面的文件流类似,只是数据读写交互的都是底层的string对象。string流使用str函数获取底层的string对象,或者写入底层的string对象,具体细节参考下面代码理解

cpp 复制代码
#include <iostream>
#include <sstream>
#include <string>
#include<vector>

int main() {
    std::stringstream ss;
    ss << "Hello, " << 123 << " world!";
    std::string result = ss.str();
    std::cout << result << std::endl; // 输出:Hello, 123 world!


    std::string input = "apple orange banana";
    std::stringstream ss1(input);
    std::string token;
    std::vector<std::string> tokens;

    while (ss1 >> token) {
        tokens.push_back(token);
    }

    for (const auto& t : tokens) {
        std::cout << t << std::endl;
    }


    std::stringstream ss2;
    ss2 << "First use";
    std::cout << ss2.str() << std::endl;

    ss2.clear();
    ss2.str("");
    ss2 << "Second use";
    std::cout << ss2.str() << std::endl;
    return 0;
}

好了,今天的内容就分享到这,我们下期再见!

相关推荐
小徐不徐说1 小时前
超详细讲解:TCP / UDP / HTTP / HTTPS 四种常见协议
c++·网络协议·tcp/ip·http·https·udp·网络编程
laoliu19961 小时前
GGE Lua 详细教程
开发语言·junit·lua
勇闯逆流河1 小时前
【C++】list及其模拟实现
开发语言·c++
小指纹2 小时前
巧用Bitset!优化dp
数据结构·c++·算法·代理模式·dp·bitset
liulilittle2 小时前
游戏加速器核心技术:动态超发
开发语言·网络·c++·网络协议·游戏·加速器·游戏加速
Humbunklung3 小时前
Rust 模块系统:控制作用域与私有性
开发语言·后端·rust
小堃学编程3 小时前
QT跨平台应用程序开发框架(9)—— 容器类控件
开发语言·qt
mit6.8243 小时前
[AI-video] 数据模型与架构 | LLM集成
开发语言·人工智能·python·微服务
hqxstudying3 小时前
Java行为型模式---策略模式
java·开发语言·建造者模式·适配器模式·策略模式
蓝婷儿3 小时前
Python 数据建模与分析项目实战预备 Day 4 - EDA(探索性数据分析)与可视化
开发语言·python·数据分析