加载中...
为自定义类型重载流操作
第1节:“流”的基本概念与应用
第2节:C++和C标准输入输出同步
第3节:C++和C的格式化输入输出
第4节:重温文件流和字符串流
第5节:为自定义类型重载流操作
第6节:三大关联知识点综合强化复习
第7节:从零开始,实现日志流
第8节:“我的日志流”终版
课文封面

通过“密码保存”程序,演示如何通过重载 “<<”和“>>” 操作符,以支持满足需要的用户自定义类型的数据输出与输入;包含三个演进版本。

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. 如前所述,二者都是自由函数(即:非成员函数);
  2. 二者中用到的 “流” 类型,都是 “抽象流”,这样才能满足重载操作可用于各种具体的输出或输入流;
  3. 同样如前所述,第1个入参都是流对象的引用;
  4. 第二个入参,是自定义类型的引用,但用于输出时,由于不修改对象内容,所以使用常用引用;而用于输入时,目的就是为了使用来自流的外部数据设置对象内容,所以使用引用;
  5. 返回值都是流对象的引用,即第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; }

再次提醒:本例程仅为演示使用,千万不要把真实的密码数据以如此形式存储到磁盘文件。