C++面向对象编程(七) 多态的详解

多态的概念:

多态性:是指具有不同功能的函数可以用同一函数名,即系统能够在运行时,

能够根据其类型确定调用哪个重载的成员函数。

面向对象中是这样表述多态性的:向不同的对象发送同一个消息,不同的对象在接收时产生不同的行为(即不同的实现)。

其实生活中,到处都是多态的例子。例如,学校校长向社会发布一个消息:9月1日新学年开学。不同的对象就会作出不同的响应:学生要准备好课本准时到校上课;家长要筹集学费;教师要备好课;后勤部门要准备好教室、宿舍和食堂....

多态性(polymorphism)提供了接口与具体实现之间的另一层隔离。

从系统实现的角度看:  多态性分为两类:静态多态性动态多态性

函数重载和运算符重载实现的多态性属于静态多态性。(静态联编,早捆绑)

动态多态性则是通过虚函数(Virtual Function)实现的。(动态联编,晚捆绑,是根据对象的类型)


虚函数允许一个类型表达自己与另一个相似类型之间的区别

虚函数的作用:允许子类中重新定义与基类同名的函数,并且可以通过父类的指针引用

访问父类和子类中的同名函数。


虚函数的使用方法是:


  1. 在父类中用  virtual 关键字声明成员函数为虚函数。这样就可以在子类中重新定义此函数,赋予新的功能。
  2. 在子类中重新定义此函数:要求函数名,函数返回值,函数的参数个数,类型全部与父类的虚函数相同
  3. C++编译器规定,当一个成员函数被声明为虚函数后,其子类中的同名函数自动成为虚函数。(即当一个函数在父类
  4. 中被声明为virtual,那么在所有的子类中它都是virtual。)因此在子类重新声明该虚函数时,可加virtual,可不加但习惯上一般加上,使程序更清晰。
  5. 在子类中的重定义通常称为重写。



#include <iostream>
using namespace std;

class Parent
{
public:
	Parent(int a)
	{
		this->a = a;
		
	}
	virtual void print()//声明为虚函数
	{
		cout << "Parent 打印 a: " << a << endl;
	}
private:
	int a;
};


class Child : public Parent
{
public:
	Child(int b):Parent(10)
	{
		this->b = b;
		 
	}
    void print()
	{
		cout << "Child 打印 b: " << b << endl;
	}

private:
	int b;
};

int main()
{

	Parent	*base = NULL;
	Parent	p1(20);
	Child	c1(30);

	base = &p1;
	base->print();//执行父类的打印函数

	base = &c1;
	base->print();//执行子类的打印函数

	Parent &base2 = p1;//执行父类的打印函数
	base2.print();
	Parent &base3 = c1;//执行子类的打印函数
	base3.print();
	system("pause");
	return 0;
}


多态成立的三个条件

  1. 要有继承
  2. 要有虚函数重写
  3. 用父类指针(父类引用)指向子类对象

<span style="font-size:18px;">#include <iostream>
using namespace std;

class HeroFighter  //第一代飞机,父类
{
public:
	virtual int power()
	{
		return 10;
	}
};

class EnemyFighter //敌机
{
public:
	int attack()
	{
		return 15;
	}
};

class SecondFighter : public HeroFighter//第二代飞机
{
public:
	virtual int power()
	{
		return 20;
	}
};

void VsPlatform(HeroFighter *hf,EnemyFighter *ef )
{
		if(hf->power()>ef->attack())
		{
			cout << "主角胜利" << endl;
		}
		else
		{
			cout << "主角失败" << endl;
		}
};

int main()
{
	HeroFighter hf1;
	EnemyFighter ef1;
	SecondFighter sf1;
	VsPlatform(&hf1, &ef1);
	VsPlatform(&sf1, &ef1);
	
	system("pause");
	return 0;
}</span>

由以上的多态案例,可以看出多态的好处,在VsPlatform()函数里面进行主角和敌机的战斗比较,只需要接受传过来的不同飞机的对象指针或者引用,即可,即使以后增加,第三代飞机,第四代飞机,....都不会影响VsPlatform()函数的任何修改,只需要增加类,修改类而已。因此多态的作用

  • 隐藏细节,使得代码能够模块化;扩展代码模块,实现代码重用。
  • 接口重用:为了类在继承和派生的时候,保证使用家族中任一类的实例的某一个属性时的正确调用。


虚析构函数

为什么需要在父类中定义虚析构函数?(虚析构函数作用)

因为当用new运算符建立临时对象,并且赋值给父类的指针变量,如果父类中的析构函数不是虚的

,delete运算符撤销对象时,会发生一个情况:系统只执行父类的析构函数,而不执行子类的析构函数。

析构函数可以是虚的,虚析构函数用于指引delete运算符正确析构动态对象。

(即通过父类的指针,释放所有子类的资源)

构造函数不能是虚函数。因为在执行构造函时,类对象还没完成建立对象过程,

建立一个派生类对象时,必须从类层次的根开始,沿着继承路径

逐个调用父类的构造函数。


#include <iostream>
using namespace std;

class Point
{
public:
	Point()
	{

	}
	virtual ~Point()//在父类中,声明为虚析构函数
	{
		cout << "我是父类的析构函数" << endl;
	}
};

class Circle : public Point
{
public:
	Circle()
	{

	}
	~Circle()
	{
		cout << "我是子类的析构函数" << endl;
	}
};
void deletobj()
{
	Point *p = new Circle; //父类指针指向 new运算符建立的临时对象
	delete p; //释放父类指针所指向的内存空间,会调用析构函数
}


int main()
{
	deletobj();
	 
	system("pause");
	return 0;
}


一个典型的多态案例深入剖析:


<span style="font-size:14px;">#include <iostream>
using namespace std;

enum note //定义个枚举
{
	middleC,Csharp,Cflat //中央C调,升C调,降C调
};

class Instrument //乐器类
{
public:
	virtual void play(note) const
	{
		cout << "乐器的播放" << endl;
	}
	virtual char* what()const
	{
		return "Instrument";
	}
	virtual void adjust(int ad) //函数体为空
	{

	}
};

class Wind : public Instrument  //Wind 是管乐器,继承于乐器类
{
public:
	virtual void play(note) const //play虚函数重写
	{
		cout << "Wind管乐器的播放" << endl;
	}
	virtual char* what()const //what虚函数重写
	{
		return "Wind";
	}
	virtual void adjust(int ad) 
	{

	}
};

class Percussion : public Instrument //Percussion是打击乐器
{
public:
	virtual void play(note) const //重写虚函数
	{
		cout << "Percussion打击乐器的播放" << endl;
	}
	virtual char* what()const //重写虚函数
	{
		return "Percussion";
	}
	virtual void adjust(int ad)
	{

	}
};

class Stringed : public Instrument //Stringed 是弦乐器
{
public:
	virtual void play(note) const
	{
		cout << "Stringed弦乐器的播放" << endl;
	}
	virtual char* what() const 
	{
		return "Stringed";
	}
	virtual void adjust(int ad)
	{

	}
};

class Brass : public Wind //Brass铜管乐器 继承于管乐器Wind
{
public:
	virtual void play(note) const
	{
		cout << "Brass铜管乐器的播放" << endl;
	}
	virtual char* what()const
	{
		return "Brass";
	}
};

class Woodwind : public Wind //Woodwind是木管乐器 继承于Wind
{
public:
	virtual void play(note) const
	{
		cout << "Woodwind木管乐器的播放" << endl;
	}
	virtual char* what() const 
	{
		return "Woodwind";
	}
};

void tune(Instrument &i) //tune是曲调, 用父类的引用做函数参数 来接收子类的对象
{
	i.play(middleC);
	//....
}

void f(Instrument &i)
{
	i.adjust(1);
}

Instrument* A[]= {  //定义一个乐器类指针数组
	new Wind,
	new Percussion,
	new Stringed,
	new Brass,
};

int main()
{
	Wind flute;			//长笛
	Percussion drum;	//鼓
	Stringed violin;	//小提琴
	Brass flugelhorn;   //粗管短号
	Woodwind recorder;  //竖笛
	tune(flute);
	tune(drum);
	tune(violin);
	tune(flugelhorn);
	tune(recorder);
	f(flugelhorn);
	system("pause");
	return 0;
}</span>


输出的结果是:



可以看出,这个例子在Wind下增加一层继承层,可是不管继承多少层,virtual机制仍会正确工作。

另外adjust函数没有实现具体重写,当出现这种情况时候,编译器自动调用继承层中“最近的”定义,

保证在调用次虚函数总是有某种定义。可以看到,在tune函数中,根据子类传过来的对象类型,

准确的输出,我们想要的结果。那是如何实现的呢?晚捆绑如何发生?

  其实所有的工作都是由C++编译器在幕后完成。也就是C++虚函数机制。

首先,在父类Instrument类中,关键字virtual告诉编译器它执行的是动态绑定,

晚捆绑,也就是常说的动态联编。为了达到这个目的,C++编译器对每个包含虚函数的类创建一个表(VTABLE(虚函数表)。在虚函数表中,编译器放置特定类的虚函数的地址。而在每个带有虚函数的类中,

编译器秘密地放置一个指针,称为(VPTR)(虚指针),指向这个对象的VTABLE(虚函数表)。当通过父类

指针或者引用做虚函数调用时,(多态调用时),编译器能取得这个VPTR并在虚函数表中查找函数地址的代码

这样就能调用正确的函数,并引起晚捆绑(动态联编)的发生。


可是我们没有看到VPTR指针,这是因为编译器帮我隐藏了。下面我们正面vptr指针的存在


<span style="font-size:14px;">#include <iostream>
using namespace std;

class NoVirtual
{
public:
	void x() const {}
	int getI()const { return 1;}
private:
	int a;
};

class OneVirtual 
{
public:
	virtual void x() const {}
	int getI()const { return 1;}
private:
	int a;
};

class TwoVirtual 
{
public:
	virtual void x() const {}
	virtual int getI()const { return 1;}
private:
	int a;
};


int main()
{
	cout << "int : " << sizeof(int) << endl;
	cout << "没有虚函数: " << sizeof(NoVirtual) << endl;
	cout << "void* : " << sizeof(void*) << endl;
	cout << "1个虚函数: " << sizeof(OneVirtual) << endl;
	cout << "2个虚函数: " << sizeof(TwoVirtual) << endl;
	system("pause");
	return 0;
}</span>

运行的结果是:




 可以看出,没有虚函数时候,就是int的长度,如果有了虚函数,不管多少个,对象的长度就是 int的长度+void指针的长度,也就是说,在这里(win32平台下) VPTR指针长度是4个字节。编译器的确在幕后,隐藏在含有虚函数的类中,加入了一个VPTR指针。

并且如果一个类中,没有任何的数据成员,C++编译器会强制对这个对象是非零长度,win32平台下是1个字节,

因为每个对象必须有一个互相区别的地址。

我们在回头看看刚才的例子,可以得出如下的图表:



在这个表中,编译器放置了在这个类中或在它的父类中的所有已经声明为virtual的函数的地址。

如果在子类中没有对在父类中声明为virtual函数的进行重定义,编译器就使用父类的这个虚函数地址

如Brass类中adjust函数,因为没有进行虚函数(adjust)进行重定义,因为使用父类的adjust函数 ,

即(&Wind::adjust)

在这种简单继承中,对于每个对象只有一个VPTR。VPTR必须初始化为指向相应的虚函数表的起始地址。

而VPTR在初始化在构造函数中进行。


VPTR指针是分步初始化的。 看下面的案例

<span style="font-size:14px;">#include <iostream>
using namespace std;

//构造函数中调用虚函数,不能发生多态,被调用的只是这个函数的本地版本。

class Parent
{
public:
	Parent(int a=0)
	{
		this->a = a;
		print();//执行的是父类的print()函数
	}

	virtual void print()  
	{
		cout<<"我是爹"<<endl;
	}

private:
	int a;
};

class Child : public Parent
{
public:
	Child(int a = 0, int b=0):Parent(a)
	{
		this->b = b;
		print();//执行的是子类的print()函数
	}

	virtual void print()
	{
		cout<<"我是儿子"<<endl;
	}
private:
	int b;
};

void HowToPlay(Parent *base)
{
	base->print();  

}

void main()
{

	Child  c1; //定义一个子类对象 ,在这个过程中,在父类构造函数中调用虚函数print  
	//c1.print();

 
	system("pause");
	return ;
}</span>



要初始化c1.vptr指针,初始化是分步的。

当执行父类的构造函数的时候,c1.vptr指向的是父类的虚函数表,

当父类的构造函数运行完毕后,会把c1.vptr指针指向 子类的虚函数表。



结论:初始化子类C1.VPRT的指针是分步完成的。


通过虚函数表指针VPTR调用重写函数是在程序运行时进行的,因此需要通过寻址才能确定真正应用

调用的函数,而普通成员是在编译时就确定了调用的函数,在效率上,虚函数的效率要低。

出于效率考虑,没有必要将所有成员函数都声明为虚函数。


多态的重要意义:

设计模式的基础,是框架的基石。












评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值