一.什么是多态(Polymorphism)

多态(Polymorphism)是面向对象(Object-Oriented,OO)思想"三大特征"之一,其余两个分别是封装(Encapsulation)和继承(Inheritance)--可见多态的重要性。或者说,不懂得什么是多态就不能说懂得面向对象。

多态是一种机制、一种能力,而非某个关键字。它在类的继承中得以实现,在类的方法调用中得以体现。

先让我们看看MSDN里给出的定义:

Through inheritance, a class can be used as more than one type; it can be used as its own type, any base types, or any interface type if it implements interfaces. This is called polymorphism. In C#, every type is polymorphic. Types can be used as their own type or as a Object instance, because any type automatically treats Object as a base type.

译文:通过继承,一个类可以被当作不止一个数据类型(type)使用,它可以被用做自身代表的数据类型(这是最常用的),还可以被当作它的任意基类所代表的数据类型,乃至任意接口类型--前提是这个类实现了这个接口。这一机制称为"多态"。在C#中,所有的数据类型都是多态的。任意一个数据类型都可以被当作自身来使用,也可以当作Object类型来使用(我怀疑原文有问题,那个instance可能是原作者的笔误),因为任何数据类型都自动以Object为自己的基类。

呵呵,除非你已经早就知道了什么是多态然后翻过头来看上面一段话,不然我敢打保票--我是清清楚楚的,你是稀里糊涂的。OK,不难为大家了,我用几个句子说明一下多态的思想。

我们先把前文中提到的"接口"理解为"一组功能的集合",把"类"理解为功能的实现体。这样的例子多了去了。我们就拿生物界做比喻了:

功能集合1:呼吸系统

功能集合2:血液循环系统

功能集合3:神经系统

功能集合4:语言系统

类1:灵长类动物。此类实现了1到3功能集合。

类2:猴子类。继承自类1。新添加了"爬树"的功能。

类3:人类。继承自类1。同时实现了功能集合4。

类4:男人类。继承自类3。新添加了"写程序"的功能。

类5:女人类。继承自类3。新添加了"发脾气"的功能。

作业:请大家把上面的关系用图画出来

OK,让我们看下面的话,判断对错:

1. 男人是男人 (√) 原因:本来就是!

2. 男人是人 (√) 原因:人类是男人类的基类

3. 男人是灵长类动物 (√) 原因:灵长类是男人类的更抽象层基类

4. 男人是会说话的 (√) 原因:男人类的基类实现了语言系统

5. 女人是猴子 (×) 原因:如果我这么说,会被蹁死

6. 猴子是女人 (×) 原因:女人不是猴子的基类

7. 人会写程序 (×) 原因:写程序方法是在男人类中才具体实现的

8. 女人会发脾气 (√) 原因:因为我说5..

哈哈!现在你明白什么是多态了吧!其实是非常简单的逻辑思维。上面仅仅是多态的一个概念,下面我们通过代码去研习一下程序中的多态到底是什么。

二.多态的基础--虚函数(virtual)和重写(override)

很多公司在面试的时候常拿下面几个问题当开胃小菜:

1. 如何使用virtual和override?

2. 如何使用abstract和override?

3. "重写"与"重载"一样吗?

4. "重写"、"覆盖"、"隐藏"是同一个概念吗?

顺便说一句:如果你确定能把上面的概念很熟练的掌握,发个Mail给我(bladey@tom.com ),也许你能收到一份薪水和福利都不错的Offer :p

今天我们学习多态,其实就是解决问题1。前面已经提到过,多态机制是依靠继承机制实现的。那么,在常规继承机制的基础之上,在基类中使用virtual函数,并在其派生类中对virtual函数进行override,那么多态机制就自然而然地产生了。

小议virtual:

呵呵,我这人比较笨--有我的老师和同学为证--学东西奇慢无比,所以当初在C++中学习virtual的历程是我心中永远挥之不去的阴影..倒霉就倒霉在这个"虚"字上了。"实"的我还云里雾里呢,更何况这"虚"的,"虚"的还没搞清楚呢,"纯虚"又蹦出来了,我#@$%!^#&&!..

还好,我挺过来了..回顾这段学习历程,我发现万恶之源就是这个"虚"字。

在汉语中,"虚"就是"无","无"就是"没有",没有的事情就"不可说"、"不可讲"--那还讲个X??老师也头疼,学生更头疼。拜初中语文老师所赐,我的语言逻辑还算过关,总感觉virtual function译为"虚函数"有点词不达意。

找来词典一查,virtual有这样一个词条:

Existing or resulting in essence or effect though not in actual fact, form, or name:

实质上的,实际上的:虽然没有实际的事实、形式或名义,但在实际上或效果上存在或产生的:

例句:

the virtual extinction of the buffalo.

野牛实际上已经绝迹(隐含的意思是"尽管野牛还木有死光光,但从效果上来讲..")

啊哦~~让我想起一句话:

有的人活着他已经死了; 有的人死了他还活着..

不禁有点惊叹于母语的博大精深--

virtual function中的virtual应该译做"名存实亡"而不是"虚"!

OK,下面就让我们看看类中的virtual函数是怎么个"名存实亡"法。

例子1:非virtual / override程序

// 水之真谛 //
// http://blog.csdn.net/FantasiaX //
// 上善若水,润物无声 //

using System;
using System.Collections.Generic;
using System.Text;

namespace Sample
{
// 演员(类)
class Actor
{
public void DoShow()
{
Console.WriteLine("Doing a show...");
}
}

// 乐手(类),继承自Actor类
class Bandsman : Actor
{
// 子类同名方法隐藏父类方法
// 其实标准写法应该是:
// public new void DoShow(){...}
// 为了突出"同名",我把new省了,编译器会自动识别
public void DoShow()
{
Console.WriteLine("Playing musical instrument...");
}
}

// 吉他手(类),继承自Bandsman类
class Guitarist : Bandsman
{
public new void DoShow()
{
Console.WriteLine("Playing a guitar solo...");
}
}

class Program
{
static void Main(string[] args)
{
// 正常声明
Actor actor = new Actor();
Bandsman bandsman = new Bandsman();
Guitarist guitarist = new Guitarist();

// 一般情况下,随着类的承继和方法的重写
// 方法是越来越具体、越来越个性化
actor.DoShow();
bandsman.DoShow();
guitarist.DoShow();

Console.WriteLine("===========================");

//尝试多态用法
Actor myActor1 = new Bandsman(); //正确:乐手是演员
Actor myActor2 = new Guitarist(); //正确:吉他手是演员
Bandsman myBandsman = new Guitarist(); //正确:吉他手是乐手

//仍然调用的是引用类型自身的方法,而非派生类的方法
myActor1.DoShow();
myActor2.DoShow();
myBandsman.DoShow();
}
}
}

代码分析:

1. 一上来,演员类、乐手类、吉他手类形成一个继承链。

2. 乐手类和吉他手类作为子类,都把其父类的DoShow()方法"隐藏"了。

3. 特别强调:"隐藏"不是"覆盖",后面要讲的"重写"才是真正的"覆盖"。

4. 隐藏是使用new修饰符实现的,但这个修饰符可以省略。

5. 隐藏(Hide)的含意是:父类的这个函数实际上还在,只是被子类的同名"藏起来"了。

6. 重写(override)与覆盖是同一个含意,只是覆盖并非编程的术语,但"覆盖"比较形象。

7. 主程序代码的上半部分是常规使用方法,没什么好说的。

8. 主程序代码的下半部分已经算是多态了,但由于没有使用virtual和override,多态最有价值的效果--个性化方法实现--没有体现出来。后面的例子专门体现这一点。

例子2:应用virtual / override,真正的多态

// 水之真谛 //
// http://blog.csdn.net/FantasiaX //
// 上善若水,润物无声 //

using System;
using System.Collections.Generic;
using System.Text;

namespace Sample
{
// 演员(类)
class Actor
{
// 使用了virtual来修饰函数
// 此函数已经"名存实亡"了
public virtual void DoShow()
{
Console.WriteLine("Doing a show...");
}
}

// 乐手(类),继承自Actor类
class Bandsman : Actor
{
// 使用了override来修饰函数
// 此函数将取代(重写)父类中的同名函数
public override void DoShow()
{
Console.WriteLine("Playing musical instrument...");
}
}

// 吉他手(类),继承自Bandsman类
class Guitarist : Bandsman
{
public override void DoShow()
{
Console.WriteLine("Playing a guitar solo...");
}
}

class Program
{
static void Main(string[] args)
{
// 正常声明
Actor actor = new Actor();
Bandsman bandsman = new Bandsman();
Guitarist guitarist = new Guitarist();

// 一般情况下,随着类的承继和方法的重写
// 方法是越来越具体、越来越个性化
actor.DoShow();
bandsman.DoShow();
guitarist.DoShow();

Console.WriteLine("===========================");

//尝试多态用法
Actor myActor1 = new Bandsman(); //正确:乐手是演员
Actor myActor2 = new Guitarist(); //正确:吉他手是演员
Bandsman myBandsman = new Guitarist(); //正确:吉他手是乐手

// Look!!!

// 调用的是引用类型所引用的实例的方法

// 引用类型本身的函数是virtual的

// 看似"存在",实际已经被其子类重写(不是隐藏,而是被kill掉了)

// 这正是virtual所要表达的"名存实亡"的本意,而非一个"虚"字所能传达
myActor1.DoShow();
myActor2.DoShow();
myBandsman.DoShow();
}
}
}

代码分析:

1. 除了将继承链中最顶层基类的DoShow()方法改为用virtual修饰;把继承链中派生类的DoShow()方法改为override修饰以重写基类的方法。

2. 主程序代码没变,但下半部分产生的效果完全不同!请体会"引用变量本身方法"与"引用变量所引用实例的方法"的不同--这是关键。

多态成因的分析:

为什么会产生这样的效果呢?这里要提到一个"virtual表"的问题。我们看看程序中继承链的构成:Actor à Bandsman à Guitarist。因为派生类不但继承了基类的代码(确切地说是public代码)而且还有自己的特有代码(无论是不是与基类同名,都是自己特有的)。从程序的逻辑视角来看,你可以这样想象:在内存中,子类的实例所占的内存块是在父类所占的内存块的基础上"追加"了一小块--拜托大家自己画画图。这多出来的一小块里,装的就是子类特有的数据和代码。

我们仔细分析这几句代码:

1. Actor actor = new Actor(); //常规的声明及分配内存方法
因为类是引用类型,所以actor这个引用变量是放在栈里的、类型是Actor类型,而它所引用的实例--同样也是Actor类型的--内存由new操作符来分配并且放在堆里。这样,引用变量与实例的类型一模一样、完全匹配。换句话说:栈里的引用变量所能"管理"的堆中的内存块大小正好、不多也不少。

2. Actor myActor1 = new Bandsman(); //正确:乐手是演员
同样是这句代码,在两个例子中产生的效果完全不同。为什么呢?且看!在例1中,在Bandsman类中只是使用new将父类的DoShow()给隐藏了--所起的作用仅限于自己对父类追加的代码块中,丝毫没有影响到父类。而栈中的引用变量是Actor类型的myActor1,它只能管理Actor类实例所占的那么大一块内存,而对追加的内存毫无控制能力(或者说看不见追加的这块内存)。因此,当你使用myActor1.DoShow();调用成员方法时,myActor1只能使唤自己能管到的那块内存里的DoShow()方法。那么例2中呢?难道例2中的myActor1就能管理追加的一块内存了吗?否也!它仍然管理不了,但不要忘了--这时候Actor类中的DoShow()方法已经被virtual所修饰,同时Bandsman类中的DoShow()方法已经被override修饰。这时候,当执行myActor1.DoShow();一句时,myActor1调用自己所管辖的内存块时,发现DoShow()这个函数已经标记为"可被重写"了(其实,在VB.NET中,与C#的virtual关键字对应的关键字就是Overridable,更直白),那么它就会尝试去发现有没有override链(也就是virtual表,即"虚表") 的存在,如果存在,那么就调用override链上的最新可用版本--这就有了我们在例2中看到的效果。

3. Actor myActor2 = new Guitarist(); //正确:吉他手是演员
通过这句代码,你也可以想象一下2级重写是怎么形成的,同时也可以感悟一下所谓"重写链上最新的可用版本"是什么意思。

4. Guitarist myActor2 = new Actor(); //错误:想一想为什么?
呵呵,这是错误的,原因是引用变量所管理的内存大小超出了实例实际的内存大小。

乱弹:

多态,台湾的兄弟们喜欢称"多型",一样的。"多"表示在实例化引用变量的时候,根据用户当时的使用情况(这时候程序已经Release了,不能再修改了,程序员已经不能控制程序了)智能地给出个性化的响应。

多,谓之变。莫非"多态"亦可称为"变态"耶?咦.."变型"..让我想起Transformer来了。

TO BE CONTINUE

C++ 什么是多态的更多相关文章

  1. Java中的多态

    1.多态:具有表现多种形态的能力的特征 父类: public abstract class Animal {public abstract void Say();} 子类: public class ...

  2. C# 工厂模式+虚方法(接口、抽象方法)实现多态

    面向对象语言的三大特征之一就是多态,听起来多态比较抽象,简而言之就是同一行为针对不同对象得到不同的结果,同一对象,在不同的环境下得到不同的状态. 实例说明: 业务需求:实现一个打开文件的控制台程序的d ...

  3. C#非常重要基础之多态

    前几天看了一位同志的博客,写的是关于他自己去支付宝面试的经历.过程大体是这样的:问答的时候,前面部分,作者都应答如流,说起自己经验如何之丰富,最后面试官问了作者一个问题:请简述多态的概念和作用.结果这 ...

  4. C++多态详解

    多态是面向对象的程序设计的关键技术.多态:调用同一个函数名,可以根据需要但实现不同的功能.多态体现在两个方面,我们以前学过的编译时的多态性(函数重载)和现在我们这一章将要学习的运行时的多态性(虚函数) ...

  5. 【那些年关于java多态应用】

    1.多态:具有表现多种形态的能力的特征 父类: public abstract class Animal { public abstract void Say();} 子类: public class ...

  6. JAVA多态

    多态是指当系统A访问系统B的服务时,系统B可以通过多种方式来提供服务,而这一切对系统A是透明的.比如动物园的饲养员能够给各种各样的动物喂食.下图显示了饲养员Feeder,食物Food和动物Animal ...

  7. C#多态“说来也说”——逻辑层BLL中的多态使用

    本文版权归博客园和作者吴双本人共同所有.欢迎转载,转载和爬虫请注明原文地址 http://www.cnblogs.com/tdws/p/5861842.html 昨天晚上,有个朋友说学了好久,依然没搞 ...

  8. java多态的理解

    面向对象语言中的类有三个特征,封装.继承.多态.封装与继承很好理解,那什么是多态呢? 1.什么是多态? 多态的定义:指允许不同类的对象对同一消息做出响应.即同一消息可以根据发送对象的不同而采用多种不同 ...

  9. java中如何实现多态

    复习基础知识 多态,就是重载和重写.重载发生在一个类中.重写发生在子类,意思就是子类重写父类相同名称的方法.刚学语言有的东西,不必搞得那么清楚,只有知道怎么用就行了,有的问题你要想真正把它搞得很懂,短 ...

  10. OC多态

    要点: 1.多种形态,引用的多种形态对于一个引用变量,可以指向任何类的对象.对于一个父类的引用(类与类之间有一种继承关系),可以指向子类,也可以指向本类,指向的类型不同.当通过此引用向对象发送消息,调 ...

随机推荐

  1. JavaScript 字符串 & Math & Date

    字符串 字符串就是零个或多个排在一起的字符,放在单引号或双引号之中. 'abc' "abc" 单引号字符串的内部,可以使用双引号.双引号字符串的内部,可以使用单引号. 'key=& ...

  2. Beta阶段项目展示博客

    Beta阶段项目展示 团队成员的简介 详细见团队简介 角色 姓名 照片 项目经理,策划 游心 策划 王子铭 策划 蔡帜 美工 赵晓宇 美工 王辰昱 开发.架构师 解小锐 开发 陈鑫 开发 李金奇 开发 ...

  3. php5.4以上运行yii框架出现问题的解决方法

    Ubuntu Server 下安装 Mcrypt Php Extension http://blog.archean.me/2013/10/22/install-mcrypt-php-extensio ...

  4. java定时执行任务(一)

    需求: 经常遇到这样的需求:要求每天执行一次任务,执行任务时间是凌晨3点 实现: 为了便于检测,我假设的是下一分钟执行任务,每10秒重复执行.(对应现实项目:每天3点执行任务.那么就是下一个3点执行任 ...

  5. iOS开发应用程序生命周期

    各个程序运行状态时代理的回调: - (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSD ...

  6. iOS-根据两个经纬度计算相距距离

    CLLocation *orig=[[[CLLocation alloc] initWithLatitude:[mainDelegate.latitude_self doubleValue] long ...

  7. 3ds Max学习日记(二)

      搞了一天终于把第二章弄完了,虽然有点慢,但也是没有办法,毕竟中途学累了总得放松一下吧.   又get到了一个新知识点,是关于3ds max的捕捉工具的,详情见3DS MAX 学习笔记 一(捕捉工具 ...

  8. 【Linux】- rm命令

    Linux rm命令用于删除一个文件或者目录. 语法 rm [options] name... 参数: -i 删除前逐一询问确认. -f 即使原档案属性设为唯读,亦直接删除,无需逐一确认. -r 将目 ...

  9. Redis集群分布(Windows版)

    Redis系列 作者Mr.Chen,转载请注明博客出处:http://www.cnblogs.com/cjh-notes/ 第一步:下载安装redis windows版的下载地址:https://gi ...

  10. 总结 java 学习

    想着想把以前学的java学习笔记整理下发上来,慢慢整理吧.