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的配合并不理想:

image

#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;
}

由于此处无需进行虚函数解析,该程序运行结果符合预期,输出为:

image

现在,请考虑以下 main() 函数:

int main()
{
    Derived d{};
    Base& bref{ d };
    std::cout << bref << '\n';

    return 0;
}

该程序输出:

image

这可能并非我们所预期的结果。其原因在于,我们用于处理基类对象的<<运算符版本并非虚函数,因此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;
}

这将输出预期结果:

image

让我们更详细地分析其工作原理。

对于基类对象 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”。

问题解决。


更灵活的解决方案

上述方案效果良好,但存在两个潜在缺陷:

  1. 它假设所需输出可表示为单个 std::string。
  2. 我们的 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;
}

这输出:

image

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

posted @ 2026-02-04 07:43  游翔  阅读(0)  评论(0)    收藏  举报