【多态】—— 我与C++的不解之缘(二十)

前言

面向对象语言的三大特性:封装继承多态

现在就一起来学习多态

一、什么是多态

通俗一点,多态就是多种形态;多态它又分为编译时动态运行时多态

1. 编译时多态

​ 编译时多态,主要就是函数重载函数模板,他们传不同的参数就可以调用不同的函数,通过传参不同达到多种形态;它们在实参传给形参的参数匹配是在编译时完成的,(也就是在编译时就已经决定不同的函数了),所以叫做编译时多态。

2. 运行时多态

​ 运行时多态,说现实一点就是,传不同的对象过去,就会产生不同的行为,达到多种形态。

举个例子:

​ 不同的异性,你会有不同的感受;就比如,你和讨厌的人交流,你会感到非常不耐烦,不想与其有任何交集;而对于Crush呢,你总会想尽一切办法去和她说上两句话。

二、多态的定义

1. 构成多态的构成

​ 多态是一个继承关系下的类对象,去调用同一个函数,产生不同的行为。

2. 实现多态的两个必要条件

  • 必须是基类的指针或者引用,调用虚函数
  • 被调用的函数必须是虚函数,并且完成了虚函数的重写/覆盖
cpp 复制代码
#include<iostream>

using namespace std;

class Person
{
public:
	virtual void func()
	{
		cout << "Person::func()" << endl;
	}
protected:
	int _i;
};

class Crush : public Person
{
public:
	virtual void func()
	{
		cout << "Crush::func()" << endl;
	}

protected: 
	char _ch;
};

void test(Person* p)
{
	p->func();
}
int main()
{
	Person per;
	Crush cru;
	test(&per);
	test(&cru);
	return 0;
}

运行结果:

Person::func()
Crush::func()

三、虚函数

1. 什么是虚函数?

虚函数:被virtual修饰的成员函数称为虚函数

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

2. 虚函数的重写

虚函数的重写:

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

cpp 复制代码
class Person
{
public:
	virtual void func()
	{
		cout << "Person::func()" << endl;
	}
};
class Crush : public Person
{
public:
	virtual void func()
	{
		cout << "Crush::func()" << endl;
	}
};

注意:

在重写基类虚函数时,派生类的虚函数可以不加virtual关键字,(因为继承后基类的虚函数被继承下来在派生类中也保持虚函数属性);但是不建议这样去写。

3. 重写的特殊情况

重写有两个特殊情况,一是协变;二就是析构函数的重写

协变

协变呢,就是是派生类在重写基类虚函数时,与基类虚函数返回值的类型不相同(但不是随便的返回值),基类的虚函数返回基类对象的指针或者引用,而派生类虚函数返回派生类对象的指针或者引用。

注意: 这里返回值基类和派生类的指针或引用,并不是指的该继承关系下的基类和派生类,其他继承关系下的基类和派生类也可以。

cpp 复制代码
class A
{};
class B : public A
{};
class person
{
public:
	virtual A* fun()
	{};
	virtual person& test()
	{}
};
class crush :public person
{
public:
	virtual B* fun()
	{}
	virtual crush& test()
	{}
};

析构函数的重写

如果基类的析构函数是虚函数,这时候派生类的析构函数,只要定义了就与基类的虚函数构成重写。

看到这里,可能有些懵了,不是说函数名字,返回值,参数列表必须相同吗?这析构函数名字都不相同,为啥也构成重写呢?

接着往下看

这里虽然我们看起来基类和派生类的析构函数名字不相同,但是编译器会对析构函数进行特殊处理,统一处理成destructor

cpp 复制代码
class person
{
public:
	virtual ~person()
	{
		cout << "person::~person()" << endl;
	}
};
class crush :public person
{
public:
	virtual ~crush()
	{
		cout << "crush::~crush()" << endl;
	}
};

int main()
{
	person* p = new person();
	crush* c = new crush();

	delete p;
	delete c;
	return 0;
}

这里,只有派生类的析构函数重写了基类的析构函数,在delete释放资源调用析构函数时,才能构成多态;才能保证pc指向的对象能够正确调用其对应的析构函数。

4. C++11关键字finaloverride

final 修饰虚函数,表示该虚函数不能被重写

cpp 复制代码
class A
{
public:
	virtual void test() final
	{
		cout << "A::test()" << endl;
	}
};
class B :public A
{
	virtual void test()
	{
		cout << "B::test()" << endl;
	}
};

overeride 判断是否重写某个虚函数

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

class crush :public person
{
public:
	virtual void fun() override
	{
		cout << "crush" << endl;
	}
	virtual void test() override
	{}
};

注意: 这里override用来检查派生类函数是否重写了基类的某个虚函数,如果没有构成重写就编译报错。

5. 重载、隐藏和重写的区别

重载:

  • 两个函数在同一个作用域
  • 函数名相同,参数不同(参数类型,参数个数)。

重写(覆盖):

  • 两个函数分别在基类和派生类的作用域中
  • 函数名、参数和返回值类型都必须相同(协变除外)
  • 两个函数都是虚函数

隐藏(重定义):

  • 两个函数分别在基类和派生类的作用域中
  • 函数名相同
  • 基类和派生类的两个同名函数不构成重写就是隐藏

6. 抽象类

纯虚函数

  • **纯虚函数:**在虚函数定义时,后面加上 =0,此时虚函数就是纯虚函数。
  • 包含纯虚函数的类就是抽象类,抽象类不能实例化处对象
  • 派生类继承抽象类后也不能实例化出对象,只有重写了纯虚函数才能实例化出对象
  • 纯虚函数规范了派生类必须重写,也更体验出接口继承
cpp 复制代码
class person
{
public:
	virtual void fun() = 0;
};

class crush :public person
{
public:
	virtual void fun()
	{
		cout << "crush" << endl;
	}
};

接口继承和实现继承

  • 普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现
  • 虚函数继承其实算是一种接口继承,派生类继承基类虚函数的接口,为了重写,达成多态,继承的是接口。

四、多态的原理

1. 虚函数表

什么是虚函数表呢?

cpp 复制代码
class person
{
public:
	virtual void fun()
	{
		cout << "person" << endl;
	}
protected:
	int _i = 1;
};
int main()
{
	person p;
	return 0;
}

我们可以看到,这里person实例化的对象中不只有一个成员变量,还有一个_vfptr的东西(这个就是虚函数表),看起来想要个指针;

一个含有虚函数的类中都至少有一个虚函数表的指针,虚函数的地址要放在虚函数表中。

cpp 复制代码
class person
{
public:
	virtual void func1()
	{
		cout << "person::func1()" << endl;
	}
	virtual void func2()
	{
		cout << "person::func2()" << endl;
	}
	void func3()
	{
		cout << "person::func3()" << endl;
	}
protected:
	int _i = 1;
};

class crush :public person
{
public:
	virtual void func1()
	{
		cout << "crush::func1()" << endl;
	}
protected:
	char _ch;
};
int main()
{
	person p;
	crush c;
	return 0;
}

可以看到虚函数表中存放了两个地址,和显然这就是两个虚函数的地址 。

  • 派生类对象c中也存在一个虚函数表,并且成员有两部分,一部分继承基类的成员,另一部分就是自己的那一部分成员(虚函数表就是自己那一部分
  • 基类对象p和派生类对象c虚函数表内容不一样(这里func1完成了重写,所以c虚表中存放的是重写后的crush::func()的地址;而func2没有完成重写,所以c虚函数表中存放的还是基类的fun2的地址。
  • func1fun2都是虚函数,存到了虚函数表中,而func3不是虚函数就没有存到虚函数表内。
  • 虚函数表本质就是一个存放虚函数指针的指针数组,一般数组最后存放了一个nullptr来判断结束。

总结:

在继承关系中:

  • 现将基类的虚表内容拷贝一份到派生类的虚函数表中
  • 如果派生类重写了某个函数,则就使用自己重写的虚函数表覆盖掉基类的虚函数表
  • 派生类自己新增的虚函数按其在派生类声明的顺序依次增加到派生类虚函数表最后

注意:

虚函数表中存的是虚函数的地址,而不是虚函数本身。

2. 多态的实现原理

说这么多,那多态如何实现的呢?

  • 每一个对象中都存在一个虚函数表,这样在调用虚函数时就去虚函数表中找该函数的地址。
  • 如果完成了重写,虚函数表中存放的就是自己重写的虚函数的地址,那调用的就是重写之后的虚函数;如果没有完成重写,那虚函数表中存放的就是基类的虚函数的地址,调用的就是基类的虚函数。
  • 这样,对于不同对象去调用,就会调用不同的函数,就展现出多种形态。

3. 动态绑定和静态绑定

  1. 静态绑定:在程序编译期间就确定了程序的行为,又称为静态多态。
  2. 动态绑定: 在程序运行期间,根据具体的类型确定程序的行为调用具体的函数,也叫做动态多态。

4. 单继承和多继承

单继承中虚函数表

这个就非常简单了,就不过多描述了。

多继承中的虚函数表

对于多继承,虚函数表又改怎样存放虚函数地址呢?

cpp 复制代码
class Base1 {
public:
	virtual void func1() { cout << "Base1::func1" << endl; }
	virtual void func2() { cout << "Base1::func2" << endl; }
private:
	int b1;
};
class Base2 {
public:
	virtual void func1() { cout << "Base2::func1" << endl; }
	virtual void func2() { cout << "Base2::func2" << endl; }
private:
	int b2;
};
class Derive : public Base1, public Base2 {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
private:
	int d1;
};

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

可以看到,d对象中存在两个基类的对象(表示继承下来的基类的成员),其中分别存在虚函数表。

到这里多态部分大致内容就结束了,继续加油!!!

我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=2oul0hvapjsws

相关推荐
Tester_孙大壮1 小时前
第4章:Python TDD消除重复与降低依赖实践
开发语言·驱动开发·python
数据小小爬虫2 小时前
如何使用Python爬虫获取微店商品详情:代码示例与实践指南
开发语言·爬虫·python
代码驿站5202 小时前
JavaScript语言的软件工程
开发语言·后端·golang
雪靡3 小时前
正确获得Windows版本的姿势
c++·windows
java1234_小锋3 小时前
Java中如何安全地停止线程?
java·开发语言
siy23333 小时前
[c语言日寄]结构体的使用及其拓展
c语言·开发语言·笔记·学习·算法
可涵不会debug3 小时前
【C++】在线五子棋对战项目网页版
linux·服务器·网络·c++·git
AI+程序员在路上3 小时前
C#调用c++dll的两种方法(静态方法和动态方法)
c++·microsoft·c#
一只会飞的猪_3 小时前
国密加密golang加密,java解密
java·开发语言·golang
四念处茫茫4 小时前
【C语言系列】深入理解指针(2)
c语言·开发语言·visual studio