0 视频
1 目标
C++ 流操作符的优点之一,就是不仅对已有的数据类型支持良好,而且对未知的,用户自定义的数据类型,也能提供良好的支持。
假设我们要用下面的自定义结构,存储密码信息:
struct PasswordInfo
{
string title; // 标题
string user_name; // 用户名,通常就是账号
string password; // 密码
string memo; // 附加说明
};
那么,我们希望密码数据也可以输出到各种各样的“输出流”,比如屏幕、内存、文件等;反过来也能从各种各样的流读入数据。以输出到文件流为例,假设我们已经有一个 PasswordInfo 的 数据,叫 “pi”,又有一个已经打开的输出文件流对象,名为 ofs ,则我们希望能这么写:
ofs << pi;
即可将数据以我们希望的格式,存到文件中。自然的,如果我们手上拥有的输出流对象是 cout,则我们希望将 ofs 换成 cout,就能将 pi 的信息按照我们需要的格式,输出到屏幕:
cout << pi;
2 方法
在 C++ 中,想要让自定义类型的数据,能被输出流接受,以及想让它能经输入流实现数据输入,方法就是分别重载 operator << 和 operator >> 。
通过我们通过自由函数,而非自定义类型的成员函数实现输入输出流的重载。其中有一个客观原因:流操作时,第一个入参都是流本身,第二个入参才是自定义类型的对象。如果使用自定义类型的成员函数重载,则第一个入参只能是自定义类型对象本身(即隐藏的 this 指针)。
以下是重载输出流和重载输入流操作函数的原型, T 表示用户自定义类型。
// 输出:
ostream& operator << (ostream& os, T const& v);
//输入:
istream& operator << (istream& is, T& v);
注意几个要点:
- 如前所述,二者都是自由函数(即:非成员函数);
- 二者中用到的 “流” 类型,都是 “抽象流”,这样才能满足重载操作可用于各种具体的输出或输入流;
- 同样如前所述,第1个入参都是流对象的引用;
- 第二个入参,是自定义类型的引用,但用于输出时,由于不修改对象内容,所以使用常用引用;而用于输入时,目的就是为了使用来自流的外部数据设置对象内容,所以使用引用;
- 返回值都是流对象的引用,即第1个入参。
第5点我们解释下,输出一个数据之后,继续返回流对象,才能实现C++输入输出流典型的“级联”操作,比如:
T t;
cout << t << "\n 输出完毕" << endl;
其中的 cout << t ,即调用了 : “ostream& operator << (ostream& os, T const& v)”,该函数在本例中,返回的正是 cout ,于是 才能有后续的 “ cout << “\n 输出完毕” ”。同时,后者又返回 cout ,于是才能继续有 “ cout << endl ” 的操作。
3 代码
3.1 演进1
ostream& operator << (ostream& os, PasswordInfo const& pi)
{
os << pi.title << pi.user_name << pi.password << pi.memo;
return os;
}
3.2 演进2
ostream& operator << (ostream& os, PasswordInfo const& pi)
{
os << pi.title << '\n'
<< pi.user_name << '\n'
<< pi.password << '\n'
<< pi.memo;
return os;
}
3.3 最终版本完整代码
#include <iostream>
#include <string>
#include <fstream> // 文件流 (包含输入文件、输出文件流)
using namespace std;
struct PasswordInfo
{
string title; // 标题
string user_name; // 用户名,通常就是账号
string password; // 密码
string memo; // 附加说明
};
void write_string(ostream& os, string const& str)
{
os << str.length() << '\n'; // 单独一行输出字符串长度
os << str << '\n'; // 为了美观,以换行符结束
}
void read_string(istream& is, string& str)
{
size_t len;
is >> len;
is.ignore(); // 跳过数字后面的 换行符
if (len > 0)
{
char *buf = new char[len];
is.read(buf, len);
str = string(buf, len);
delete [] buf;
}
is.ignore(); // 跳过最后为了美观的换行符
}
// 为 PasswordInfo 类型 重载 << 操作符
// (相当于让 << 操作符 “认识” PasswordInfo 类型)
ostream& operator << (ostream& os, PasswordInfo const& pi)
{
write_string(os, pi.title);
write_string(os, pi.user_name);
write_string(os, pi.password);
write_string(os, pi.memo);
return os;
}
// 为 PasswordInfo 类型 重载 >> 操作符
// (相当于让 >> 操作符 “认识” PasswordInfo 类型)
istream& operator >> (istream& is, PasswordInfo& pi)
{
read_string(is, pi.title);
read_string(is, pi.user_name);
read_string(is, pi.password);
read_string(is, pi.memo);
return is;
}
int main()
{
PasswordInfo pi1, pi2;
pi1.title = "QQ\n(常用)";
pi1.user_name = "13788889999";
pi1.password = "ilovexxx";
pi1.memo = "2000年申请的\n(内有女神Q号)";
pi2.title = "工商银行";
pi2.user_name = "6800911111223";
pi2.password = "200101019999";
pi2.memo = "余额2个亿\n(千万别泄露!)";
char const* fn = "passwords.txt";
ofstream ofs(fn);
ofs << pi1 << pi2;
ofs.close();
PasswordInfo piA, piB;
ifstream ifs(fn);
ifs >> piA >> piB;
ifs.close();
cout << piA << "\n~~~~~~~~~~~~~~~~\n" << piB << endl;
return 0;
}
再次提醒:本例程仅为演示使用,千万不要把真实的密码数据以如此形式存储到磁盘文件。