在上一章中,你已学习了如何利用继承从现有类派生出新类。本章将聚焦于继承中最重要且最强大的特性之一——虚函数(virtual functions)。
在探讨虚函数之前,我们先阐明其必要性。
在“派生类构造”章节中曾提到:创建派生类对象时,其由多个部分组成:每个被继承的类各一部分,以及自身特有的部分。
示例
#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” 关系,故 Derived
对象包含 Base
部分是合理的。
指针、引用与派生类
直观上,我们可以用 Derived*
/ Derived&
指向 Derived
对象:
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';
}
输出:
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++ 允许将 Base*
或 Base&
指向 Derived
对象:
int main() {
Derived derived{ 5 };
Base& rBase{ derived }; // 左值引用
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';
}
输出:
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()
遮蔽(shadow)了Base::getName()
,Base*
/Base&
也无法看见Derived::getName()
。 - 因此,
rBase
/pBase
调用的是Base::getName()
,故报告为Base
。 - 同理,无法通过
rBase
/pBase
调用Derived::getValueDoubled()
。
再看一个稍复杂的示例(在“虚函数与多态”章节将继续扩展):
#include <iostream>
#include <string_view>
#include <string>
class Animal {
protected:
std::string m_name;
// 构造函数设为 protected,防止直接创建 Animal 对象,
// 但允许派生类使用。
Animal(std::string_view name) : m_name{ name } {}
// 防止切片(slicing,后续讨论)
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';
}
输出:
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()
,而非 Dog::speak()
或 Cat::speak()
。
指向基类指针/引用的用途
你可能会问:“上述示例似乎有些牵强,既然已有派生对象,为何还要用基类指针/引用?”实际上,原因很多且重要。
示例 1:编写打印动物名与叫声的函数。
若不使用基类指针,需要为每种动物重载:
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 个几乎相同的函数!新增动物时又得再写。而使用基类引用,可简化为:
void report(const Animal& rAnimal) {
std::cout << rAnimal.getName() << " says " << rAnimal.speak() << '\n';
}
该函数可接受任何 Animal
的派生类,甚至未来新增的类。但问题在于:rAnimal.speak()
仍会调用 Animal::speak()
,而非派生类版本。
顺带提及:
也可用模板函数减少重载:
template <typename T>
void report(const T& rAnimal) {
std::cout << rAnimal.getName() << " says " << rAnimal.speak() << '\n';
}
然而,模板方案存在缺陷:
- 不再清晰表明
T
应为Animal
; - 只要对象拥有
getName()
与speak()
,无论是否“合理”,函数都会接受。
示例 2:欲将 3 只猫与 3 只狗放入数组以便遍历。
若无基类指针,需为各派生类分别建数组:
const auto cats{ std::to_array<Cat>({{"Fred"}, {"Misty"}, {"Zeke"}}) };
const auto dogs{ std::to_array<Dog>({{"Garbo"}, {"Pooky"}, {"Truffle"}}) };
// 遍历 cats 与 dogs...
若动物类型 30 种,则需 30 个数组!
由于 Cat
与 Dog
均派生自 Animal
,可改用:
const Cat fred{ "Fred" };
const Dog garbo{ "Garbo" };
// ...
const auto animals{ std::to_array<const Animal*>({&fred, &garbo, /*...*/}) };
for (const auto animal : animals) {
std::cout << animal->getName() << " says " << animal->speak() << '\n';
}
虽然编译通过,但每个元素是 Animal*
,导致输出:
Fred says ???
Garbo says ???
...
问题依旧:基类指针调用的是基类函数版本。
如果能令基类指针调用派生类函数,岂不美哉?
——猜一猜,虚函数(virtual functions)用来做什么? :)
测验时间
上文 Animal
/Cat
/Dog
示例因基类指针无法调用派生类的 speak()
而无法正确工作。一种权宜之计是将 speak()
返回值作为 Animal
的成员变量(类似 m_name
)。
请更新上述 Animal
、Cat
、Dog
类,新增 m_speak
成员,并在构造时正确初始化。要求以下程序正常运行:
#include <array>
#include <iostream>
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" };
const auto animals{ std::to_array<const Animal*>({
&fred, &garbo, &misty, &pooky, &truffle, &zeke
}) };
for (const auto animal : animals) {
std::cout << animal->getName() << " says " << animal->speak() << '\n';
}
return 0;
}
显示答案
为何上述方案并非最优?
提示:
- 考虑未来
Cat
与Dog
需进一步区分的情形。 - 思考在初始化时设置成员变量所带来的限制。