C++基础知识之虚函数的原理及其应用
在认识虚函数之前,我先给出一个案例:
#include <iostream>
#include <algorithm>
//基类
class Base {
public:
void fun() {
std::cout<<"在基类调用fun函数"<<endl;
}
};
//子类
class Derived :public Base {
public:
void fun() {
std::cout<<"在子类调用fun函数"<<endl;
}
};
int main() {
//用基类指针初始化子类
Base* b = new Derived();
b -> fun();
return 0;
}
请问上述的控制台输出是什么?
答案如下:
不难发现,这与我们想要达成的结果并不一致。我们想要用基类初始化子类,使得基类转化成子类,具有子类的功能,但实际上结果与我们想要的不同。出现这样的原因主要是因为C++的静态联编,在这可以简化为一条原则:“如果你以一个基础类指针指向一个衍生类对象,那么通过该指针,你只能访问基础类定义的成员函数”,所以,我们需要一种声明来使得基类指针能够指向正确的子类,即虚函数。
虚函数是在函数返回值前增加 virtual 关键字,使得该函数成为虚函数,实例如下:
#include <iostream>
#include <vector>
#include <map>
#include <unordered_map>
#include <unordered_set>
#include <algorithm>
using namespace std;
class Base {
public:
virtual void fun() {
std::cout<<"在基类调用fun函数"<<endl;
}
};
class Derived :public Base {
public:
void fun() {
std::cout<<"在子类调用fun函数"<<endl;
}
};
int main() {
//用基类指针初始化子类
Base *b = new Derived();
b -> fun();
delete b;
return 0;
}
可以看到,与实例1相比只在基类中的fun方法增加了 virtual 关键字,查看控制台输出可知:
现在方法的输出就符合我们的预期了。接下来说说虚函数的原理:
首先,在上一个实例中,我们讲到静态联编的概念,意思就是编译器会在编译阶段就将函数实现和函数调用绑定关联起来,也叫做早绑定,所以在实例1中编译阶段fun函数就与基类绑定了,输出的也是基类的函数;有静态就有动态,动态联编则是指在程序执行阶段将函数实现与函数调用绑定,也叫运行时绑定,虚函数使用的就是动态联编。它使得编译器在运行时才将函数与类绑定起来,使得函数能够找到实际调用的类,输出该类函数的结果。
虚函数的实现是依靠虚函数表和虚函数表指针。每个包含虚函数的类都有自己的虚函数表。这个表是在编译时确定的静态数组,里面包含了指向每个虚函数的函数指针,能够让对象调用。在基类中还定义了一个隐藏指针FunctionPointer *_vptr,指向类的虚函数表。当基类被实例化时,该指针会指向基类的虚函数表;当子类被实例化时,该指针会指向子类的虚函数表,而这项操作是在编译器运行阶段执行的,这就是虚函数延时绑定的意义。接下来看看虚函数表里存放着什么内容。实例如下:
#include <iostream>
using namespace std;
class Base {
public:
virtual void fun1() {
std::cout<<"在基类调用fun1函数"<<endl;
}
virtual void fun2() {
std::cout<<"在基类调用fun2函数"<<endl;
}
};
class Derived1 :public Base {
public:
void fun1() {
std::cout<<"在子类1调用fun1函数"<<endl;
}
};
class Derived2 :public Base {
public:
void fun2() {
std::cout<<"在子类2调用fun2函数"<<endl;
}
};
int main() {
Base *b = new Derived1();
b -> fun2();
delete b;
return 0;
}
大家可以先思考一下上述程序的执行结果
答案如下:
也许一些同学觉得会报错,因为Derived1并没有fun2的实现,其实不会。首先看下这三个类的虚函数表:
| 类 | 虚函数表项1 | 虚函数表项2 |
|---|---|---|
| Base | Base::fun1() | Base::fun2() |
| Derived1 | Derived1::fun1() | Base::fun2() |
| Derived2 | Base::fun1() | Derived2::fun2() |
这里重点看看子类。可以看到,Derived1由于只实例化了fun1,对fun2的访问还是访问基类的fun2;相反,Derived2由于只实例化了fun2,对fun1的访问还是访问基类的fun1。至此,我们认识了虚函数,它是由于编译器的动静态联编的不同衍生的一种延时绑定的方法,可以将方法从静态联编转化为动态联编。
那虚函数有什么作用呢?它应用在什么地方呢?
总的来说,虚函数是为了实现多态设计的,它可以让成员函数的操作一般化,用基类变量代表子类执行统一的操作,而不用担心其函数指向问题,如实例1,以下实例是对虚函数作用比较好的例子: