本文将介绍C++中虚函数的作用、特点及存储方式。
1、虚函数的作用
在介绍虚函数之前,需要了解一下早绑定(静态多态)和晚绑定(动态多态)的概念。
1.1 静态多态
早绑定,一般可通过模板实现。例如函数重载,对于同名的两个或多个函数,在编译阶段,编译器根据函数的形参个数及类型来决定调用哪个函数。
观察下面的例子:
#include<iostream>
using namespace std;
class Base{
public:
void Output(){cout<<"Base 类"<<endl;}
void Output(int value){cout<<"传入数据值为: "<<value<<endl;}
};
int main()
{
Base b;
b.Output(); //输出“Base 类”
b.Output(3); //输出“出入数据值为: 3”
return 0;
}
上面的例子通过函数重载的方式,简单的实现了静态多态。
另外,静态多态的实现不一定非得依赖于面向对象编程。他提供了一个实现某个功能的模板,由编译器来决定采用模板中的哪一个。
1.2 动态多态
动态多态(晚绑定),一般通过虚函数实现,依赖于面向对象编程。简言之,虚函数可以实现晚绑定即动态多态。
观察下面的例子:
class Base{ //基类
public:
virtual void Output(){cout<<"Base 类"<<endl;}
~Base(){cout<<"父类的析构函数!"<<endl;}; //基类的析构函数
};
class Child:public Base{ //公有继承自Base类
public:
virtual void Output(){cout<<"Child 类"<<endl;}
~Child(){cout<<"子类的析构函数!"<<endl;}; //子类的析构函数
};
int main()
{
Base *b=new Child;
b->Output(); //输出“Child 类”
delete b; //删除父类的指针,此时只会输出“父类的析构函数!”
return 0;
}
通过将Output()函数声明为虚函数,从而可以用父类的指针访问的基类的成员函数,实现了“动态多态”。动态多态是在程序运行阶段实现的。
分析以上过程,可以发现,在此过程中,只申请了父类的指针,当指针的动作完成后,会自动的调用父类的析构函数,而子类的析构函数却始终没有运行。若在子类中,在堆上申请了一块内存,此时有可能会造成内存泄露。
为了避免内存泄露,需要将父类的析构函数声明为虚析构函数。此时,在删除父类指针时,首先调用子类的析构函数,然后调用父类的析构函数。
2、虚函数的存储方式
一般,函数在内存中存储时,都有一个入口地址。那么,对于虚函数来说,他们的入口地址是怎么存储的呢?是虚函数表。
当子类继承自一个父类时,对于父类和子类中的虚函数,有两种情况,覆盖和不覆盖。
所谓覆盖,就是子类中的虚函数名称与父类的虚函数名称相同,此时子类中函数的入口地址会覆盖父类中对应的虚函数的入口地址。从而,可以利用父类的指针访问子类的函数成员。
对于不覆盖的情形,各个虚函数在虚函数表中的位置遵循“父类的在前,子类的在后,而且按声明顺序排列”。
观察下面的代码:
#include<iostream>
using namespace std;
class Base{
public:
virtual void func1(){cout<<"Base func1"<<endl;}
virtual void func2(){cout<<"Base func2"<<endl;}
virtual void func3(){cout<<"Base func3"<<endl;}
};
class Child:public Base{
public:
virtual void func1(){cout<<"Child func1"<<endl;} //覆盖
virtual void func4(){cout<<"Child func4"<<endl;} //不覆盖
virtual void func5(){cout<<"Child func5"<<endl;} //不覆盖
};
int main()
{
typedef void (*Fun)(void);
Child c; //**子类对象**
Fun pFun=NULL;
pFun=(Fun)*((int*)*(int*)(&c)+0); //取虚函数表中第一个虚函数的入口地址
pFun(); //运行,
pFun=(Fun)*((int*)*(int*)(&c)+1); //取虚函数表中第二个虚函数的入口地址
pFun();
pFun = (Fun)*((int*)*(int*)(&c) + 2); //取虚函数表中第三个虚函数的入口地址
pFun();
pFun = (Fun)*((int*)*(int*)(&c) + 3); //取虚函数表中第四个虚函数的入口地址
pFun();
pFun = (Fun)*((int*)*(int*)(&c) + 4); //取虚函数表中第五个虚函数的入口地址
pFun();
return 0;
}
上面的程序依次输出:
”
Child func1
Base func2
Base func3
Child func4
Child func5
”
可以发现,“Child func1”把“Base func1”覆盖,而没被覆盖的函数遵循“父类在前,子类在后,并且按声明顺序排列”。