真实的谎言——Upcasting的戏法
2005-11-29 01:35 FantasySoft 阅读(2376) 评论(9) 编辑 收藏 举报0.继续Allen Lee的大片激赏:
Allen Lee在我是谁一文中探讨了Interface选择性透过的问题,可谓是绘声绘色,精彩纷呈。我虽言辞拙劣,只因自己还有几下C/C++的三脚猫功夫,又被曾经风靡一时的大片所动,遂延续Allen Lee的精彩,斗胆跟Allen抢抢生意。嘿,开场时间到了,帷幕拉开……
首先出场的是一位长者——Michael:
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 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!");
}
}
接着,不可思议的事情发生了。噢,英俊的小伙子怎么摇身一变,成了长者?!
1.真实的谎言
小伙子相貌成了老者,甚是惟妙惟肖,所以他大言不惭地说自己很老了,到处招摇撞骗:
还不告诉别人自己的id,更可恶的是,他连跑步都不会了!
clazz.run(); // 编译错误!
但它的身手却依旧矫健,谎言终究变得无力:
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一个倒栽葱跌下地来——噢,这小样终于原形毕露了!
subClazz.lie(); // 打印I'm Perhaps.My age is 20
System.out.println(subclazz.age); // 打印结果为20,这下子终于说实话了!
4.让我们再深入一些
好,戏都演完了,但这仍然只是个铺垫。以上讲到的问题其实并不复杂,就是通过基类指针访问派生类实例的时候,为什么无法调用子类中非继承方法呢?为什么调用到了派生类的继承方法的同时却只能访问属于基类的数据成员呢?要回答这个问题,还是先让我们看看类DerivedClass的实例在内存中的layout吧:
还记得前面所卖的关子吗?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