一、C语言的输入与输出
我们先来复习一下C语言中的IO流
C语言中我们用到的最频繁的输入输出方式就是scanf()和printf()。
scanf() : 从标准输入设备(键盘)读取数据,并将值存放到变量中。
printf() : 将指定的文字/字符串输出到标准输出设备(屏幕)。
C语言借助了相应的缓冲区进行输入与输出:
对于输入输出缓冲区的理解:
1. 可以屏蔽掉低级I/O的实现,低级I/O的实现依赖操作系统本身内核的实现,所以如果能够给屏蔽这部分的差异,可以很容易写出可移植的程序。
2. 可以使用这部分的内容实现"行"读取的行为,对于计算机而言是没有"行"这个概念,有了这个部分,就可以定义"行"的概念,然后解析缓冲区的内容,返回一个"行"。
二、流是什么
"流" 即流动的意思,是物质从一处向另一处流动的过程,是对一种有序连续且具有方向性的数据(其单位可以是bit,byte,packet)的抽象描述。
C++流是指信息从外部输入设备(如键盘)向计算机内部(如内存)输入和从内存向外部输出设备(显示器)输出的过程。这种输入输出的过程被形象的比喻为"流"。
其特性为:有序连续,具有方向性。
为了实现这种流动,C++定义了I/O标准类库,这些每个类都成为流/流类,用以完成某方面的功能。
三、 C++IO流
C++系统实现了一个庞大的类库,其中ios为基类,其他类都是直接或间接派生自ios类。
3.1 C++标准IO流
C++标准IO流是一组用于输入和输出的类和函数。它们提供了一种通用的方式来处理不同的数据源和目标,例如键盘、屏幕、文件等。
C++标准IO流库包括了三个主要的类:istream、ostream和iostream。istream类用于输入操作,ostream类用于输出操作,iostream则同时支持输入和输出操作。
C++标准库提供了4个全局流对象cin、cout、cerr、clog,使用cout进行标准输出,即数据从内存流向控制台。使用cin进行标准输入即数据通过键盘输入到程序中,同时C++标准库还提供了cerr用来进行标准错误的输出,以及clog进行日志输出。
3.2 operator bool
// 单个元素循环输入
while(cin>>str)
{
// ...
}
OJ是如何判断当前是输出结束的呢?
底层调用了 operator bool .
C++中的输入流操作符
>>
会返回左操作数(cin
对象--istream)的引用。此时 while 条件判断需要一个bool类型,则会自动调用operator bool函数,该函数会将一个对象隐式地转换为 bool 类型。
当输入流(cin)达到文件末尾(EOF)或者输入格式错误时,operator bool
会返回false,循环终止;反之会返回true,继续读取。
通过这种方式,while(cin>>str)
循环会自动判断输入结束的条件,无需用户手动输入特定的结束标志。编译器会根据输入流的状态自动决定循环何时结束。
tips:
(库中的explicit是防止隐式类型转化的,如果使用了explict就禁掉了隐式类型转化,只能使用显示类型转换,用于提高代码可读性)
在C++中还有很多这种对象转换为内置类型的函数,比如我们可以编写一个operator int 、operator string ,当需要int、string类型的时候,会自动调用operator int/operator string,返回int类型的值。
3.3 C++文件IO流
C++根据文件内容的数据格式分为二进制文件和文本文件。采用文件流对象操作文件的一般步骤:
1. 定义一个文件流对象
- ifstream ifile(只输入用)
- ofstream ofile(只输出用)
- fstream iofile(既输入又输出用)
2.使用文件流对象的成员函数打开一个函数文件,使得文件流对象和磁盘文件之间建立联系。
3.使用提取和插入运算符对文件进行读写操作,或使用成员函数进行读写。
4.关闭文件。
读取文件的使用举例:
我们除了可以读取同一种类型,我们还可以使用流提取不同类型的数据。
并将其取出。
//data类:
class Date
{
friend ostream& operator << (ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day<<"日";
return out;
}
friend istream& operator >> (istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year), _month(month), _day(day)
{}
private:
int _year,_month,_day;
};
C++中的IO流对自定义类型进行了更好的支持,使用的方式进行了同一的管理,对控制台、文件的读写十分便捷。
注意:
不要使用string这种涉及深浅拷贝的类进行文件读写。
当我们运行写程序时,程序将Serverinfo中的数据整个写入到文件中,string中写入的是_ptr指针,而_ptr指向着实际的字符串,该字符串存储在堆上;当该进程退出,程序地址空间回收,则_ptr成为野指针,此时文件便存储着一系列无效信息。
当我们再运行读进程时,程序将文件中的数据再写入到info中,此时文件中存储着上个进程中的数据,此进程因为拷贝来的_ptr是野指针,便无法获取到字符串;则内部都存放着非法地址(野指针),程序崩溃。
即使现在我们对程序进行改写,让其读、写文件在同一个进程中:
但是程序仍然出现了奔溃,因为我们我们ServerInfo中的string指向了同一空间,我们如果直接使用ReadBin将文件中的内容直接拷贝(浅拷贝)到rinfo中,此时rinfo中的string是浅拷贝,当出作用域时,winfo、rinfo中的string都会调用析构函数,则会产生内存泄漏,便程序崩溃。
这里给出几种解决方法:
1. 自己写一个string,不编写析构函数,当文件关闭时,自己释放string指向的空间,
2. 自己写一个string,使用智能指针来控制析构函数。
3. 直接使用C语言的字符数组,简单省事。
3.4 二进制读写
二进制读写和文本读写
配合(或 | )操作符来设置打开方式
接下来我们使用二进制来进行读和写:
//存储的文件
struct ServerInfo
{
char _address[32];
int _port;
};
//进行存储的对象
struct ConfigManager_Bin
{
public:
ConfigManager_Bin(const char* filename)
:_filename(filename)
{}
void WriteBin(const ServerInfo& info)
{
ofstream ofs(_filename, ios_base::out | ios_base::binary);
ofs.write((const char*)&info, sizeof(info));
}
void ReadBin(ServerInfo& info)
{
ifstream ifs(_filename, ios_base::in | ios_base::binary);
ifs.read((char*)&info, sizeof(info));
}
private:
string _filename; // 配置文件
};
写入后,查看文件:
发现其中字符串部分正常显示,而数字部分为乱码。
这是因为:
数字其以补码的形式进行存放,在程序中,数字以特定的方式进行存储的,当以二进制的方式写入到文件中,其会按存储方式的二进制写入文件,当打开文件时,该二进制会根据编码进行显示,则会出现乱码。
如果想让该数字正常显示,要将其转为字符串,再进行文件的写入
但是其确是是将内容以二进制的形式进行了写入。
然后我们使用二进制读取的方式将bin.txt中的内容读取出来:
3.5 文本读写
此时我们只用修改打开方式,并将其数据部分改为字符串类型进行写入,修改后的ConfigManager对象如下:
struct ConfigManager_Text
{
public:
ConfigManager_Text(const char* filename)
:_filename(filename)
{}
//文本的形式写
void WriteText(const ServerInfo& info)
{
ofstream ofs(_filename, ios_base::out );
ofs.write(info._address, strlen(info._address));
char str[20]="";
sprintf(str, "\n%d\n", info._port); //将整形写入到str中
ofs.write(str, strlen(str));
}
//文本的形式读
void ReadText(ServerInfo& info)
{
ifstream ifs(_filename, ios_base::in );
//行读取
ifs.getline(info._address, 128);
char port[32];
ifs.getline(port, 32);
info._port = atoi(port);
}
private:
string _filename; // 配置文件
};
读/写的函数如下:
void test_file03()
{
ServerInfo winfo = { "192.0.0.1", 80 };
ConfigManager_Text cf_bin("bin2.txt");
cf_bin.WriteText(winfo);
}
void test_file04()
{
ServerInfo rinfo;
ConfigManager_Text cm("bin2.txt");
cm.ReadText(rinfo);
cout << rinfo._address << endl << rinfo._port << endl;
}
tips:
此处我们使用C语言中的sprintf,将字符串作为输入,可以格式化写入到数组中,读的时候,也可以将数字作为输入,使用sscanf,将数字写入到字符串中。
但是这样的写入和输出,非常麻烦,所以C++库提供了流插入和流提取,让数据的读写变得十分便捷。修改如下:
//文本的形式写
void WriteText(const ServerInfo_date& info)
{
ofstream ofs(_filename, ios_base::out);
ofs << info._address << endl;
ofs << info._port << endl;
ofs << info._date << endl;
}
//文本的形式读
void ReadText(ServerInfo_date& info)
{
ifstream ifs(_filename, ios_base::in);
ifs >> info._address >> info._port >> info._date;
}
运行结果如下:
四、stringstream的简单介绍
4.1 stringstream转换与拼接
在C语言中,想将一个整形变量的数据转化为字符串格式,我们会使用以下两种方式:
- 使用itoa()函数
- 使用sprintf()函数
但是两个函数在转化时,都得需要先给出保存结果的空间,那空间需要给多大呢?这个问题就不界定了,而且转化格式不匹配时,还会得到错误的结果甚至程序崩溃。
以下是使用sscanf、sprintf进行整形和字符串之间的转换。
但是在C++中,可以使用stringstream类对象来避免此问题。
使用方式如下:
注意:
多次转换后,必须使用clear()将上次转换状态进行清空。同样的可以使用s.str()取出stringstream中存储的数据,然后进行赋值。
我们还可以使用stringstream进行字符串的拼接:
4.2 stringstream序列化操作
我们还可以使用stringstream进行序列化和反序列化的操作,
void test_chat_info()
{
ChatInfo winfo = { "张三", 15, { 2022, 4, 10 }, "晚上一起看电影吧"};
ostringstream oss;
oss << winfo._name << endl;
oss << winfo._id << endl;
oss << winfo._date << endl;
oss << winfo._msg << endl;
string str = oss.str();
cout << str << endl;
//网络传输str……
//一般会使用json、xml等方式进行更好的支持
//字符串解析成结构信息
// 反序列化 ----------------------
ChatInfo rinfo;
istringstream iss(str);
iss >> rinfo._name >> rinfo._id >> rinfo._date >> rinfo._msg;
cout << "-----------------" << endl;
cout << "日期:" << rinfo._date << endl;
cout << rinfo._id<<" 名字:" << rinfo._name << endl;
cout << rinfo._msg << endl;
cout << "-----------------" << endl;
//更甚至,直接使用stringstream来进行读/写 -- 其同时具有ostringstream/istringstream的功能
stringstream ss;
}
注意:
- stringstream实际是在其底层维护了一个string类型的对象用来保存对象。
- 多次数据类型转化时,一定要使用clear()来清空,才能正确转化,但clear()不会将stringstream底层的string对象清空。
- 可以使用s.str("")方法将底层string对象设置为""空字符串
- stringstream使用string类对象代替字符数组,可以避免缓冲区溢出的危险,而且其会对参数类型进行推演,不需要格式化控制,也不会出现格式化失败的风险,因此使用过更方便,更安全。