面向对象的三大特征:
1.封装:保证对象自身数据的完整性、安全性
2.继承:建立类之间的关系,实现代码复用、方便系统的扩展
3.多态:相同的方法调用可实现不同的实现方式。多态是指两个或多个属于不同类的对象,对于同一个消息(方法调用)作出不同响应的方式。(同样的操作,不同的对象执行,就会出现不同的结果。)
、、、、、、、、、
实现多态的方式:
函数重载;运算符重载;虚函数
、、、、、、、、、
C++有两种多态:
1.编译时的多态:函数重载和运算符重载,在编译时就决定调用哪个函数
2.运行时的多态:通过类继承和虚函数实现的。
、、、、、、、、
函数重载包括:普通函数重载和成员函数重载。函数的参数个数、类型、顺序不同
运算符重载:实现两个对象的相加。
、、、、、、、、
当基类中的某个函数被定义为虚函数之后,该函数可以在一个或多个派生类中重新定义,但其函数原型,包括返回值类型,参数个数,参数类型和参数顺序都必须和基类中的一致。
多态性是面向对象程序设计的重要特征之一。它与封装性和继承性共同构成了面向对象程序设计的三大特征。封装性是基础 ,继承性是关键 ,多态性是补充 ,多态性又存在于继承的环境之中 ,所以这三大特征是相互关联的 ,相互补充的。C++ 语言中有两种重载 :函数重载和运算符重载。运算符重载很重要 ,它的实质就是函数重载。我们接触的还有另一种是指同样的消息被不同类的对象接受时产生完全不同的实现 ,该情况大多产生在多类继承中不同类中的相同说明的成员函数的多态行为。
··································································
多态是基于对抽象方法的覆盖来实现的,用统一的对外接口来完成不同的功能。重载也是用统一的对外接口
来完成不同的功能。那么两者有什么区别呢?
重载,是指允许存在多个同名方法,而这些方法的参数不同。重载的实现是:编译器根据方法不同的参数表
,对同名方法的名称做修饰。对于编译器而言,这些同名方法就成了不同的方法。它们的调用地址在编译期
就绑定了。
多态:是指子类重新定义父类的虚方法(virtual,abstract)。当子类重新定义了父类的虚方法后,父类根据
赋给它的不同的子类,动态调用属于子类的该方法,这样的方法调用在编译期间是无法确定的。
不难看出,两者的区别在于编译器何时去寻找所要调用的具体方法,对于重载而言,在方法调用之前,编译
器就已经确定了所要调用的方法,这称为“早绑定”或“静态绑定”;而对于多态,只有等到方法调用的那一刻
,编译器才会确定所要调用的具体方法,这称为“晚绑定”或“动态绑定”。
·························································································································································································
一.多态
(链接机制)
多态(Polymorphism)按字面的意思就是“多种形状”。引用Charlie Calverts对多态的描述——多态性是允许你将父对象设置成为和一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作(摘自“Delphi4 编程技术内幕”)。简单的说,就是一句话:允许将子类类型的指针赋值给父类类型的指针。多态性在Object Pascal和C++中都是通过虚函数(Virtual Function) 实现的。
多态性是允许将父对象设置成为和一个和多个它的子对象相等的技术,比如Parent:=Child; 多态性使得能够利用同一类(基类)类型的指针来引用不同类的对象,以及根据所引用对象的不同,以不同的方式执行相同的操作.
多态的作用:把不同的子类对象都当作父类来看,可以屏蔽不同子类对象之间的差异,写出通用的代码,做出通用的编程,以适应需求的不断变化。
赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。也就是说,父亲的行为像儿子,而不是儿子的行为像父亲。
举个例子:从一个基类中派生,响应一个虚命令,产生不同的结果。
比如从某个基类继承出多个对象,其基类有一个虚方法Tdoit,然后其子类也有这个方法,但行为不同,然后这些子对象中的任何一个可以附给其基类的对象,这样其基类的对象就可以执行不同的操作了。实际上你是在通过其基类来访问其子对象的,你要做的就是一个赋值操作。
使用继承性的结果就是可以创建一个类的家族,在认识这个类的家族时,就是把导出类的对象 当作基类的的对象,这种认识又叫作upcasting。这样认识的重要性在于:我们可以只针对基类写出一段程序,但它可以适 应于这个类的家族,因为编译器会自动就找出合适的对象来执行操作。这种现象又称为多态性。而实现多态性的手段又叫称动态绑定(dynamic binding)。
简单的说,建立一个父类的变量,它的内容可以是这个父类的,也可以是它的子类的,当子类拥有和父类同样的函数,当使用这个变量调用这个函数的时候,定义这个变量的类,也就是父类,里的同名函数将被调用,当在父类里的这个函数前加virtual关键字,那么子类的同名函数将被调用
class A {
public:
A() {}
virtual void foo() {
cout << "This is A." << endl;
}
};
class B : public A {
public:
B() {}
void foo() {
cout << "This is B." << endl;
}
};
int main(int argc, char* argv[]) {
A *a = new B();
a->foo();
return 0;
}
这将显示:
This is B.
如果把virtual去掉,将显示:
This is A.
前面的多态实现使用抽象类,并定义了虚方法.
二.重载
(编译机制)
重载决策是一种编译时机制,用于在给定了参数列表和一组候选函数成员的情况下,选择一个最佳函数成员来实施调用。函数重载就是一个类中有几个同名函数但参数表不同:
重载分为普通方法重载和基类(也就是父类)虚方法的重载!
普通方法的重载指的是:类中两个以上的方法(包括隐藏的继承而来的方法),取的名字相同,但使用的参数类型或者参数个数不同!
对基类方法的重载是函数重载的另一种特殊形式。在派生类中重新定义此虚函数!方法名称,返回值类型,参数表中的参数个数,类型,顺序都必须和基类中的虚函数完全一致!在派生类中声明对虚方法的重载,要求在声明中加上override关键字,而且不能有new,static或virtual修饰符!
·························································································································································································
··································································
水煮多态——对C++多态性的形象解释
水是什么形状的?
乍一看这个问题似乎问得很没有道理,其实仔细想想,水正是自然界中“多态”的完美体现。不是么?用圆柱形容器装水,那么水就是圆柱形的;换用圆锥形 容器盛之,水则又会成为圆锥形的了。在这个过程中,我们并不需要关心水是如何改变形状的,亦无需关心水在改变形状的过程中具体做了哪些事情;我们所要关心 的,只是提供给它一个什么形状的容器,这就足够了。
OO(面向对象)中所谓的多态性,也正是这个道理。对于一个同名的方法(Water),我们在不同的情况(Container)下对其进行调用,那么它所完成的行为(Where_Am_I)也是不一样的。以下我将解说的,便是C++之中对于“多态”几种不同的实现形式。
函数的重载(Overload)
这儿是一个非常简单的函数max,它返回两个传入参数中较大的那一个。
int max( int a, int b )
{
if ( a > b )
return a;
else
return b;
}
相信这段代码的具体内容不用我解释了,是的,这是一段非常基本的代码。你可能会发现,这个max函数只适用于int类型的参数。那么,如果我同时还需要一个针对double类型的max,又该怎么办呢?
所幸C++语言本身提供了这一功能,它允许我们在定义函数的时候使用相同的名称——是为函数的重载。也就是说,我们可以继续定义一个double版本的max:
double max( double a, double b )
{
if ( a > b )
return a;
else
return b;
}
然后,在我们的代码中对这两个函数分别进行调用:
void f( void )
{
int a = max( 1, 2 );
double b = max( 1.5, 2.5 );
}
这样一来,我们无需关心调用的是哪个版本的max,编译器会自动根据我们给定的参数类型(int或double)挑选出最适当的max函数来进行调用。
模板(Template)
函数的重载的确为我们提供了很大的方便,我们不需要关心调用哪个函数,编译器会根据我们给定的参数类型挑选出最适当的函数进行调用。但是对于下面的情况,函数的重载就不是很适用了:
函数体代码内容基本相同。
需要为多个类型编写同样功能的函数。
也就是说,我们也许需要更多版本(int、double,甚至更多自定义类型,如复数complex之类)的max,但是它们的代码却无一例外的都是:
if ( a > b )
return a;
else
return b;
这样一来,我们需要做的事情就更倾向于一种体力劳动,而且,如是过多重复的工作也必然存在着错误的隐患。C++在这一方面,又为我们提供了一个解决方法,那就是模板。对于上面这众多版本且内容基本相同的max函数,我们只需要提供一个像下面这样函数模板即可:
template < typename T >
T max( const T& a, const T& b )
{
if ( a > b )
return a;
else
return b;
}
template是C++的关键字,表示它以下的代码块将使用模板。尖括号里面的内容称为模板参数,表示其中的T将在下面的代码模板中作为一种确定 的类型使用。参数之所以使用const引用的形式,是为了避免遇到类对象的时候不必要的传值开销。在这个模板定义完毕之后,我们就可以像这样使用了:
void f( void )
{
int a = max< int >( 1, 2 );
double b = max< double >( 1.5, 2.5 );
}
对于这段代码,编译器会分别将int与double填充到函数模板中T所在的位置,也就是分别为max< int >和max< double >各自产生一份max函数的实体代码。这样一来,就达到了与函数重载一样的效果,但是程序员的工作量却是不可同日而语的。
虚函数(Virtual Function)
下面来以水为例,说说虚函数提供的多态表现形式。首先我们建立一个Water类,用来表示水。
class Water
{
public:
virtual void Where_Am_I() = 0;
};
正如单独讨论水的形状没有意义一样,我们在这里当然也不能允许Water类的实例化,所以成员函数Where_Am_I被定义为了纯虚函数。下面,我们来分别定义水(Water)在瓶子(Bottle)、玻璃杯(Glass)以及湖泊(Lake)中的三种不同情况:
class Water_In_Bottle : public Water
{
public:
virtual void Where_Am_I()
{
cout << "Now I'm in a bottle." << endl;
}
};
class Water_In_Glass : public Water
{
public:
virtual void Where_Am_I()
{
cout << "Now I'm in a glass." << endl;
}
};
class Water_In_Lake : public Water
{
public:
virtual void Where_Am_I()
{
cout << "Now I'm in a lake." << endl;
}
};
这三者分别实现了成员函数Where_Am_I。然后,多态性的实现就可以通过一个指向Water的指针来完成:
void f( void )
{
Water_In_Bottle a;
Water_In_Glass b;
Water_In_Lake c;
Water *pWater[3];
pWater[0] = &a;
pWater[1] = &b;
pWater[2] = &c;
for ( int i = 0; i < 3; i++ )
{
pWater[i]->Where_Am_I();
}
}
这样,程序的运行结果是:
Now I'm in a bottle.
Now I'm in a glass.
Now I'm in a lake.
好了,如你所见,我们并不需要关心pWater指向的是哪一种水,而只需要通过这个指针进行相同的调用工作,水本身就可以根据自身的所在来选择相应 的行为。虚函数的多态性是非常有用的,尤其是在使用C++进行Windows程序设计的时候。考虑那些不同的窗口针对用户的相同行为而能够做出不同反应, 也正是由于相应的消息响应虚函数的具体实现不同,方能达到这样的效果。
水煮多态,暂且煮到这里。这里所谈及的仅仅是C++对于多态的表现形式,而并未对文中三种技术(重载、模板、虚函数)的具体内容进行过多的解说—— 毕竟稍微一深入就难免会对某些技术细节进行大篇幅追究,譬如说到重载难免就会说到参数的匹配,说到模板又难免与泛型进行挂钩,到了虚函数又不能不提一下VTable的东西……在这里我一概全免,因为我的目的也就是希望通过上面几个简单的例子让诸位看官能对OO本身的多态有一个感性的认识,感谢您们的阅读。