在课程“指向与引用派生对象的基类”中,我们看到使用指向基类的指针或引用有助于简化代码。然而,在全部示例中都存在一个共同问题:基类指针或引用只能调用基类版本的成员函数,无法调用派生类版本。
下面给出一个演示该行为的简化示例:
#include <iostream>
#include <string_view>
class Base
{
public:
    std::string_view getName() const { return "Base"; }
};
class Derived: public Base
{
public:
    std::string_view getName() const { return "Derived"; }
};
int main()
{
    Derived derived {};
    Base& rBase{ derived };
    std::cout << "rBase is a " << rBase.getName() << '\n';
    return 0;
}
程序输出:
rBase is a Base
由于 rBase 是 Base 类型的引用,因此调用的是 Base::getName(),即便它实际引用的是 Derived 对象中的 Base 部分。
本文将说明如何通过虚函数解决该问题。
虚函数
虚函数是一种特殊成员函数。当通过指向或引用类类型对象的指针或引用调用该函数时,实际执行的是该对象最派生类中的版本。
派生类中的函数若与基类虚函数具有完全相同的签名(函数名、形参类型以及是否 const)和相同的返回类型,则称为重写(override)。
仅需在函数声明前加关键字 virtual 即可将其声明为虚函数:
#include <iostream>
#include <string_view>
class Base
{
public:
    virtual std::string_view getName() const { return "Base"; } // 注意 virtual
};
class Derived: public Base
{
public:
    virtual std::string_view getName() const { return "Derived"; }
};
int main()
{
    Derived derived {};
    Base& rBase{ derived };
    std::cout << "rBase is a " << rBase.getName() << '\n';
    return 0;
}
输出:
rBase is a Derived
提示:某些现代编译器会针对“具有虚函数却未声明虚析构函数”给出警告。如遇此情况,可在基类中添加:
virtual ~Base() = default;关于虚析构函数,在课程“虚析构函数、虚赋值与抑制虚函数”中将详细说明。
由于 rBase 引用的是 Derived 对象中的 Base 部分,而 Base::getName() 被声明为虚函数,因此运行时查找并调用了 Derived::getName()。
再看一个略复杂的例子:
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.getName() << '\n';
}
程序首先创建 C 对象;rBase 是 A 的引用,绑定到该 C 对象的 A 部分。rBase.getName() 原应调用 A::getName(),但因 A::getName() 为虚函数,故在 A 与 C 之间寻找最派生匹配,最终调用 C::getName()。不会调用 D::getName(),因为对象类型为 C 而非 D。
输出:
C
关键洞察
**只有当虚成员函数通过指针或引用调用时,才会发生动态绑定。**若直接对对象本身调用虚函数,则始终执行该对象类型的版本:
C c{};
std::cout << c.getName(); // 始终调用 C::getName
A a { c }; // 仅拷贝 c 的 A 子对象(不建议如此使用)
std::cout << a.getName(); // 始终调用 A::getName
多态
在编程中,**多态(polymorphism)**指“多种形态”。例如:
int add(int, int);
double add(double, double);
标识符 add 呈现了两种形态:add(int,int) 与 add(double,double)。
- 编译时多态由编译器在编译期解析,如函数重载、模板实例化。
- 运行时多态在运行期解析,如虚函数动态绑定。
更复杂的示例:Animal 类
原始版本:
class Animal
{
protected:
    std::string m_name{};
    Animal(std::string_view name) : m_name{ name } {}
public:
    const std::string& getName() const { return m_name; }
    std::string_view speak() const { return "???"; }
};
class Cat: public Animal { /*...*/ std::string_view speak() const { return "Meow"; } };
class Dog: public Animal { /*...*/ std::string_view speak() const { return "Woof"; } };
void report(const Animal& animal)
{
    std::cout << animal.getName() << " says " << animal.speak() << '\n';
}
int main()
{
    Cat cat{ "Fred" };
    Dog dog{ "Garbo" };
    report(cat);
    report(dog);
}
输出:
Fred says ???
Garbo says ???
将 speak() 声明为虚函数后:
class Animal
{
    ...
    virtual std::string_view speak() const { return "???"; }
};
class Cat: public Animal { ... virtual std::string_view speak() const override { return "Meow"; } };
class Dog: public Animal { ... virtual std::string_view speak() const override { return "Woof"; } };
输出:
Fred says Meow
Garbo says Woof
进一步,可将不同派生类对象纳入统一接口:
Cat fred{ "Fred" };
Dog garbo{ "Garbo" };
// ...
Animal* animals[]{ &fred, &garbo, /*...*/ };
for (const auto* animal : animals)
    std::cout << animal->getName() << " says " << animal->speak() << '\n';
无论将来新增何种派生类,只要继承 Animal 并重写 speak(),即可直接复用上述代码,无需修改。这正是虚函数的最大优势——开闭原则的有力体现。
重要提示
- 签名必须完全一致:形参、返回类型、const属性等均须匹配,否则无法构成重写。
- 继承链中的隐含虚函数:若基类函数已声明为 virtual,其所有派生类中的同名同参同返回类型函数隐式为虚函数,无需再写virtual。
- 返回类型规则:正常情况下,虚函数与其重写版本返回类型必须相同。若返回类型不同,则无法通过编译。
- 构造/析构函数中勿调用虚函数:- 构造基类子对象时,派生类尚未构造完成,此时调用虚函数将解析为基类版本。
- 析构基类子对象时,派生类已析构,同样只会调用基类版本。 最佳实践:切勿在构造函数或析构函数中调用虚函数。
 
虚函数的开销
若将所有函数均设为虚函数,则会带来额外的运行时开销与内存开销:
- 虚函数调用需通过虚表间接寻址,耗时多于普通函数。
- 每个具有虚函数的类对象需额外存储一个指向虚表的指针,对小对象而言内存开销显著。
测验
请通过代码阅读判断以下程序的输出(勿编译运行)。
1a)
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 { /* 无 getName() */ };
class D: public C { public: virtual std::string_view getName() const { return "D"; } };
int main()
{
    C c {};
    A& rBase{ c };
    std::cout << rBase.getName() << '\n';
}
解答:
BC未重写getName(),因此最近的有效重写来自B。
1b)
// 同上,但 rBase 为 B&
int main()
{
    C c;
    B& rBase{ c };
    std::cout << rBase.getName() << '\n';
}
解答:
C尽管引用类型为B,但对象类型为C,且C重写了getName()。
1c)
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"; } };
int main()
{
    C c {};
    A& rBase{ c };
    std::cout << rBase.getName() << '\n';
}
解答:
A基类函数未声明为虚,因此无动态绑定。
1d)
class A { public: virtual std::string_view getName() const { return "A"; } };
class B: public A { public: std::string_view getName() const { return "B"; } }; // 无 virtual
class C: public B { public: std::string_view getName() const { return "C"; } };
class D: public C { public: std::string_view getName() const { return "D"; } };
int main()
{
    C c {};
    B& rBase{ c };
    std::cout << rBase.getName() << '\n';
}
解答:
C虽然B、C、D的函数未写virtual,但由于A::getName()已为虚函数,派生类同名函数隐式为虚,因此输出C。
1e)
class A { public: virtual std::string_view getName() const { return "A"; } };
class B: public A { public: virtual std::string_view getName() { return "B"; } }; // 非 const
class C: public B { public: virtual std::string_view getName() { return "C"; } };
int main()
{
    C c {};
    A& rBase{ c };
    std::cout << rBase.getName() << '\n';
}
解答:
AB和C的函数与基类函数签名不同(缺少const),因此不构成重写,调用的是A::getName()。
1f)
class A
{
public:
    A() { std::cout << getName(); }
    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"; } };
int main()
{
    C c {};
}
解答:
A在构造C对象时,先执行A的构造函数。此时派生类部分尚未构造,因此getName()调用解析为A::getName()。
