C++之多态二三事

本文深入探讨了C++中的多态性,包括重写、隐藏和重载的区别,着重解释了重写的要求和特殊情况——协变。此外,介绍了C++11新增的`final`和`override`关键字,以及它们在防止继承和确保函数重写方面的应用。同时,讨论了抽象类的概念,强调了纯虚函数在强制子类重写中的作用,以及虚析构函数在资源管理中的重要性。
摘要由CSDN通过智能技术生成


什么是多态
所谓的多态就是就是有多种形态。对应的就是同一个行文,会出现不同的形态。具体到一个生活中的实例就是:在火锅店消费的时候。同样是结账,但是却有不同的结果

普通用户:全价
vip会员:88折
学生:69折

类似这样的例子还有很多。而在面向对象程序设计中也需要多态,所以c++语言中也引进了多态!


重写的概念
首先,在谈多态之前不得不提一下一个概念—>重写,而在谈到重写这个概念之前,就要提到隐藏。来看下面这段代码:

class A
{
public:
	void func()
	{
		cout << "A::func()" << endl;
	}
};
class B :public A
{
public:
	void func()
	{
		cout << "B::func()" << endl;
	}
};
int main()
{  
	B b;
	b.func();
	return 0;
}

先看运行结果
在这里插入图片描述
很显然,这里的func和父类中的同名函数func构成的是隐藏的关系 再来看下面这一段代码

class A
{
public:
//virtual关键字声明函数,此时的函数就变成了虚函数
	virtual void func()
	{
		cout << "A::func()" << endl;
	}
};
class B :public A
{
public:
	virtual void func()
	{
		cout << "B::func()" << endl;
	}
};

此时的B类的func和A类的func不再是隐藏关系了,而是更加特殊的重写关系 两个函数构成重写有如下的要求:

1.两个函数要分别在两个类的作用域,并且这两个类必须要有继承关系
2.父类的同名函数必须是虚函数
3.构成重写关系的两个函数返回值,函数。参数名相同
4.父子函数构成协变也构成重写

满足以上四个条件,子类和父类的同名函数构成的关系就是重写,而重写关系和隐藏关系是互斥的,也就是说如果两个函数构成重写,那么这两个函数就不是隐藏关系!
那么前面三个条件已经演示过了,那么我们重点来看最后一个重写的特例:两个函数构成协变也是重写的关系 首先,我们通过一段代码来看看什么是协变

class A
{
public:
	virtual A* func()
	{  
		cout << "A::func()" << endl;
		return nullptr;
	}
};
class B :public A
{
public:
	virtual B* func()
	{   
		cout << "B::func()" << endl;
		return nullptr;
	}
};

在上面的代码里面,这里的返回值一个是A类的指针,一个是B类的指针,不符合我们重写的第二条的规则:虚函数的返回值的类型不同。但是这里同样构成重写关系 但是前提是,这两个函数仅仅只能是返回值不一样,并且返回值分别是必须是指针或者引用,并且返回的指针或者引用必须构成父子关系!
假设把返回类型改成下面这样:

class A
{
public:
	virtual A func()
	{  
		cout << "A::func()" << endl;
		return A();
	}
};
class B :public A
{
public:
	virtual B func()
	{   
		cout << "B::func()" << endl;
		return B();
	}
};

在这里插入图片描述
那么,在实际的应用中,协变应用相对较少。这也可以认为是c++语法设计的一个"坑",所以我们只要了解有协变这种特殊的情况就可以了。尽量在实际的设计中严格按照前面的3个条件设计重写。
值得一提的是,如果父类函数里面的虚函数加了virtual,即使子类的函数前面不加vitual也是虚函数。这也是c++设计的一个不足的地方,虽然可以不加,但是良好的习惯还是在子函数前面也加上virtual。


C++11新增的两个关键字
虽然继承可以实现代码的复用,但是在有的实际的场景下,有一些类不适合设计继承,所以为了解决这个问题。c++11引进了final关键字,接下来我们就通过代码来看看final关键字。

//final关键字
//final关键字,修饰类的时候,这个类不能被继承
class A final
{
public:
	void func()
	{
		cout << "A::func()" << endl;
	}
};
class B :public A
{
public:
};
int main()
{   

	return 0;
}

在这里插入图片描述
final不仅可以修饰类,还可以修饰父类的虚函数,表示这个虚函数不能被继承。

class A
{
public:
//final修饰虚函数,这个函数无法被子类继承
	virtual void func() final
	{
		cout << "A::func()" << endl;
	}
};
class B:public A
{
public:
	virtual void func()
	{
		cout << "B::func()" << endl;
	}
};

在这里插入图片描述
这是c++11新增的final关键字。但是又有一种情况,有的时候父类的虚函数必须被子类重写,但是由于各种原因没有进行重写,造成了严重的错误。为了避免这种情况发生带来的损失。 c++11引入了一个新的关键字—>override

//override的作用:强制检查子类是否重写父类的虚函数
class A
{
public:
	 void func() 
	{
		 cout << "A::func()" << endl;
	}
};
class B :public A
{
public:
//父类同名函数不是虚函数,所以这里并没有重写父类的func
	virtual void func() override
	{
		cout << "B::func()" << endl;
	}
};

在这里插入图片描述
对比final和override我们不难可以得出下面的结论

1.final是在父类中使用,限制子类不能继承父类
2.override是在子类中使用,强制子类要重写父类中的虚函数


重载,隐藏,重写三者的区别
结合前面的对于重写的分析,接下来我们来对比一下三个概念:

1.重载:两个函数在同一个作用域,两个函数根据不同的参数的类型和顺序修饰形成不同的函数地址.
2.隐藏:两个函数分别在在基类和派生类的作用域,当前作用域的同名函数隐藏外部作用域同名的函数,隐藏也叫做重定义.
3.重写:两个同名虚函数在基类和派生类的作用域,函数的返回值,参数完全相同(协变例外).这时候两个函数构成重写关系.


多态如何触发
接下来,我们就来谈一谈在c++里面怎么触发多态.要触发多态有如下两个条件:

1.基类的指针或者引用指向派生类对象
2.派生类的虚函数完成了对基类虚函数的重写.

这两个条件都很重要,缺少任何一个都不能构成多态的调用
接下来,我们用代码来验证多态的触发条件.

class A
{
public:
	virtual void func()
	{
		cout << "A::func()" << endl;
	}
};
class B:public A
{
public:
	virtual void func()
	{
		cout << "B::func()" << endl;
	}
};
int main()
{
	A a;
	B b;
	A* pa = &a;
	pa->func();
	cout << endl;
	B* pb = &b;
	pb->func();
	cout << endl;
	pa = pb;
	pa->func();
	return 0;
}

先来看对应的调用结果:
在这里插入图片描述
首先,第1和第2个易于理解,A类的指针指向A类对象的地址调用的自然就是A类的func,对于pb也是如此.而当pa指向B类对象的地址时,首先满足多态的第一个条件:基类的指针或引用指向派生类对象,接着派生类完成了对基类虚函数的重写,满足多态的两个条件!所以最后调用的是B类的重写的虚函数.


抽象类的概念
有的时候,一些东西是没办法具体化出实际对象的。而在c++语言里面也提供了抽象类的这一个概念.
所谓的抽象类,就是没办法实例化出对象的类!接下来我们来看怎么从语法上定义一个抽象类.

//}
//抽象类:只有纯虚函数的类是抽象类
//纯虚函数:虚函数声明后面=0
class A
{
public:
	virtual void func() = 0;
};
//抽象类不能创建对象
int main()
{
      A a;
    return 0;
}

在这里插入图片描述
假如这个时候有一个类B来继承这个抽象类A,但是没有重写里面的纯虚函数,那么这个B也是抽象类.

/抽象类:只有纯虚函数的类是抽象类
//纯虚函数:虚函数声明后面=0
class A
{
public:
	virtual void func() = 0;
};
class B:public A
{  
public:
};
int main()
{
	//A a;
	B b;
	return 0;
}

在这里插入图片描述
而当子类重写纯虚函数以后,子类才能创建对象!

/抽象类:只有纯虚函数的类是抽象类
//纯虚函数:虚函数声明后面=0
class A
{
public:
	virtual void func() = 0;
};
//B类必须重写才能创建对象!
class B:public A
{  
public:
	virtual void func()
	{
		cout << "B::func()" << endl;
	}
};
int main()
{
	//A a;
	B b;
	return 0;
}

值得一提的是:纯虚函数是可以有函数体的,但是实际意义并不大,因为纯虚函数是一定要被重写的!而g++下会直接报错
对比override我们可以得到如下的结论;

1.override只是检查是否重写,而抽象类是强迫子类必须重写
2.override修饰函数,子类依旧可以创建对象


虚拟构函数
前面我们说过,父子析构函数会构成隐藏关系.但是有的时候,子类直接管理堆上的动态资源.单纯只用父类的析构函数没办法解决问题!所以这时候就需要使用子类特定的析构函数,所以我们把父类的析构定义成virtual

class A
{
public:
	A(int a)
		:_a(a)
	{}
	~A()
	{
		cout << "~A" << endl;
	}
protected:
	int _a;
};
class B :public A
{
public:
	B()
		:A(0)
		,pb(new int(4))
	{}
	~B()
	{  
		cout << "~B" << endl;
		delete pb;
		pb = nullptr;
	}
private:
	int* pb;
};
int main()
{  //赋值兼容的转换
	A* pa = new B;
	//delete会调用析构函数
	delete pa;
	return 0;
}

运行结果:
在这里插入图片描述
可以看到这里只调用了A的析构函数所以这里存在严重的内存泄露问题! 所以我们希望调用到B的析构函数,所以我们就要把父类的析构函数声明成虚函数

class B :public A
{
public:
	B()
		:A(0)
		,pb(new int(4))
	{}
	virtual ~B()
	{  
		cout << "~B" << endl;
		delete pb;
		pb = nullptr;
	}
private:
	int* pb;
};
int main()
{  
	A* pa = new B;
	delete pa;
	return 0;
}

在这里插入图片描述
可以看到,这里我们调用到了子类的析构函数处理了资源,所以我们的建议是基类的析构函数尽量声明成虚函数,基类的成员的访问控制符尽量用保护!


以上就是本文的主要内容,希望大家可以一起进步.

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值