C++进阶: 虚函数1-----继承中的灵魂
继承中最重要、最强大的功能之一——虚函数。
文章目录
- 回顾:
- 指向派生对象基类的指针和引用
- 可以使用指向派生对象基类的指针和引用
- 用于指向基类的指针和引用
- 虚函数和多态
- 虚函数
- 多态性
- 不要从构造函数或析构函数中调用虚函数
回顾:
当创建派生类时,它由多个部分组成:每个继承的类占一个部分,而其自身占一个部分。

我们在C++继承中说到, 根据继承的原则(这里按照一般到具体的顺序),从水果(基类)派生出苹果和香蕉(派生类), 这个类层次表示苹果, 香蕉是水果(箭头指向的是源头)

拿Apple来说, 它继承于Fruit, 所以Apple 里面包含Fruit和Apple两部分, Apple 也是Fruit,包含Fruit也合理.
指向派生对象基类的指针和引用
#include <iostream>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 getDoubleValue() const { return m_value*2; }
};
当我们创建一个 Derived 对象时,它包含一个 Base 部分(首先构造)和一个 Derived 部分(其次构造)。记住,继承意味着两个类之间存在 is-a 关系。由于 Derived 是 Base 类,因此 Derived 包含 Base 部分是合理的。
我们可以设置 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::cotu << "pDerived is a " << pDerived.getName() << " and has value " << pDerived.getValue() << '\n';return 0;
}

可以使用指向派生对象基类的指针和引用
然而,由于 Derived 包含 Base 部分,一个更有趣的问题是 C++ 是否允许我们将 Base 指针或引用设置为 Derived 对象。事实证明,我们可以!
int main()
{Derived derived{ 5 };std::cout << "Derived is a " << derived.getName() << " and has value " << derived.getValue() << '\n';Base base { derived };std::cout << "Base is a " << base.getName() << " and has value " << base.getValue() << '\n';Base& rBase{ derived };std::cout << "rBase is a " << rBase.getName() << " and has value " << rBase.getValue() << '\n';Base* pBase{ &derived };std::cout << "pBase is a " << pBase->getName() << " and has value " << pBase->getValue() << '\n';return 0;
}
运行一下

这个结果可能并不完全符合我们的最初的预期!
事实证明,由于 rBase 和 pBase 是 Base 的引用和指针,它们只能访问 Base 的成员(或 Base 继承的任何类)。因此,即使 Derived::getName() 遮蔽(隐藏)了 Derived 对象的 Base::getName(),Base 指针/引用也无法访问 Derived::getName()。因此,它们会调用 Base::getName(),这就是为什么 rBase 和 pBase 会报告它们是 Base 而不是 Derived 的原因。
请注意,这也意味着无法使用 rBase 或 pBase 调用 Derived::getValueDoubled()。它们无法在 Derived 中看到任何内容。
这个问题后面解决, 接下来再看另外一个例子:
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()
{Cat cat{ "Kaka" };std::cout << "cat is named " << cat.getName() << ", and it says " << cat.speak() << '\n';Dog dog{ "KeKe" };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 << "pAnmal is named " << pAnimal->getName() << ", and it syays " << pAnimal->speak() << '\n';return 0;
}
运行一下

我们在这里看到了同样的问题。因为 pAnimal 是一个 Animal 指针,它只能看到 Animal 类的部分。因此,它调用pAnimal->speak()的是 Animal::speak() 函数,而不是 Dog::Speak() 或 Cat::speak() 函数。
用于指向基类的指针和引用
现在你可能会说:“上面的例子看起来有点傻。既然我可以直接使用派生对象,为什么还要设置指向派生对象基类的指针或引用呢?” 事实证明,有很多很好的理由。
首先,假设你想写一个函数来打印某种动物的名字和声音。如果不使用指向基类的指针,你就必须使用重载函数来编写它,如下所示:
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';
}
考虑派生类数量很多时,使用重载函数就会很多, 但这种只是传入参数不同的重载函数有必要写那么多吗?------> 这时候就需要用到指向派生类的基类的指针了.
由于 Cat 和 Dog 都源自 Animal,因此 Cat 和 Dog 都具有 Animal 部分。因此,我们可以这样做:
void report(const Animal& rAnimal)
{std::cout << rAnimal.getName() << " says " << rAnimal.speak() << '\n';
}
用于指向基类的指针和引用(下面用"它"指代) 让我们知道它有多么大的作用----可以简化代码.
在我们实现一个函数来打印某种动物的名字和声音的功能时, 它不必为每个派生类专门写一个函数, 只需在基类中写一个函数, 其他派生类调用就够了, 不再需要重载那么多函数.
但它达不到我们想要的效果, 尽管我们在派生类中写了匹配度相同的同名函数来覆盖基类中对应的函数(比如speak()), 但是上面测试的结果都显示出它只会调用派生类中基类的部分, 是一点也不管我们需要的派生类部分, 这个现象叫做对象分片.
注解:对象分片,是指发生在一个派生类对象被赋值给一个基类对象时, 用于指向基类的指针和引用, 只调用它继承的基类部分, 访问不到派生类的部分
你以为这就完了! 没有, 讲了这么多, 是时候让我们的主角登场了, 进入下一个主题—虚函数和多态.
虚函数和多态
删繁就简,抓住重点:
上面所遇到的问题归为:指向派生类的基类的指针和引用, 只调用它继承的基类部分
#include <iostream>
#include <string>
#include <string_view>class Base
{
public:const std::string_view speak() { return "Base"; }
};class Derived: public Base
{
public:const std::string_view speak() { return "Derived"; }
};int main()
{Derived derived{};Base* pBase{ &derived };std::cout << "pBase is " << pBase->speak() << '\n';return 0;
};
我们的需求:指向派生类的基类的指针和引用, 我们希望可以调用派生类的派生部分, 并且与基类函数有相同的签名时, 覆盖掉其基类.
虚函数
虚函数是一种特殊类型的成员函数,当调用它时,它会解析为所引用或指向的对象的实际类型的函数的最终派生版本。
如果派生函数与基函数具有相同的签名(名称、参数类型以及是否为 const)和返回类型,则该派生函数被视为匹配。此类函数称为覆盖(overrides)。
要使函数变得虚,只需在函数声明前放置“virtual”关键字。
#include <iostream>
#include <string>
#include <string_view>class Base
{
public:virtual const std::string_view speak() { return "Base"; }
};class Derived: public Base
{
public:virtual const std::string_view speak() { return "Derived"; }
};int main()
{Derived derived{};Base* pBase{ &derived };std::cout << "pBase is " << pBase->speak() << '\n';return 0;
};

虚函数就像是一座桥梁, 使得指向派生类的基类的指针(或者是引用)可以访问其派生类的派生部分,
而且在基类和派生类同签名的成员函数下, 派生类的可以覆盖掉基类的.
怎么理解虚函数?
个人理解:它首先有个条件:
- 继承: 具有继承关系的基类和派生类
- 有指向派生类对象的基类的指针(或者引用) Base& pBase{ Derived object}
- 虚函数打通基类指针可访问派生类的派生部分, 这样能游走于派生的基类部分和派生部分,在必要时能覆盖.
总结一下:
在基类和其派生类同签名的成员函数下, 我们想要派生类的成员函数, 有两种方法:
- 派生类对象--------> 无增效
- 指向派生类对象的基类的指针(或者引用) + 虚函数机制--------> 具有节省代码的功能
太棒了, 这下我们可以探索很多事情了
其实上面的代码虽然通过构建运行了, 但是有个错误
编译器报: 你的Base类中有virtual函数, 释放的析构函数也得是virtual的才行.

class Base
{
public:virtual const std::string_view speak() { return "Base"; }virtual ~Base() {} // add the code
};

再来测试一个例子:
#include <iostream>
#include <string_view>class A
{
public:virtual std::string_view getName() const { return "A"; }
};class B: public A
{
public:virtual std::string_view getName() const { return "B"; }
};class C: public B
{
public:virtual std::string_view getName() const { return "C"; }
};class D: public C
{
public:virtual std::string_view getName() const { return "D"; }
};int main()
{C c {};A& rBase{ c };std::cout << "rBase is a " << rBase.getName() << '\n';return 0;
}

多态性
在编程中,多态性是指一个实体具有多种形式的能力(“多态性”一词的字面意思是“多种形式”)。
int add(int, int);
double add(double, double);
该标识符add有两种形式:add(int, int)和add(double, double)。
编译时多态性是指由编译器解析的多态性形式。这些包括函数重载解析以及模板解析。
运行时多态性是指在运行时解析的多态性形式。这包括虚函数解析。
只有有虚函数, 这样前面的那个代码也可以运行了, 我们来运行一下:
#include <iostream>class Base
{
protected:int m_value {};
public:Base(int value):m_value{value}{}virtual std::string_view getName() const { return "Base"; }int getValue() const { return m_value; }virtual ~Base() {}
};class Derived: public Base
{
public:Derived(int value): Base{value}{}std::string_view getName() const { return "Derived"; }int getDoubleValue() const { return m_value*2; }
};
int main()
{Derived derived{ 5 };std::cout << "Derived is a " << derived.getName() << " and has value " << derived.getValue() << '\n';Base base { derived };std::cout << "Base is a " << base.getName() << " and has value " << base.getValue() << '\n';Base& rBase{ derived };std::cout << "rBase is a " << rBase.getName() << " and has value " << rBase.getValue() << '\n';Base* pBase{ &derived };std::cout << "pBase is a " << pBase->getName() << " and has value " << pBase->getValue() << '\n';return 0;
}

成功通过, 我们注意到, 这个我们只需要在基类中添加虚函数就不用在派生类中添加了, 这是虚函数的单向传染性, 我们需要在基类中添加虚函数后, 其派生类会延续基类的virtual 特性.
不要从构造函数或析构函数中调用虚函数
这是另一个经常困扰毫无戒心的新程序员的陷阱。你不应该在构造函数或析构函数中调用虚函数。为什么?
请记住,创建派生类时,会先构造基类部分。如果您从基类构造函数调用虚函数,而该类的派生部分尚未创建,则无法调用该函数的派生版本,因为没有派生对象可供派生函数使用。在 C++ 中,它将改为调用基类版本。
析构函数也存在类似的问题。如果在基类析构函数中调用虚函数,它将始终解析为该函数的基类版本,因为该类的派生部分已经被销毁.
