概念
虚函数是以virtual关键字声明的函数,如果在基类中将某个函数指定为virtual,并且派生类中有该函数的另外一个定义,那么编译器将会知道我们并不想静态链接改函数。我们需要的是基于调用该函数的对象的种类,在程序的特定位置选择调用哪一个函数。
如:以下代码未用virtual:
#include<bits/stdc++.h>
using namespace std;
class A
{
public:
void get()
{
fun();
}
void fun()
{
cout << "A" << endl ;
}
};
class B : public A
{
public:
void fun()
{
cout << "B" << endl ;
}
};
int main()
{
A a;
a.get() ;
B b;
b.get() ;
return 0;
}
/* 输出:
A
A
*/
可以看出,当未用虚函数时,通过父类的get函数调用的fun也是父类的。而我们需要调用各自对应的fun()函数。
要使得调用的是对应的类调用对应的函数,则需要在父类的fun()函数声明为虚函数。
virtual void fun()
{
cout << "A" << endl ;
}
纯虚函数
当一个类中的某一个函数被定义为纯虚函数,那么这个类被称为抽象类,所有继承它的类都要实现这个类,否则也是抽象类,抽象类不能实例化。
纯虚函数的表示为
virtual void fun() = 0;
纯虚函数不需要被定义,只要有声明就可以了,子类会去定义它。这就是多态的实现原理。
下面这个A就是抽象类,B实现了A中的虚函数,所以B不是抽象类。
#include<bits/stdc++.h>
using namespace std;
class A
{
public :
virtual void fun() = 0;
};
class B : public A
{
public :
void fun()
{
cout << 'B';
}
};
int main()
{
B b;
return 0;
}
虚析构函数
如果我们有两个类A和B,B继承自A,那么当我们使用父类指针指向子类时:
A *p = new B;
delete p;
当delete p时,只调用了父类A的析构函数,并不能调用子类B的析构函数,此时要想正确的释放掉p指向的内存,则需要将父类A中的析构函数定义为虚函数。
虚函数表
当一个类包含虚函数时,无论是基类还是继承自基类的子类,都包含一个虚表。用来存放虚函数的地址。同一个类的所有实例化对象中有一个指针指向这个虚表,所以,一个类的所有实例化对象都共用这个虚表,可以理解为虚表是static类型的。
即:
- 虚表是类的,不是对象的。(static)
- 同一个类的所有对象共用这个虚表(通过指针)
- 每一个对象被创建的时候自动包含了这个指针,指向虚表
下面这个类,类A包含了3个虚函数:
class A
{
public:
virtual void v_fun1();
virtual void v_fun2();
virtual void v_fun3();
...
};
所以它的虚表为
假如A创建了两个对象a、b,那么a和b都有一个虚表指针指向这个虚表:
假如有以下继承关系:
class A
{
public:
virtual void v_fun1();
virtual void v_fun2();
virtual void v_fun3();
...
};
class B : public A
{
public :
virtual void v_fun1()
{ }
....
};
class C : public B
{
public:
virtual void v_fun2()
{ }
...
}
B重写了v_fun1(), C重写了v_fun2()。
由于这三个类都有虚函数,所以这三个类都有一个虚表,如果一个类没有实现某个虚函数,那么这个类就会往上寻找第一个实现虚函数的地址。
对象的虚表指针用来指向自己所属类的虚表,虚表中的指针会指向其继承的最近的一个类的虚函数。
动态绑定
还是上面A、B、C三个类。
假设:
B b;
A *p = &b;
我们声明一个A类型的指针指向B对象的实例化对象,虽然p是基类的指针只能指向基类的部分,但是虚表指针亦属于基类部分,所以p可以访问到对象B的虚表指针。B的虚表指针指向类B的虚表,所以p可以访问到B的虚表。
如果我们:
p->v_fun1();
- 在执行这个语句时,会发现p是个指针,并且调用的函数是虚函数。
- 执行p->__vptr来访问到对象B对应的虚表。
- 虚表中的函数指针指向B::v_fun1()。
所以最终调用的是B::v_fun1()。
如果:
A a;
A *p = &a;
p->v_fun1();
当A创建对象时,已经将虚表指针指向A的虚表。所以最终调用的是A::v_funq1()。
我们把经过虚表调用的虚函数称之为动态绑定,其表现出来的现象称为运行时多态。实现动态绑定需要满足以下条件:
- 通过指针来调用函数
- 指针向上转型
- 调用的是虚函数
总结
虚函数要满足两点,一就是对象里面的函数,二就是函数可以取地址。
所以,以下几种函数不能声明为虚函数:
- 内联函数:内联函数只是在函数调用点将其代码段展开,它不能产生函数符号,所以不能往虚表中存放,自然就不能成为虚函数。
- 静态函数:定义为静态函数的函数,这个函数只和类有关系,并不是对象的调用,所以也不能成为虚函数。
- 构造函数:都知道只有当调用了构造函数,这个对象才能产生,如果把构造函数写成虚函数,这时候我们的对象就没有办法生。
参考
- 《Visula.C.2010入门经典(第五版)》
- https://blog.csdn.net/LC98123456/article/details/81143102
- https://blog.csdn.net/lihao21/article/details/50688337