面向对象编程基于三个基本概念:数据抽象、继承和动态绑定,在C++中,用类进行数据抽象,用类派生从一个类继承另一个类。派生类继承基类的成员。动态绑定能够时编译器在运行时决定是使用基类中的函数还是继承类之中的函数。继承和动态绑定大大地简化了程序,而虚函数便是其中的一个关键因素。
什么是虚函数?
虚函数是在类中被声明为virtual的成员函数,当编译器看到通过指针或引用调用此类函数时,对其执行晚绑定,即通过指针(或引用)指向的类的类型信息来决定该函数是哪个类的。通常此类指针或引用都声明为基类的,它可以指向基类或派生类的对象。
为什么需要虚函数?
在C++中,基类必须指出希望派生类重新定义哪些函数,定义为virtual的函数即是基类期待派生类重新定义的,基类希望派生类继承的函数不能定义为虚函数。这便是多态的表现,多态是指使用相同的函数名来访问函数不同的实现方法,即“一种接口,多种方法”,用相同的形式访问一组通用的运算,每个运算可能对应的行为不同。
C++支持编译时多态和运行时多态,运算符重载和函数重载就是编译时多态,而派生类和虚函数实现运行时多态。运行时多态的基础是基类指针,基类指针可以指向任何派生类对象。
编译器到底做了什么实现的虚函数的晚绑定呢?
编译器对每个包含虚函数的类创建一个表(称为V TA B L E)。在V TA B L E中,编译器放置特定类的虚函数地址。在每个带有虚函数的类 中,编译器秘密地置一指针,称为v p o i n t e r(缩写为V P T R),指向这个对象的V TA B L E。通过基类指针做虚函数调 用时(也就是做多态调用时),编译器静态地插入取得这个V P T R,并在V TA B L E表中查找函数地址的代码,这样就能调用正确的函数使晚捆绑发生。为每个类设置V TA B L E、初始化V P T R、为虚函数调用插入代码,所有这些都是自动发生的,所以我们不必担心这些。利用虚函数, 这个对象的合适的函数就能被调用,哪怕在编译器还不知道这个对象的特定类型的情况下。(《C++编程思想》)
接下来看一个简单的实例。
#include <iostream>
using namespace std;
class F
{
public:
void novir()
{
cout << "father novir" <<endl;
}
virtual void vir()
{
cout<< "father vir"<<endl;
}
};
class S : public F
{
public:
void novir()
{
cout << "son novir"<<endl;
}
void vir()
{
cout << "son vir"<<endl;
}
};
int main()
{
F f;
S s;
s.novir();// 子类起作用,输出为 son novir
s.vir();//子类起作用,输出为son vir
f.novir();//父类起作用(因为其本身在这里与子类没有关系),输出father novir
f.vir();// 父类起作用(因为其本身在这里与子类没有关系),输出father vir
F *ff;
ff = &s;//将一个父类指针指向一个子类对象;子类指针不能指向父类对象
ff->novir();//父类起作用,显示father novir
ff->vir();//子类起作用,虚函数多态的用处,显示 son vir,具体参见下面解释
return 0;
}
输出:
[root@localhost root]# ./vir
son novir
son vir
father novir
father vir
father novir
son vir
使用虚函数也有一些需要注意的地方
(1)构造函数和static成员不能被声明为虚函数。每个类都有自己的构造函数,不能用继承来说明,而static声明的为静态成员,在整个派生树中为唯一的实例,不能被重写。
(2)虚函数与重载函数的区别:重载函数之间必须在参数的类型或者数量上加以区分,而重新定义的虚函数在参数的类型和数量上必须与原型相同;在定义虚函数的类中,虚函数必须声明为类的成员而不能是友元,可以被声明为其他类的友元;析构函数可以是虚函数,构造函数不行。
(3)纯虚函数及其作用: 纯虚函数是指在基类中声明但是没有定义的虚函数:virtual type func(param list) = 0;。把虚函数声明为纯虚函数可以强制在派生类中重新定义虚函数,否则编译器会报错。如果一个类至少有一个纯虚函数,则称为抽象类。抽象类只能用来作为其他类的基类,不能定义抽象类的对象,因为在抽象类中有一个或者多个函数没有定义。但是能够使用抽象类来声明指针或者引用。