本篇博客由 CSDN@先搞面包再谈爱 原创,转载请标注清楚,请勿抄袭。
前言
C语言的输入与输出
C语言中我们用到的最频繁的输入输出方式就是scanf ()与printf()。
scanf(): 从标准输入设备(键盘)读取数据,并将值存放在变量中。
printf(): 将指定的文字/字符串输出到标准输出设备(屏幕)。
注意宽度输出和精度输出控制。
C语言借助了相应的缓冲区来进行输入与输出(各位要是对缓冲区不了解的可以看我这篇博客:【Linux】基础文件IO、动静态库的制作和使用,以语言层面来讲缓冲区的话肯定是讲不清楚的,这篇不仅讲了语言级别的缓冲区,还讲了操作系统级别的缓冲区,更加的深入):
对输入输出缓冲区的理解:
- 可以屏蔽掉低级I/O的实现,低级I/O的实现依赖操作系统本身内核的实现,所以如果能够屏蔽这部分的差异,可以很容易写出可移植的程序。
- 可以使用这部分的内容实现“行”读取的行为,对于计算机而言是没有“行”这个概念,有了这部分,就可以定义“行”的概念,然后解析缓冲区的内容,返回一个“行”。
正式开始
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语言中,如果想要将一个整形变量的数据转化为字符串格式,如何去做?
- 使用itoa()函数
- 使用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;
}
注意:
- stringstream实际是在其底层维护了一个string类型的对象用来保存结果。
- 多次数据类型转化时,一定要用clear()来清空,才能正确转化,但clear()不会将stringstream底层的string对象清空。
- 可以使用s. str(“”)方法将底层string对象设置为""空字符串。
- 可以使用s.str()将让stringstream返回其底层的string对象。
- stringstream使用string类对象代替字符数组,可以避免缓冲区溢出的危险,而且其会对参数类型进行推演,不需要格式化控制,也不会出现格式化失败的风险,因此使用更方便,更安全。
到此结束。。。