关于流和流对象,以std::cout为例

什么是流?什么是流对象?我们在打印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 函数本身输出到流中。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值