多态是面向对象理论的一个重要概念,类型转换是程序开发中不可避免的行为,两者总是贯穿在我们程序开发中,对开发者来说并不陌生。然而,就是这么熟悉的内容,我还是犯过一些错误,因此有必要对其主要内容做一整理来加深理解。本文的重点是总结C++和C#中有关类型转换的一些基本原理,之所以提到多态是因为类型转换后多态函数的执行是比较容易被误解的一点。基于这些出发点,本文并不是一篇介绍多态的详细文章。希望本文能给对类型转换有所疑问的开发者提供一定帮助,若能如此则是最大欣慰。
多态简述
简单的来说,多态就是可以通过基类来执行子类的行为,这种设计便于程序的扩展和抽象。如下的代码是多态的一个简单例子:
#include "stdafx.h" class A { public: virtual void PrintInt(int i) { printf("Print int from class A: %d", i); } }; class B { public: virtual void OutputInt(int i) { printf("Output int from class B: %d\n", i); } }; class C: public A, public B { public: void PrintInt(int i) { printf("Print int from class C: %d\n", i); } void OutputInt(int i) { printf("Output int from class C: %d\n", i); } };
对于这段代码,开发者可以通过A类型的变量来引用一个实际类型为C类型的对象。然而在有继承和多态的情况下,开发者容易在类型转换上犯一些小错误。
类型转换的讨论
C++普通类型的转换比较简单,本文不再做解释。指针在C++中是一个重要的元素,然而其类型转换比较复杂,但可以这样简单的理解:“能转换则转换,不能转换则强制转换”。接下来的讨论将基于这个简单的观点。
合理的类型转换
对于合理的类型转换应用“能转换则转换”的理论。下面的代码毫无疑问是多态的一个典型应用,即声明为基类型的变量保持了对继承类型的引用,所以最终被执行的函数就是对象c所属的函数。
C *c = new C(); A * a = c; B *b = c; a->PrintInt(10); b->OutputInt(10);
对于变量c,其在内存中的状态可用下图来表示(图中箭头所指处为c的引用首地址):
表达式A *a = c;中 所声明的变量a指向的正是c的首地址,且A.PrintInt()被C修改(override)为C.PrintInt(),这就实现了通过基类来访问继承类的机制。而表达式B *b= c; 却不尽相同,转换后b所指向的地址不再是c的首地址,而是指向B的首地址,如下图所示:
看的出,对象引用的首地址变了,因此b->OutputInt(10)才能有正确的输出。对于前面的关于首地址的“理论”需要一定的证明来保证其正确性,我采用如下的代码来打印对象引用的首地址:
template<class T>void printObjAddr(T *t) { printf("The address is: %0x\n", t); } int _tmain(int argc, _TCHAR* argv[]) { C *c = new C(); A * a = c; B *b = c; printObjAddr(c); printObjAddr(a); printObjAddr(b); delete c; c = NULL; return 0; }
输出结果为如下图所示:
可以看出b比a或c偏移了4位(对象A的大小),首地址“理论”得以验证。
对于合理的类型转换,本身的合理性决定了其让开发者产生误解的几率比较小。那么对于不合理的类型转换呢?
不合理的类型转换
对不合理的类型转换应用“不能转换则强制转换”。所谓不合理,其实很简单,指两个对象类型之间既没有继承关系也没有定义相关的类型转换符号,即两者之间没有“相互转换性”。不合理的转换会产生什么结果呢?看如下的代码:
B* b = (B*)a;
b->OutputInt(10);
类型A*和B*之间不存在转换关系,此处把A*类型强制转换为B*类型,其结果是编译器直接返回a的地址。在这种情况下b->OutputInt(10)语句的执行就产生了“形式”和“行为”的不一致性。所谓“形式”是指函数的执行是按照B类型所定义的签名来执行;所谓“行为”是指函数真正执行的却是A.PrintInt。之所以产生这样的结果是因为对象的实际地址是A的地址,编译器却“想当然”的认为是B的地址,然后按照B的“形式”来调用函数,其结果便把A.PrintInt给执行了。示例代码故意把A.PrintInt和B.OutputInt声明成同一个签名(即参数类型相同),意图就是让程序能运行而不抛出签名不一致的异常。假如把OutputInt的签名做一下修改,比如参数改为double类型,程序便会抛出异常:
这其中有一点需要澄清,变量a虽然指向的是一个C类型的实例,且C和B类型存在转换关系,而编译器却没有那么聪明来理清这个关系,而是直接做A*->B*的类型转换。对于类型转换的这个特性,需要做一下巩固,看如下的代码:
C *c = new C(); void *p = c; A *a = (A*)p; a->PrintInt(10); B *b = (B*)p; b->OutputInt(10);
void *类型的变量p虽然实际上指向了c对象,但由于其被声明成了void*类型,而void*向A*或B*转换都是不合理的,因此其行为和最终输出结果根据前面的分析讨论应该不难得出。
结论
本文所做的讨论以及得出的“理论”只适用于C++指针类型的对象间转换,因为对于编译器来说指针类型只是一个地址,没有更多的信息,因此容易产生一些类型转换的错误。对于普通类型的转换,问题就发生的比较少,因为本文所提到的“不合理的转换”对于普通类型来说根本不能通过编译。
下一篇将讲述C#中的类型转换。