章节目录

使用运算符<<打印继承的类

本节阅读量:

考虑使用虚函数的以下程序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#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.print() 会调用 Derived::print()(因为 b 引用的是 Derived 对象,Base::print() 是虚函数,而 Derived::print() 是派生类中的重写实现)。

虽然这样调用成员函数进行输出是可行的,但这类函数与 std::cout 搭配起来并不自然。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <iostream>

int main()
{
	Derived d{};
	Base& b{ d };

	std::cout << "b is a ";
	b.print(); // 这里使用我们自己的打印函数,中断了cout
	std::cout << '\n';

	return 0;
}

在本课中,我们将研究如何为使用继承的类重写运算符«,如下所示:

1
std::cout << "b is a " << b << '\n'; // 更好的版本

使用「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
#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;
}

因为这里不需要虚函数解析,所以该程序按预期工作,并打印:

1
2
Base
Derived

现在,考虑下面的main()函数:

1
2
3
4
5
6
7
8
int main()
{
    Derived d{};
    Base& bref{ d };
    std::cout << bref << '\n';

    return 0;
}

这个程序打印:

1
Base

这可能不是我们期望的结果。发生这种情况是因为 operator« 不是虚函数,因此调用的是 Base 对象对应的函数,而不是 Derived 对象对应的函数。

这就是挑战所在。


可以使「operator«」成为虚函数吗?

我们能简单地把 operator« 变成虚函数吗?

简而言之,答案是否定的。

这有许多原因。

首先,只有成员函数可以是虚函数——这是合理的,因为只有类可以从其他类继承,而类外函数无法被重写(您可以重载非成员函数,但不能重写它们)。由于我们通常将 operator« 实现为友元函数,而友元函数不被视为成员函数,因此友元版本的 operator« 不符合成为虚函数的条件。

其次,即使我们可以让 operator« 成为虚函数,也会遇到另一个问题:Base::operator« 和 Derived::operator« 的函数参数不同(Base 版本接收 Base 参数,而 Derived 版本接收 Derived 参数)。因此,Derived 版本不会被视为 Base 版本的重写,也就无法参与虚函数解析。

那么,应该做什么呢?


一个解决方案

答案出奇地简单。

首先,我们像往常一样在基类中把 operator« 设置为友元。但不要让 operator« 直接决定打印什么,而是让它调用一个可以成为虚函数的普通成员函数!这个虚函数负责决定每个类要打印的内容。

在这个解决方案中,成员虚函数(这里称为 identify())返回一个 std::string,然后由 Base::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
#include <iostream>

class Base
{
public:
	// 新的 operator<<
	friend std::ostream& operator<<(std::ostream& out, const Base& b)
	{
		// 调用虚函数 identify() 获取要打印的信息
		out << b.identify();
		return out;
	}

	// 依靠成员函数 identify() 获取要打印的信息
	// 因为 identify() 是普通的成员函数, 所以可以被设置为虚函数
	virtual std::string identify() const
	{
		return "Base";
	}
};

class Derived : public Base
{
public:
	// 重写的 identify() 函数,处理 Derived 的情况
	std::string identify() const override
	{
		return "Derived";
	}
};

int main()
{
	Base b{};
	std::cout << b << '\n';

	Derived d{};
	std::cout << d << '\n'; // 注, operator<< 可以处理 Derived 对象

	Base& bref{ d };
	std::cout << bref << '\n';

	return 0;
}

这将打印预期的结果:

1
2
3
Base
Derived
Derived

让我们更详细地看看它是如何工作的。对于 Base b,会调用 operator«。虚函数调用 b.identify() 因而解析为 Base::identify(),它返回要打印的 “Base”。这里没什么特别的。

对于 Derived,编译器首先查看是否存在接收 Derived 对象的 operator«,但我们没有定义。接下来,编译器查看是否存在接收 Base 对象的 operator«。这个版本存在,因此编译器会将 Derived 对象隐式向上转换为 Base& 并调用该函数(我们也可以自己进行向上转换,但编译器会自动完成)。因为参数 b 引用的是 Derived 对象,所以虚函数调用 b.identify() 解析为 Derived::identify(),它返回要打印的 “Derived”。

请注意,我们不需要为每个派生类定义 operator«!处理 Base 对象的版本可以很好地处理 Base 对象,以及任何从 Base 派生的类。

第三种情况是前两种情况的组合。首先,编译器将变量 bref 与接收 Base 引用的 operator« 匹配。由于参数 b 引用的是 Derived 对象,因此 b.identify() 解析为 Derived::identify(),它返回 “Derived”。

问题已解决。


更灵活的解决方案

上述解决方案工作良好,但有两个潜在的缺点:

  1. 它假设所需的输出可以表示为单个std::string.
  2. 我们的identify()成员函数不能访问ostream对象。

后一种情况在需要 stream 对象时会有问题,例如当我们想打印某个已经重载了 operator« 的成员变量时。

幸运的是,只需对上面的示例做一个简单修改,就可以解决这两个问题。在上一个版本中,虚函数 identify() 返回字符串,再由 Base::operator« 打印。在新版本中,我们改为定义虚成员函数 print(),并把直接打印的责任委托给该函数。

下面是一个说明该想法的示例:

 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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include <iostream>

class Base
{
public:
	// 新的 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;
	}
};

// 其它实现了 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 }
	{
	}

	// 这里是处理 Derived 的 print() 函数
	std::ostream& print(std::ostream& out) const override
	{
		out << "Derived: ";

		// 使用 stream 对象,打印 Employee
		out << m_e;

		return out;
	}
};

int main()
{
	Base b{};
	std::cout << b << '\n';

	Derived d{ Employee{"Jim", 4}};
	std::cout << d << '\n'; // 注, operator<< 可以处理 Derived 对象

	Base& bref{ d };
	std::cout << bref << '\n';

	return 0;
}

这输出:

1
2
3
Base
Derived: Employee(Jim, 4)
Derived: Employee(Jim, 4)

在此版本中,Base::operator« 本身不做任何打印。相反,它只是调用虚成员函数 print(),并把 stream 对象传给它。然后,print() 函数使用该 stream 对象完成自己的打印。Base::print() 使用 stream 对象打印 “Base”。更有趣的是,Derived::print() 使用 stream 对象打印 “Derived:”,并调用 Employee::operator« 来打印成员 m_e 的值。


25.9 dynamic_cast

上一节

25.11 第25章总结

下一节