【c++面向对象编程】第22篇:输入输出运算符重载:<< 与 >> 的友元实现

目录

一、为什么不能是成员函数?

二、标准写法(两步法)

第1步:在类中声明友元函数

第2步:实现全局函数

三、为什么要返回引用?

支持链式输出

[正确 vs 错误示例](#正确 vs 错误示例)

四、为什么需要友元?能否不用友元?

五、输入运算符的注意事项

[1. 参数是 non-const 引用](#1. 参数是 non-const 引用)

[2. 需要处理输入错误](#2. 需要处理输入错误)

[3. 处理空白字符](#3. 处理空白字符)

六、完整例子:支持多种格式的输入输出

七、输入输出的链式原理

链式输出

链式输入

八、常见错误

[1. 忘记返回引用](#1. 忘记返回引用)

[2. 忘记加 const(输出运算符)](#2. 忘记加 const(输出运算符))

[3. 输入运算符没有处理错误状态](#3. 输入运算符没有处理错误状态)

[4. 把 << 和 >> 声明为成员函数](#4. 把 << 和 >> 声明为成员函数)

九、这一篇的收获


一、为什么不能是成员函数?

尝试把 operator<< 写成成员函数:

cpp

复制代码
class Point {
    int x, y;
public:
    Point(int a, int b) : x(a), y(b) {}
    
    // ❌ 如果作为成员函数
    ostream& operator<<(ostream& os) const {
        os << "(" << x << "," << y << ")";
        return os;
    }
};

int main() {
    Point p(3, 4);
    p << cout;   // 要这样调用,太别扭了!
    // 我们想要的是 cout << p
}

成员函数要求左侧操作数是 Point 对象,所以只能写成 p << cout。这不符合直觉,也不符合标准库的惯例。

正确的方式 :全局函数,左侧是 ostream&,右侧是 const Point&


二、标准写法(两步法)

第1步:在类中声明友元函数

cpp

复制代码
class Point {
    int x, y;
public:
    Point(int a, int b) : x(a), y(b) {}
    
    // 声明友元(注意不是成员函数)
    friend ostream& operator<<(ostream& os, const Point& p);
    friend istream& operator>>(istream& is, Point& p);
};

第2步:实现全局函数

cpp

复制代码
ostream& operator<<(ostream& os, const Point& p) {
    os << "(" << p.x << "," << p.y << ")";
    return os;   // 返回os引用,支持链式输出
}

istream& operator>>(istream& is, Point& p) {
    // 假设输入格式: (3,4)  或 3 4
    char ch;
    is >> ch;  // 读取 '(' 或数字
    if (ch == '(') {
        is >> p.x >> ch >> p.y >> ch;  // 读取 x , y )
    } else {
        is.putback(ch);  // 不是括号,把字符放回去
        is >> p.x >> p.y;
    }
    return is;   // 返回is引用,支持链式输入
}

三、为什么要返回引用?

支持链式输出

cpp

复制代码
Point p1(1,2), p2(3,4);
cout << p1 << ", " << p2 << endl;

编译器把这句话翻译成:

cpp

复制代码
((cout << p1) << ", ") << p2;

如果 operator<< 返回的是 ostream&(cout的引用),那么 (cout << p1) 的结果还是 cout,可以继续 << ", "。如果返回 void 或复制了一个临时对象,链式调用就会失败。

正确 vs 错误示例

cpp

复制代码
// ✅ 正确:返回引用
ostream& operator<<(ostream& os, const Point& p) {
    os << p.x << "," << p.y;
    return os;
}

// ❌ 错误:返回void,不能链式
void operator<<(ostream& os, const Point& p) { ... }

// ❌ 错误:返回值不是引用,链式时操作的是临时对象
ostream operator<<(ostream& os, const Point& p) { ... }

四、为什么需要友元?能否不用友元?

如果类提供了 getX() / getY() 等公共接口,可以不用友元:

cpp

复制代码
class Point {
    int x, y;
public:
    Point(int a, int b) : x(a), y(b) {}
    int getX() const { return x; }
    int getY() const { return y; }
};

// 不用友元
ostream& operator<<(ostream& os, const Point& p) {
    os << "(" << p.getX() << "," << p.getY() << ")";
    return os;
}

但通常用友元更好

  • 性能:避免函数调用开销(虽然编译器可能内联)

  • 语义:<< 是"输出对象状态",属于类的外部接口,友元表达这种紧密关系

  • 一致性:标准库对自定义类型的 << 通常通过友元实现(如 complex

结论:可以用 getter,但友元更直接、更高效、更符合惯例。


五、输入运算符的注意事项

1. 参数是 non-const 引用

cpp

复制代码
istream& operator>>(istream& is, Point& p)  // p 要能被修改

不能是 const Point&,因为要修改对象。

2. 需要处理输入错误

cpp

复制代码
istream& operator>>(istream& is, Point& p) {
    int x, y;
    if (is >> x >> y) {   // 读取成功才赋值
        p.x = x;
        p.y = y;
    }
    return is;
}

更好的做法是设置 failbit 或抛出异常,但初学阶段先保证基本功能。

3. 处理空白字符

>> 默认会跳过空白(空格、换行、制表符),通常不需要特殊处理。


六、完整例子:支持多种格式的输入输出

cpp

复制代码
#include <iostream>
#include <string>
#include <sstream>
using namespace std;

class Complex {
private:
    double real;
    double imag;
    
public:
    Complex(double r = 0, double i = 0) : real(r), imag(i) {}
    
    // 友元声明
    friend ostream& operator<<(ostream& os, const Complex& c);
    friend istream& operator>>(istream& is, Complex& c);
};

// 输出:支持两种格式
// 虚数部分为正:3+4i
// 虚数部分为负:3-4i
// 虚数为0:3
ostream& operator<<(ostream& os, const Complex& c) {
    if (c.imag == 0) {
        os << c.real;
    } else {
        os << c.real;
        if (c.imag > 0) os << "+";
        os << c.imag << "i";
    }
    return os;
}

// 输入:支持多种格式
// 3+4i  3-4i  3 4  3+4i   (3,4)
istream& operator>>(istream& is, Complex& c) {
    double r = 0, i = 0;
    char ch = 0;
    
    // 尝试读取一个数字
    is >> r;
    if (!is) return is;  // 读取失败
    
    // 检查下一个字符
    is >> ch;
    if (ch == '+' || ch == '-') {
        // 格式:3+4i 或 3-4i
        double imagPart;
        is >> imagPart;
        if (ch == '-') imagPart = -imagPart;
        is >> ch;  // 读取 'i'
        if (ch != 'i') is.setstate(ios::failbit);
        i = imagPart;
    } else if (ch == 'i') {
        // 格式:3i(虚数部分为3,实数部分为0)
        i = r;
        r = 0;
    } else {
        // 格式:3 4(只有两个数字)
        is.putback(ch);
        is >> i;
    }
    
    if (is) {
        c.real = r;
        c.imag = i;
    }
    return is;
}

int main() {
    Complex c1(3, 4);
    Complex c2(5, -2);
    Complex c3(7, 0);
    
    cout << "c1 = " << c1 << endl;
    cout << "c2 = " << c2 << endl;
    cout << "c3 = " << c3 << endl;
    
    cout << "\n链式输出测试: " << c1 << " | " << c2 << " | " << c3 << endl;
    
    cout << "\n请输入复数 (格式: a+bi 或 a b 或 a): ";
    Complex c4;
    cin >> c4;
    cout << "你输入了: " << c4 << endl;
    
    // 演示错误处理
    cout << "\n尝试输入无效格式: ";
    Complex c5;
    cin >> c5;
    if (cin.fail()) {
        cout << "输入失败,格式错误" << endl;
        cin.clear();  // 清除错误状态
    }
    
    return 0;
}

运行示例:

text

复制代码
c1 = 3+4i
c2 = 5-2i
c3 = 7

链式输出测试: 3+4i | 5-2i | 7

请输入复数 (格式: a+bi 或 a b 或 a): 2-3i
你输入了: 2-3i

七、输入输出的链式原理

链式输出

cpp

复制代码
cout << p1 << ", " << p2 << endl;

展开为:

cpp

复制代码
(((cout << p1) << ", ") << p2) << endl;

每一步返回 cout 本身,所以可以继续。

链式输入

cpp

复制代码
cin >> p1 >> p2;

展开为:

cpp

复制代码
(cin >> p1) >> p2;

先执行 cin >> p1,返回 cin,再执行 cin >> p2


八、常见错误

1. 忘记返回引用

cpp

复制代码
ostream operator<<(ostream& os, const Point& p) {  // 返回值不是引用
    return os;   // 复制了一个ostream,通常不可复制
}

2. 忘记加 const(输出运算符)

cpp

复制代码
ostream& operator<<(ostream& os, Point& p) {  // 应该用 const Point&
    // 不能输出 const 对象
}

3. 输入运算符没有处理错误状态

cpp

复制代码
istream& operator>>(istream& is, Point& p) {
    is >> p.x >> p.y;  // 如果读取失败,p可能处于不一致状态
    return is;
}

更好的做法:先读入临时变量,成功后再赋值。

4. 把 <<>> 声明为成员函数

cpp

复制代码
class Point {
    ostream& operator<<(ostream& os) const;  // ❌ 调用时会是 p << cout
};

九、这一篇的收获

你现在应该理解:

  • <<>> 不能是成员函数,必须是全局函数

  • 为了访问私有成员,通常需要友元(也可以用 public getter)

  • 必须返回 ostream& / istream& 才能支持链式操作

  • 输入运算符要处理错误,避免对象处于不一致状态

  • 输出运算符参数要用 const,因为输出不应修改对象

💡 小作业:为第21篇的 Rational 类完善 <<>> 运算符。<< 输出格式如 3/45(整数时只输出分子)。>> 支持 3/431 2(空格分隔)等多种格式。


下一篇预告 :第23篇《自增/自减运算符重载:前置与后置的区别》------前置 ++obj 和后置 obj++ 如何区分?它们的效率差异在哪?为什么后置通常用前置来实现?下篇解答。

相关推荐
redreamSo1 小时前
14 小时烧光 200 美金:Codex 和 Claude 的 /goal 命令打开了"放手跑"模式
前端
旷世奇才李先生1 小时前
Java虚拟线程原理与实践
java
TingTing1 小时前
Webpack5 前端工程化建设
前端
heimeiyingwang1 小时前
【架构实战】RPC框架Dubbo3.0:高性能Java通信之道
java·rpc·架构
北山有鸟1 小时前
解决香橙派没有适配ov13855的3A算法
linux·c++·相机·isp
i220818 Faiz Ul1 小时前
宠物猫之猫咖管理系统|基于java + vue宠物猫之猫咖管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·论文·毕设·宠物猫之猫咖管理系统
A不落雨滴AI1 小时前
DKERP客户端重构纪实:4天自研控件库的“短命”教训,以及为什么我坚定选择原生Qt
前端
我叫黑大帅1 小时前
通过白名单解决 pnpm i 报错 Ignored build scripts
前端·javascript·面试
风止何安啊1 小时前
用 APP 背单词太无聊?我用 Trae Solo 移动端写个小游戏来准备 6级
前端·人工智能·trae