C++中的虚函数作用主要是实现多态的机制。基类定义虚函数,子类可以重写该函数;在派生类中对基类定义的虚函数进行重写时,需要在派生类中声明该方法为虚方法。当子类重新定义了父类的虚函数后,当父类的指针指向子类对象的地址时,[即B b; A a=&b;]父类指针根据赋给它的不同子类指针,动态的调用子类的该函数,而不是父类的函数(如果不使用virtual,请看后面★*),且这样的函数调用发生在运行阶段,而不是发生在编译阶段,称为动态编译。而函数的重载可以认为是多态,只不过静态的。注意,非虚函数静态联编,效率要比虚函数高,但是不具备动态联编能力。
★如果使用virtual关键字,程序将根据引用或指针指向的对象类型来选择方法,否则使用引用类或指针类型来选择方法。
class A
{
private:
int i;
public:
A();
A(int num) : i(num){};
virtual void fun1();
virtual void fun2();
};
class B : public A
{
private:
int j;
public:
B(int num) : j(num){};
virtual void fun2(); // 重写了基类的方法
};
// 为方便解释思想,省略很多代码
A a(1);
B b(2);
A *a1_ptr = &a;
A *a2_ptr = &b;
// 当派生类“重写”了基类的虚方法,调用该方法时
// 程序根据 指针或引用 指向的 “对象的类型”来选择使用哪个方法
a1_ptr->fun2(); // call A::fun2();
a2_ptr->fun2(); // call B::fun2();
// 否则
// 程序根据“指针或引用的类型”来选择使用哪个方法
a1_ptr->fun1(); // call A::fun1();
a2_ptr->fun1(); // call A::fun1();
2、虚函数的底层实现机制(虚函数表+虚表指针)
编译器处理虚函数的方法是:为每个类对象添加一个隐藏成员,隐藏成员中保存了一个指向函数地址数组的指针,称为虚表指针(vptr),这种数组成为虚函数表(vtbl),即,每个类使用一个虚函数表,每个类对象用一个虚表指针。
举个例子:基类对象包含一个虚表指针,指向基类中所有虚函数的地址表。派生类对象也将包含一个虚表指针,指向派生类虚函数表。看下面两种情况:
如果派生类重写了基类的虚方法,该派生类虚函数表将保存重写的虚函数的地址,而不是基类的虚函数地址。
如果基类中的虚方法没有在派生类中重写,那么派生类将继承基类中的虚方法,而且派生类中虚函数表将保存基类中未被重写的虚函数的地址。注意,如果派生类中定义了新的虚方法,则该虚函数的地址也将被添加到派生类虚函数表中。
3、虚函数的工作原理
编译器处理虚函数的方法是:
给每个对象添加一个指针,存放指向虚函数表的地址,虚函数表存储为类对象声明的虚函数地址。比如基类对象包含一个指针,该指针指向基类所有虚函数的地址表,派生类对象将包含一个指向独立地址表的指针,如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址,如果派生类没有重新定义虚函数,该虚函数表将保存函数原始版本的地址。如果派生类定义了新的虚函数,则该函数的地址将被添加到虚函数表中,注意虚函数无论多少个都只需要在对象中添加一个虚函数表的地址。
使用虚函数后的变化:
(1) 对象将增加一个存储地址的空间(32位系统为4字节,64位为8字节)
(2) 每个类编译器都创建一个虚函数地址表
(3) 对每个函数调用都需要增加在表中查找地址的操作
虚函数的注意事项:
(1) 基类方法中声明了方法为虚后,该方法在基类派生类中是虚的
(2) 若使用指向对象的引用或指针调用虚方法,程序将根据对象类型来调用方法,而不是指针的类型
(3) 如果定义的类被用作基类,则应将那些要在派生类中重新定义的类方法声明为虚。