25-11 使用operator<<打印继承类
请考虑以下使用虚函数的程序:
#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(); // will call Derived::print()
return 0;
}
到目前为止,你应该已经理解b.print()会调用Derived::print()(因为b引用的是派生类对象,基类Base::print()是虚函数,而派生类Derived::print()是重写函数)。
虽然通过这种方式调用成员函数进行输出是可行的,但此类函数与std::cout的配合并不理想:

#include <iostream>
int main()
{
Derived d{};
Base& b{ d };
std::cout << "b is a ";
b.print(); // messy, we have to break our print statement to call this function
std::cout << '\n';
return 0;
}
在本节课中,我们将探讨如何通过继承为类重写operator<<运算符,从而能够按预期使用operato<<运算符,例如:
std::cout << "b is a " << b << '\n'; // much better
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;
}
由于此处无需进行虚函数解析,该程序运行结果符合预期,输出为:

现在,请考虑以下 main() 函数:
int main()
{
Derived d{};
Base& bref{ d };
std::cout << bref << '\n';
return 0;
}
该程序输出:

这可能并非我们所预期的结果。其原因在于,我们用于处理基类对象的<<运算符版本并非虚函数,因此std::cout << bref会调用处理基类对象的operator<<运算符版本,而非派生类对象的版本。
挑战正在于此。
能否将operator<<设为虚函数?
如果问题在于operator<<运算符不是虚函数,难道不能直接将其设为虚函数吗?
简短的回答是:不行。原因有以下几点:
首先,只有成员函数才能被虚拟化——这合乎逻辑,因为只有类才能从其他类继承,而无法覆盖类外部的函数(非成员函数可以重载,但不能被覆盖)。由于我们通常将operator<<运算符实现为友元函数,而友元函数不被视为成员函数,因此友元版本的operator<<运算符不符合虚拟化条件。(关于为何采用此种方式实现operator<<运算符,请参阅第21.5课——使用成员函数重载运算符)。
其次,即使能将operator<<虚拟化,仍存在参数差异问题:基类Base::operator<<接受Base参数,派生类Derived::operator<<则接受Derived参数。因此派生版本无法被视为基类的重写,自然不符合虚拟函数解析条件。
那么程序员该如何应对?
解决方案
答案其实出人意料地简单。
首先,我们照常在基类中将operator<<运算符设为友元。但不同于让operator<<运算符直接决定输出内容,我们将让它调用可虚拟化的普通成员函数!这个虚函数将负责为每个类确定具体输出内容。
在首个解决方案中,我们的虚成员函数(命名为identify())返回std::string类型,该字符串由Base::operator<<进行输出:
#include <iostream>
class Base
{
public:
// Here's our overloaded operator<<
friend std::ostream& operator<<(std::ostream& out, const Base& b)
{
// Call virtual function identify() to get the string to be printed
out << b.identify();
return out;
}
// We'll rely on member function identify() to return the string to be printed
// Because identify() is a normal member function, it can be virtualized
virtual std::string identify() const
{
return "Base";
}
};
class Derived : public Base
{
public:
// Here's our override identify() function to handle the Derived case
std::string identify() const override
{
return "Derived";
}
};
int main()
{
Base b{};
std::cout << b << '\n';
Derived d{};
std::cout << d << '\n'; // note that this works even with no operator<< that explicitly handles Derived objects
Base& bref{ d };
std::cout << bref << '\n';
return 0;
}
这将输出预期结果:

让我们更详细地分析其工作原理。
对于基类对象 b,调用 operator<< 时参数 b 指向基类对象。因此虚拟函数调用 b.identify() 解析为 Base::identify(),该函数返回“Base”进行打印。此处并无特殊之处。
对于派生类对象 d,编译器首先查找是否存在接受派生类对象的 operator<<。由于我们未定义该运算符,编译器接着检查是否存在接受基类对象的<<运算符。存在该运算符时,编译器会将派生对象隐式向上转换为Base&类型并调用函数(虽然我们可手动执行此向上转换,但编译器在此提供了便利)。由于参数b引用的是派生对象,虚函数调用b.identify()将解析为Derived::identify(),该函数返回“Derived”进行打印。
请注意:我们无需为每个派生类单独定义operator<<运算符!处理基类对象的版本既适用于基类对象,也适用于任何从基类派生的类!
第三种情况融合了前两种特性:编译器首先将变量bref与接受基类引用参数的operator<<运算符匹配。由于参数b指向派生类对象,b.identify()解析为Derived::identify(),最终返回“Derived”。
问题解决。
更灵活的解决方案
上述方案效果良好,但存在两个潜在缺陷:
- 它假设所需输出可表示为单个 std::string。
- 我们的 identify() 成员函数无法访问流对象。
后者在需要流对象的情况下会引发问题,例如当我们需要打印具有重载 operator<< 的成员变量值时。
所幸只需简单修改即可解决这两个问题。在旧版本中,虚函数 identify() 返回字符串由 Base::operator<< 负责打印。新版本则定义虚成员函数 print(),将打印责任直接委托给该函数。
以下示例说明了该思路:
#include <iostream>
class Base
{
public:
// Here's our overloaded operator<<
friend std::ostream& operator<<(std::ostream& out, const Base& b)
{
// Delegate printing responsibility for printing to virtual member function print()
return b.print(out);
}
// We'll rely on member function print() to do the actual printing
// Because print() is a normal member function, it can be virtualized
virtual std::ostream& print(std::ostream& out) const
{
out << "Base";
return out;
}
};
// Some class or struct with an overloaded 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 now has an Employee member
public:
Derived(const Employee& e)
: m_e{ e }
{
}
// Here's our override print() function to handle the Derived case
std::ostream& print(std::ostream& out) const override
{
out << "Derived: ";
// Print the Employee member using the stream object
out << m_e;
return out;
}
};
int main()
{
Base b{};
std::cout << b << '\n';
Derived d{ Employee{"Jim", 4}};
std::cout << d << '\n'; // note that this works even with no operator<< that explicitly handles Derived objects
Base& bref{ d };
std::cout << bref << '\n';
return 0;
}
这输出:

在此版本中,Base::operator<< 自身并不执行任何打印操作。它仅调用虚拟成员函数 print() 并向其传递流对象。随后 print() 函数利用该流对象执行自身的打印操作。基类 Base::print() 使用流对象输出“Base”。更有趣的是,派生类 Derived::print() 不仅使用流对象输出“Derived: ”,还会调用 Employee::operator<< 输出成员变量 m_e 的值。在之前的示例中实现后者会困难得多!


浙公网安备 33010602011771号