什么是流?什么是流对象?我们在打印C++程序的时候肯定用到过std::cout这个东西,std::cout就是一个流对象,注意说法,它是一个对象,是在标准库std中就初始化定义好的一个对象,如果去看std命名空间源码,可以发现类似下面这样的一段代码
namespace std {
extern ostream cout; // 标准输出流对象
}
那我们经常使用的#include <iostream>头文件又和它有什么关系呢?
它作为输入输出流头文件,引入 C++ 的标准输入输出库iostream。在iostream这个库中定义了用于输入输出操作的类和对象,ru std::cin、std::cout、std::cerr等,这些对象用于处理标准输入、标准输出和标准错误输出。iostream库是 C++ 标准库的一部分,提供了与控制台输入输出流相关的功能。引入这个库后,程序中才可以使用这些标准流对象。
iostream中定义的全局对象,例如 std::cin、std::cout、std::cerr,这些对象是 ostream 和 istream 类的实例,用于处理输入和输出操作。具体来说:
std::cout
是ostream类的一个对象,用于向标准输出(通常是控制台)写数据。std::cin
是istream类的一个对象,用于从标准输入(通常是键盘)读取数据。std::cerr
是ostream类的另一个对象,用于输出错误信息。
这些对象都在iostream库中声明,并属于标准命名空间std,因此完整的名称是std::cin、std::cout、std::cerr。这也是为什么我们会说它是标准输出流,在代码中的使用要么是std::cout,要么需要加上using namespace std;的原因呢。
我们知道了流对象,那进一步来看看什么是流?
基础理解:流的概念
在 C++ 中,流(stream)是一种处理数据的方式。可以将它简单地理解为一种数据的“流动”:
- 输入流(
istream
):从外部设备(如键盘、文件)“流入”程序。 - 输出流(
ostream
):从程序中“流出”到外部设备(如屏幕、文件)。
以我目前的理解,流都是基于程序来操作的。
对照着上面提到:
std::cout
是ostream类的一个对象,用于向标准输出(通常是控制台)写数据。std::cin
是istream类的一个对象,用于从标准输入(通常是键盘)读取数据。std::cerr
是ostream类的另一个对象,用于输出错误信息。
我们应该有了一个初步的理解,这样我们再来看看我们的打印程序
#include <iostream>
int main() {
std::cout << "Hello, world!" << std::endl; // 直接使用 std::
return 0;
}
上面的程序中还有一个操作符<<,它又是什么?
上面的代码里<<是 C++ 中的流插入操作符,它的主要作用是将右侧的值插入到左侧的输出流中。就是将"Hello, world!"插入到std::cout中。实际上<<本身作为一个操作符,是被函数重载用于流的插入操作的。
<<操作符的逻辑非常简单:
- 左侧的对象是一个输出流对象(如
std::cout
),它管理着数据的输出方向。 - 右侧的对象是要输出的数据。
- 操作符<<将右边的数据插入到左边的输出流中,并返回左侧的输出流对象的引用,以便支持链式调用。
既然是函数重载出来,那么
std::cout << "Hello, world!" << std::endl;
在“语法”上等价于 (注意我说的是在语法上等价于)
std::cout.operator<<("Hello").operator<<(std::endl);
这句代码实际上打印出来的是流的地址,具体感兴趣的小伙伴可以自行研究。
自定义重载<<操作符
<<作为重载操作符,我们当然也可以重载,允许我们自定义如何将某种类型的对象插入到流中。例如:
#include <iostream>
class Point {
public:
int x, y;
Point(int x, int y) : 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, 4);
std::cout << "Point is: " << p << std::endl;
return 0;
}
对<<进行操作,我们需要两个操作数,左操作数和右操作数,即两个参数。我们可以通过重载 <<打印我们的自定义类型。
上面例子中的重载是作为非成员函数进行的,那为什么不用成员函数的方式呢?有什么区别呢?
首先类的成员函数都有一个隐藏指针this,它指向调用该成员函数的对象。所以,当你重载操作符时,如果使用成员函数的形式,this指针会作为隐含的第一个参数传递给该函数。
这就是为什么当你将 <<
操作符作为成员函数重载时,调用方式会变成 object<<stream,而不是 stream<<object,因为成员函数的第一个参数是this指针(指向对象的引用),而不是像我们希望的那样,第一个参数是流对象std::ostream。
当你在类中定义operator<<作为成员函数时,它的调用方式会与我们期望的std::cout<<object不同。原因是,this指针始终是左操作数,因此你只能使用object<<std::cout这样的语法。
这显然不符合我们想要的操作顺序。我们希望的语法是std::cout<<object,其中流对象std::cout在左边,而object对象在右边。但如果<<操作符是成员函数,那么必须以object对象为左操作数,流对象作为参数,这显然与常规用法相冲突。
再来看这样一段代码
#include <iostream>
#include <iomanip>
using namespace std;
ostream &output(ostream &stream)
{
stream.setf(ios::left);
stream << setw(10) << hex << setfill('-');
return stream;
}
int main() {
cout << 123 << endl;
cout << output << 123 << endl;
return 0;}
这个函数output是一个自定义的输出函数,具体作用是对传入的输出流对象进行一些设置,那么主函数中的 cout << output << 123 << endl;具体执行逻辑是怎样的呢?
-
调用
output
函数:- 当遇到 cout << output 时,C++ 语言会自动调用output 函数,并将cout 作为参数传递给它。
- 这个过程相当于:
output(cout);
-
执行
output
函数:- 在
output
函数内部,stream(这里是cout
)会被进行设置。 output
函数会对cout
进行相应的格式设置后,返回cout
对象的引用。
- 在
-
继续流插入操作:
- 由于
output
返回的是ostream
的引用,因此 cout << output 这部分实际上被替换为 cout (经过格式设置的流)。 - 然后,紧接着执行
<< 123
,这意味着将123
插入到经过格式化的cout
中。
- 由于
-
输出
endl
:- 最后,执行
<< endl
,这个操作会插入换行符并刷新流。
- 最后,执行
cout << output << 123 << endl;
//等价于
output(cout) << 123 << endl;
这是C++ 中的「操纵符(manipulator)」行为。虽然看起来像是 output
函数被插入到 cout
中进行输出,实际上是因为 output
函数的类型和返回值恰好匹配了操纵符的机制。
当你编写以下代码
std::cout << output;
其中的 output
是一个函数,它接受一个 std::ostream&
参数,并返回一个 std::ostream&
,这符合操纵符的特性。因此,编译器将其视为一个操纵符,而不是将 output
函数本身输出到流中。