使用operator <<打印继承的类
考虑以下使用虚函数的程序:
class Base
{
public:
Base() {}
virtual void print() const { std::cout << "Base"; }
};
class Derived : public Base
{
public:
Derived() {}
virtual void print() const override { std::cout << "Derived"; }
};
int main()
{
Derived d;
Base &b = d;
b.print(); // 将调用Derived :: print()
return 0;
}
到目前为止,您应该对b.print()将调用Derived :: print()这一事实感到满意(因为b指向Derived类对象,Base :: print()是一个虚函数,并且Derived :: print()是一个覆盖)。
虽然调用这样的成员函数来输出是可以的,但这种函数风格与std :: cout不能很好地混合:
#include <iostream>
int main()
{
Derived d;
Base &b = d;
std::cout << "b is a ";
b.print(); // 麻烦,我们必须打破我们的print语句来调用这个函数
std::cout << '\n';
return 0;
}
在本课中,我们将介绍如何使用继承覆盖operator << ,以便我们可以按预期使用operator <<,如下所示:
std::cout << "b is a " << b << '\n'; // 更好一点
operator<<的挑战
让我们从典型的方式重载operator <<开始:
#include <iostream>
class Base
{
public:
Base() {}
virtual void print() const { std::cout << "Base"; }
friend std::ostream& operator<<(std::ostream &out, const Base &b)
{
out << "Base";
return out;
}
};
class Derived : public Base
{
public:
Derived() {}
virtual void print() const override { std::cout << "Derived"; }
friend std::ostream& operator<<(std::ostream &out, const Derived &d)
{
out << "Derived";
return out;
}
};
int main()
{
Base b;
std::cout << b << '\n';
Derived d;
std::cout << d << '\n';
return 0;
}
因为这里不需要虚函数解析,所以这个程序按照我们的预期工作,并打印:
Base
Derived
现在,请考虑以下main()函数:
int main()
{
Derived d;
Base &bref = d;
std::cout << bref << '\n';
return 0;
}
该程序打印:
Base
那可能不是我们所期待的。发生这种情况是因为我们处理Base对象的operator <<版本不是虚拟的,所以std :: cout << bref调用处理Base对象而不是Derived对象的operator <<版本。
这就是挑战。
我们可以让operator<<虚拟?
如果这个问题是operator<<不是虚拟的,我们不能简单地将它变为虚拟吗?
最简洁的答案是不。有许多的原因。
首先,只有成员函数才能被虚拟化 - 这是有道理的,因为只有类可以从其他类继承,并且没有办法覆盖一个存在于类之外的函数(你可以重载非成员函数,但不能覆盖它们) )。因为我们通常将operator <<实现为友元函数,而友元函数不被视为成员函数,所以operator <<的友元函数版本不具有虚拟化的资格。(有关我们为何以这种方式实现运算符的回顾,请重新阅读前面使用成员函数重载运算符)。
其次,即使我们可以虚拟化operator<<存在Base :: operator <<和Derived :: operator <<的函数参数不同的问题(Base版本将采用Base参数,Derived版本将采用Derived参数)。因此,Derived版本不会被视为Base版本的覆盖,因此不适合虚拟函数解析。
那么程序员要做什么?
解决方案
事实证明,答案非常简单。
首先,我们像往常一样在我们的基类中设置operator <<作为友元函数。但是,我们不是让operator<<自己进行打印,而是将该责任委托给可以虚拟化的普通成员函数!
这是完整的解决方案:
#include <iostream>
class Base
{
public:
Base() {}
// 在这里是我们重载的operator<<
friend std::ostream& operator<<(std::ostream &out, const Base &b)
{
// 将打印的责任委派给成员函数print()
return b.print(out);
}
// 我们将依靠成员函数print()来进行实际打印
//由于print是普通的成员函数,因此可以进行虚拟化
virtual std::ostream& print(std::ostream& out) const
{
out << "Base";
return out;
}
};
class Derived : public Base
{
public:
Derived() {}
// 这是我们的覆盖打印功能来处理Derived案例
virtual std::ostream& print(std::ostream& out) const override
{
out << "Derived";
return out;
}
};
int main()
{
Base b;
std::cout << b << '\n';
Derived d;
std::cout << d << '\n'; // 请注意,即使没有显式处理派生对象的operator<<
Base &bref = d;
std::cout << bref << '\n';
return 0;
}
以上程序适用于所有三种情况:
Base
Derived
Derived
让我们更详细地研究一下。
首先,在Base的情况下,我们调用operator <<,它调用虚函数print()。由于我们的Base引用参数指向Base对象,因此b.print()解析为Base :: print(),它执行打印。这里没什么特别的。
在Derived案例中,编译器首先查看是否存在带有Derived对象的operator <<。没有一个,因为我们没有定义一个。接下来,编译器会查看是否有一个带有Base对象的operator <<。有,所以编译器将我们的Derived对象隐式向上转换为Base&并调用该函数(我们可以自己完成这个upcast,但编译器在这方面很有帮助)。然后该函数调用virtual print(),它解析为Derived :: print()。
请注意,我们不需要为每个派生类定义operator<!处理Base对象的版本适用于Base对象和从Base派生的任何类!
第三种情况是前两种情况的混合。首先,编译器将变量bref与带有Base的operator <<匹配。这会调用我们的虚拟print()函数。由于Base引用实际上指向Derived对象,因此我们按照预期解析为Derived :: print()。
问题解决了。