目录
前言
C语言的输入输出
C++IO流基本介绍
流的概念
IO流类库
iostream
fstream
stringstream
后记
前言
学过C语言的输入输出相关知识点的童鞋应该多多少少会觉得有些许麻烦,反正我就是这么觉得的,scanf、printf等函数不仅数量众多,而且转化格式必须匹配,否则会得到错误的结果,有些函数还必须得预留出保存结果得空间,而这个空间又不好界定。那这就是C语言这套面向过程的输入输出相关函数使用的弊端,在c++面向对象的环境中,这些输入输出功能必定会被封装成类,正如大家熟知的cin、cout就是相关类多定义出来的对象,使用起来非常之舒服,那么c++的输入输出就叫做IO流。下面会先回忆一下C语言的输入输出函数,对比之下来拓展学习c++IO流,接下来就开始吧!
C语言的输入输出
初学C语言时,想必大家一定经常用scanf、printf两个函数输入输出吧,但是对于fprintf、fscanf、sprintf、sscanf等函数,大部分同学应该是见过都很少用过,这些函数是后面学习到字符串和文件两部分知识点才会接触到的函数,也是用于输入输出的。下面我们分别大概介绍一下:
- scanf:
- printf:
其中,第一个参数是一个常量字符指针,第二个参数是可变参数列表,表示用户自定义传入参数个数,返回值int表示按照指定的格式符正确读入/输出的数据的个数,但如果输入/输出数据与指定格式不符,则会产生错误,函数会立即终止,设置错误码,并返回已经成功读取/输入的数据的个数。
- fscanf:
- fprintf:
可见,fprintf、fscanf函数与printf、scanf函数不同的只是多了一个参数——文件指针。回想一下,其实scanf函数默认是从键盘拿数据,因此fscanf函数是从指定文件拿数据,而printf默认是输出到显示屏,fprintf则是输出到指定文件。
- sscanf:
- sprintf:
- snprintf:
对于sscanf、sprintf函数也是一样,不同的地方在于多了一个参数——常量字符指针,因此sscanf是从一个字符串拿数据,而sprintf是将数据按照指定格式输出到一个字符串当中。snprintf更多了一个参数——字符个数,可以指定输入到字符串中的字符个数。
C++IO流基本介绍
-
流的概念
流是对一种有序连续且具有方向性的数据( 其单位可以是bit、byte、packet )的抽象描述,而C++流是指信息从外部输入设备(如键盘)向计算机内部(如内存)输入和从内存向外部输出设备(显示器)输出的过程,这种过程被形象的比喻为“流”,具有有序连续、方向性的特性。
-
IO流类库
为了实现这种流动,C++定义了I/O标准类库,如下图,这些每个类都称为流/流类,用以完成某方面的功能。其中ios为基类,其他类都是直接或间接派生自ios类。其中,不看第三列,派生类分为三种——iostream、fstream、stringstream,而第三列的cin、cout等是iostream定义出的对象,其中cin、cout也是我们自学习c++以来经常使用的输入输出流。宏观来看,iostream是对scanf、printf的替换,fstream是对fscanf、fprintf的替换,stringstream是对sscanf、sprintf的替换,下面分别介绍:
iostream
iostream类库是istream、ostream类库合并而来,没什么别的意义,就是定义了这一个头文件,两个类的功能都可以使用。istream是输入流类,定义出cin进行标准输入即数据通过键盘输入到程序中,ostream是输出流类,定义出cout、cerr、clog,其中cout进行标准输出,即数据从内存流向显示屏,cerr用来进行标准错误的输出,以及clog进行日志的输出,这三个对象现在基本没有区别,只是应用场景不同。下面强调几个使用过程中的重点注意事项。
1.空格和回车都可以作为数据之间的分格符,因此多个数据可以在一行输入,也可以分行输 入。但同时数据如果是字符型和字符串,则空格将无法用cin输入,也不能将空格通过键盘输入到字符串中。
eg(输入多个值的方法):
如果遇到数据之间没有加空格或换行的情况呢?可能c++方法要比C语言方法稍复杂一点。
2.在笔试一些oj题时,有很多题目需要循环输入得到多个数据,c++的方法就很简单,对于一个值循环输入:
while(cin>>a) { //... }
对于多个值循环输入:
while(cin>>a>>b>>c) { //... }
大部分oj题的输入数据都是用空格或换行分割,因此使用以上方法拿到数据绝对没有任何问题。此外还值得提一嘴的是在vs系列编译器下连续输入时输入ctrl+z停止输入。
3. istream类型转换为逻辑判断值
为什么上面的cin>>a在输入结束之后会停止循环,那肯定是while条件为假,但是cin>>a之后的返回值依旧是istream类型(istream& operator>> (int& val);),这该如何知道这是真是假呢?其实在istream类型当中,有这样一个成员函数——explicit operator bool() const{},不看explicit和const两个关键字,可以看到这个函数是没有返回值的,其实并不是,这是它的特殊实现方式,返回值就是bool类型,因为我们需要一个逻辑判断值,所以在operator后加上bool,然后函数体内就可以实现将istream类型转bool值得逻辑,实则就是看指向键盘数据的标识符是否指向结尾,指向结尾说明拿到了所有的数据,之后返回false,while循环条件拿到false则循环输入结束。这样说有点混乱,因为拿不到istream类型的实现机制,下面举两个例子:
1)如下图,类A我们实现了构造函数,那么我们相当于实现了将一个内置类型转换为一个自定义类型的功能,其实就是一种隐式类型转换;此时我们也想实现将一个自定义类型转换为一个内置类型的功能,比如说转换为int类型,那么我们就可以operator加上int实现这样一个函数,函数体内写上转为内置类型的逻辑,也就是拿到成员变量,将其返回出来,实现成功。
eg1:
2)以下代码块实现一个Date类型和能够将其输入输出的operator重载函数。其中我们想要实现一个将这个Date类型转换为bool类型的功能,就是成员函数operator加上bool,函数体内实现转换逻辑,这里比如说是看year变量,为0返回false,为非0返回true,如此就实现了将自定义类型转内置类型的功能。
eg2:
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)
{}
operator bool()
{
// 这里是随意写的,假设输入_year为0,则结束
if (_year == 0)
return false;
else
return true;
}
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;
}
fstream
不同于iostream的是,c++并没有提供像cin、cout这样的全局对象供我们使用,这里就需要我们自己去定义,包含头文件<fstream>后,三种定义方式如下:
- 只输入用:ifstream 对象名(文件名)
- 只输出用:ofstream 对象名(文件名)
- 输入输出两个都能用:fstream 对象名(文件名)
定义好之后,对象的用法就如同cin、cout一样了。举个例子,定义一个类ServerInfo作为文件IO的数据,包括ip地址、端口号、时间(前面模拟实现Date类),再定义一个类Consoler对一个文件实现文本读、文本写、二进制读、二进制写的功能,构造函数就是传入一个文件名即可。
因为文件分为文本文件和二进制文件,所以有四个读写函数。对于二进制写,ofstream定义一个对象ofs,其中第一个参数就是文件名,第二个参数表示是二进制读写,之后使用write函数将信息数据写入文件;二进制读也是一样,使用read函数将信息数据读出来;对于文本写,不需要加第二个参数,因为默认就是文本读写,之后使用如同cin、cout的形式读写数据。注意要分清:ifs对应cin,cin是将键盘数据读到内存中,而ifs是将文件数据读到内存中;ofs对应cout,cout是将内存数据写到显示屏,而ifs是将内存数据写到文件中。
综上,二进制文件和文本文件的读写方式不一样,c++文件流的优势就是可以对内置类型和自定义类型,都使用一样的方式,去流插入和流提取文件数据,不过前提是要自定义类型需要重载>> 和 <<。
eg:
class ServerInfo
{
public:
char _ipaddr[32];
int _port;
Date _date;
};
class Consoler
{
public:
Consoler(const char* filename)
:_filename(filename)
{}
//二进制写
void WriteBin(const ServerInfo& info)
{
ofstream ofs(_filename, ios_base::out | ios_base::binary);
/*ofs << info._ipaddr << endl;
ofs << info._port << endl;
ofs << info._date << endl;*/ //这样写的话还是文本写
ofs.write((char*)&info, sizeof(info));
}
//二进制读
void ReadBin(ServerInfo& info)
{
ifstream ifs(_filename, ios_base::out | ios_base::binary);
ifs.read((char*)&info, sizeof(info));
cout << info._ipaddr << endl;
cout << info._port << endl;
cout << info._date << endl;
}
//文本写
void WriteText(const ServerInfo& info)
{
ofstream ofs(_filename);
ofs << info._ipaddr << endl;
ofs << info._port << endl;
ofs << info._date << endl;
}
//文本读
void ReadText(ServerInfo& info)
{
ifstream ifs(_filename);
ifs >> info._ipaddr >> info._port >> info._date;
cout << info._ipaddr << endl;
cout << info._port << endl;
cout << info._date << endl;
}
private:
string _filename;
};
int main()
{
ServerInfo info = { "192.168.1.1",28,{2024,1,26} };
Consoler clr("draft.txt");
clr.ReadBin(info); //二进制读
clr.WriteBin(info); //二进制写
clr.ReadText(info); //文本读
clr.WriteText(info); //文本写
return 0;
}
stringstream
stringstream库的学习可以参考fstream,只不过第一个参数不是文件指针,而是string类型的对象,包含<sstream>后,就可以将数据输入到一个string中,也可以将string中的数据读出来。stringstream库分为istringstream库和ostringstream库,使用stringstream定义的对象可以读也可以写,使用istringstream定义的对象只可以写,使用ostringstream定义的对象只可以读,下面举几个例子简单说明一下用法。
1.不同数值类型与string之间的转换
eg:
#include <iostream>
#include <sstream>
#include <string>
using namespace std;
int main()
{
//stringstream oss; //一开始字符串流中啥也没有
//stringstream oss("8888"); //一开始字符串流中就存在一个8888的字符串,默认是覆盖写
stringstream oss("8888 ", ios_base::out | ios_base::ate); //追加写
int i = 123;
double d = 3.14;
string s = "hello";
oss << i <<" " << d <<" " << s;
cout << oss.str() << endl;
return 0;
}
2.序列化和反序列化
在通过网络传递信息的过程中,我们是需要一个string表示各种信息,比如说qq中发送的一条信息,包括发送者、发送对象、发送时间、发送内容等等,将这些信息序列化成一个字符串发送过来,接收后将这个字符串反序列化出来得到这些信息,这个过程就可以用到stringstream类。
eg:
class QQInfo
{
public:
string _name;
Date _date;
string _msg;
};
int main()
{
QQInfo info = { "张三",{2024,2,2},"你吃饭了没?" };
//序列化
stringstream oss;
oss << info._name << " " << info._date << " " << info._msg;
string str = oss.str();
//通过网络发送这个str
//...
//反序列化
QQInfo info2;
stringstream iss(str);
iss >> info2._name >> info2._date >> info2._msg;
//验证
cout << info2._name << " | " << info2._date << " | " << info2._msg << endl;
return 0;
}
注意:
1.stringstream实际上是在其底层维护了一个string类型的对象;
2.可以使用s. str("")方法将底层string对象设置为空字符;
3.可以使用s.str()将让stringstream返回其底层的string对象。
总的来说,stringstream使用string类对象代替字符数组,可以避免缓冲区溢出的危险,而且其会对参数类型进行推演,不需要格式化控制,也不会出现格式化失败的风险,因此使用更方便,更安全。
后记
从上面的学习可以看出,c++IO流是一个庞大的类库,很多类都是直接或间接派生基类所得,非常符合c++的世界观,使用起来非常方便,基类能支持的,子类都可以使用。此外,无论是输入输出方式、格式、类型安全、缓冲区和错误处理,C++的IO流相较于C语言的输入输出更加高级、安全和方便使用。而且从学习成本上看,当我们学会使用iostream类的使用,fstream、string stream类的使用也是得心应手。因此建议多多使用c++IO流来输入输出,拜拜!