目录
[正确 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/4或5(整数时只输出分子)。>>支持3/4、3、1 2(空格分隔)等多种格式。
下一篇预告 :第23篇《自增/自减运算符重载:前置与后置的区别》------前置 ++obj 和后置 obj++ 如何区分?它们的效率差异在哪?为什么后置通常用前置来实现?下篇解答。