C++虚函数

概念


       虚函数是以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();
  1. 在执行这个语句时,会发现p是个指针,并且调用的函数是虚函数。
  2. 执行p->__vptr来访问到对象B对应的虚表。
  3. 虚表中的函数指针指向B::v_fun1()。

所以最终调用的是B::v_fun1()。

 

如果:

A a;
A *p = &a;
p->v_fun1();

 当A创建对象时,已经将虚表指针指向A的虚表。所以最终调用的是A::v_funq1()。

 

我们把经过虚表调用的虚函数称之为动态绑定,其表现出来的现象称为运行时多态。实现动态绑定需要满足以下条件:

  • 通过指针来调用函数
  • 指针向上转型
  • 调用的是虚函数

 

总结


虚函数要满足两点,一就是对象里面的函数,二就是函数可以取地址。

所以,以下几种函数不能声明为虚函数:

  1. 内联函数:内联函数只是在函数调用点将其代码段展开,它不能产生函数符号,所以不能往虚表中存放,自然就不能成为虚函数。
  2. 静态函数:定义为静态函数的函数,这个函数只和类有关系,并不是对象的调用,所以也不能成为虚函数。
  3. 构造函数:都知道只有当调用了构造函数,这个对象才能产生,如果把构造函数写成虚函数,这时候我们的对象就没有办法生。

 

参考


  1. 《Visula.C.2010入门经典(第五版)》
  2. https://blog.csdn.net/LC98123456/article/details/81143102
  3. https://blog.csdn.net/lihao21/article/details/50688337

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值