在前面的学习中我知道使用cout<<
可以将数据输出到屏幕上面,比如说下面的代码:
int main()
{
int a = 10;
char b = 'b';
double c=10.1;
string str("abcd");
cout << a << endl;
cout << b << endl;
cout << c << endl;
cout << str << endl;
return 0;
}
代码的运行结果如下:
可以看到使用相同的输出形式,却能够输出不同形式的数据,那么这是因为c++内部对cout<<
进行了许多形式的重载,所以可以打印出来各种各样的数据,比如说下面的图片:
并且对于cin>>
c++也做了很多类型的重载比如说下面的图片:
流提取是一个缓冲操作,它默认是以空格或者换行符来作为读取的结尾,所以我们经常使用流提取来实现下面的操作:
int main()
{
string tmp;
while (cin >> tmp)
{
cout << tmp << endl;
}
return 0;
}
只要我们输入数据它就会读取数据并将数据打印出来,当数据读取完了并且我们没有输入任何数据了的话它就会一直在那里进行等待我们输入数据,比如说下面的图片:
可是这个程序如何结束呢?有小伙伴们可能会说按ctrl c来结束程序比如说下面这个样子:
但是ctrl c是一种很暴力的结束方式,他是通过发送信号的方式来结束这个程序并不是程序的正常的结束,所以要想正常的结束这个程序就得输入ctrl z 换行来结束上面的程序,比如说下面的图片:
可以看到这样的退出方式就是合理的,退出码也为0,那这里就一个问题,为什么ctlr z就可以使上面的循环结束进而让程序结束的呢?首先我们要知道cin >> tmp
的返回值是一个istream的对象,while在判断循环是否继续的时候是如果为0就循环结束,非0就进入循环,也就是说结果为真就继续循环结果为假就停止循环,但是这是是stream的对象啊,他不能隐式转换成为整型那他如何来判断真假呢?所以库中为了让istream对象支持判断真假就提供了一个这样的操作:
有了这个特殊的重载之后就可以将一个istream类型的对象转换成为一个bool类型的对象,那么上面代码判断数据的过程就是cin>>tmp
返回一个istream的对象,然后在istream进行判断的时候就会调用函数operator bool
,在这个函数内部就会判断这个对象里面的一些信息,当我们输入ctrl z的话就会将类里面的一些信息进行更改,一旦函数返现这些信息被修改了就会直接返回false,那么这就是上面代码的执行原理,有了这个原理之后我们就可以创建一个日期类,在类里面实现operator bool,然后把这个类创建出来的对象作为while循环的判断,那么这里的代码就如下:
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;
}
int main()
{
// 自动识别类型的本质--函数重载
// 内置类型可以直接使用--因为库里面ostream类型已经实现了
int i = 1;
double j = 2.2;
cout << i << endl;
cout << j << endl;
// 自定义类型则需要我们自己重载<< 和 >>
Date d(2022, 4, 10);
cout << d;
while (d)
{
cin >> d;
cout << d;
}
return 0;
}
当对象内部的_year为0时,operator bool就会直接返回false,比如说下面的运行结果:
那么这就是operator bool的原理,希望大家能够理解。
我们首先创建一个类用来记录要往文件里面写入的信息,比如说下面的代码:
struct ServerInfo
{
char _address[32];
int _port;
};
然后我们再创建一个类专门用来把上面类创建出来的对象写入到文件里面,或者把文件里面的内容读进来,那么这个类里面就含有一个string类型的成员变量用来记录配置文件的文件名,然后构造函数就需要一个参数用来初始化这个成员变量,那么这里的代码就如下:
struct ConfigManager
{
public:
ConfigManager(const char* filename)
:_filename(filename)
{}
private:
string _filename; // 配置文件
};
c语言存在两种读写形式,一个是二进制读写一个是文本读写,二进制读写就是将内存中的数据不经过处理直接输出到文件里面,而文本读写则是将内存中的数据经过读写再输出到文件里面,那么c++也存在对应的两种读写方式
ifstream类就是把文件里面的数据读到程序里面,ofstream就是将程序里面的数据写入到文件里面,我们先来看看ofstream:
c语言里面有个fopen函数用来打开指定的文件,那么c++的ofstream里面也存在一个与之对应的open函数来打开文件:
第一个参数表示要打开的文件名,第二个参数则表示这个文件打开的方式,比如说以读的方式打开以写的方式打开,以二进制的形式打开,在尾部添加的方式打开等等,所以就有了下面这样的符号:
你想要以什么样的形式打开文件就添加什么样的类型,然后用|
将这些形式链接起来,我们可以看看文档给出的解释:
比如说我想要打开一个名为test.txt的文件,并且想往这个文件的尾部添加数据的话就可以使用out和app,那么这里的代码就如下:
ofstream ofs;
ofs.open ("test.txt", ofstream::out | ofstream::app);
我们这里是用无参的形式创建ofstream对象,然后再调用open来以一种形式来打开文件,那么这里还有一种方法就是在创建ofstream对象的时候添加要打开的文件和对文件操作的形式,我们来看看ofstream的构造函数:
那么这里我们就可以创建一个函数并以二进制的形式往文件里面写入数据:
void WriteBin(const ServerInfo& info)
{
ofstream ofs("test.txt", ios::out | ios::binary);
}
因为ofstream类是继承的ios类,ios类里面就有这些标记位,
所以我们可以直接使用ios里面的标记位来打开标记位,文件打开之后就要往文件里面写入数据,ofstream类里面提供了一个名为write的函数,他的作用就是往文件里面写入数据,我们来看看这个函数的介绍:
一个参数是一个指针表示要写入数据的地址(注意这个地址的类型为char*),第二个参数就表示要把多少个字节的数据写入到文件里面,那么这里的代码就如下:
void WriteBin(const ServerInfo& info)
{
ofstream ofs("test.txt", ios::out | ios::binary);
ofs.write((char*)&info, sizeof(info));
}
那么这里可能就有小伙伴问我们需要添加文件的关闭函数吗?答案是不需要的,因为这里是创建类来打开文件,当函数结束类的生命周期就结束了,然后就会调用析构函数来关闭文件,有了写入函数我们就得再创建一个读取函数,这个函数跟上面差不多就ofstream改成istream把out改成in,然后ifstream类里面有个函数叫做read,这个函数的参数如下:
第一个参数表示读取的数据存在的位置,第二个数据表示要读取多少个字节,那么有了这些东西之后我们就可以写出下面的代码:
void ReadBin(ServerInfo& info)
{
ifstream ifs("test.txt", ios::in | ios::binary);
ifs.read((char*)&info, sizeof(info));
}
然后我们就可以用下面的代码来进行测试:
i
nt main()
{
ServerInfo winfo = { "192.0.0.1", 80 };
ConfigManager cf_bin("test.txt");
cf_bin.WriteBin(winfo);
//ServerInfo rbinfo;
//cf_bin.ReadBin(rbinfo);
//cout << rbinfo._address << " " << rbinfo._port << endl;
return 0;
}
运行一下代码然后就可以看到当前程序所在的路径下存在一个名为test.txt的文件:
打开这个文件就可以看到里面存在着我们输入的内容:
因为这里是二进制所以这里显示的是乱码,然后我们再将这个内容读取并可以看到输出的结果符合我们的预期:
那么这就是二进制读写的过程,这里大家注意一点就是使用二进制读写的时候里面这里的字符数组不能改成string,比如说下面的代码:
struct ServerInfo
{
string _address;
int _port;
};
这里变成string,然后我们再运行一下就可以看到文件里面的内容不一样了:
并且在读取的时候也会出现问题:
这里虽然打印成功了但是退出码是有问题的,原因很简答string对象里面并没存储数据,而是存储数据的地址和数据的属性,当我们把数据写到文件里面时实际上是把数据的地址和地址的属性写到文件里面,当我们再读的时候得到是这个数据所在地址,我们这里是同一个进程,所以数据还在,如果是不同进程的话很可能写的时候数据还在,但是另外一个进程在读的时候可能就不在了,所以这里就存在问题。所以c++就不推荐使用二进制读写而是使用文本读写。
文本读写在使用的时候默认就给了缺省参数,所以我们就直接传递一个文件名就可以了,文本读写的时候都是以字符串的形式来写,如果传递过来的是字符串的话还好说,如果传递过来的是整型或者其他类型的话我们还得将其转换为其他的形式来传递,所以我推荐大家使用下面的方法来进行实现,c++重载了很多类型的>> <<
,
虽然这些重载并不是在ifstream和ofstream里面实现的,但是这些类是有继承关系的啊
所以我们就可以使用操作符>>和<<
来往文件里面写入或者读取数据,比如说下面的代码:
void WriteText(const ServerInfo & info)
{
ofstream ofs(_filename);
ofs << info._address;
ofs<< info._port ;
}
void ReadText(ServerInfo& info)
{
ifstream ifs(_filename);
ifs >> info._address;
ifs>> info._port ;
}
然后我们可以用下面的代码来进行测试:
int main()
{
ServerInfo winfo = { "192.0.0.1", 80 };
ConfigManager cf_bin("test.txt");
cf_bin.WriteText(winfo);
//ServerInfo rbinfo;
//cf_bin.ReadText(rbinfo);
//cout << rbinfo._address << " " << rbinfo._port << endl;
return 0;
}
代码运行之后就可以看到文件里面的内容变成下面这样:
读取的内容就变成下面这样:
可以看到这里出现了问题,原因也很简单cout和cin在读取和打印的时候是以空格或者换行符来作为分隔符,那么这里的读取的时候也是同样的道理,我们得添加endl来添加分隔符,那么这里的代码就如下:
void WriteText(const ServerInfo & info)
{
ofstream ofs(_filename);
ofs << info._address<<endl;
ofs<< info._port <<endl;
}
void ReadText(ServerInfo& info)
{
ifstream ifs(_filename);
ifs >> info._address;
ifs>> info._port;
}
运行的结果如下:
可以看到这里的运行结果就是正常的,可是这里就存在一个问题对于内置类型编译器提供了重载我们这里就可以很方便的实现,那对于一些编译器没有实现的重载的类又该怎么办呢?比如说数据变成了下面这样:
struct ServerInfo
{
string _address;
int _port;
Date _date;
};
那么这里就是c++设计的精髓所在,我们上面不是自己实现了日期类的>>和<<
的重载了嘛,那么这里就可以直接使用我们自己重载的来往文件里面写入或者读取内容,比如说下面的代码:
void WriteText(const ServerInfo & info)
{
ofstream ofs(_filename);
ofs << info._address<<endl;
ofs<< info._port <<endl;
ofs << info._date << endl;
}
void ReadText(ServerInfo& info)
{
ifstream ifs(_filename);
ifs >> info._address;
ifs>> info._port;
ifs >> info._date;
}
测试的代码如下:
int main()
{
ServerInfo winfo = { "192.0.0.1", 80 ,{1010,10,10} };
ConfigManager cf_bin("test.txt");
cf_bin.WriteText(winfo);
ServerInfo rbinfo;
cf_bin.ReadText(rbinfo);
cout << rbinfo._address << " " << rbinfo._port <<" "<<rbinfo._date<< endl;
return 0;
}
运行的结果如下:
文件的内容如下:
符合我们的预期,那么这里之所以可以这么实现的原因就是我们重载的是istream和ostream,然后这两个最后又都继承到了ifstream和ofsteam,虽然日期类的重载里面是ostream类型的参数,而传递的时候我们传的是istream类型的参数,但是这里依然可以跑的过去,因为父类类型可以接收子类的类型。
c语言提供了一个名为to_string的函数,他可以将一些类型的数据转直接转换成为字符串类型,那么c++也提供了
ostringstream和istringstream这两个类来实现类似的功能,首先创建一个ostringstream的对象,然后通过操作符>>将数据输出到这个对象里面,比如说下面的代码:
#include
int main()
{
int i = 1234;
double di = 11.11;
Date d = { 2023,5,16 };
ostringstream oss;
oss << i << " ";
oss << di << " ";
oss << d << " ";
}
然后这类里面提供了一个名为str的函数来显示对象里面的内容:
那么我们就可以写出下面的代码:
#include
int main()
{
int i = 1234;
double di = 11.11;
Date d = { 2023,5,16 };
ostringstream oss;
oss << i << " ";
oss << di << " ";
oss << d << " ";
cout << oss.str() << endl;
}
代码的运行结果如下:
可以看到确实变成了字符串,既然可以将多个不同类型的变量合成一个字符串,那么同样的道理我们还可以使用istringstrem来讲一个字符串分解给多个类型的变量,这里也是使用操作符>>来实现,那么这里的代码就如下:
#include
int main()
{
int i = 1234;
double di = 11.11;
Date d = { 2023,5,16 };
ostringstream oss;
oss << i << " ";
oss << di << " ";
oss << d << " ";
string tmp = oss.str();
istringstream iss(tmp);
int j;
double ji;
Date dd;
iss >> j >> ji >> dd;
cout << j << endl;
cout << ji << endl;
cout << dd << endl;
}