C++虚函数、纯虚函数、虚析构、纯虚析构、动态绑定和抽象类详解。

目录

1.虚函数的概念

2.虚函数的定义

3.虚函数的作用

4.用虚函数实现多态的方法

5.动态绑定和静态绑定

6.纯虚函数和抽象类

7.虚析构和纯虚析构


1.虚函数的概念

在C++程序中我们经常可以看见关键字virtual来定义一个函数,在这里我们需要知道虚函数的概念:在某基类中声明为virtual并且在它的一个或多个派生类中被重写的成员函数成为虚函数。

重写(发生在继承类中)方法名称、参数类型和返回值类型完全相同。

2.虚函数的定义

class Base {		//基类
public:
	virtual void fun();		//格式 virtual +  成员函数说明
};
class Son :Base{		//派生类  格式 class 类名: 基类名
public:
	//对基类的虚函数进行重写
	virtual void fun();		//格式 virtual +  成员函数说明
};

3.虚函数的作用

在C++中,虚函数是实现多态性的主要手段之一。多态性是指用一个名字定义不同的函数,这些函数执行不同但又类似的操作,这样就可以用同一个函数名调用不同内容的函数。换言之,可以用同样的接口访问功能不同的函数,从而实现“一个接口,多种方法”。

4.用虚函数实现多态的方法

在基类中定义一个虚函数,它的派生类继承该虚函数并且重写该函数,对于不同派生类的对象接收同一个信息,调用相同的函数名,但是执行的操作不同(执行的为各派生类中重写的函数),这样就利用虚函数实现了多态。

代码详解

#include<iostream>
using namespace std;
class Base {		//基类
public:
	virtual void fun()	//格式 virtual +  成员函数说明
	{
		cout << "Base-fun()" << endl;
	}
};
class Son1 :public Base{		//派生类  格式 class 类名: 基类名
public:
	//对基类的虚函数进行重写
	virtual void fun()		//格式 virtual +  成员函数说明
	{
		cout << "Son1-fun()" << endl;
	}
};
class Son2 :public Base {		//派生类  格式 class 类名: 基类名
public:
	//对基类的虚函数进行重写
	virtual void fun()		//格式 virtual +  成员函数说明
	{
		cout << "Son2-fun()" << endl;
	}
};
void test1()
{
	Base* p1 = new Son1;//一个基类指针p1指向新建的派生类Son1对象
	Base* p2 = new Son2;//一个基类指针p2指向新建的派生类Son2对象
	p1->fun();
	p2->fun();
	delete p1; delete p2;
}
int main()
{
	test1();
}

 在本例中,Base中的虚函数fun()被派生类Son1和Son2所继承,在每一个派生类的定义中,fun()函数都被重写。在main函数里定义两个基类指针p1和p2,分别指向new出的不同派生类的对象。

然后当我们通过基类指针调用fun()函数时,基类指针指向的是哪一个派生类的对象就执行该类下对应的fun()函数。即执行的是Son1::fun()和Son2::fun()。

其实在派生类中重新定义基类的虚函数时不再需要virtual,读者可以自行实践。


5.动态绑定和静态绑定

理解C++中动态绑定和静态绑定的区别可以帮助我们更好的理解多态性。

静态绑定:绑定的是对象的静态类型,某特性(比如函数)依赖于对象的静态类型,发生在编译期。
动态绑定:绑定的是对象的动态类型,某特性(比如函数)依赖于对象的动态类型,发生在运行期。

c++中,我们在使用基类的引用(指针)调用虚函数时,就会发生动态绑定。所谓动态绑定,就是在运行时,虚函数会根据绑定对象的实际类型,选择调用函数的版本。在上述例子中已经有体现。

并且只有采用“指针->函数()”或“引用变量. 函数()”的方式调用虚函数才会执行动态绑定。

代码详解

(基类和派生类的定义与前图相同,不再赘述)

void test2()
{
	Base obj_base;
	Son1 obj_son1;
	Base* p_1 = &obj_son1;//指针方式
	Base& p_2 = obj_son1;//引用方式
	obj_base.fun();			//静态绑定:调用对象本身(基类Base对象)的fun()
	obj_son1.fun();			//静态绑定:调用对象本身(派生类Son1对象)的fun()
	p_1->fun();				//动态绑定:调用被引用对象所属类(派生类Son1)的fun()
	p_2.fun();					//动态绑定:调用被引用对象所属类(派生类Son1)的fun()
}
int main()
{
	test2();
}


6.纯虚函数和抽象类

在C++中,许多情况下,在基类中不能对虚函数给出有意义的实现,故给它说明为纯虚函数,它的实现由该基类的派生类去完成(即重写该函数)。带有纯虚函数的类称为抽象类

纯虚函数的定义

virtual <类型><函数名>(<参数表>)=0;

class Base {		//基类
public:
	/*虚函数*/
	//virtual void fun()	//格式 virtual +  成员函数说明
	//{
	//	cout << "Base-fun()" << endl;
	//}

	/*纯虚函数*/
	virtual void fun() = 0;
};

 当基类中出现有纯虚函数之后,该类便被称为抽象类,抽象类具有一个特点:无法实例化对象

当Base类是抽象类之后如果我们写一行代码 Base b;希望创建一个Base类的对象,编译器便会报错(VS2019):E0322    不允许使用抽象类类型 "Base" 的对象:   

基类的派生类中如果没有对纯虚函数进行重写,则该派生类也无法实例化对象。只有派生类对该纯虚函数进行重写操作后,该派生类便能实例化对象。

/*基类Base中有纯虚函数*/
void test3()
{
	Base obj_base2;  //错误  基类是抽象类,不允许使用抽象类类型"Base"的对象

	/*如果派生类Son1中没有对纯虚函数进行重写*/
	Son1 obj_son2;		//错误  Son1是抽象类,不允许使用抽象类类型"Son1"的对象

	/*派生类Son1中有对纯虚函数进行重写*/
	Son1 obj_son3;		//正确  Son1有对纯虚函数进行重写,可以实例化对象
}

 纯虚函数和虚函数的最主要区别就是:是否可以实例化对象,其余操作大体相同。


7.虚析构和纯虚析构

什么时候需要用到虚析构?为什么要使用虚析构?

事实上我们在C++中使用虚析构是为了解决基类类指针释放子类对象不干净的问题

原因是基类的指针在析构时不会调用派生类中的析构函数,导致子类如果有堆区属性,会造成内存泄漏

下面用一组代码来分析

#include<iostream>
using namespace std;
#include<string>
//虚构和纯虚构
class Animal
{
public:
	Animal()
	{
		cout << "Animal构造函数调用" << endl;
	}
	~Animal()//普通析构函数
	{
		cout << "Animal析构函数调用" << endl;
	}

	//virtual ~Animal()//虚析构函数
	//{
	//	cout << "Animal析构函数调用" << endl;
	//}

	virtual void speak() = 0;//当类中有纯虚函数,类为抽象类
};

class Dog :public Animal
{
public:
	Dog() { m_Name = NULL; }
	Dog(string name)
	{
		cout << "Dog构造函数调用" << endl;
		m_Name = new string(name);		//在堆区开辟内存,最后需要回收
	}
	virtual void speak()		//基类虚函数的重写
	{
		cout << *m_Name << "小狗在说话" << endl;
	}
	~Dog()
	{
		if (m_Name != NULL)
			delete m_Name;//在析构函数中回收开辟的内存
		cout << "Dog析构函数调用" << endl;
	}

	string* m_Name;
};

void test01()
{
	Animal* dog = new Dog("PETTER");
	dog->speak();
	delete dog;
}
int main()
{
	test01();
}

从程序的执行结果来看,我们调用了Animal1和Dog的构造函数,但是我们发现程序却只执行了基类Animal的析构函数,很显然我们派生类Dog的析构函数没有被执行。但是我们在派生类中为m_Name在堆区开辟了空间,只有执行了它的析构函数才能对这部分空间进行回收,否则便造成了内存泄漏。 那么怎么解决这个问题呢?很简单,我们只需要将基类中的析构函数前加上关键字virtual使得它变为虚析构函数,这时我们便发现程序便能执行派生类的析构函数了!成功对内存进行回收。

更改代码

    //~Animal()//普通析构函数
	//{
	//    cout << "Animal析构函数调用" << endl;
	//}

virtual ~Animal()//虚析构函数
	{
		cout << "Animal析构函数调用" << endl;
	}

现在我们可以执行派生类的析构函数啦!


从上面我们得知虚函数和纯虚函数的关系,那么我们也不难知道纯虚析构的定义

纯虚析构语法:virtual ~类名()=0;
                                类名::~类名(){}

与上面纯虚函数不同的是:纯虚函数需要有声明,同时也需要有实现过程。(虚函数只需要有声明)否则在编译过程便会报错:无法解析的外部符号。

class Animal
{
public:
	virtual ~Animal() = 0;//纯虚析构的声明,必须有实现过程
};
Animal::~Animal()//Animal纯虚析构的实现代码
{  
		cout << "Animal纯虚析构函数调用" << endl;
}

 值得一提的是,如果基类中含有纯虚析构函数,那么该类也是抽象类,无法实例化对象。


 

虚析构和纯虚析构的共性

1.都可以解决父类指针释放子类对象
2.都需要具体的函数实现

虚析构和纯虚析构的区别:
如果是纯虚析构,则该类属于抽象类,无法实例化对象。

猜你喜欢

转载自blog.csdn.net/m0_52910424/article/details/121452005