【C++】IO流

在这里插入图片描述
本篇博客由 CSDN@先搞面包再谈爱 原创,转载请标注清楚,请勿抄袭。

前言

C语言的输入与输出

C语言中我们用到的最频繁的输入输出方式就是scanf ()与printf()。

scanf(): 从标准输入设备(键盘)读取数据,并将值存放在变量中。
printf(): 将指定的文字/字符串输出到标准输出设备(屏幕)。
注意宽度输出和精度输出控制。

C语言借助了相应的缓冲区来进行输入与输出(各位要是对缓冲区不了解的可以看我这篇博客:【Linux】基础文件IO、动静态库的制作和使用,以语言层面来讲缓冲区的话肯定是讲不清楚的,这篇不仅讲了语言级别的缓冲区,还讲了操作系统级别的缓冲区,更加的深入):
在这里插入图片描述

对输入输出缓冲区的理解:

  1. 可以屏蔽掉低级I/O的实现,低级I/O的实现依赖操作系统本身内核的实现,所以如果能够屏蔽这部分的差异,可以很容易写出可移植的程序。
  2. 可以使用这部分的内容实现“行”读取的行为,对于计算机而言是没有“行”这个概念,有了这部分,就可以定义“行”的概念,然后解析缓冲区的内容,返回一个“行”。

正式开始

C++的IO流

C++系统实现了一个庞大的类库,其中ios为基类,其他类都是直接或间接派生自ios类。
在这里插入图片描述

这张图在cplusplus官网的reference中就有,这里把链接给出来:cplusplus — Reference

上面的继承关系简单说一下,我们平时用的cin、cout其实就是类对象,这在我最开始讲C++的博客中也说过,而这两个对象的所对应的类就是istream和ostream。

istream和ostream都继承自ios类,ios类又继承自ios_base。这两个最顶上的就不讲了。

我们平时引的头文件iostream,继承了istream和ostream这两个类,没错,这里产生菱形继承了,这里也是库中很少见的出现菱形继承的地方之一。但是也问题不大,我们平时用起来还是正常的。

像C语言中,想要进行流插入和流提取的话,用printf和scanf会比较麻烦:
在这里插入图片描述

而C++中打印某个变量的时候,不需要再加什么%d啥的了:
在这里插入图片描述

cout、cin自动识别类型源自于对>>和<<的重载:
在这里插入图片描述
>>就不展示了。

而且C++ istream 和 ostream 这套流,可以更好的支持自定义类型对象的流插入和流提取,自定义类型,可以自己重载。

这里就拿前面博客中写的Date类,来看看:

class Date
{
	friend ostream& operator << (ostream& out, const Date& d);
	friend istream& operator >> (istream& in, Date& d);
public:
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{}


private:
	int _year;
	int _month;
	int _day;
};

istream& operator >> (istream& in, Date& d)
{
	in >> d._year >> d._month >> d._day;
	return in;
}

ostream& operator << (ostream& out, const Date& d)
{
	out << d._year << " " << d._month << " " << d._day;
	return out;
}

里面的 << 和 >> 重载,对于初学者来说,看起来非常的懵逼,无法理解,就当是背书那样死记下来了。这里我再截出来看看:

在这里插入图片描述

istream和ostream,就是两个类。但对于当时初学的我来说,完全看不懂。

但是还是会用的:
在这里插入图片描述
这里没有带上那个0是因为按照整数打的。

像Date这样自定义类型的类,能够重载 << 和 >> 运算符。这样就能直接通过统一的方式打印出来想要的东西。

输入分割标志

看段代码:
在这里插入图片描述
上面这是用C语言写的,各位能看出来有啥毛病吗?

其实scanf的%d中间不需要加空格,可以直接连着的:
在这里插入图片描述

我前面的博客中也说过:C中的scanf和C++中的cin,输入多个值的时候,默认都是用空格或者换行来分割的。不需要我们去手动添加空格。

但如果是20230829这样连着的呢?

C语言中可以这样:
在这里插入图片描述

也可以直接用字符串或者整数接收,然后再将接收后的字符串或整数分开处理就行。

但是C++中并没有C语言中的第一种做法,只能是整个接受,再处理,不过C++中string用起来挺方便的:
在这里插入图片描述
substr就是取子串,取第一个参数位置的第二个参数个字符,这个接口我在我string的博客中也讲了。其他就不细说了。

多行测试用例

如果刷题比较多的同学可能对上面这句话很敏感。因为如果没有加while(cin >> ***)的可能会报错。

先不说为啥,先说如果我们写出来这样的代码怎样终止掉:
在这里插入图片描述

真别说,可能有的同学真不会。

两种方式,一种是ctrl + z再按下回车就行了:
在这里插入图片描述
这里的流结束,就类似于内存中读到文件末尾了。正常结束。

还有一种方式是ctrl + c。
在这里插入图片描述
这个方法是直接让进程结束,相当于kill -9命令。

C语言中也是,while( scanf(…) != EOF)。
不过vs下,需要按三次 ctrl + z + 回车。ctrl + c一次就行。

然后再来说C++中为什么这样搞。
while(cin >> str),本质上是去调用str中重载的>>,也就是operator>>(cin, str),函数的返回值类型是istream&,其实就相当于是cin。但是cin如何做了这里的判断条件呢?一般做判断的都是数、bool、指针、比较运算符什么的,但是这里cin,其实不是cin,因为发生了隐式类型转换。
C++98时,转换成了void*:
在这里插入图片描述

C++11中转换成了bool:
在这里插入图片描述

这里大家把这两个重载的函数当成特例来看,当cin读到流末尾或者读到错误的时候就会这个函数就会返回false,一般也不会读到错误,读到末尾就相当于上面的ctrl + z。

也就四说,cin >> str去调用operator>>(cin, str),返回值隐式类型转换后又调用了operator bool(),这样就完成了判断。

所以说,上面的cin >> str就可以作为判断结束的标志。

可能有老铁听得比较懵,下面我就写一个类来用一下这个operator类型():

class A
{
public:
	A(int a)
		:_a(a)
	{}

private:
	int _a;
};

对于普通的内置类型转自定义类型发生的隐式类型转换:
在这里插入图片描述

那么能不能自定义类型转内置类型int呢?
可以的,加个operator int就行:
在这里插入图片描述

这里返回什么取决于你自己,你返回个10都行。

测试:
在这里插入图片描述
没问题。

如果构造函数加上explicit,内置类型转自定义类型隐式转换就不行了:
在这里插入图片描述

这里operator int加上explicit也是:
在这里插入图片描述

但是想要转的话也可以:
在这里插入图片描述

然后再来看一下Date类,加一个operator bool():

operator bool()
{
	// 这里是随意写的,假设输入_year为0,则结束
	if (_year == 0)
		return false;
	else
		return true;
}

测试一下:
在这里插入图片描述

文件读写

简单演示

其实部分功能就和C中的差不多。
在这里插入图片描述

就讲一下红框框起来的。

ifstream

前面cin的类型是istream,从标准输入流中读,也就是从键盘上读。

这里ifstream,中间加了个f,就是从文件中读。

在这里插入图片描述

来个例子:

int main()
{
	ifstream ifs("Test.cpp");
	char ch = ifs.get();
	while (ifs)
	{
		cout << ch;
		ch = ifs.get();
	}

	return 0;
}

当前路径下就一个Test.cpp文件,也就是正在写代码的这个,这运行起来就是将整个文件的内容打印到显示器上:
在这里插入图片描述

简单解释一下:

ifstream ifs("Test.cpp");

在这里插入图片描述
这里构造函数就相当于是打开文件,然后默认情况下打开方式就是in,也就是输入,也就是读:

在这里插入图片描述

可以看到,还有另外的五中,out、binary、ate和trunc,这里就不细讲了。

我之前讲Linux库中的write中讲过若对某一文件要有多重打开方式,直接用 | 就可以刻,每一种方式用 一个数来代表,不懂得同学可以看看我这篇博客:点击目录【open的第二个参数flags】

关于app和ate可以看看这篇:fsteam in out ate app理解

get是一个获得字符的函数:
在这里插入图片描述
while(ifs),ifstream中有opeartor bool(),这里while每次都会判断一下是否到达文件末尾。是从ios中继承下来的。

可以看到,其实上面的用法就和C中的差不多,那有没有不一样的用法呢?

肯定是有的。

C++中可以直接用 >> 和 << 来进行读写。ifstream继承的函数中就有oeprator >> 。所以我们还可以这样用:
在这里插入图片描述

自定义类型也可:
在这里插入图片描述

这里调用 operator >>,是ifs.operator>>(i)(以i为例),传过去的是ifs的指针,但是调用的是istream类中的 >>,我们点cplusplus网站中ifstream的>>会跳到istream的>>:
在这里插入图片描述
那么传过去的是ifs的指针,用的是istream的指针接收,这样就会发生切片,所以也是支持的。

同样的。字符流istringstream中的>>也是如此。

所以说C++能够更好的支持自定义类型的终端、文件、字符流的读写。

正式讲解

如果对一个文件进行读写,那么可以分为两种方式,一种为二进制读写,一种为文本读写。

二进制读写就是数据在内存中是如何存储的就如何在磁盘中读写。但是文件中都是按照字符流形式读写的,按照二进制写入磁盘,比如一个int是10的话,4个字节,都写到磁盘中,按照反码形式存储的四个字节,按照一个字节一个字节的方式去读,也就是按照字符流的形式去读,我们正常人是看不懂的,因为每个字节内容按照编码翻译出来的东西正常情况下都比较怪。

文本读写就是将对象数据序列化(序列化就是值无论什么类型都转成字符串拼到一块),还是一个值为10的int,序列化之后就是一个字符串 “10”,然后将这个字符串存储到文件中,这样我们打开文件就直接是我们想要看到的正常情况下的10了,如果再读回来还需要反序列化,就是将文件中的字符串转换成对应的对象数据。

这里就以值为10的int来说。

二进制读写
优点:读写速度快,不需要转换字符串,也就是序列化和反序列化。
缺点:写到文件中的内容没法看,看不懂。

文本读写
优点:写到文件中的内容可以直接看,只要认识字就能看懂。
缺点:存在一个转换过程,慢了一点。

这里按照二进制写的话,10要写4个字节,而按照文本读写就2个字节。但也不能这么比,如果是10000二进制就还是4个字节,文本就要5个。

那么下面写两个类,就专门演示这里的二进制读写和文本读写。

struct ServerInfo
{
	char _address[32]; // 地址
	int _port; // 端口号

	Date _date; // 日期
}; 

上面这个用来表示读写内容。

下面这个用来进行读写操作,这里面只写一个基本的类:

struct ConfigManager
{
public:
	// 这里默认就用的是server.config这个文件
	ConfigManager(const string& filename = "server.config")
		:_filename(filename)
	{}
	
	// 。。。 对文件的读写操作

private:
	string _filename; // 配置文件
};

然后就来分二进制和文本来读写。

二进制读写

先来说写,写好了之后再读。

写文件,要先打开,就是用ofstream的构造函数:
在这里插入图片描述

第二个参数 ios_base::openmode就是打开文件的方式,这里默认给的是out:
在这里插入图片描述

就指的是往文件中写。

然后就往里写,就是直接将内存中的直接放到文件中,这里要用到ofstream中的write函数。但其实也是从ostream中继承下来的,看:
在这里插入图片描述

然后就可以在类中写一个以二进制写的方式的接口:
在这里插入图片描述

测试:
在这里插入图片描述
这里Date用的缺省参数构造。就是1 1 1。

得到的文件:
在这里插入图片描述
可以看到,后面的888,还有那个Date,是看不到的。所以前面说二进制读写正常人看不懂。

然后再来说读。
同样的,读文件, 要先打开文件,也是构造函数打开,还是前面的那个表格:
在这里插入图片描述
那个in、out、binary的表格就不给了。

然后要用到read:
在这里插入图片描述

测试:
在这里插入图片描述

二进制这里的读写操作和C中的差不多,就不多说了。

但是这里有一点问题。

注意到,我第一个类中用的是char数组,但是C++之后就不提倡用这个数组了,直接用string,改一下:
在这里插入图片描述

但是如果我同样的代码写一下:
在这里插入图片描述

可以看到,和前面用数组的写入有点不一样,多了一点东西,这是因为string类中还有一些杂七杂八的东西,像size,capacity什么的。

但是如果我这里读一下,出问题:
在这里插入图片描述
为啥呢?

如果我再把string对象中的内容搞长一点:
在这里插入图片描述

再读:
在这里插入图片描述
直接崩掉了。

这是因为vs实现的string中底层机制不太一样,前面我的那篇string博客中也说过。vs的string中有一个大小为16个字节的_buff数组,如果存放的字符串长度在0~15,那么就直接存到_buff中,并不会去堆上开空间,所以前面第一种方式是直接放在栈上的数组,以二进制写的时候,整个数组_buff是可以写进文件中去的;第二种方式是在堆中开空间,所以用的是string中的指针_str,堆上开空间,_str存放的只是一个地址,那么写到文件中的也只是个地址,当第二次取的时候取到的是一个地址,而这个地址是无法访问的,因为第一次写入的时候已经释放了,所以这里就发生了经典的野指针问题。

所以说,二进制读写文件的时候,不适用于能进行深拷贝的类,像string、vector什么的,写入的地址可以说是无效的,一读就野指针了。

所以读写一些发生了一些深拷贝的内容就需要换一种方式来搞了。
向文件内写入时,先将内容大小写入文件,然后再将数据写入。
读取时,先读取大小,然后再取数据。

string对象二进制文件读取可以看一下这篇:[C/C++笔记] fstream以二进制模式读写字符串类型string

文本读写

文本读写我们可以像C中的那一套一样,但是也可用C++中比较便捷的方式,但是先演示一下类似C中的做法。

类似C中的做法

以文本方式写,首先就是要将数据全部序列化。

那么这里就暂时把成员_date去掉,不然搞起来有点麻烦。

将所有的数据序列化之后,还是用write接口,把所有序列化之后的数据,也就是字符串,写到文件中。
在这里插入图片描述
测试:
在这里插入图片描述
这样文本写的就能看懂了。

读的话,这里用的一行一行读:
在这里插入图片描述

测试:
在这里插入图片描述

再来个长的:
在这里插入图片描述
在这里插入图片描述

但是这里这样的读写操作就和C中的差不多。

更便捷的做法

fstream中继承了 << 和 >> 的,可以直接用。

这里把_date加上。
在这里插入图片描述
这就是直接用 >> 和 << 进行读写,更加方便。注意这里写入字符流的时候,需要在每个字符串的后面加上换行或空格,不然会导致读取时的错误。因为读取的时候就是按照空格和换行来分割的,没有分割,就认不出来谁是谁的数据,只要是挤到一块的就变成了一个数据。

先来个正常的

写:
在这里插入图片描述
读:
在这里插入图片描述
对于自定义类型用起来要方便的多,只要重载 >> 和 << 就好。

再来个写入时候去掉换行的

写:在这里插入图片描述
读:
在这里插入图片描述
这样读出来的就是乱的。所以说记得加换行或空格。

字符流读写

在C语言中,如果想要将一个整形变量的数据转化为字符串格式,如何去做?

  1. 使用itoa()函数
  2. 使用sprintf()函数
    但是两个函数在转化时,都得需要先给出保存结果的空间,那空间要给多大呢,就不太好界定,
    而且转化格式不匹配时,可能还会得到错误的结果甚至程序崩溃

C++中提供了istringstream、ostringstream、stringstream来避开C中的问题。

直接把string当做你要读写的文件,ostringstream就是往字符串中写,istringstream就是从字符串中读。stringstream就是包含了二者的功能。

简单讲一点。也是序列化和反序列化。

假如说现在搞一个发送信息到接收信息的过程。

一个类,该类能够保存发送信息的一些信息,比如说,发送人姓名、其id、发送时间、发送的信息:

struct ChatInfo
{
	string _name; // 名字
	int _id;      // id
	Date _date;   // 时间
	string _msg;  // 聊天信息
};

我们想把这些信息传输过去,直接以字符流的方式传输。

首先就是要把所有的数据序列化拼到一块。这里就直接用stringstream来搞了:
在这里插入图片描述
上面oos的操作就是将winfo中的内容全部拼接到一个字符串中(中间带换行),oss类中的str可以直接获得拼接后的字符串。

然后假如中间经过了网络传输,然后接收端收到了拼接后的字符串:
在这里插入图片描述
就和文件中的差不多。

再演示一下用istringstream 和 ostringstream的:
在这里插入图片描述

字符串拼接:

int main()
{
	stringstream sstream;
	// 将多个字符串放入 sstream 中
	sstream << "first" << " " << "string,";
	sstream << " second string";
	cout << "strResult is: " << sstream.str() << endl;
	// 清空 sstream
	sstream.str("");
	sstream << "third string";
	cout << "After clear, strResult is: " << sstream.str() << endl;
	return 0;
}

在这里插入图片描述

clear和str(“”):

int main()
{
	int a = 12345678;
	string sa;
	// 将一个整形变量转化为字符串,存储到string类对象中
	stringstream s;
	s << a;
	s >> sa;
	cout << s.str() << endl;
	// clear()
	// 注意多次转换时,必须使用clear将上次转换状态清空掉
	// stringstreams在转换结尾时(即最后一个转换后),会将其内部状态设置为badbit
	// 因此下一次转换是必须调用clear()将状态重置为goodbit才可以转换
	// 但是clear()不会将stringstreams底层字符串清空掉
	// s.str("");
	// 将stringstream底层管理string对象设置成"",
	// 否则多次转换时,会将结果全部累积在底层string对象中
	s.str("");
	s.clear(); // 清空s, 不清空会转化失败
	double d = 12.34;
	s << d;
	s >> sa;
	string sValue;
	sValue = s.str(); // str()方法:返回stringsteam中管理的string类型
	cout << sValue << endl;
	return 0;
}

在这里插入图片描述

注意:

  1. stringstream实际是在其底层维护了一个string类型的对象用来保存结果。
  2. 多次数据类型转化时,一定要用clear()来清空,才能正确转化,但clear()不会将stringstream底层的string对象清空。
  3. 可以使用s. str(“”)方法将底层string对象设置为""空字符串。
  4. 可以使用s.str()将让stringstream返回其底层的string对象。
  5. stringstream使用string类对象代替字符数组,可以避免缓冲区溢出的危险,而且其会对参数类型进行推演,不需要格式化控制,也不会出现格式化失败的风险,因此使用更方便,更安全。

到此结束。。。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

先搞面包再谈爱

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值