多态到底怎么一会子事情?把今天的复习内容弄完了,决定整理性复习一下“多态(Polymorphisn)”。
既然是整理性复习,就先把一些概念整理出来,其实有的不是标准的概念,只是我为了自己理解的方便:
1、变量:
这个称谓包括两个部分。
内建类型实例,比如:整型(int),浮点型(float),字符型(char)等;
用户自定义类型实例,比如:类(class),联合(union),结构(struct)等;
2、程序成员:
泛泛的称变量和函数。
3、父类/子类:
这是相对于继承层次而言,被继承的为父类;继承的为子类。
4、对象:
很多E文经典的书籍中是这么说的:an instance of a class,我也就这么翻译了:类(class)类型的实例。
5、存储空间:
程序成员在程序中的存储形式。
6、生命周期:
程序成员在那个时间段是可以利用的,简单一点讲就是程序运行中有一段时间某个成员是可以引用的,我们就把这段时间称为这个成员的生命周期。要是再深一点理解,我认为是:变量放在程序运行的哪个内存部分,比如说程序数据段,程序栈等等。
7、可见性:
这可是面向对象中最基础的概念,程序成员可以被引用的范围。由我们最常见的public,protected,private等几个兄弟定义。
好了,一些概念先弄清楚了,为了我以后的说明,我定义两个很简单的类,为了方便我以后的说明
| class CFather { public: BuyCigarette() { } }; class CSon :public CFather { public: BuyCandy() { } }; |
根据我对多态的理解可以得到这样三个定律:
定律一:
如果我们以一个【父类指针】pointer指向一个【子类对象】,那么通过pointer我们只能呼叫引用父类类型(指类定义)中所定义的函数。
比如我们写这样的代码:
| CFather* pFather; |
虽然我们可以用指针pFather指向CSon的对象,但是由于pFather是一个CFather类型指针,所以我们只能呼叫(或引用)BuyCigarette(),不可能是BuyCandy()。
用我曾经《当一个妇科医生》(一、二、三、四、五、六、七)系列的语言方法来解释这个定律,可以这样解释:即使儿子拿着老爸的手机,别老爸的同时人打电话进来,还是找他老爸,叫他老爸买包香烟(引用BuyCigarette())。不会是让儿子买包糖果(不引用BuyCandy())。在国外,未成年人购买香烟可是犯法的。
定律二:
如果我们以一个【子类指针】指向一个【父类对象】,那我们在引用函数前,必须通过RTTI(Run-Time Type Identification)来确定指针指向对象的具体类型。也就是说要做显式的造型转换。这种做是我深恶痛绝的做法。老爸拿着儿子的手机,儿子的同学打电话进来,要说说一句:叔叔好,我找令公子。
定律三:
如果父类和子类都定义了【相同名称的成员函数】,我们这个时候通过指针呼叫引用成员函数的决议,就必须根据指针类型(注意不是指针实际指向对象的类型)来确定我们引用的是哪个函数。实际上这个结论就是函数屏蔽(mask)功能,这个定律很重要的,因为这里不管是不是虚拟函数,统统管用的,所以这个结论杀伤力很大的。爸爸和儿子都可以去买瓶酱油,妈妈就想起谁的手机号码,就随便打了。
三大定律说完了,我们把刚才的定义类扩充一下接着说:
| class CFather { public: int FatherAge; void BuyCigarette() { } void PlayFootball() { } virtual void vBuynewspaper() { } virtual void vVisitToPub() { } }; class CSon: public CFather { public: int SonAge; void PlayFootball() { } void DoHomework() { } virtual void vBuynewspaper() { } }; |
函数定义有了,我们再做一个函数,为的是看看他们父子俩交换手机之后的反映:
| void ChangeMobileTelephone(CFather& cf) { cf.BuyCigarette(); cf.PlayFootball(); cf.DoHomework(); cf.vBuynewspaper(); cf.vVisitToPub(); } |
诞生吧,可爱的父亲和儿子
| CFather cf; CSon cs; |
这个时候,我们可以发现DoHomework()函数是不能调用的,这个函数在老爸类CFather中是根本不存在的。原因很简单,复杂的马上就来了,我们让他们开始交换手机:
| ChangeMobileTelephone(cs); |
许说这样应该可以调用DoHomework()函数了吧,不错CSon中的确是存在一个名字叫DoHomework()的函数,但是我们仍然不能通过上面那个函数调用他的,为什么呢?咱们下回再说……别找打,还是马上说完吧。因为我们是用一个【父类的引用】引用一个【子类的类型】。从函数的实现细节,我们的目的很明确的,就是要实现多态性,因为我们在函数使用的时候才能知道函数参数引用指向对象的真正类型,可能是CFather也可能是CSon的,当然我们就不能贸然的调用DoHomework()那个函数了。我们那样实现函数在编译器的时候就会被挡住了。
这还是在进一步的解释定律一,回想一下这种用法我在从前的程序中使用过的,这大概也是多态最容易让人理解的一种做法了。
至于定律二,怎么说呢。我是没用过,谁要是用了……我总不能说这是你“编程的能力问题”。因为这是因为语言提供了灵活性,大概也是C++为了完善面向对象概念的一种完整性做法。但用不用就是写程序人本身的问题了。至少我是不敢这样用的,如果我没有很好的理解语言本身,我写出这样的代码就是经常会出现问题,这个只能怪我自己,要进修,我是绝对不会动辄就说这个语言不好的。
又到了最恐怖的一个定律了,这个定律看起来也是真的很好理解,也是很容易使用的,但实质,这就是C++中与override,overload齐名的mask问题了。那对父子已经为我们服务这个长时间了,我就让他们俩再变变脸,来为我们服务。但这次变脸有点“万圣节”的感觉,呵呵,做好心里准备:
| class CFather { public: void CallFriend(const CFather & father) { } virtual void GetMonet(int money) { } }; class CSon: public CFather { public: virtual void GetMonet(double *pmoney); }; |
一眼能看出来这次变脸最明显的是出现了虚函数的override。至于那两个虚函数,呵呵,不是我BT,我来说说原因,老爸是赚钱的啊,赚的当然是大钱,我姑且就用整型(int)来表示了。而儿子,总是找老爸要零花钱,能多要一毛也是好的啊!想当年上小学的时候,能在方学候买一根一毛五的冰棍都跟过年似的。
呵呵,闲话少说,进入正题。大家帮我看看,我后年的代码有没有问题
| CSon* pson = new CSon; pson->GetMonet(10); |
错!错!错!错在pson->GetMonet(10)!这里其实就是发生了函数屏蔽(mask)作用。
其实,我一直认为这不很合理,但很多大师对这种行为提供了解释。假设调用GetMonet时,你真的是想调用CSon中的版本,但不小心用错了参数类型。进一步假设CSon是在继承层次结构的下层,你不知道CSon间接继承了某个基类CFather而且CFather中声明了一个带int参数的虚函数GetMonet。这种情况下,你就会无意中调用了CFather::GetMonet,一个你甚至不知道它存在的函数!在使用大型类层次结构的情况下,这种错误会时常发生;所以,为了防患于未然,Stroustrup决定让派生类成员按名字隐藏掉基类成员。
感谢Bjarne Stroustrup的慷慨!
