虚函数

本篇目录

************************************

*     什么是虚函数                           *

*     虚函数指针以及虚函数表          *

*     虚函数表重写的问题                *

*     成为虚函数的条件                    *

*     虚函数的生存周期                    *

*     为什么要使用虚析构函数          *

*     虚函数与访问限定                    *

************************************

一、什么是虚函数?

虚函数就是在普通函数前面加了virtual关键字。

声明时要加virtual,定义时不用加。

虚函数的作用==》用指针/引用调用时会发生动态的绑定。

二、虚函数指针

(1)有了虚函数,会产生一个虚函数指针。

(2)一个类中有无数多个虚函数都只会产生一个虚函数指针。


由上面例子可以看出,多了一个虚函数指针,导致sizeof(Base)=8;尽管有两个虚函数,但是sizeof(Base)=8.

(3)虚函数指针的优先级最高,在内存布局中在最前面。

(4)虚函数指针指向的是虚函数表。


查看内存布局可以验证。

三、虚函数表

(1)虚函数表中存放的信息。


(2)虚函数表与对象类型对应,一个类型只有一张虚函数表。

比如定义了Base b1(10),Base b2(20),Base b3(30)……,但仍然只有一张虚函数表。一个类型对应一个虚函数表!!!

(3)虚函数表在编译期间产生,存放在只读数据段,生存周期从程序开始到程序结束。

四、虚函数表重写的问题

class Base{
public:
	Base(int a):ma(a){
		cout<<"Base(int)"<<endl;
		clear();//调用了clear函数
	}
	void clear(){
		memset(this,0,sizeof(Base));//将对象清零
	}
	virtual ~Base(){
		cout<<"~Base()"<<endl;
	}
	virtual void show(){
		cout<<"Base::ma="<<ma<<endl;//打印一下ma的值
	}
protected:
	int ma;
};

class Derive:public Base{
public:
	Derive(int b):Base(b),mb(b){
		cout<<"Derive(int)"<<endl;
	}
	~Derive(){
		cout<<"~Derive()"<<endl;
	}
	void show(){
		cout<<"Base::ma="<<ma<<" Derive::mb="<<mb<<endl;//打印ma和mb的值
	}
private:
	int mb;
};

(1)在堆上开辟一个基类的对象,进行打印:


程序崩溃,原因在于p1调用show函数时,因为是虚函数,所以要去虚函数表中去找,但是在构造对象时,已经将对象进行清0,0地址访问函数出错。

(2)用基类指针指向派生类对象时,会发生什么情况呢?


程序运行正确,你可能会想,在派生类构造之前需要构造基类部分,因此在派生类生成的过程中一定调用了基类的构造函数,但为什么程序正常呢?

原因就是在继承过程中,每个类型对应一个虚函数表,每一层的构造刚开始之前都需要把虚函数表的地址往虚函数指针写一遍,因为指向什么对象,指针就应该存谁的虚函数表。

顺一下思路:生成派生类对象时,先构造基类部分,再构造派生类部分。在基类部分构造的时候,clear将对象清零,基类虚函数指针为0了;在构造派生类的时候,派生类中也应该有一张虚函数表,虚表重写,虚函数指针不为0,只是在打印继承过来的ma时,为0.

五、生成虚函数的条件

因为虚函数表中存放的时虚函数的地址  ==》  因此虚函数一定要能取地址

因为虚函数指针存在于对象中 ==》  因此虚函数要用对象来调用

上面两条必不可少,综上,我们来判断一下,下面哪些函数能成为虚函数?

1)构造函数      =》不能成为虚函数,因为对象还未生成。

2)析构函数      =》可以成为虚函数,满足上面两条。

3)静态成员方法   =》不可以成为虚函数,因为不依赖于对象的调用。

4)内联函数       =》不可以成为虚函数,因为不能取地址。

六、虚函数的生存周

我们写出以下代码:派生类继承了基类,并且在基类和派生类中都有一个虚函数void show(){}

class Base{
public:
	Base(int a):ma(a)
	{
		cout<<"Base(int)"<<endl;
		//show();//*1
	}
	virtual ~Base()
	{
		//show();//*2
		cout<<"~Base()"<<endl;
	}
	virtual void show()
	{
		cout<<"Base::ma="<<ma<<endl;
	}
protected:
	int ma;
};

class Derive:public Base
{
public:
	Derive(int b):Base(b),mb(b)
	{
		cout<<"Derive(int)"<<endl;
		//show();//*1
	}
	~Derive()
	{
		//show();//*2
		cout<<"~Derive()"<<endl;
	}
	void show()
	{
		cout<<"Base::ma="<<ma<<" Derive::mb="<<mb<<endl;
	}
private:
	int mb;
};

我们分别在构造函数和析构函数中调用show()方法

int main()
{
	Derive* p=new Derive(20);
	p->show();
	delete p;

	return 0;
}

(1)去掉代码中 *1 的注射,查看反汇编

Base下,构造函数中调用虚函数show:


Derive下,构造函数中调用虚函数show:


可以看出在构造函数中虚函数的调用不是动态的绑定,而是静态的绑定。因为在构造函数中对象正在生成,因此没有发生多态。

(2)去掉代码中的 *2 注释,查看反汇编

Base下析构函数中的调用虚函数show


Derive下析构函数中的调用虚函数show


可以看出在析构函数中虚函数 的调用步数动态的绑定,而是静态的绑定。因为在析构函数中对象正在死亡,逻辑意义上对象已经不完整,因此调用虚函数没发生多态。

综上:虚函数的生存周期:从构造函数开始,到析构结束。

七、为什么要使用虚析构函数?

(1)如果一个基类指针指向派生类时,并且派生类对象在堆上构造时,调用delete时,只看指针的类型,那么就会造成指针只会析构基类的对象,而派生类的对象并未析构,将会导致内存泄漏。

class Base{
public:
	Base(int a):ma(a){
		cout<<"Base(int)"<<endl;
	}
	~Base(){
		cout<<"~Base()"<<endl;
	}
	void operator delete(void *p){
		cout<<"free start addr:"<<p<<endl;
		free(p);
	}
private:
	int ma;
};
class Derive:public Base
{
public:
	Derive(int b):Base(b),mb(b){
		cout<<"Object start addr:"<<this<<endl;
		cout<<"Derive(int)"<<endl;
	}
	~Derive(){
		cout<<"~Derive()"<<endl;
	}
	void* operator new(size_t size){
		void* p=malloc(size);
		cout<<"malloc start addr:"<<p<<endl;
		return p;
	}
private:
	int mb;
};
int main(){
	Base* pb=new Derive(20);
	delete pb;
	return 0;
}

打印结果:


可以看出:代码运行正常,但是对于析构只是析构了基类的一部分,并没有调用派生类的析构,造成内存泄漏。

(2)接着,当派生类中有虚函数时,调用delete会使程序奔溃。因为生成对象时,指针永远保存的是基类的起始地址。
class Base{
public:
	Base(int a):ma(a){
		cout<<"Base(int)"<<endl;
	}
	~Base(){
		cout<<"~Base()"<<endl;
	}
	void operator delete(void *p){
		cout<<"free start addr:"<<p<<endl;
		free(p);
	}
private:
	int ma;
};
class Derive:public Base
{
public:
	Derive(int b):Base(b),mb(b){
		cout<<"Object start addr:"<<this<<endl;
		cout<<"Derive(int)"<<endl;
	}
	~Derive(){
		cout<<"~Derive()"<<endl;
	}
	virtual void show(){
		cout<<"mb="<<mb<<endl;
	}//只有派生类有虚函数
	void* operator new(size_t size){
		void* p=malloc(size);
		cout<<"malloc start addr:"<<p<<endl;
		return p;
	}
private:
	int mb;
};
int main(){
	Base* pb=new Derive(20);
	delete pb;//程序会崩溃
	return 0;
}


程序出现奔溃,从打印的地址上可以看出,派生类在生成时,是从0x01344560处开始开辟,因此指针也是从该地址处指向,但是在delete指针时,只是看指针的类型,而指针的类型是Base*,所以它就会调用基类的析构函数,基类部分和对象开辟的起始位置不一样,所以程序奔溃。

派生类的内存布局:


(3)写成虚析构

class Base{
public:
	Base(int a):ma(a){
		cout<<"Base(int)"<<endl;
	}
	virtual ~Base(){
		cout<<"~Base()"<<endl;
	}
	void operator delete(void *p){
		cout<<"free start addr:"<<p<<endl;
		free(p);
	}
private:
	int ma;
};
class Derive:public Base
{
public:
	Derive(int b):Base(b),mb(b){
		cout<<"Object start addr:"<<this<<endl;
		cout<<"Derive(int)"<<endl;
	}
	~Derive(){
		cout<<"~Derive()"<<endl;
	}
	void* operator new(size_t size){
		void* p=malloc(size);
		cout<<"malloc start addr:"<<p<<endl;
		return p;
	}
private:
	int mb;
};
int main(){
	Base* pb=new Derive(20);
	delete pb;
	return 0;
}

运行成功并且没有内存泄漏:



总结:将析构函数写成虚析构的前提条件是什么?===》在堆上生成对象时。

八、虚函数与访问限定

写出如下代码:

class Base{
public:
	Base(int a):ma(a){}
	virtual ~Base(){}
	//virtual void show(){
	//	cout<<"Base::ma="<<ma<<endl;
	//}
	//*1
protected:
	//virtual void show(){
	//	cout<<"Base::ma="<<ma<<endl;
	//}
	//*2
	int ma;
};

class Derive:public Base{
public:
	Derive(int b):Base(b),mb(b){}
	~Derive(){}
private:
	void show()
	{
		cout<<"Base::ma="<<ma<<" Derive::mb="<<mb<<endl;
	}
	int mb;
};
int main()
{
	Base* p=new Derive(20);
	p->show();//动态的绑定
	delete p;

	return 0;
}

在上述代码中,

1)派生类中的虚函数不论写在private/protected/public下,只要基类的虚函数写成private/protected下时,会出现编译错误。

2)派生类中的虚函数不论写在private/protected/public下,只要基类的虚函数写在public下时,运行成功。

原因:

1)访问限定符只在编译期间起作用。

2)p为Base*类型(基类指针),编译期间调用基类的show方法(放在private/protected下时,编译错误),发现show方法为虚函数,动态的绑定,p指向的是derive类型的对象,运行的时候从派生的的虚表中找虚函数,运行时访问限定不起作用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章