18.11 - 使用 << 运算符打印继承类
Key Takeaway
下面这个程序使用了虚函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
b.print()
会调用 Derived::print()
(因为 b
指向 Derived
类型的对象 object,Base::print()
是一个虚函数,而且 Derived::print()
是重写函数)。
虽然调用print()
这样的成员函数来执行输出是可以的,但这种类型的函数不能很好地与std::cout
一起使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
在这节课中,我们会学习如何使用 operator<<
打印继承类的信息,使我们可以使用下面的风格来使用:
1 |
|
挑战
首先我们使用重载的方式实现 operator<<
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
|
由于不需要虚函数解析,所以程序能够正确地工作并打印:
1 2 |
|
再考虑下面的 main()
:
1 2 3 4 5 6 7 8 |
|
打印结果:
1 |
|
显然输出结果不是我们想要的。当配合Base
使用非虚的 operator<<
时,std::cout << bref
会调用基类的 operator<<
。
这就是我们要面临的挑战。
Operator << 可以是虚函数吗?
如果问题的原因在于 operator<<
不是虚函数,那么可以把它设为 virtual
吗?
不行!而且有很多原因:
首先,只有成员函数可以被虚拟化——这是有意义的,因为只有类可以从其他类继承,没有办法重写存在于类外部的函数(可以重载非成员函数,但不能重写它们)。因为我们通常将操作符<<
实现为友元,而友元不被视为成员函数,所以操作符<<的友元版本不符合称为虚函数的条件。(要了解为什么要以这种方式实现操作符<<,请看14.5 -使用成员函数重载运算符)。
其次,即使我们可以把 operator<<
定义为虚函数,也存在 Base::operator<<
和 Derived::operator<<
的函数形参不同的问题(Base版本将接受Base形参,而Derived版本将接受Derived形参)。因此,Derived
版本不会被认为是Base
版本的重写,因此不符合虚函数解析的条件。
那么应该怎么做呢?
解决办法
答案非常简单。
首先,在类中添加友元函数 operator<<
。但是,不要让 operator<<
自己否则实际的打印,而是将这打印工作委托给一个可以被虚化的普通成员函数!
以下是有效的解决方案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
|
三条语句都能正确执行:
1 2 3 |
|
让我们更详细地研究一下如何做到这一点。
首先,对于 Base
的例子,调用 operator<<
时会调用虚函数 print()
。因为的Base
引用参数指向一个Base
对象,b.print()
解析为Base::print()
并执行打印。这里没什么特别的。
在对于Derived
的例子,编译器首先查看是否有接受 Derived
对象的 <<
。没有,因为我们没有定义,接下来,编译器查看是否有接受Base
对象的<<
。有,所以编译器将Derived
对象隐式上转换为Base&
并调用函数,然后调用虚print()
,解析为Derived::print()
。
注意,我们不需要为每个派生类定义operator<<
,处理 Base
对象的版本对Base
及其派生的任何类可用!
第三种情况是前两种情况的混合。首先,编译器将变量bref
与带有 Base 参数的 operator<<
匹配。它调用了虚函数print()
。因为Base
引用实际上是指向一个Derived
对象,所以它解析为Derived::print()
,正如我们期望的那样。
问题搞定!