好的,各位观众老爷们,大家好!今天咱们来聊点刺激的——如何把 C++ 的 std::cout
和 std::cin
这两位老伙计,打扮得更符合咱们的口味,让它们更听话,更懂事!
开场白:为啥要定制?
std::cout
和 std::cin
,C++ 标准库里自带的输入输出流对象,就像厨房里的刀叉碗筷,用起来方便,但总觉得缺了点个性。想象一下,你想打印一个日期,默认情况下可能就是一串数字,但你希望它显示成 "年-月-日" 的格式,是不是得自己写代码转换?又或者,你想让 std::cout
输出的布尔值不再是 0 和 1,而是 "True" 和 "False",是不是也得费一番功夫?
所以,定制 std::cout
和 std::cin
,就是为了让它们更贴合我们的需求,提高代码的可读性和可维护性。这就像给你的工具打磨得更锋利,用起来更顺手一样。
第一幕:流操作符重载——让 std::cout
和 std::cin
认识新朋友
C++ 的流操作符 <<
(插入操作符,用于 std::cout
) 和 >>
(提取操作符,用于 std::cin
),就像两扇大门,连接着数据和流。我们可以通过重载这两个操作符,让 std::cout
和 std::cin
认识我们自定义的类型,从而实现自定义类型的输入输出。
1. std::cout
的朋友:重载 <<
假设我们有一个 Point
类,表示一个二维坐标点:
#include <iostream>
class Point {
public:
int x;
int y;
Point(int x = 0, int y = 0) : x(x), y(y) {}
};
如果我们直接用 std::cout << point;
,编译器会报错,因为它不知道怎么把 Point
对象转换成可输出的格式。这时候,我们就需要重载 <<
操作符了:
#include <iostream>
class Point {
public:
int x;
int y;
Point(int x = 0, int y = 0) : x(x), y(y) {}
};
std::ostream& operator<<(std::ostream& os, const Point& point) {
os << "(" << point.x << ", " << point.y << ")";
return os;
}
int main() {
Point p(3, 5);
std::cout << "The point is: " << p << std::endl; // 输出:The point is: (3, 5)
return 0;
}
代码解读:
std::ostream& operator<<(std::ostream& os, const Point& point)
: 这就是重载<<
操作符的函数。std::ostream& os
:std::ostream
类型的引用,表示输出流对象,通常就是std::cout
。const Point& point
:Point
类型的常量引用,表示要输出的Point
对象。使用引用可以避免拷贝,提高效率。std::ostream&
: 函数返回std::ostream
类型的引用,这是为了支持链式调用,比如std::cout << p1 << p2 << std::endl;
os << "(" << point.x << ", " << point.y << ")";
: 这行代码负责把Point
对象的 x 和 y 坐标格式化成字符串,然后输出到流中。return os;
: 返回输出流对象,支持链式调用。
把 <<
操作符当成友元函数
如果 Point
类的成员变量是私有的,我们就需要把 <<
操作符重载函数声明为 Point
类的友元函数,才能访问私有成员:
#include <iostream>
class Point {
private:
int x;
int y;
public:
Point(int x = 0, int y = 0) : x(x), y(y) {}
friend std::ostream& operator<<(std::ostream& os, const Point& point);
};
std::ostream& operator<<(std::ostream& os, const Point& point) {
os << "(" << point.x << ", " << point.y << ")";
return os;
}
int main() {
Point p(3, 5);
std::cout << "The point is: " << p << std::endl; // 输出:The point is: (3, 5)
return 0;
}
2. std::cin
的朋友:重载 >>
类似地,我们也可以重载 >>
操作符,让 std::cin
认识我们的 Point
类:
#include <iostream>
class Point {
public:
int x;
int y;
Point(int x = 0, int y = 0) : x(x), y(y) {}
};
std::istream& operator>>(std::istream& is, Point& point) {
std::cout << "Enter x coordinate: ";
is >> point.x;
std::cout << "Enter y coordinate: ";
is >> point.y;
return is;
}
int main() {
Point p;
std::cout << "Enter the point coordinates:" << std::endl;
std::cin >> p;
std::cout << "The point you entered is: " << p << std::endl;
return 0;
}
代码解读:
std::istream& operator>>(std::istream& is, Point& point)
: 重载>>
操作符的函数。std::istream& is
:std::istream
类型的引用,表示输入流对象,通常就是std::cin
。Point& point
:Point
类型的引用,表示要读取数据的Point
对象。注意,这里必须使用引用,因为我们需要修改Point
对象的值。std::istream&
: 函数返回std::istream
类型的引用,同样是为了支持链式调用。
is >> point.x;
: 从输入流中读取 x 坐标,并赋值给Point
对象的 x 成员。return is;
: 返回输入流对象,支持链式调用.
错误处理:让 std::cin
更健壮
在实际应用中,用户很可能输入错误的数据,比如输入字母而不是数字。为了让 std::cin
更健壮,我们需要进行错误处理:
#include <iostream>
#include <limits> // numeric_limits
class Point {
public:
int x;
int y;
Point(int x = 0, int y = 0) : x(x), y(y) {}
};
std::istream& operator>>(std::istream& is, Point& point) {
std::cout << "Enter x coordinate: ";
if (!(is >> point.x)) {
std::cerr << "Invalid input for x coordinate." << std::endl;
is.clear(); // 清除错误标志
is.ignore(std::numeric_limits<std::streamsize>::max(), 'n'); // 忽略剩余的输入
}
std::cout << "Enter y coordinate: ";
if (!(is >> point.y)) {
std::cerr << "Invalid input for y coordinate." << std::endl;
is.clear();
is.ignore(std::numeric_limits<std::streamsize>::max(), 'n');
}
return is;
}
int main() {
Point p;
std::cout << "Enter the point coordinates:" << std::endl;
std::cin >> p;
std::cout << "The point you entered is: " << p << std::endl;
return 0;
}
代码解读:
if (!(is >> point.x))
: 判断输入是否成功。如果输入失败 (比如输入了字母),is >> point.x
会返回false
。is.clear()
: 清除错误标志,使输入流恢复正常状态。is.ignore(std::numeric_limits<std::streamsize>::max(), 'n')
: 忽略剩余的输入,直到遇到换行符。这样做可以避免错误的输入影响后续的读取。std::numeric_limits<std::streamsize>::max()
表示忽略尽可能多的字符。
第二幕:格式化输出——让 std::cout
变得更漂亮
除了重载操作符,我们还可以使用 C++ 提供的格式化工具,让 std::cout
输出更漂亮的数据。
1. 使用 std::setw
和 std::setfill
控制输出宽度和填充字符
#include <iostream>
#include <iomanip> // 包含 setw 和 setfill
int main() {
int num = 42;
std::cout << std::setw(5) << std::setfill('0') << num << std::endl; // 输出:00042
return 0;
}
代码解读:
#include <iomanip>
: 必须包含这个头文件才能使用std::setw
和std::setfill
。std::setw(5)
: 设置输出宽度为 5 个字符。如果输出的内容不足 5 个字符,则用填充字符填充。std::setfill('0')
: 设置填充字符为 ‘0’。
表格总结:常用格式化控制符
控制符 | 作用 | 示例 | 输出结果 |
---|---|---|---|
std::setw(n) |
设置输出宽度为 n 个字符 | std::cout << std::setw(5) << 10; |
10 |
std::setfill(c) |
设置填充字符为 c | std::cout << std::setfill('*') << std::setw(5) << 10; |
***10 |
std::left |
左对齐 | std::cout << std::left << std::setw(5) << 10; |
10 |
std::right |
右对齐 (默认) | std::cout << std::right << std::setw(5) << 10; |
10 |
std::fixed |
使用定点表示浮点数 | std::cout << std::fixed << std::setprecision(2) << 3.14159; |
3.14 |
std::scientific |
使用科学计数法表示浮点数 | std::cout << std::scientific << 1234.567; |
1.234567e+03 |
std::setprecision(n) |
设置浮点数的精度为 n 位 | std::cout << std::setprecision(3) << 3.14159; |
3.14 |
std::boolalpha |
将布尔值输出为 "true" 或 "false" | std::cout << std::boolalpha << true; |
true |
std::noboolalpha |
将布尔值输出为 0 或 1 (默认) | std::cout << std::noboolalpha << true; |
1 |
std::hex |
以十六进制输出整数 | std::cout << std::hex << 255; |
ff |
std::dec |
以十进制输出整数 (默认) | std::cout << std::dec << 255; |
255 |
std::oct |
以八进制输出整数 | std::cout << std::oct << 255; |
377 |
std::showbase |
显示进制前缀 (0x for hex, 0 for oct) | std::cout << std::showbase << std::hex << 255; |
0xff |
std::noshowbase |
不显示进制前缀 (默认) | std::cout << std::noshowbase << std::hex << 255; |
ff |
std::uppercase |
以大写字母显示十六进制数和科学计数法中的 ‘e’ | std::cout << std::uppercase << std::hex << 255; |
FF |
std::nouppercase |
以小写字母显示十六进制数和科学计数法中的 ‘e’ (默认) | std::cout << std::nouppercase << std::hex << 255; |
ff |
2. 使用 std::cout.precision
控制浮点数精度
#include <iostream>
int main() {
double num = 3.1415926535;
std::cout.precision(3); // 设置精度为 3 位
std::cout << num << std::endl; // 输出:3.14
return 0;
}
注意: std::cout.precision()
设置的是总的有效数字位数,而不是小数点后的位数。要控制小数点后的位数,需要结合 std::fixed
使用。
3. 使用 std::cout.flags
进行更精细的控制
std::cout.flags()
可以获取和设置 std::cout
的格式标志。虽然不如 iomanip
方便,但在某些情况下也很有用。
#include <iostream>
int main() {
std::cout.flags(std::ios::hex | std::ios::uppercase | std::ios::showbase);
std::cout << 255 << std::endl; // 输出:0XFF
std::cout.flags(std::ios::dec | std::ios::nouppercase | std::ios::noshowbase);
std::cout << 255 << std::endl; //输出:255
return 0;
}
第三幕:自定义流操作符——打造专属的输出格式
如果 C++ 提供的格式化工具还不能满足你的需求,你可以自定义流操作符,实现更复杂的格式化输出。
1. 无参数的自定义流操作符
#include <iostream>
std::ostream& tab(std::ostream& os) {
return os << 't';
}
int main() {
std::cout << "Hello" << tab << "World" << std::endl; // 输出:Hello World (中间有一个制表符)
return 0;
}
代码解读:
std::ostream& tab(std::ostream& os)
: 自定义的流操作符函数。- 参数是
std::ostream
类型的引用。 - 返回值是
std::ostream
类型的引用。
- 参数是
return os << 't'
: 向流中插入一个制表符。
2. 带参数的自定义流操作符
带参数的自定义流操作符稍微复杂一些,需要借助辅助类来实现:
#include <iostream>
class Color {
public:
enum Code {
BLACK = 30,
RED = 31,
GREEN = 32,
YELLOW = 33,
BLUE = 34,
MAGENTA = 35,
CYAN = 36,
WHITE = 37
};
Code code;
Color(Code code) : code(code) {}
};
std::ostream& operator<<(std::ostream& os, const Color& color) {
return os << "33[" << color.code << "m";
}
int main() {
std::cout << Color(Color::RED) << "This text is red." << Color(Color::WHITE) << " This text is white." << std::endl;
return 0;
}
代码解读:
Color
类:表示颜色,包含颜色代码。operator<<
: 重载<<
操作符,将颜色代码插入到输出流中。33[
和m
: 是 ANSI 转义码,用于控制终端输出的颜色。
总结:
定制 std::cout
和 std::cin
是一个强大的工具,可以帮助我们编写更清晰、更易于维护的代码。通过重载流操作符,我们可以让 std::cout
和 std::cin
认识我们自定义的类型。通过使用格式化工具和自定义流操作符,我们可以让 std::cout
输出更漂亮的数据。
记住,定制 std::cout
和 std::cin
的目的是为了提高代码的可读性和可维护性,所以要根据实际情况选择合适的定制方法。不要为了定制而定制,否则可能会适得其反。
好了,今天的讲座就到这里。希望大家能够学有所获,把 std::cout
和 std::cin
这两位老伙计,打扮得漂漂亮亮的,让它们更好地为我们服务! 谢谢大家!