1、虚函数
虚函数是类中比较特殊的成员函数,通过在普通成员函数的前面加上“virtual”关键字声明。派生类可以重新定义(重写、覆盖)基类的虚函数,通过基类指针去指向其派生类对象,进行动态绑定(延迟绑定,即一个类成员函数的调用不在编译时确定,延迟到运行时确定),达到利用基类访问派生类虚函数的目的。如果没有虚函数,则总被限制在基类函数本身,无法调用派生类中被重写的函数。虚函数是C++中实现多态性的关键。
2、虚函数原理
有下面这个类A,包含两个成员变量、两个普通成员函数以及三个虚函数,析构函数也被定义为虚函数。虚函数的原理为:当一个类中包含虚函数时,编译器会在编译的时候为类生成一个虚函数表,这个表用于存放虚函数指针;另外,还会在类中插入一个指针变量,称为虚函数表指针,在构造函数中,将虚函数表的地址赋予该指针变量。
Class A{
private:
int x;
int y;
public:
void func1(){}
void func2(){}
virtual void vfunc1(){}
virtual void vfunc2(){}
virtual void ~A(){}
};
现在,如果将类A实例化,其对象在内存中的布局如下图所示,可以看到虚函数表、虚函数、普通成员函数并不占用对象内存空间,它们属于类A的组成部分。
3、使用虚函数实现多态性
为了实现多态性,这里从类A中派生出两个子类B、C,并重写基类A的虚函数vfunc1,如以下代码所示
Class B : public A{
public:
virtual void vfunc1(){
cout << "class B" << endl;
}
private:
int a;
};
Class C : public A{
public:
virtual void vfunc1(){
cout << "class C" << endl;
}
private:
int b;
};
现在观察一下这三个类对象的内存布局,由于子类只重写了vfunc1虚函数,所以子类的虚函数表要将基类的vfunc1地址改为子类的,其他虚函数地址不变,仍属于基类A的,如下图所示。那么,当用基类指针指向子类,并调用vfunc1函数的时候,执行的将是对应子类的功能,即实现了多态。
4、扩展
(1)基类析构函数为什么要声明为虚函数,而构造函数不能声明为虚函数?
构造函数用于实例化过程中对对象进行初始化,此时对象还未完全创建,虚函数表和虚函数表指针还未明确,所以构造函数不能声明为虚函数。
由对象的内存布局可知,如果不将基类的析构函数声明为虚函数,那么当用基类指向派生类对象,在析构的时候,只会调用基类的析构函数,而不会调用派生类的析构函数,此时派生类对象分配的内存可能得不到释放,从而造成内存泄漏。
(2)纯虚函数和抽象类
纯虚函数在类中只是声明接口,没有定义,例如,如果将vfunc1()声明为纯虚函数,那么其形式为
virtual void vfunc1() = 0;
虽然基类中纯虚函数没有定义,但是要求其任何派生类必须定义实现纯虚函数,以实现多态性。
带有纯虚函数的类是抽象类,抽象类将一系列方法作为接口组织在一个继承层次结构中,为派生类提供公共接口,具体实现内容由各派生类自己定义。关于抽象类有以下两点需要注意:
- 抽象类不能实例化对象,只能作为基类使用,指向其派生类对象
- 如果派生类没有重新定义纯虚函数,那该派生类仍是一个抽象类。