[C++]——多态

🌇个人主页麦麦

📚今日名言:海水有尽头,月亮有圆缺,人间有不足,但你在,就能弥补。------ 钱钟书

目录

一、前言

二、正文

[2.1 多态的概念](#2.1 多态的概念)

[2.2 多态的定义及实现](#2.2 多态的定义及实现)

2.2.1多态的构成条件

2.2.2虚函数

2.2.3虚函数的重写

[2.2.4 override和final(C++11)](#2.2.4 override和final(C++11))

[2.2.5 重载、覆盖(重写)、隐藏(重定义)的对比](#2.2.5 重载、覆盖(重写)、隐藏(重定义)的对比)

[2.3 抽象类](#2.3 抽象类)

2.3.1概念

[2.3.2 接口继承和实现继承](#2.3.2 接口继承和实现继承)

[2.4 多态的原理](#2.4 多态的原理)

2.4.1虚函数表

2.4.2多态的原理

[2.5 单继承和多继承关系的虚函数表](#2.5 单继承和多继承关系的虚函数表)

2.5.1单继承中的虚函数表

[2.5.2 多继承中的虚函数表](#2.5.2 多继承中的虚函数表)

[2.5.3 菱形继承、菱形虚拟继承](#2.5.3 菱形继承、菱形虚拟继承)

三、结语


一、前言

今天为大家带来多态的讲解,文章若有不足之处,欢迎大家给出指正!

二、正文

2.1 多态的概念

多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。

既然C++是一门面向对象的语言,自然多态这一特性是在我们的生活中有其应用的场景。就拿买票就这一件事来说好了,火车票发售的价格是固定的,但是它会随着购买人群的价格而有所不同,普通人买票可能是全价买票,学生买票可能就是打九五折。又或者是逛超市买东西,会员和非会员可能会享受不同的购买价格,而这恰恰符合多态的概念。

2.2 多态的定义及实现

2.2.1多态的构成条件

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了 Person。Person对象买票全价,Student对象买票九五折。

在了解了多态的概念后,在继承中构成多态还有两个条件:

1. 必须通过基类的指针或者引用调用虚函数

2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

具体代码如下:

cpp 复制代码
class Person
{
public:
	//基类的虚函数
	virtual void Buy_tickets()
	{
		cout << "Person->买票全价" << endl;
	}

};

class Student :public Person
{
public:
	//子类的虚函数重写
	virtual void Buy_tickets()
	{
		cout << "Student->买票九五折" << endl;
	}
};

void func(Person& p)
{
	p.Buy_tickets();
}

void func(Person* p)
{
	p->Buy_tickets();
}

void text_1()
{
	Person p;
	Student s;
	cout << "引用调用" << endl;
	func(p);
	func(s);
	cout << "指针调用" << endl;
	func(&p);
	func(&s);
}

int main()
{
	text_1();

	return 0;
}

2.2.2虚函数

在多态的构成条件中我们提到了虚函数,那么虚函数是什么呢,或者我们该如何写出一个虚函数呢?其实很简单,只需要在我们想要的类成员函数前面加上virtual这一关键字就可以了,注意要和上一节虚继承virtual加的位置加以区分嗷。

cpp 复制代码
class Person
{
public:
	//基类的虚函数
	virtual void Buy_tickets()
	{
		cout << "Person->买票全价" << endl;
	}

};

2.2.3虚函数的重写

多态中除了要求基类对象要有虚函数之外,子类还要对该虚函数进行重写

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

代码如下:

cpp 复制代码
class Person
{
public:
	//基类的虚函数
	virtual void Buy_tickets()
	{
		cout << "Person->买票全价" << endl;
	}

};

class Student :public Person
{
public:
	//子类的虚函数重写
	virtual void Buy_tickets()
	{
		cout << "Student->买票九五折" << endl;
	}

    //注:以下这种子类重写方式也可以,不过不太规范,不建议使用
	void Buy_tickets()
	{
		cout << "Student->买票九五折" << endl;
	}

};

既然在上面的代码中我们说到了子类的虚函数重写可以不用加virtual关键字,那么我们借来我们就来谈谈虚函数重写的两个例外:协变和析构函数的重写

首先是协变,虽然虚函数要求三同,即返回值,函数名和参数类型相同,但是有一种对于返回值有一种情况下虽然不用,但是也可以形成多态。 即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。不过要注意的是,若是采取协变的写法,父类和子类的返回值必须同时是指针或者引用,不可一个指针,一个引用,以及父类只能返回父类的对象和指针,子类只能返回子类的对象和指针。

然后是析构函数的重写,我们在类和对象中学过一个类的析构函数的函数名为"~函数名()"的形式,那么就不满足重写三同中的函数名相同这一条件,但是由于底层的时候析构函数都会处理成destructor这一个统一的名字 。在知道析构函数能够重写之后,那么析构函数重写有什么用呢?一个比较重要的作用呢就是可以避免内存泄漏,因为当子类中又动态申请的空间时,但是我们又是申请的子类对象并切片在一个父类的指针或引用时,在delete这个子类对象的时候,由于析构函数在底层的函数名相同,若是不重写就只能完成父类资源的清理,子类就会出现内存泄漏了。

具体代码如下:

cpp 复制代码
//虚函数的协变
class Person
{
public:
    //返回父类的指针/引用
	virtual Person* Buy_tickets()
	{
		cout << "Person->买票全价" << endl;
		return this;
	}

	virtual ~Person()
	{}
};

class Student :public Person
{
public:
	//返回子类的指针/引用
	virtual Student* Buy_tickets()
	{
		cout << "Student->买票九五折" << endl;
		return this;
	}
	virtual ~Student()
	{
	}
};
cpp 复制代码
//析构函数的重写
class Person
{
public:
    //重写的
	virtual ~Person()
	{
		cout << "~Person" << endl;
	}
    //未重写
    ~Person()
	{
		cout << "~Person" << endl;
	}
};

class Student :public Person
{
public:

	 ~Student()
	{
		cout << "~Student" << endl;
	}
private:
	int* arr=new int[10];
};

void  text_2()
{
	Person* pptr = new Person;
	delete pptr;

	pptr = new Student;
	delete pptr;

}

int main()
{
	text_2();

	return 0;
}

2.2.4 override和final(C++11)

从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写或不能重写。

cpp 复制代码
// 1.final:修饰虚函数,表示该函数不能被重写
class Car
{
public:
 virtual void Drive() final {}
};
class Benz :public Car
{
public:
 virtual void Drive() {cout << "Benz-舒适" << endl;}
};


//2.override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
class Car{
public:
 virtual void Drive(){}
};
class Benz :public Car {
public:
 virtual void Drive() override {cout << "Benz-舒适" << endl;}
};

2.2.5 重载、覆盖(重写)、隐藏(重定义)的对比

到目前为止,我们已经认识了重载,重写和重定义,那么这三者之间有什么联系和区别呢,见下图。

注:重写是特殊的重定义,可以说重写是重定义的子集

2.3 抽象类

2.3.1概念

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

cpp 复制代码
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();
}

2.3.2 接口继承和实现继承

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

2.4 多态的原理

2.4.1虚函数表

在经过前面的学习后,相信大家已经能够熟练的使用多态来满足自己的需求,接下来让我们来进行多态原理的学习,即多态是如何实现的。在进行讲解之前,大家可以先做一下下面这道题,看看sizeof(Base)是多大?

cpp 复制代码
//  sizeof(Base)是多少?

class Base
{
public:
 virtual void Func1()
 {
 cout << "Func1()" << endl;
 }
private:
 int _b = 1;
};

通过观察测试我们发现b对象是8bytes,除了_b成员,还多一个__vfptr放在对象的前面(注意有些 平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针( v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表,那么派生类中这个表放了些什么呢?我们接着往下分析

cpp 复制代码
// 针对上面的代码我们做出以下改造
// 1.我们增加一个派生类Derive去继承Base
// 2.Derive中重写Func1
// 3.Base再增加一个虚函数Func2和一个普通函数Func3

class Base
{
public:
 virtual void Func1()
 {
     cout << "Base::Func1()" << endl;
 }
 virtual void Func2()
 {
     cout << "Base::Func2()" << endl;
 }
 void Func3()
 {
     cout << "Base::Func3()" << endl;
 }
private:
 int _b = 1;
};


class Derive : public Base
{
public:
 virtual void Func1()
 {
    cout << "Derive::Func1()" << endl;
 }
private:
 int _d = 2;
};

int main()
{
   Base b;
   Derive d;
   return 0;
}

通过观察和测试,我们发现了以下几点问题:

  1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。

  2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表 中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数 的覆盖。重写是语法的叫法,覆盖是原理层的叫法

  3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函 数,所以不会放进虚表。

  4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。

  5. 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生 类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

  6. 这里还有一个童鞋们很容易混淆的问题:虚函数存在哪的?虚表存在哪的? 答:虚函数存在虚表,虚表存在对象中。注意上面的回答的错的。但是很多童鞋都是这样深以为然的。注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是 他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针

注:在vs平台下,虚函数是存在代码段的

2.4.2多态的原理

那么在了解完虚函数之后,多态原理相信大家的心里也有了自己的答案。虽然子类在创建父类的时候会对父类的成员和函数进行拷贝,但是对于虚函数,子类会其进行重写,那么根据不同的类型的引用和指针自然就能达到相应的效果。

在讲完多态的原理之后,我们来想想为什么多态的条件必须是以下两个:1.必须是父类的指针或引用 2.虚函数的重写

对于后者而言,很简单,如果虚函数没有进行重写,在调用的时候就都是父类的函数,就实现不了多态。前者呢,为什么不能是子类的指针或者引用又或者是父类的对象?如果是子类的指针或者引用,是调不到父类的虚函数的。那如果是父类的对象可以实现多态的,那么给我们一个父类对象时我们并不知道其虚函数到底是其自己的还是子类的,就会导致一系列的乱套,因此对于父类的对象是不能实现多态的。

2.4.3 动态绑定与静态绑定

在讲到虚函数这里,我们还会了解到动态绑定和静态绑定两个术语。

  1. 静态绑定 又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态, 比如:函数重载

  2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体 行为,调用具体的函数,也称为动态多态。

2.5 单继承和多继承关系的虚函数表

2.5.1单继承中的虚函数表

cpp 复制代码
class Base { 
public :
 virtual void func1() { cout<<"Base::func1" <<endl;}
 virtual void func2() {cout<<"Base::func2" <<endl;}
private :
 int a;
};

class Derive :public Base { 
public :
 virtual void func1() {cout<<"Derive::func1" <<endl;}
 virtual void func3() {cout<<"Derive::func3" <<endl;}
 virtual void func4() {cout<<"Derive::func4" <<endl;}
private :
 int b;
};

在了解完虚函数表后,我们知道了在父类Base中存储一张虚函数表,且如果子类若是有重写的虚函数,就会对父类中的虚函数进行覆盖重写,实现多态。但是对于子类中其他的未重写的虚函数,诸如func3(),func4(),他们会放在什么地方呢?通过下面的代码我们来看一看。

cpp 复制代码
typedef void(*Func_ptr) ();

void print_table(Func_ptr* table)
{
	for (size_t i = 0; table[i] != nullptr; ++i)
	{
		printf("第%d个虚函数地址:%p-->", i, table[i]);
		Func_ptr f = table[i];
		f();
	}
}

class Person
{
public:

	virtual  void func1()
	{
		cout << "Person::func1()" << endl;
	}

};

class Student :public Person
{
public:

	virtual  void func1()
	{
		cout << "Student::func()" << endl;
	}

	virtual  void func2()
	{
		cout << "Student::func2()" << endl;
	}

	virtual  void func3()
	{
		cout << "Student::func3()" << endl;
	}


private:
	int* arr=new int[10];
};


void text_3() 
{
	Person p;
	Student s;

	int vft1 = *(int*)&p;
	int vft2 = *(int*)&s;
	print_table((Func_ptr*)vft1);
	cout << endl;
	print_table((Func_ptr*)vft2);

}

int main()
{
	text_3();


	return 0;
}

通过上述代码,我们会发现子类中没有重写的虚函数地址会紧跟着父类虚函数表中重写函数地址的下方

2.5.2 多继承中的虚函数表

对于单继承的虚函数表,我们已经知道了子类重写的虚函数会对父类的进行覆盖,子类未重写的虚函数会放在重写的虚函数下方,也就是是放在父类的的虚函数表。那么对于多继承而言,派生类就有多个基类,对于重写的虚函数依旧是逐一覆盖重写,但是子类未重写的虚函数要何去何从呢?下面我们就以继承两个类的派生来研究多继承中的虚函数表,具体代码如下:

cpp 复制代码
class basic1
{
public:
	virtual  void func1()
	{
		cout << "basic1::func1()" << endl;
	}
};

class basic2
{
public:

	virtual  void func1()
	{
		cout << "basic2::func1()" << endl;
	}
};

class converge:public basic1,public basic2
{
public:

	virtual  void func1()
	{
		cout << "converge::func1()" << endl;
	}

	virtual  void func2()
	{
		cout << "converge::func2()" << endl;
	}
	virtual  void func3()
	{
		cout << "converge::func3()" << endl;
	}
};

void text_4()
{
	converge con;

	basic1* b1 = &con;
	basic2* b2 = &con;


	int vft1 = *(int*)b1;
	int vft2 = *(int*)b2;

	print_table((Func_ptr*)vft1);
	cout << endl;
	print_table((Func_ptr*)vft2);

}

int main()
{
	text_4();
	return 0;
}

通过派生类两个继承类虚表的可以看出:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中

2.5.3 菱形继承、菱形虚拟继承

实际中我们不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的 模型,访问基类成员有一定得性能损耗。所以菱形继承、菱形虚拟继承我们的虚表我们就不看 了,一般我们也不需要研究清楚,因为实际中很少用。

三、结语

到此为止,关于优先级队列的讲解就告一段落了,至于其他的内容,小伙伴们敬请期待呀!

关注我 _麦麦_分享更多干货:麦麦-CSDN博客

大家的「关注❤️ + 点赞👍 + 收藏⭐」就是我创作的最大动力!谢谢大家的支持,我们下期见!

相关推荐
虽千万人 吾往矣23 分钟前
golang gin入门
开发语言·后端·网络协议·tcp/ip·golang·gin
赤橙红的黄26 分钟前
责任链模式
java·开发语言
satan–02 小时前
R语言绘制面积图
开发语言·windows·vscode·信息可视化·r语言·数据可视化
黎明smaly2 小时前
从零学编程-C语言-第17天
c语言·数据结构·c++·算法·visual studio
zhouzhurong3 小时前
C语言scanf用%d读入字符型变量,通过输入字符的ASCII码输入字符
c语言·开发语言·算法
api茶飘香3 小时前
淘宝商品评论API返回值中的品牌忠诚度评价
开发语言·python·django·flask·virtualenv·pygame·tornado
o0o_-_3 小时前
【rust/egui/android】在android中使用egui库
android·开发语言·rust
李元中3 小时前
2024下半年软考中级软件设计师,这100题,必做!
java·开发语言·javascript·人工智能·算法·ecmascript
Lill_bin3 小时前
高并发处理方案:构建可扩展的系统
java·服务器·开发语言·后端·微服务