谈谈C++多态的基本使用和总结

本文详细阐述了C++中多态的原理,包括虚函数的定义、重写规则、多态调用的规则,以及析构函数的重写、抽象类、接口继承与实现继承的区别。还介绍了如何通过final和override关键字控制继承和重写,并展示了如何设计不被继承的基类和使用这些特性进行实际编程示例。
摘要由CSDN通过智能技术生成

前言

1. 多态的理解

简单的说:多态就是根据你传入的不同的对象,去做同一样的事情,会有不同的结果和行为的表现产生;

在C++中,传入不同的对象意思:给父类传入不同的子类;

去做同一样的事意思是:父类指针或者引用调用了同一样的虚函数;

产生不同的结果意思是:调用同样的虚函数,但是结果却不一样;


2. 构成多态的条件

继承中要构成多态还有两个条件:
1. 必须通过基类的指针或者引用调用虚函数;
2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写;


3. 虚函数

虚函数:即被virtual修饰的类非静态成员函数称为虚函数。(静态成员不可以作为虚函数,全局函数也不行)
虚函数的重写;

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl;}
};

4. 虚函数的重写(覆盖)

派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。

class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
/*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后
基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用*/
/*void BuyTicket() { cout << "买票-半价" << endl; }*/ 
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }

5.多态调用虚函数的规则

构成多态,跟父类的指针或者引用的类型没有关系,传的哪个类型的对象,调用的就是这个类型的虚函数 – 跟对象有关;
不构成多态,调用就是父类指针或者引用类型的函数 – 跟类型有关;


#include<iostream>
using namespace std;
class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void test1(Person& p)
{
	p.BuyTicket();
}
void test2(Person* p)
{
	p->BuyTicket();
}
int main()
{
	Person ps;
	Student st;
	//父类引用调用虚函数,子类重写虚函数,构成多态;那么调用函数就传入的对象有关
	//这里传入的对象ps的类型为 Person 所以调用Person的虚函数;
	Person& p1 = ps;
	p1.BuyTicket();
	//父类引用调用虚函数,且子类重写父类虚函数,构成多态,那么调用函数就和传入的对象有关;
	//这里传入的对象是 Student,所以调用的是Student的虚函数;
	Person& p2 = st;
	p2.BuyTicket();

	//指针的分析和引用一致
	Person* p3 = &ps; //构成多态
	p3->BuyTicket();

	Person* p4 = &st; //构成多态
	p4->BuyTicket();

	getchar();
	return 0;
}

符合预期:
在这里插入图片描述


测试:假如不是父类指针或者引用调用虚函数会发生什么;

去除条件1,不是父类指针或引用而是父类对象去调用虚函数,是否会发生多态?

  1. 假如 子类对象赋值给父类对象会不会发生多态?肯定不会?所以父类对象调用虚函数,只看父类的类型;
	//子类对象赋值给父类对象,没有父类指针或引用指向子类对象,这不会构成多态,所以调用虚函数
	//只看p5的类型;
	Person p5 = st;
	p5.BuyTicket(); //调用的是父类的虚函数,不构成多态
  1. 即使有父类的指针或引用调用了虚函数,假如子类没有重写虚函数,依旧不会发生多态,没有多态那么,父类指针或引用去调用虚函数时候,都是看父类的类型的;
    (ps:我们可以修改父类和子类虚函数,只要不构成重写,就可以验证我上面的结论)

6. 析构函数的重写(基类与派生类析构函数的名字不同)

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。


class Person {
public:
virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person {
public:
virtual ~Student() { cout << "~Student()" << endl; }
};
// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,才能构成
//多态,才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{
	Person* p1 = new Person;
	Person* p2 = new Student;
	
	delete p1; // 编译器默认转化了调用虚构函数的形式 p1->destructor()      
	delete p2; // 编译器默认转化了调用虚构函数的形式 p2->destructor(),
				//由于构成多态,所以会正确的嗲用传入对象是子类的虚析构函数
	return 0;
}

构成多态,正确的调用了子类的析构函数;
这也是唯一的场景:是析构函数,需要写成虚析构的形式;
在这里插入图片描述


没有发生多态时候,不管你的父类还是子类是否为虚析构,都可以正确的调用析构函数
在这里插入图片描述


非虚析构函数,依旧可以正确释放
在这里插入图片描述


上面两种原因很简单:继承体系中,子类对象指向析构时候,本身就会现执行子类再执行父类,和你的析构函数是否为虚函数没关系;


所以说析构函数是否需要写成虚析构函数,首先得发生多态行为,才需要父类析构写成虚析构,这也才可以保证delete 父类指针时候,能够去调用子类的析构函数;


本质我们 delete 父类指针,就是为了析构子类对象的,因为父类指针指向了子类对象,我们希望的就是做这样的事;delete父类指针,为了析构子类对象,那么就需要发生多态,这个行为才会产生;有多态,就必须,子类重写父类的虚析构函数;


7. 设计一个不能被继承的基类

在C++98时候,有一种方式可以设计不被继承:那就是设计基类的构造函数为私有的

#include<iostream>
using namespace std;


class Base{
private:
	Base(){
		cout << "Base::Base()的构造函数调用" << endl;
	}
private:
	int _b;
};
class Derive :Base{
private:
	int _d;
};
int main(){

	Derive son;

	return 0;
}

这种方式:当我屏蔽了:Derive son这句代码,也就是,不创建子类对象时候,上面的代码编译是可以通过的;

但是当我写上了Derive son也就是创建子类对象时候,那么就会编译报错:原因就是基类的构造函数为私有,派生类创建对象时候,也是执行Derive son代码时候,需要先调用父类的构造函数,但是由于父类的构造函数时私有的,不可以被调用,所以就会编译报错了;

设计基类的构造函数是私有的:这也就解决了一个类不可以被继承的情况;


8. final关键字和override 关键字

C++11通过新增一个关键字final:解决了基类不可以被继承问题, 也就是不需要设计基类的构造函数为私有的;
具体操作就是:在声明基类的后面加多一个关键字 final就可以达到效果;

class Base final{//final关键字修饰,使得该类不可以被继承
public:
	Base(){
		cout << "Base::Base()的构造函数调用" << endl;
	}
private:
	int _b;
};
class Derive :public Base{
private:
	int _d;
};
int main(){

	Derive son;

	return 0;
}

在这里插入图片描述


final关键还可以修饰基类的虚函数,目的就是为了使得基类的虚函数不被子类重写;

class Base {
public:
	Base(){
		cout << "Base::Base()的构造函数调用" << endl;
	}
	virtual void fun() final{ //使得该虚函数不能够被重写
		cout << "Base::fun()被调用" << endl;
	}
private:
	int _b;
};

class Derive :public Base{
	public
	virtual void fun(){ //由于基类这个函数fun被final修饰,所以派生类不可以重写
		cout << "Derive::fun()被调用" << endl;
	}
private:
	int _d;
};
int main(){

	Derive son;

	return 0;
}

在这里插入图片描述


c++11还有一个关键字override,它是修饰派生类的虚函数,目的为的是:派生类必须重写基类虚函数,如果基类没有该虚函数,那么就会报错,或者派生类重写错乱基类虚函数,那么就会报错;

反正就是在基类中找不到你要重写的虚函数,那么就会直接报错;

所以说:override 有检查机制,帮你检查你的派生类是否正确的重写了基类的虚函数;


class Base {
public:
	Base(){
		cout << "Base::Base()的构造函数调用" << endl;
	}
	virtual void fun() { 
		cout << "Base::fun()被调用" << endl;
	}
private:
	int _b;
};

class Derive :public Base{
public:
	virtual void fun1() override{ 
		cout << "Derive::fun()被调用" << endl;
	}
private:
	int _d;
};
int main(){

	Derive son;

	return 0;
}

在这里插入图片描述


9. 抽象类

3.1 概念
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。
包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。
派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。
纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。


由于抽象类不能实例化对象,所以虚函数实现也是没有任何意义的,所以不会有人实现纯虚函数;
纯虚函数是可以实现的,但是就是没有意义罢了;


在这里插入图片描述

class Car
{
public:
	virtual void Drive() = 0; //纯虚函数
};
class Benz :public Car
{
public:
		virtual void Drive()
	{
	cout << "Benz-舒适" << endl;
	}
};
class BMW :public Car
{
public:
		virtual void Drive()
	{
		cout << "BMW-操控" << endl;
	}
};
void Test()
{
	Car* pBenz = new Benz;
	pBenz->Drive();
	Car* pBMW = new BMW;
	pBMW->Drive();
}

有纯虚函数,只能用父类指针或引用指向子类对象了,不可能再指向父类对象,因为父类对象是不可以被创建的;

10. 接口继承和实现继承区别

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。


  • 6
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

呋喃吖

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值