多态的概念:
多态性:是指具有不同功能的函数可以用同一函数名,即系统能够在运行时,
能够根据其类型确定调用哪个重载的成员函数。
面向对象中是这样表述多态性的:向不同的对象发送同一个消息,不同的对象在接收时产生不同的行为(即不同的实现)。
其实生活中,到处都是多态的例子。例如,学校校长向社会发布一个消息:9月1日新学年开学。不同的对象就会作出不同的响应:学生要准备好课本准时到校上课;家长要筹集学费;教师要备好课;后勤部门要准备好教室、宿舍和食堂....
多态性(polymorphism)提供了接口与具体实现之间的另一层隔离。
从系统实现的角度看: 多态性分为两类:静态多态性和动态多态性。
函数重载和运算符重载实现的多态性属于静态多态性。(静态联编,早捆绑)
动态多态性则是通过虚函数(Virtual Function)实现的。(动态联编,晚捆绑,是根据对象的类型)
虚函数允许一个类型表达自己与另一个相似类型之间的区别。
虚函数的作用:允许子类中重新定义与基类同名的函数,并且可以通过父类的指针或引用
访问父类和子类中的同名函数。
虚函数的使用方法是:
- 在父类中用 virtual 关键字声明成员函数为虚函数。这样就可以在子类中重新定义此函数,赋予新的功能。
- 在子类中重新定义此函数:要求函数名,函数返回值,函数的参数个数,类型全部与父类的虚函数相同。
- C++编译器规定,当一个成员函数被声明为虚函数后,其子类中的同名函数自动成为虚函数。(即当一个函数在父类
- 中被声明为virtual,那么在所有的子类中它都是virtual。)因此在子类重新声明该虚函数时,可加virtual,可不加但习惯上一般加上,使程序更清晰。
- 在子类中的重定义通常称为重写。
#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;
}
- 要有继承
- 要有虚函数重写
- 用父类指针(父类引用)指向子类对象
<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调用重写函数是在程序运行时进行的,因此需要通过寻址才能确定真正应用
调用的函数,而普通成员是在编译时就确定了调用的函数,在效率上,虚函数的效率要低。
出于效率考虑,没有必要将所有成员函数都声明为虚函数。
多态的重要意义:
设计模式的基础,是框架的基石。