请先观察下面这段使用了虚函数的程序:
#include <iostream>
class Base
{
public:
virtual void print() const { std::cout << "Base"; }
};
class Derived : public Base
{
public:
void print() const override { std::cout << "Derived"; }
};
int main()
{
Derived d{};
Base& b{ d };
b.print(); // 将调用 Derived::print()
return 0;
}
至此,你已应熟悉:由于 b
引用的是 Derived
对象,Base::print()
为虚函数且 Derived::print()
为覆盖,因此 b.print()
会调用 Derived::print()
。
虽然通过成员函数输出尚可接受,但这种风格与 std::cout
并不协调:
#include <iostream>
int main()
{
Derived d{};
Base& b{ d };
std::cout << "b is a ";
b.print(); // 写法凌乱,必须中断输出语句
std::cout << '\n';
return 0;
}
本课将展示如何在继承体系中正确重载 operator<<
,使我们能够像下面这样自然地使用:
std::cout << "b is a " << b << '\n'; // 简洁得多
operator« 面临的挑战
先按“常规方式”重载 operator<<
:
#include <iostream>
class Base
{
public:
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:
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
的版本。
能否把 operator« 设为虚函数?
若问题出在 operator<<
不是虚函数,是否可直接将其设为虚?答案是否定的,原因有二:
只有成员函数才能声明为虚——这很合理,因为只有类才能继承,而类外函数无法被覆盖(只能重载)。我们通常把
operator<<
实现为友元,而友元并非成员函数,因此无法声明为虚。(如需回顾为何如此实现,请回看在《使用成员函数重载运算符》。)即使可以声明为虚,
Base::operator<<
与Derived::operator<<
的参数类型不同(前者接受Base
,后者接受Derived
),因此Derived
版本不被视为对Base
版本的覆盖,同样无法利用虚决议。
那么,程序员该如何做?
一种解决方案
答案出奇地简单:
- 照例在基类中将
operator<<
设为友元; - 不让
operator<<
决定输出内容,而是让它调用一个可声明为虚的普通成员函数,由该虚函数负责输出。
在第一种实现里,我们把虚成员函数命名为 identify()
,它返回 std::string
,再由 Base::operator<<
打印:
#include <iostream>
class Base
{
public:
// 重载 operator<<
friend std::ostream& operator<<(std::ostream& out, const Base& b)
{
out << b.identify(); // 调用虚函数获取待输出字符串
return out;
}
// 普通成员函数,可设为虚
virtual std::string identify() const
{
return "Base";
}
};
class Derived : public Base
{
public:
// 覆盖 identify()
std::string identify() const override
{
return "Derived";
}
};
int main()
{
Base b{};
std::cout << b << '\n';
Derived d{};
std::cout << d << '\n'; // 无需显式为 Derived 重载 operator<<
Base& bref{ d };
std::cout << bref << '\n';
return 0;
}
输出:
Base
Derived
Derived
深入考察其工作过程:
Base b
:调用operator<<(ostream&, const Base&)
,参数b
引用Base
对象,虚调用b.identify()
解析到Base::identify()
,返回"Base"
。Derived d
:编译器首先寻找接受Derived
的operator<<
,未找到;随后发现接受Base&
的版本,于是对d
执行隐式向上转型并调用之。因参数b
实际引用Derived
,虚调用解析到Derived::identify()
,返回"Derived"
。Base& bref{ d }
:同上,最终输出"Derived"
。
注意:我们无需为每个派生类再写 operator<<
!处理 Base
的版本对 Base
及其所有派生类均适用。
更灵活的解决方案
上述方案已足够,但仍有两点潜在不足:
- 假设输出能用一个
std::string
表示; identify()
无法拿到流对象,因而无法利用流的特性(例如打印拥有自定义operator<<
的成员变量)。
稍作修改即可解决这两处限制:不再让虚函数返回字符串,而是把打印职责直接交给虚函数 print()
,并把流对象传给它。
示例:
#include <iostream>
class Base
{
public:
friend std::ostream& operator<<(std::ostream& out, const Base& b)
{
return b.print(out); // 把打印任务完全委托给虚函数
}
virtual std::ostream& print(std::ostream& out) const
{
out << "Base";
return out;
}
};
// 具有重载 operator<< 的结构体
struct Employee
{
std::string name{};
int id{};
friend std::ostream& operator<<(std::ostream& out, const Employee& e)
{
out << "Employee(" << e.name << ", " << e.id << ")";
return out;
}
};
class Derived : public Base
{
private:
Employee m_e{}; // Derived 拥有一个 Employee 成员
public:
Derived(const Employee& e)
: m_e{ e }
{
}
std::ostream& print(std::ostream& out) const override
{
out << "Derived: ";
// 利用流对象输出 Employee 成员
out << m_e;
return out;
}
};
int main()
{
Base b{};
std::cout << b << '\n';
Derived d{ Employee{"Jim", 4} };
std::cout << d << '\n';
Base& bref{ d };
std::cout << bref << '\n';
return 0;
}
输出:
Base
Derived: Employee(Jim, 4)
Derived: Employee(Jim, 4)
此版本中,Base::operator<<
自身不再打印任何内容,只负责调用虚函数 print()
并将流对象传入。
Base::print()
用流输出"Base"
;Derived::print()
用同一流先输出"Derived: "
,再调用Employee::operator<<
打印成员m_e
,若沿用前一方案则较难优雅实现。