7. 重载输入输出运算符:让自定义类型也能流式读写

说实话,刚学C++那会儿,我总觉得cout << myObject这种写法很神奇。为什么int、double可以直接打印,我自己写的类就不行?

答案其实很简单——<<>>也是运算符,它们可以被重载。今天我们就来聊聊怎么给你的自定义类型加上流式输入输出的能力。

为什么需要重载<<和>>?

你想想看,一个类里可能有十几个成员变量。每次调试都要写一堆getX()getY(),再拼成字符串打印出来,多累啊。

我在项目中遇到过这样一个场景:一个Person类有姓名、年龄、地址、电话……每次打印日志都要写个toString()方法。后来我直接重载了<<,一行cout << person搞定所有信息。嗯,真香。

基本语法:友元函数是首选

重载<<>>有个特殊之处——它们不能是成员函数。为什么?

因为左操作数必须是流对象(ostreamistream),而不是你的类对象。所以,我们通常把它们声明为友元函数

核心要点:流运算符重载必须是非成员函数,通常声明为类的友元。

来看一个完整的例子:

#include <iostream>
#include <string>

class Person {
private:
    std::string name;
    int age;
    double height;

public:
    Person(const std::string& n, int a, double h)
        : name(n), age(a), height(h) {}

    // 声明为友元,方便访问私有成员
    friend std::ostream& operator<<(std::ostream& os, const Person& p);
    friend std::istream& operator>>(std::istream& is, Person& p);
};

// 输出运算符重载
std::ostream& operator<<(std::ostream& os, const Person& p) {
    os << "姓名: " << p.name
       << ", 年龄: " << p.age
       << ", 身高: " << p.height << "m";
    return os;
}

// 输入运算符重载
std::istream& operator>>(std::istream& is, Person& p) {
    std::cout << "请输入姓名、年龄、身高(用空格分隔): ";
    is >> p.name >> p.age >> p.height;
    return is;
}

int main() {
    Person p("张三", 28, 1.75);
    std::cout << p << std::endl;  // 输出: 姓名: 张三, 年龄: 28, 身高: 1.75m

    Person p2("", 0, 0.0);
    std::cin >> p2;                // 从键盘读取
    std::cout << p2 << std::endl;  // 打印刚输入的内容
    return 0;
}

返回值为什么是引用?

你可能注意到了,两个函数都返回了std::ostream&std::istream&。这是为了支持链式调用

比如:cout << a << b << c,实际上等价于((cout << a) << b) << c。每次<<返回的还是同一个cout对象,才能继续往下拼接。

个人习惯:我写输出运算符时,一定会先写return os;这行,再写中间的逻辑。这样不容易忘记返回值。

输入运算符的陷阱

重载>><<要小心得多。我曾经踩过一个坑——用户输入格式不对,程序直接崩了。

来看一个更健壮的版本:

std::istream& operator>>(std::istream& is, Person& p) {
    // 先读取,再检查
    std::string n;
    int a;
    double h;

    if (is >> n >> a >> h) {
        // 读取成功,才赋值
        p.name = n;
        p.age = a;
        p.height = h;
    } else {
        // 读取失败,设置流为失败状态
        is.clear(std::ios::failbit);
    }

    return is;
}

避坑指南:千万不要在输入运算符里直接is >> p.name而不做错误检查。我曾经因为用户输入了字母而不是数字,导致后续所有读取都乱套了。一定要先读取到临时变量,确认成功后再赋值。

格式化输出:让输出更漂亮

有时候我们想要控制输出的格式,比如对齐、精度等。可以在<<运算符里使用流操纵符:

std::ostream& operator<<(std::ostream& os, const Person& p) {
    // 保存原来的格式
    std::ios_base::fmtflags old_flags = os.flags();
    std::streamsize old_precision = os.precision();

    os << std::left << std::setw(10) << p.name
       << " | " << std::right << std::setw(3) << p.age
       << " | " << std::fixed << std::setprecision(2) << p.height;

    // 恢复原来的格式
    os.flags(old_flags);
    os.precision(old_precision);

    return os;
}

嗯,这里要注意:修改流的格式会影响后续所有输出。所以我习惯在函数开头保存原始格式,结束前恢复。这样不会污染调用者的流状态。

知识结构图

下面这张图帮你理清重载输入输出运算符的核心逻辑:

流运算符重载核心结构 流运算符重载 operator<< (输出) 返回 ostream& 支持链式调用 operator>> (输入) 先读临时变量 检查失败状态 必须声明为友元函数(非成员)

常见问题与最佳实践

场景 推荐做法 不推荐做法
输出私有成员 声明为友元函数 写一堆public的getter
输入错误处理 先读临时变量,检查成功再赋值 直接读成员变量,不检查
格式化输出 保存/恢复流的格式状态 直接修改流格式不恢复
链式调用 返回流引用 返回值(会拷贝流对象)

我建议:如果你的类需要序列化到文件或网络,可以考虑用<<>>实现简单的序列化。但要注意,这种方式不适合二进制数据,只适合文本格式。

总结

重载输入输出运算符其实不复杂,记住三个要点:

  • 必须是友元函数,不能是成员函数
  • 返回流引用,支持链式调用
  • 输入要谨慎,先读临时变量再赋值

我个人觉得,给自定义类型加上流式输入输出,是C++里性价比最高的特性之一。写一次,到处用,调试、日志、序列化都能受益。你想想看,是不是这个理?

公众号:蓝海资料掘金营,微信deep3321