C++多态(超详解哦)

2024-01-10 09:05:56

引言

在生活中不乏这样的例子:成人与儿童在买票时会有不同的价格,儿童可以半价买票;又如大家耳熟能详的电商企业“并夕夕”,新用户与老用户在扫同样的福利二维码时,会获得不同的收益。

这种同样的事件对不同种类的对象会产生不同的状态的不同对象执行多种状态的行为,就是多态

在上一篇中详细介绍了继承的行为, 继承中的父类与子类就是天然的两种不同对象,并且有着密切的联系。所以多态就天然的在继承下实现。戳我see继承详解哦

定义及实现

为方便叙述,我们直接引入基类Person类,与派生类Children类。相同的事件就是类中的同名函数(就是虚函数,马上会介绍),通过基类的指针或引用调用这个 “ 相同事件 ” 时,就会产生不同的状态:

class Person
{
public:
	virtual void pay(int x)
	{
		cout << "person pay:" << x << endl;
	}
};
class Children : public Person
{
public:
	virtual void pay(int x)
	{
		cout << "children pay:" << x / 2 << endl;
	}
};

void buyTickets(Person& person, int price)
{
	person.pay(price);
}

int main()
{
	int price = 100;
	Person person;
	Children child;

	buyTickets(person, price);
	buyTickets(child, price);

	return 0;
}	

在这里插入图片描述

多态的条件

由上面的例子,我们可以很容易的获得多态的条件:

  • 基类必须定义有虚函数,并且派生类要对虚函数进行重写;
  • 必须由基类的指针或引用调用虚函数。

第二个条件其实很好理解:由如果由派生类的指针或引用来调用的话,由于这个指针只可能指向派生类,所以只可能调用到派生类中重写后的虚函数;而由于继承中天然的切片行为,基类的指针可能指向基类,也可能指向派生类,这就给了基类的指针或引用了调用基类中虚函数与派生类中重写的虚函数的条件

而第一个条件中的必须有虚函数:可以理解为虚函数的定义与重写区分了指向基类的基类指针指向派生类的基类指针。从而使多态行为可行(这其中的原理后面就会讲到)。

虚函数与虚函数的重写

在继承中存在成员函数的隐藏,即派生类中的函数会隐藏基类中的同名函数,如果一个基类指针指向一个派生类对象,由于切片的效果,通过这个基类指针来访问元素时,访问的依旧是基类的元素。
但是由于虚函数的存在,我们通过基类的指针或引用访问到的就是它实际指向的对象的成员函数。

虚函数,就是被关键字virtual修饰的函数:
virtual 返回值类型 函数名 (参数列表);

例如上面基类中的虚函数:

	class Person
	{
	public:
		virtual void pay(int x)
		{
			cout << "person pay:" << x << endl;
		}
	};

虚函数的重写是指派生类中存在与基类中虚函数返回值类型、函数名、参数列表完全相同的虚函数,例如上面派生类中的虚函数:

	class Children : public Person
	{
	public:
		virtual void pay(int x)
		{
			cout << "children pay:" << x / 2 << endl;
		}
	};

虚函数的重写存在着两个特例

  1. 协变(重写返回值不同):
    当虚函数需要返回当前类对象的指针或引用时,可以基类虚函数返回基类的指针或引用,派生类虚函数返回派生类的指针或引用,称为协变:
	class A
	{
	public:
		A(int a = 0)
			:_a(a)
		{}
		virtual A* func() //返回基类指针
		{
			cout << _a << endl;
			return new A(_a + 1);
		}
	public:
		int _a;
	};
	class B : public A
	{
	public:
		B(int a = 0, int b = 0)
			:A(a)
			,_b(b)
		{}
		virtual B* func() //返回派生类指针
		{
			cout << _a << " " << _b << endl;
			return new B(_a + 1, _b + 1);
		}
	public:
		int _b;
	};
  1. 析构函数的重写(重写函数名不同):
    当基类中的析构函数为虚函数时,派生类中定义的析构函数一定为虚函数。将析构函数定义为虚函数,可以使我们通过基类的指针或引用释放派生类空间时能够正确释放,不造成内存泄漏。
    但是,基类的析构函数与派生类析构函数的函数名是不同的。其实编译器会将析构函数的函数名统一处理为destructor,以应对这里的函数名不相同的问题:
	class A
	{
	public:
		virtual ~A()
		{
			cout << "destruct A" << endl;
		}
	};
	class B : public A
	{
		virtual ~B()
		{
			cout << "destruct B" << endl;
		}
	};
	
	int main()
	{
		A* pa = new A;
		A* pb = new B;
	
		delete pa;
		cout << endl;
		delete pb;
	
		return 0;
	}

在这里插入图片描述

接口继承与实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,相当于将基类搬到派生类中,在派生类中可以使用函数,继承的是函数的实现。
虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,相当于将函数的原型继承下来(虚函数的要求即函数原型相同),而函数的饿实现由派生类重写,继承的是函数接口。

函数重载,隐藏,重写的区别

函数重载指在同一域中,存在函数名相同,但是函数返回值类型或参数列表不同的多个函数,调用重载的函数时,总是匹配最合适的那个重载函数;

函数隐藏是指在基类与派生类作用域中存在同名函数,基类中的那个同名函数会被隐藏;

函数重写是指在基类与派生类作用域中存在两个函数名、返回值与参数列表完全相同的虚函数,运用在多态中。

抽象类

在虚函数的后面加上 =0,表示这个函数为纯虚函数,包含纯虚函数的类叫做抽象类。
抽象类不能实例化对象,只有当派生类重写该纯虚函数后,才可以使用这个派生类实例化对象:

class A
{
public:
	virtual void Func() = 0;
};
class B : A
{
public:
	virtual void Func()
	{
		cout << "B" << endl;
	}
};

int main()
{
	//A a; 错误代码,抽象类不能被实例化
	B b;
	return 0;
}

多态的原理

class A
{
public:
	virtual void func1()
	{
		cout << "A1" << endl;
	}
	virtual void func2()
	{
		cout << "A2" << endl;
	}
	virtual void func3()
	{
		cout << "A3" << endl;
	}
public:
	int _a = 10;
};
class B : public A
{
public:
	virtual void func1()
	{
		cout << "B1" << endl;
	}
	virtual void func2()
	{
		cout << "B2" << endl;
	}
	virtual void func3()
	{
		cout << "B3" << endl;
	}
public:
	int _b = 20;
};

void testFunc(A* pa)
{
	pa->func1();
	pa->func2();
	pa->func3();
}

int main()
{
	A a;
	B b;

	testFunc(&a);
	testFunc(&b);

	return 0;
}

在这里插入图片描述

虚函数表(虚表)

当在类中定义虚函数后,在实例化的基类时,会定义一个函数指针数组,这个函数指针数组就是虚函数表,虚表的指针存在对象中(vfptr——virtual function pointer,通常以nullptr为止):

  • 在基类中对象中,包含虚表的指针及其成员变量。虚函数表中包含基类中虚函数的函数指针
  • 在派生类对象中,包含基类的部分,派生类的虚表指针及成员变量。派生类虚表的指针会替换基类中的虚表指针,指向的虚表中包含重写后的虚函数的函数指针

在这里插入图片描述

在了解虚表之后,要解释多态的原理就变得很简单了,当我们在通过基类的指针或引用调用虚函数时:

  • 对于指向基类对象的基类指针,它指向空间中的虚表中的函数指针是基类中的虚函数指针

  • 对于指向派生类对象的基类指针,发生切片后,它指向空间中的虚表中的函数指针是派生类中重写过的虚函数指针

在这里插入图片描述

而普通的继承不能实现多态的原因就在这里,当切片后,不论指向基类对象还是派生类对象的基类指针都只能访问到基类中的成员函数。

动态绑定与静态绑定

刚才提到的多态行为,实在运行时才确定基类的指针是指向基类对象还是派生类对象的。即运行时根据拿到的类型确定要调用哪个函数。这样的行为称为动态绑定,或动态多态;
我们在之前的重载也是一种多态行为,只是它在编译阶段就确定了要调用哪个函数。这样的行为称为静态绑定,或静态多态。

总结

到此,关于多态的相关知识就介绍完了

如果大家认为我对某一部分没有介绍清楚或者某一部分出了问题,欢迎大家在评论区提出

如果本文对你有帮助,希望一键三连哦

希望与大家共同进步哦

文章来源:https://blog.csdn.net/weixin_73450183/article/details/135311288
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。