C++ IO流

目录

1 标准IO流

2 文件IO流

3 序列化和反序列化


本文主要对比C语言的三套IO的机制来认识C++的IO流

1 标准IO流

cin -- scanf  , cout -- printf , cerr ,  clog  不过我们常用的就是cin和cout。

cin是标准输入流。它以换行或者空格结束作为结束标志。

所以我们在使用cin输入字符串时,要注意中间如果有空会结束本次从输入缓冲区的读取。

而cout则是标准输出流。

他们都是全局的类对象,在命名空间 std 中。

我们可以直接使用 >> 和 << 来对内置类型进行输入和输出,因为在这两个类里面已经重载了自定义类型的流提取和流插入。

如果我们要实现自定义类型的输入和输出,就需要我们自己重载。

同时,string 是支持了 << 和 >> 的,不需要我们自己实现。

有一点要注意的是:

cin 输入的类型必须要和从缓冲区提取的数据类型一致,如果不一致就会出错,并将流的状态字state的对应位置为1,程序继续向后执行,但是后续的 cin 都无效

我们在有的代码中见过类似这样的写法:

	int a;
	while (cin >> a)
		cout << a << endl;

将cin作为循环的判断条件。 

我们知道,流插入的返回值是 cin 的引用,而istream 的类型的对象怎么能够转换为 bool 作为循环判断条件呢?

这就不得不提到C++的一些特殊的地方了。

在C++98 时,为了支持这样的写法,istream 类提供了一个这样的方法:

这里的函数很奇怪, 说他是运算符重载吧,又没有返回类型,说他是不同函数吧,他前面又有一个operator 同时函数名我们也不知道是哪个 。

其实这是C++提供的用于支持 隐式类型转换 的方式,他的返回值或者说转换的目标类型就是我们上面写的operator 后面的 void* ,他的整个函数体就是 operator + 类型 +() ,当然为了支持const对象也能隐式类型转换,把函数设置为 const 。

为什么要这样定义呢?

我们强制类型转换使用的符号是 () ,按理来说重载 () 来表示隐式类型转换是最合适的,但是由于函数的调用的运算符也是 () ,()已经被仿函数占用了,所以迫不得已只能这么用了。

那么在函数体中,我们就可以根据自己的逻辑来将自定义对象转换为一个想要的类型进行返回。

比如:

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

	operator bool()
	{
		if (_a == 1)
			return true;
		return false;
	}

private:
	int _a;
};

int main()
{
	A a(1);
	if (a)
		cout << "true" << endl;
	return 0;
}

当然我们也可以支持多种类型隐式转换,编译器在调用的时候会去调用最匹配当前所需的类型的那个函数。

在C++98中,istream 类支持隐式类型转换为 void* 类型,然后再由 void* 转换为 bool 作为逻辑的判断条件。

不过我们可能会有点奇怪为什么要这样做,不直接转换为 bool 的类型,可能是因为 void* 作为中间的产物,它可以转换为更多的类型,而不只限于 bool 。但是好像确实也没必要。

于是C++11就支持了直接隐式类型转换为 bool 类型

那么他什么时候转换为 false 呢?也就是我们上面的循环什么时候结束呢?

被设置了标记位时就返回false

比如我们上面所说的 类型对不上时会在状态字设置标志位,这种情况就会返回fasle。

同时还有就是可以输入 ctrl z 来设置标记位让其返回false,不过这种方式需要我们将 ctrl z刷新到输入缓冲区的时候,才会设置标记位,也就是需要按下 回车 之后才生效。

cin和cout有一个接口叫做 sync_with_stdio

这个接口的功能是 让C++的iostream 的流 与 C语言的stdio 的输出输入保持同步。

由于C++要兼容C语言,所以C语言的输入输出也要被保留,但是由于C语言的 pritnf 和 C++ 的cout是不同的东西,一个是函数,一个是对象。同样,scanf 和 cin 也是类似的。 他们各自有各自的缓冲区。 但是有时候,比如我们先使用 printf 将数据写入到printf 的缓冲区中,但是没有刷新,这时候再使用 cout 来输出数据,我们是先输出 printf 的缓冲区的呢?还是cout 的呢?如果我们想要他们保持同步,也就是按照写到缓冲区的顺序来输出到显示器,那么我们就可以使用这个函数。

istream类 是专门用来输入, ostream类 是专门用来输出,而如果既想要输入也要输出的话,可以使用 iostream 类。 

他们的构造函数都需要缓冲区,需要传streambuf 类型的对象的地址 ,不过该类的构造函数是 protected ,所以我们也调不动。所以这里也不好演示。

2 文件IO流

对比C语言的fopen的各种打开方式, 二进制写入 , 文本写入 ,二进制读取, 文本读取等。

C++的标记位的选择: in 和out 没什么好说的,binary 就是二进制的意思,不过他要和in 和out 来配合使用,in和out默认是文本读取和写入。

ate 是打开文件时将文件指针置于文件末尾,我们可以跳转位置来进行读取或者插入。

而app则是能用于写入,在文件末尾追加写入,不能读取。

trunc 是打开文件时直接清空内容。

读取文件用 ifstream 

参数是文件名和打开模式,如果不传模式的话,默认以 in 打开,文本读写。

ofstream 用于向文件写入,

参数也是文件名,模式默认是以文本形式写入。

文本模式读写和二进制读写的区别就是,二进制是将内存的数据写到文件中(0和1)以及从文件中读取二进制直接写到内存,而文本读写会将内存中的数据转换为字符写入到文件中,从文件中读取的字符也会转换为二进制写入到内存中。

总之,二进制形式读写的话,数据在内存中是什么样子,在文件中就是什么样子。而文本读写则需要进行二进制与字符的转换。

如果既要读又要写,可以使用 fstream

默认是以文本的读和写模式打开。

而在操作完文件之后,要调用 close 来关闭文件。

C++中的读写可以用 read 和 write  来进行,

读取指定个字节的数据到缓冲区

将指定缓冲区的 n 个字节写入到文件中

不过我们一般不这样用,我们可以看一下C++的IO流的继承关系

文件 IO 是继承标准IO类的,所以标准IO的流插入和流提取也会被继承下来,他们有各自的缓冲区,我们可以猜测一下缓冲区是在ios_base或者ios中定义的,所以标准IO和文件IO的流插入和流提取可以写到各自指向的缓冲区以及从该缓冲区读取进来数据。

虽然C++把这里的继承关系设计的较为复杂了,但是对于我们而言,使用起来就方便了很多。

如此一来,我们可以照猫画虎,怎么使用cin和cout ,就能怎么使用 ifstream 和 ofstream 的对象对文件进行操作。

	const char* filename = "log.txt";
	ofstream fp(filename,ios_base::out | ios_base::binary);
	string str("stringstringstringstring");
	fp << 1 << endl;
	fp << "isoiaofia" << endl;
	fp << str << endl;
	fp.close();

我们使用 文件IO流的时候要注意,string 直接使用重载的 << 看似好像没问题,但是我们要时刻注意string 的底层是有一个 buf 数组的,当长度小于16时,并不会去堆区申请空间,而是直接存储在静态数组中。

当我们使用二进制形式将string写入到文件中时,如果我们使用 << 的话,会出现这种情况:

当string的size大于15时,string 会采用堆空间来存储,而这时候如果我们采取二进制的写入,那么写入到内存中的就是一个指针,而不会把指针所指向的堆空间也写进去,因为string 的内存中的额值就是一个静态数组buf,一个堆空间指针ptr,还有size和capacity,如果size小于16时,那么数据是存在静态数组的,这时候数据是在对象的空间内的,而大于15时,数据是存储在堆空间的,对象内只是有一个指针指向这个堆空间。

那么这时候就会出现两个问题,1 如果是在同一个进程中进行大于15的string先写后读的话,那么就会导致两个string对象内的ptr指向同一块空间,这时候析构函数会出问题。 2 如果是写和读分靠的话,读出来的指针的值在读的进程看来就是一个野指针,这时候就是野指针的问题。

当然这个问题不一定会出现,可能较新的编译器将该情景优化了。

总之跟指针有关的二进制的读写我们要多加注意。

同时C++的文件IO我们更推荐使用文本读写。

3 序列化和反序列化

对标的就是C语言的sscanf和sprintf ,将数据转换为字符串以及将字符串按照指定格式转化为对应的数据。

由于他还是继承自 istream 和 ostream 类的,所以我们还是可以使用 <<  和>> 的重载来进行操作。

进行序列化时要注意的是,如果你后续还需要将字符串的数据提取出来,那么我们就需要在进行序列化的时候为每一个数据加上分隔符。

	ostringstream outs;
	outs << 1234 << " " << "string" << " " << 'a' << endl;

这是序列化的操作,而反序列化则使用istringstream

它支持用一个string对象来进行构造,但是有一个问题就是,比如我们上面是用了ostringstream 来进行序列化,那么如何拿到他序列化之后的 字符串呢? 我们可是需要这个字符串来进行反序列化的,很简单,ostreamstring内部本质上就是使用一个string对象来存储序列化之后的结果的,ostringstream 类提供了一个方法, str() ,能够返回序列化之后的string对象。

那么我们就可以这样用:

	ostringstream outs;
	outs << 1234 << " " << "string" << " " << 'a' << endl;

	istringstream ins(outs.str());
	int num;
	string str;
	char ch;
	ins >> num >> str >> ch;
	cout << num << endl << str << endl << ch << endl;

但是其实C++的序列化和反序列化这一套机制还是不好用,尤其是对于容器,特别是有特殊存储结构的容器比如map,set等,C++这一套无法对其进行序列化。以后我们使用序列化更多的还是用到 json 来完成,而json我们会在网络专栏中讲到。

其实总体来说,C++的这套方案其实和C语言的差别也没有说十分大,总体就是将面向过程的方案改成了面向对象的方案。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

我没有空军

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

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

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

打赏作者

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

抵扣说明:

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

余额充值