c++ -- 多态(类)

↘锁芯ラ 提交于 2020-02-07 02:12:58
假期 2020.02.06

学习资源来源于中国MOOC以及c语言中文网


前言

c++之所以是面向对象的是因为该语言既支持类也支持多态,而多态是什么呢?


定义

多态就是同一个操作作用于不同的对象会产生不同的结果。这个操作一般指的是函数的调用等等。
标准定义是对于通过基类指针调用基类和派生类中都有的同名、同参数表的虚函数的语句,编译时并不确定要执行的是基类还是派生类的虚函数;而当程序运行到该语句时,如果基类指针指向的是一个基类对象,则基类的虚函数被调用,如果基类指针指向的是一个派生类对象,则派生类的虚函数被调用。这种机制就叫作“多态(polymorphism)”。
虚函数:在成员函数前加virtual即可生成虚函数。注意虚函数只能在类的内部定义成员函数时使用,不能在类外部写成员函数时使用,构造函数与静态成员都不能是虚函数。
多态类:包含虚函数的类称为“多态类”。



如何构成多态

常判断条件是

  1. 有继承关系
  2. 继承关系中有同名的虚函数,并且是覆盖关系
  3. 存在基类的指针,指向派生类的虚函数,或者引用

那么什么时候声明虚函数呢?

  1. 成员函数所在的类是否是基类
  2. 成员函数在继承后是否会被新定义或者更改,是的话,需要;否则,不需要。

多态应用

举一个指针类型的例子:
在这里插入图片描述
注意:多态的语句调用哪个类的成员函数是在运行时才能确定的,编译时不能确定。多态的函数调用语句被称为是“动态联编”,而普通的函数调用语句是“静态联编”。

上述程序是通过指针调用的虚函数,而引用也可以实现该功能。如果引用的是基类的对象,则调用的是基类的虚函数,若引用的是派生类的对象,那么调用的是派生类的虚函数。还需注意的是通过基类的指针只能访问从基类继承过去的成员,不能访问派生类新增的成员,即另外添加的函数或者变量成员。

再看一个引用类型的例子
在这里插入图片描述
在基类中没有添加virtual会发现调用的时候,两个都是基类的成员函数,根本没有访问派生类的成员函数。显然,通过基类指针只能访问派生类的成员变量,但是不能访问派生类的成员函数。为了消除这种困境,让基类指针能够访问派生类的成员函数,C++ 增加了虚函数。

再看修改后的引用结果

在这里插入图片描述
这里用到一个简便方式,如果派生类中出现覆盖函数,那么只需要在基类中将被覆盖函数定义为虚函数即可,那么编译时会默认派生类中的函数也为虚函数。

注意:

  1. 当基类中有虚函数,但是派生类没有定义覆盖函数,那么调用该函数时,将使用基类的虚函数。
  2. 构造函数不能是虚函数。对于基类的构造函数,它仅仅是在派生类构造函数中被调用,这种机制不同于继承。
  3. 析构函数可以声明为虚函数。

对于为什么构造函数不能设置成虚函数的原因,本贾尼·斯特劳斯特卢普(C++语言之父)说道:虚函数调用是在部分信息下完成工作的机制,允许我们只知道接口而不知道对象的确切类型。 要创建一个对象,你需要知道对象的完整信息。 特别是,你需要知道你想要创建的确切类型。 因此,构造函数不应该被定义为虚函数。

多态中的特殊例子:

在这里插入图片描述
以上程序中,因为class B中没有fun1,因此调用基类函数fun1,但fun1函数调用fun2函数使用的是B中的覆盖函数,与前面B的指针有关。


纯虚函数与抽象类

纯虚函数不含有函数实体,只有声明。在虚函数声明的结尾加上=0,表明此函数为纯虚函数。包含纯虚函数的类成为抽象类,因为他无法实例化,无法创建对象。
例如:

virtual 返回值类型 函数名(函数参数) = 0;

程序

#include<iostream>
using namespace std;
class A {
protected:
	int a;
public:
	virtual int aa() = 0;//纯虚函数
	virtual void ab() = 0;
};
class B :public A {
protected:
	int b1;
	int b2;
public:
	B(int _i,int _n):b1(_i),b2(_n){ }
	int aa() {//定义两个虚函数的实体
		return b1 * b2;
	}
	void ab() {
		cout << "第二个虚函数被调用" << endl;
	}
};
class C :public A {
protected:
	int c;
public:
	C(int _i):c(_i){ }
	int aa() {//定义两个虚函数的实体
		return c;
	}
	void ab() {
		cout << "第二个虚函数被调用" << endl;
	}
};
int main()
{
	C t(2);
	cout << t.aa() << endl;
	t.ab();
	B tq(2, 3);
	cout << tq.aa() << endl;
	tq.ab();
	return 0;
}

执行结果
在这里插入图片描述
分析:A 类不需要被实例化,但也不能直接用来定义类的对象,但是它为派生类提供了“约束条件”,派生类必须要实现这两个函数,才能使用这两个函数。即如果一个类从抽象类派生而来,那么当且仅当它实现了基类中的所有纯虚函 数,它才能成为非抽象类。

简单说来就是基类只是提供一个简要的框架,具体内容需要派生类自己去完善自己需要的部分。

注意:

  1. 只要包含纯虚函数的类就都是抽象类
  2. 虚函数只能在类中才能被声明为纯虚函数
  3. 在抽象类的成员函数内可以调用纯虚函数,但是在构造函数或析构函数内部 不能调用纯虚函数。

几何体处理程序

在这里插入图片描述
在这里插入图片描述

#include<iostream>
using namespace std;
constexpr auto PAI = 3.14159265;
int MyCompare(const void* s1, const void* s2);
class shape {
public:
	virtual double aera() = 0;//虚函数
	virtual void print() = 0;
};
class retangle :public shape {
public:
	double width, length;
	double aera() { return width * length; }
	void print() { cout << "retangle:" << aera() << endl; }
};
class circle :public shape {
public:
	int r;
	double aera() { return PAI* r* r; }
	void print() { cout << "circle:" << aera() << endl; }
};
class triangle :public shape {
public:
	int a, b, c;
	double aera() {
		double temp = (a + b + c) / 2;
		return sqrt(temp * (temp - a) * (temp - b) * (temp - c));
	}
	void print() { cout << "triangle:" << aera() << endl; }
};
int MyCompare(const void* s1, const void* s2) {
	double a1, a2; 
	shape** p1 = (shape**)s1; //s1,s2指向shape数组中的元素,数组元素的类型是shape * 
	shape** p2 = (shape**)s2; // 故 p1,p2都是指向指针的指针,类型为 shape ** 
	a1 = (*p1)->aera(); // * p1 的类型是 shape * ,是基类指针,故此句为多态 
	a2 = (*p2)->aera();
	if( a1 < a2 ) 
		return -1;
	else 
		return 1;
}
int main()
{
	shape* sh[100];
	retangle* ptr;
	circle* pc;
	triangle* pt;
	int n;
	cin >> n;
	for(int i = 0; i <  n; i++)
	{
		char c;
		cin >> c;
		switch (c) {
			case 'R':
				ptr = new  retangle;
				cin >> ptr->width >> ptr->length;
				sh[i] = ptr;
				break;
			case 'C':
				pc = new circle;
				cin >> pc->r;
				sh[i] = pc;
				break;
			case 'T':
				pt = new triangle;
				cin >> pt->a >> pt->b >> pt->c;
				sh[i] = pt;
				break;
		}
	}
	qsort(sh, n, sizeof(shape*), MyCompare);
	for (int i = 0; i < n; i++)
		sh[i]->print();
	return 0;
}

执行效果
在这里插入图片描述
分析:用基类指针数组存放指向各种派生类对象的指 针,然后遍历该数组,就能对各个派生类对象 做各种操作,是很常用的做法。

比较好的一个处理是:派生类中和基类中虚函数同名同参数表的函数,不加virtual也自动成为虚函数。


虚函数的访问权限

在这里插入图片描述
分析:存在编译错误的原因是私有成员不可访问,即使我们访问的是该函数的覆盖函数,因为语法检查是不考虑运行结果的。解决方案是将private换成public即可通过。


多态的实现原理

动态联编定义:“多态”的关键在于通过基类指针或引用调用 一个虚函数时,编译时不确定到底调用的是基类还 是派生类的函数,运行时才确定。

示例
在这里插入图片描述
我们会发现每一个输出的字节数中都比原来的大4(32位时),其实这是因为每一个虚函数类都有一个虚函数边,该类的任何对象中都放着虚函数边的指针,即存着表的地址。这列的虚函数暂时不用考虑内存空间,因为还没有进行实体定义。
多态的函数调用语句被 编译成一系列根据基类指 针所指向的(或基类引用 所引用的)对象中存放的 虚函数表的地址,在虚函 数表中查找虚函数地址,
并调用虚函数的指令。


虚析构函数

  1. 通过基类的指针删除派生类对象时,通常情况下只调用基类的析构 函数 。 但是,删除一个派生类的对象时,应该先调用派生类的析构函 数,然后调用基类的析构函数。
  2. 解决办法:把基类的析构函数声明为virtual  派生类的析构函数可以virtual不进行声明。通过基类的指针删除派生类对象时,首先调用派生类的析构函 数,然后调用基类的析构函数
  3. 一般来说,一个类如果定义了虚函数,则应该将析构函数也定义成 虚函数。或者,一个类打算作为基类使用,也应该将析构函数定义 成虚函数。

如果是一个基类,那通常情况下需要将析构函数定义成虚函数,这样子,他会先调用派生函数的析构函数,然后在调用基类析构函数,达到释放两者的目的。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!