代码改变世界

真实的谎言——Upcasting的戏法

2005-11-29 01:35 FantasySoft 阅读(...) 评论(...) 编辑 收藏

0.继续Allen Lee的大片激赏
        Allen Lee我是谁一文中探讨了Interface选择性透过的问题,可谓是绘声绘色,精彩纷呈。我虽言辞
拙劣,只因自己还有几下C/C++的三脚猫功夫,又被曾经风靡一时的大片所动,遂延续Allen Lee的精彩,斗胆跟Allen抢抢生意。嘿,开场时间到了,帷幕拉开……
        首先出场的是一位长者——Michael:    

public class BaseClass {
    
public String name = "Michael"
;
    
public int age = 70
;
    
public int grade = 2
;
    
public void lie() 
{
        System.out.println(
"I'm Michael. My age is " +
 age);
    }

    
public void stump() {
        System.out.println(
"I'm too old. I stump so hard."
)
    }

}

        
随后,是一位英俊的小伙子——Perhaps:

public class DerivedClass extends BaseClass {
    
public String name = "Perhaps"
;
    
public int age = 20
;
    
public int id = 1011
;
    
public void lie() 
{
        System.out.println(
"I'm Perhaps. My age is " +
 age);
    }

    
public void run() {
        System.out.println(
"I'm so young. I can run fast!"
);
    }

}
   

       
接着,不可思议的事情发生了。噢,英俊的小伙子怎么摇身一变,成了长者?!        

BaseClass clazz = new DerivedClass();


1.真实的谎言
        小伙子相貌成了老者,甚是惟妙惟肖,所以他大言不惭地说自己很老了,到处招摇撞骗:

System.out.println("My age is " + clazz.age); //打印出来的结果是70!

        
还不告诉别人自己的id,更可恶的是,他连跑步都不会了!

System.out.println("My id is " + clazz.id);  // 编译错误!
clazz.run();                                 // 编译错误!

       
但它的身手却依旧矫健,谎言终究变得无力:

clazz.lie();        // 还是打印I'm Perhaps. My age is 20  


2.Class Casting的威力
       虽然小伙子的变身并不完美,但是我们还是能够从中感受到变身的威力。BaseClass clazz = new
DerivedClass()到底做了些什么呢?
       首先,new关键字为构造DerivedClass实例申请了一块足够大的内存空间;接着构造函数
DerivedClass根据类定义创建了该类的实例。这个创建的过程包括:调用BaseClass的构造函数、初始化类变量和构建Virtual Function Table;随后,构造函数返回一个DerivedClass类型的指针;最后,该指针被Upcast成BaseClass类型的指针。前面几步,大部分朋友应该都很熟悉了,而变身的关键则在于最后一步:Upcasting。那么Upcasting又做了什么呢?且让我暂时卖个关子。

3. 揭穿真实的谎言  
       成龙大哥在直升机上被扔了下来,掉入森林中失掉了记忆;而我们的DerivedClass不是掉下来
(Downcast),而是被Upcast捧上了天,成了BaseClass后就把自己的身份抛到九霄云外了。为了不让DerivedClass洋洋自得,还是要让它狠狠地摔一跤,恢复本来面目。于是,我就拿出Downcast的魔杖对在天上腾云驾雾的DerivedClass一指,霎时间DerivedClass一个倒栽葱跌下地来——噢,这小样终于原形毕露了! 

DerivedClass subClazz = (DerivedClass) clazz;
subClazz.lie();                   
// 打印I'm Perhaps.My age is 20

System.out.println(subclazz.age); // 打印结果为20,这下子终于说实话了!


4.让我们再深入一些
        好,戏都演完了,但这仍然只是个铺垫。以上讲到的问题其实并不复杂,就是通过基类指针访问派生类实例的时候,为什么无法调用子类中非继承方法呢?为什么调用到了派生类的继承方法的
同时却只能访问属于基类的数据成员呢?要回答这个问题,还是先让我们看看类DerivedClass的实例在内存中的layout吧:
   

MemoryLayout.bmp


还记得前面所卖的关子吗?Upcasting的变身戏法并没有改变clazz所指向的内存位置,却改变了clazz所指向内存的大小。在Java当中,没
有sizeof操作符,我无法得知clazz所指向内存的大小,但是通过相对应的C++代码可以验证以上推论。因此,将DerivedClass类型的指针UpCast为BaseClass类型的指针的时候,该指针所指向内存所包含的内容就只有图中红色框1包括的部分了,这很好地说明了为什么Upcast之后的clazz只能访问基类的数据成员。Upcasting除了改变指向内存的大小之外,还缩减了Virtual Table的长度[1],也就是另外红色框2包括的部分,这也正好回答了clazz无法调用run方法的原因。在这里,要注意Function Pointer的先后顺序:首先是重载的方法,接着是从基类继承过来的方法,最后才是类本身独有的方法。

5.还有一个问题
        我们都在关注BaseClass和DerivdeClass的数据成员以及DerivedClass独有的成员函数,却把体现多态
的方法——lie()晾在了一边。lie()是让我们明察秋毫的依据,因为不管Upcast还是Downcast都没有改变它的立场。lie方法坚定的立场却引发了另外一个问题:我们可以从DerivedClass的实例中一个不落地找到BaseClass所有的数据成员,但是我们彻彻底底把BaseClass的lie方法都丢了。我们可以找到丢掉的lie方法吗?我们真的还需要它吗?

[1] 有关Virtual Table Pointer和Virtual Table的介绍可以参考Wikipedia中相关的部分
[2] 本文参考资料:Polymorphism in C    &   Inside the C++ Object Model