在实现C++多态时会用到虚函数。使用虚函数目的是通过基类指针访问派生类定义的函数。
典型使用:1
2Base* ba = new Derive();
Derive *de = ba;
反射性地会说ba是个基类指针,指向派生类对象。但是运行程序竟然报错了: error: C2440: “初始化”: 无法从Base *
转换为 Derive *
从基类型到派生类型的强制转换需要 dynamic_cast 或 static_cast 。也就是需要向下转型,为什么?ba不是指向派生类对象吗?
这就是虚函数的动态绑定, 在运行期才指向派生类对象,而在编译期还是基类指针,不能赋给de
,语法检查就过不了,当然报错了。
我们一般说ba的静态类型是Base*
,动态类型是Derive*
。如果第一行改为Base* ba
,那么静态类型是Base*
,没有动态类型。
所谓静态多态是指通过模板或者函数重载实现的多态,其在编译期确定行为。动态多态是指通过虚函数技术实现在运行期动态绑定的技术。
普通函数在编译时就确定了函数地址,但虚函数在运行时查询虚函数表,所以效率较低。
要正确实现虚函数,只能通过基类的指针或引用来指向派生类对象
派生类重新实现基类中的虚函数,这就叫覆盖
成员虚函数不能有同名同参数的静态函数
多个虚函数对sizeof的计算相当于一个,因为它们都存在虚函数表里,虚函数表只对应一个虚指针,所以不会扩大占用内存
纯虚函数
基类有纯虚函数时,就是抽象类,抽象类无法实例化,也无法声明指针。派生类可以不实现这个纯虚函数,但这样就没有意义了。 基类中的纯虚函数其实也可以实现,但没有必要,因为派生类都会重新实现。
1 | virtual void testPureVirtual() = 0; // 基类中声明纯虚函数 |
这里的代码在派生类中调用了基类中的纯虚函数,由于抽象类无法实例化,所以基类纯虚函数是不能直接调用的。
不要重新定义继承而来的虚函数的默认参数值
绝对不要重新定义一个继承而来的virtual函数的缺省参数值,因为缺省参数值都是静态绑定(为了执行效率),而virtual函数却是动态绑定。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22class Base
{
public:
Base();
virtual ~Base();
virtual void test_virtual(int i=0); // 默认值为0
};
void Base::test_virtual(int i)
{
cout<<"base virtual "<<i<<endl;
}
class Derive : public Base
{
public:
Derive();
void test_virtual(int i=1); // 默认值改为1
};
void Derive::test_virtual(int i)
{
cout<<"derive virtual "<<i<<endl;
}
还是那个常用的调用:1
2Base* ba = new Derive();
ba->test_virtual();
结果竟然是:1
derive virtual 0 // 函数为派生类的,默认值为基类的
原因在于虚函数是动态绑定的,但参数是静态绑定的, 覆盖虚函数时只能覆盖动态绑定的部分,所以不能改变参数。虚函数的效率较差,如果对参数也进行动态绑定,那么对执行速度和编译器简易度不利,所以C++做了分开的处理。
不能声明为虚函数的函数
构造函数
首先明确一点,在编译期,编译器完成了虚表的创建,而虚指针在构造函数期间被初始化
如果构造函数是虚函数,那必然需要通过虚指针来找到虚构造函数的入口地址,但是这个时候我们还没有把虚指针初始化。因此,构造函数不能是虚函数。
內联函数
编译期內联函数在调用处被展开,而虚函数在运行时才能被确定具体调用哪个类的虚函数。內联函数体现的是编译期机制,而虚函数体现的是运行期机制。 二者不是同一范畴的东西。
静态成员函数
静态成员函数和类有关,即使没有生成一个实例对象,也可以调用类的静态成员函数。而虚函数的调用和虚指针有关,虚指针存在于一个类的实例对象中,如果静态成员函数被声明成虚函数,那么调用成员静态函数时又如何访问虚指针呢。也就是说,静态成员函数与类有关,而虚函数与类的实例对象有关。 二者也不是同一范畴的东西。
多态
多态存在的3个必要条件:
- 继承
- 函数的重写
- 父类引用或指针指向子类对象
1 | class B0 //基类BO声明 |
运行结果是B0 display0 B0 display0
。
虚函数的动态绑定仅在 基类指针或引用 绑定派生类对象时发生,fun的形参不是指针而是对象,所以调用哪个版本的函数在编译时就已经确定。