指向派生对象的基类指针和引用
本节阅读量:
在前一章中,我们学习了如何使用继承从现有类派生新类。在本章中,我们将关注继承中最重要、最强大的方面之一——虚函数。
但在讨论什么是虚函数之前,让我们首先看看为什么需要虚函数。
在创建派生类时,它由多个部分组成:继承自基类的部分,以及自身新增的部分。
例如,这里有一个简单的例子:
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
|
#include <string_view>
class Base
{
protected:
int m_value {};
public:
Base(int value)
: m_value{ value }
{
}
std::string_view getName() const { return "Base"; }
int getValue() const { return m_value; }
};
class Derived: public Base
{
public:
Derived(int value)
: Base{ value }
{
}
std::string_view getName() const { return "Derived"; }
int getValueDoubled() const { return m_value * 2; }
};
|
当我们创建 Derived 对象时,它包含 Base 部分(先构造)和 Derived 部分(后构造)。记住,继承意味着两个类之间存在 is-a 关系。由于 Base 是基类,因此 Derived 包含 Base 部分是合理的。
指针、引用和派生类
很容易理解,我们可以创建指向派生对象的指针和引用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
#include <iostream>
int main()
{
Derived derived{ 5 };
std::cout << "derived is a " << derived.getName() << " and has value " << derived.getValue() << '\n';
Derived& rDerived{ derived };
std::cout << "rDerived is a " << rDerived.getName() << " and has value " << rDerived.getValue() << '\n';
Derived* pDerived{ &derived };
std::cout << "pDerived is a " << pDerived->getName() << " and has value " << pDerived->getValue() << '\n';
return 0;
}
|
这会产生如下输出:
1
2
3
|
derived is a Derived and has value 5
rDerived is a Derived and has value 5
pDerived is a Derived and has value 5
|
然而,由于 Derived 有一个 Base 部分,一个更有趣的问题是:C++ 是否允许我们创建指向 Derived 对象的 Base 指针或引用?事实证明,是可以的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
#include <iostream>
int main()
{
Derived derived{ 5 };
// 下面都是合法的!
Base& rBase{ derived }; // rBase 是 左值引用
Base* pBase{ &derived };
std::cout << "derived is a " << derived.getName() << " and has value " << derived.getValue() << '\n';
std::cout << "rBase is a " << rBase.getName() << " and has value " << rBase.getValue() << '\n';
std::cout << "pBase is a " << pBase->getName() << " and has value " << pBase->getValue() << '\n';
return 0;
}
|
这会产生如下结果:
1
2
3
|
derived is a Derived and has value 5
rBase is a Base and has value 5
pBase is a Base and has value 5
|
这个结果可能并不是您最初期望的那样!
由于 rBase 和 pBase 是 Base 引用和指针,因此它们只能看到 Base 的成员(或 Base 继承来的成员)。即使派生对象的 Derived::getName() 隐藏了 Base::getName(),它们也仍然会调用 Base::getName()。这就是 rBase 和 pBase 表现为 Base 而不是 Derived 的原因。
请注意,这也意味着不能使用rBase或pBase调用Derived::getValueDoubled()。它们看不到 Derived 中的任何成员。
这里还有一个稍微复杂一些的示例,我们将在下一课中继续使用:
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
|
#include <iostream>
#include <string_view>
#include <string>
class Animal
{
protected:
std::string m_name;
// 将构造函数设置为 protected
// 因为我们不想Animal被直接构造
// 但是派生类仍然可以使用
Animal(std::string_view name)
: m_name{ name }
{
}
Animal(const Animal&) = delete;
Animal& operator=(const Animal&) = delete;
public:
std::string_view getName() const { return m_name; }
std::string_view speak() const { return "???"; }
};
class Cat: public Animal
{
public:
Cat(std::string_view name)
: Animal{ name }
{
}
std::string_view speak() const { return "Meow"; }
};
class Dog: public Animal
{
public:
Dog(std::string_view name)
: Animal{ name }
{
}
std::string_view speak() const { return "Woof"; }
};
int main()
{
const Cat cat{ "Fred" };
std::cout << "cat is named " << cat.getName() << ", and it says " << cat.speak() << '\n';
const Dog dog{ "Garbo" };
std::cout << "dog is named " << dog.getName() << ", and it says " << dog.speak() << '\n';
const Animal* pAnimal{ &cat };
std::cout << "pAnimal is named " << pAnimal->getName() << ", and it says " << pAnimal->speak() << '\n';
pAnimal = &dog;
std::cout << "pAnimal is named " << pAnimal->getName() << ", and it says " << pAnimal->speak() << '\n';
return 0;
}
|
这会产生如下结果:
1
2
3
4
|
cat is named Fred, and it says Meow
dog is named Garbo, and it says Woof
pAnimal is named Fred, and it says ???
pAnimal is named Garbo, and it says ???
|
这里出现了同样的问题。因为 pAnimal 是一个 Animal 指针,所以它只能看到对象中的 Animal 部分。因此,pAnimal->speak() 会调用 Animal::speak。
使用指向基类的指针和引用
现在您可能会说:“上面的示例似乎有点奇怪。既然已经有派生对象了,为什么还要创建指向它的基类指针或引用?”事实证明,这样做有许多充分的理由。
首先,假设您想编写一个打印动物名称和声音的函数。如果不使用指向基类的指针或引用,就必须通过函数重载来实现,例如:
1
2
3
4
5
6
7
8
9
|
void report(const Cat& cat)
{
std::cout << cat.getName() << " says " << cat.speak() << '\n';
}
void report(const Dog& dog)
{
std::cout << dog.getName() << " says " << dog.speak() << '\n';
}
|
这看起来很简单,但如果有 30 种不同的动物会怎样?你必须编写 30 个几乎相同的函数!另外,如果你添加了一种新的动物,你也必须为它编写一个新函数。考虑到唯一真正的区别是参数的类型,这会浪费大量时间。
而且,由于猫和狗都是从 Animal 派生出来的,猫和狗对象中都包含 Animal 部分。因此,我们应该能够做这样的事情:
1
2
3
4
|
void report(const Animal& rAnimal)
{
std::cout << rAnimal.getName() << " says " << rAnimal.speak() << '\n';
}
|
这将允许我们传入任何从 Animal 派生的类,甚至是我们在编写函数后创建的类!这样就不需要为每个派生类各写一个函数,而是只用一个函数处理所有从 Animal 派生的类!
当然,问题是,由于 rAnimal 是一个 Animal 引用,因此 rAnimal.speak() 将调用 Animal::speak。
其次,假设您有 3 只猫和 3 只狗,并希望将它们放在一个数组中以便访问。由于数组只能保存一种类型的对象,如果没有指向基类的指针或引用,您就必须为每个派生类型创建不同的数组,如下所示:
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
|
#include <array>
#include <iostream>
// 上面例子中的 Cat 和 Dog
int main()
{
const auto& cats{ std::to_array<Cat>({{ "Fred" }, { "Misty" }, { "Zeke" }}) };
const auto& dogs{ std::to_array<Dog>({{ "Garbo" }, { "Pooky" }, { "Truffle" }}) };
// 在 C++20 以前
// const std::array<Cat, 3> cats{{ { "Fred" }, { "Misty" }, { "Zeke" } }};
// const std::array<Dog, 3> dogs{{ { "Garbo" }, { "Pooky" }, { "Truffle" } }};
for (const auto& cat : cats)
{
std::cout << cat.getName() << " says " << cat.speak() << '\n';
}
for (const auto& dog : dogs)
{
std::cout << dog.getName() << " says " << dog.speak() << '\n';
}
return 0;
}
|
现在,考虑一下如果你有 30 种不同类型的动物会发生什么。你需要 30 个数组,每种动物一个!然而,由于猫和狗都是从 Animal 派生而来的,因此我们应该能够这样做:
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
|
#include <array>
#include <iostream>
// 上面例子中的 Cat 和 Dog
int main()
{
const Cat fred{ "Fred" };
const Cat misty{ "Misty" };
const Cat zeke{ "Zeke" };
const Dog garbo{ "Garbo" };
const Dog pooky{ "Pooky" };
const Dog truffle{ "Truffle" };
// 存储指向动物的数组,里面装着指向Cat和Dog的指针
const auto animals{ std::to_array<const Animal*>({&fred, &garbo, &misty, &pooky, &truffle, &zeke }) };
// 在 C++20 前, 数组大小需要显式指定
// const std::array<const Animal*, 6> animals{ &fred, &garbo, &misty, &pooky, &truffle, &zeke };
for (const auto animal : animals)
{
std::cout << animal->getName() << " says " << animal->speak() << '\n';
}
return 0;
}
|
在编译和执行时,不幸的是,数组 “animals” 的每个元素都是指向 Animal 的指针,这意味着 animal->speak() 将调用 Animal::speak。
输出为:
1
2
3
4
5
6
|
Fred says ???
Garbo says ???
Misty says ???
Pooky says ???
Truffle says ???
Zeke says ???
|
这两种技术都可以为我们节省大量的时间和精力,但它们有相同的问题。基类的指针或引用会调用函数的基类版本,而不是派生类版本。我们需要某种机制,让这些基类指针或引用调用函数的派生类版本,而不是基类版本。
现在,想猜一猜虚函数是用于什么目的?