当我们使用基类的引用或指针调用一个虚成员函数时会发生动态绑定,知道运行时才会知道到底调用了哪个版本的虚函数,因此所有的虚函数都必须有定义。
对虚函数的调用可能在运行时才被解析
面向对象编程的核心思想是多态性(polymorphism)。多态性这个词源于希腊语,其含义是“多种形态”,引用或指针的静态类型与动态类型不同这一事实是C++语言支持多态性的根本所在。
当使用基类的引用或指针调用基类中定义的一个函数时,我们并不知道该函数真正作用的对象是什么类型的(可能是基类的对象也可能是派生类的对象)。如果该函数是虚函数,则直到运行时才会确定是哪个版本,判断的依据是引用或指针所绑定的对象的真实类型。
另一方面,非虚函数的调用在编译时进行绑定。类似的,通过对象进行绑定的函数(虚函数或非虚函数)调用也在编译时进行绑定,因为对象的类型是不会改变的。
派生类中的虚函数:
在派生类中覆盖某个虚函数时,可以加上virtual关键字,但是没有必要,因为一个函数被声明为虚函数后,所有派生类中的它都是虚函数,且它的形参类型与返回值类型(一个例外是返回类型是类本身的指针或引用)都和基类中相同。
final和override说明符:
派生类如果定义了一个和基类中的虚函数名字相同但是此参数列表不同的函数时,这依然是合法的行为,但是并不是我们希望的情况。在C++11新标准中可以使用override来说明基类中的虚函数,这么做的好处是使得程序员的意图更清晰的同时让编译器可以发现这一错误:当使用override标记了某个函数,但该函数并没有覆盖已存在的虚函数,编译器将报错。
struct A{
virtual void f1(int) const;
virtual void f2();
void f3();
};
struct B : A {
void f1(int) const override; //正确
void f2(int) override; //错误,参数列表不匹配
void f3() override; //错误,f3不是虚函数
void f4() override; //错误,A中没有f4()
};
还能将某个函数指定为final,表示之后任何尝试覆盖该函数的行为都会引发错误:
struct A {
void f1();
virtual void f2() final;
};
struct B : A {
void f1();
void f2(); //错误A中的f2()已经是final的了
};
虚函数与默认实参:
和其它函数一样,虚函数也可以拥有默认实参,如果某此函数调用使用了默认实参,则该实参值由本次调用的静态类型决定。也就是说当使用基类的引用或指针调用函数时会使用基类中定义的的默认实参,即使实际上运行的是派生类中的函数版本也是如此,因此虚函数如果使用默认实参,则基类和派生类中定义的默认实参最好是一致的。
回避虚函数的机制:
有时希望调用虚函数不根据动态绑定,而是自己指定版本,比如在派生类的虚函数中想要调用基类的虚函数版本,可以通过作用域运算符来实现:
//f为虚函数
//强行调用Obj类的虚函数版本,不管p所指向的对象的类型到底是什么
auto ret = p->Obj::f();
还是之前的例子,如果没有作用域运算符,那么在派生类的虚函数中还会调用自身的虚函数,就可能导致无限递归调用下去。