7. 重载输入输出运算符:让自定义类型也能流式读写
说实话,刚学C++那会儿,我总觉得cout << myObject这种写法很神奇。为什么int、double可以直接打印,我自己写的类就不行?
答案其实很简单——<<和>>也是运算符,它们可以被重载。今天我们就来聊聊怎么给你的自定义类型加上流式输入输出的能力。
为什么需要重载<<和>>?
你想想看,一个类里可能有十几个成员变量。每次调试都要写一堆getX()、getY(),再拼成字符串打印出来,多累啊。
我在项目中遇到过这样一个场景:一个Person类有姓名、年龄、地址、电话……每次打印日志都要写个toString()方法。后来我直接重载了<<,一行cout << person搞定所有信息。嗯,真香。
基本语法:友元函数是首选
重载<<和>>有个特殊之处——它们不能是成员函数。为什么?
因为左操作数必须是流对象(ostream或istream),而不是你的类对象。所以,我们通常把它们声明为友元函数。
核心要点:流运算符重载必须是非成员函数,通常声明为类的友元。
来看一个完整的例子:
#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;
}
嗯,这里要注意:修改流的格式会影响后续所有输出。所以我习惯在函数开头保存原始格式,结束前恢复。这样不会污染调用者的流状态。
知识结构图
下面这张图帮你理清重载输入输出运算符的核心逻辑:
常见问题与最佳实践
| 场景 | 推荐做法 | 不推荐做法 |
|---|---|---|
| 输出私有成员 | 声明为友元函数 | 写一堆public的getter |
| 输入错误处理 | 先读临时变量,检查成功再赋值 | 直接读成员变量,不检查 |
| 格式化输出 | 保存/恢复流的格式状态 | 直接修改流格式不恢复 |
| 链式调用 | 返回流引用 | 返回值(会拷贝流对象) |
我建议:如果你的类需要序列化到文件或网络,可以考虑用<<和>>实现简单的序列化。但要注意,这种方式不适合二进制数据,只适合文本格式。
总结
重载输入输出运算符其实不复杂,记住三个要点:
- 必须是友元函数,不能是成员函数
- 返回流引用,支持链式调用
- 输入要谨慎,先读临时变量再赋值
我个人觉得,给自定义类型加上流式输入输出,是C++里性价比最高的特性之一。写一次,到处用,调试、日志、序列化都能受益。你想想看,是不是这个理?