虚函数
按照上一节引入的子类型多态,首先来尝试一个现实的例子:
#include <string>
#include <iostream>
struct Animal {
std::string getName() const {
return "animal";
}
};
struct Cat : public Animal {
std::string getName() const {
return "cat";
};
};
struct Dog : public Animal {
std::string getName() const {
return "dog";
};
};
int main() {
Animal* pet{nullptr};
std::cout << "If you want a cat, press 1." << std::endl;
int n;
std::cin >> n;
if (n == 1) {
pet = new Cat{};
} else {
pet = new Dog{};
}
std::cout << "Here is your new " << pet->getName() << std::endl;
delete pet;
}
上面这段代码试图实现这样的功能:根据用户的输入决定指针 pet 指向的类型:如果输入为 1 则指向一个新的 Cat 对象,否则指向一个新的 Dog 对象,然后把它的名字输出。看上去挺像那么回事儿,但它并不工作。你会发现,不管你输入什么,输出的总是
Here is your new animal
而不是 cat 或者 dog。奇怪,在 Cat 类的 getName 成员函数的返回值不是 "cat" 么?讲道理,如果输入为 1,当执行 new Cat 之后,pet 指向的这个对象的 getName() 调用应该得到 "cat" 呀。
问题出现在之前提到的继承中成员名的处理方式上了。当派生类中出现一个和基类名字相同的成员时,并不会覆盖基类的成员实现,而是在保留基类成员的同时增加一个同名派生类成员。也就是说,new Cat 中包含两份 getName,其中一个是 Animal::getName,返回 "animal";另一个是 Cat::getName,返回 "cat"。
那么现在执行 pet->getName(),或者说 (*pet).getName() 时,找到的是 Animal::getName 还是 Cat::getName 呢?前面说过,这里省略的作用域是左侧操作数的类型。也就是说,这个 getName 是哪个取决于 (*pet) 的类型—— pet 是 Animal* 类型的,那么 (*pet) 自然就是 Animal 类型的,那么 (*pet).getName 就相当于 (*pet).Animal::getName!所以,不论是猫是狗,最终执行的总是 Animal::getName,不会是我们想要的 Cat::getName 或 Dog::getName。
于是怎么办呢?C++ 提供了虚函数机制,它可用于重写(或者说覆盖)基类的成员函数。
对于我们这个例子,它的实现很简单:在 Animal::getName 前面加上 virtual 关键字修饰即可:
struct Animal {
virtual std::string getName() const {
return "animal";
}
};
然后再跑一遍程序,一切输出就都正常了。像这样被 virtual 修饰的成员函数就是虚函数了,它是实现子类型多态的一个重要组成部分。
什么是虚函数
虚函数(Virtual function)是一种特殊的成员函数。形象地讲,其特殊性在于它可以被派生类的同名成员函数覆盖。比如对于 Base 类的虚函数 Base::f,如果它拥有一个子类 Derived,且子类也定义了同名的函数 Derived::f, 则在某些时刻下,本应调用 Base::f 的场合实际却调用了 Derived::f:或者说 Base::f 的调用被 Derived::f 所覆盖。
那么上文中的“某些时刻”是什么时刻呢?唯有以下两种情形会发生:
Base*类型的指针,却指向一个Derived类型的对象;Base&类型的引用,却绑定到一个Derived类型的对象。
这时,如果对这样的指针做成员函数调用一个虚函数:
Derived d;
Base* pb{&d};
Base& rb{d};
pb->f(); // 通过 Base* 调用虚函数
rb.f(); // 通过 Base& 调用虚函数
那么,此时实际被调用的是 Derived::f()。反之,如果 f 不是虚的,那么调用的就是 Base::f()。
显然,典型的虚函数的写法是在成员函数前加以 virtual 关键字限定:
virtual 返回值类型 成员函数名(参数列表) 函数体
注意事项
在成员函数中也可调用虚函数。
#include <iostream>
struct Base {
void callF() {
f(); // 相当于 this->f();
}
virtual void f() { // 虚函数,可被子类覆盖
std::cout << "Base called" << std::endl;
}
};
struct Derived : Base {
void f() {
std::cout << "Derived called" << std::endl;
}
};
int main() {
Base* b{new Derived{}};
b->callF(); // 输出 Derived called
}
这是因为成员函数中的成员使用相当于隐含了前缀 this->,而在这里的 this->f() 和刚刚的情形是一致的,会调用虚函数的实际覆盖函数而非 this 本身的类型。所以,callF 在刚才的例子中调用了 Derived::f 而非 Base::f。但是,构造函数和析构函数中无法调用到派生类的虚函数覆盖:因为在运行构造函数和析构函数的时期派生类尚未形成或已经消失。
此外,虚函数拥有这样的特点:任何用于覆盖虚函数的子类同名函数,也是虚函数。即:
在上面的例子中,我们已经知道了可能会发生 B::f 覆盖 A::f —— 因为 A::f 是虚函数。但正如刚刚所讲,B::f 因为和 A::f 同名,所以 B::f 也是虚函数(尽管它没有显式地用 virtual 修饰)。所以 main 函数中的操作会调用 C::f 而非 B::f。
虚函数的实现?虚函数表?虚指针?自己找国内那些教材看去。