Java中继承、多态、重载和重写介绍(6)
我们再来分析一下以下三句代码
Base *pb = new Derive();
pb->fun(1);//Base::fun(int i)
pb->fun((double)0.01);//Base::fun(int i)
这第一句是关键,基类指针指向派生类的对象,我们知道这是多态调用;接下来第二句,运行时基类指针根据运行时对象的类型,发现是派生类对象,所以首先到派生类的vtable中去查找派生类的虚函数版本,发现派生类没有覆盖基类的虚函数,派生类的vtable只是作了一个指向基类虚函数地址的一个指向,所以理所当然地去调用基类版本的虚函数。最后一句,程序运行仍然埋头去找派生类的vtable,发现根本没有这个版本的虚函数,只好回头调用自己的仅有一个虚函数。
这里还值得一提的是:如果此时基类有多个虚函数,此时程序编绎时会提示”调用不明确”。示例如下
#include <iostream> using namespace std; class Base{ public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl; } virtual void fun(char c){ cout <<"Base::fun(char c)"<< endl; } }; class Derive : public Base{ public: void fun(double d){ cout <<"Derive::fun(double d)"<< endl; } }; int main() { Base *pb = new Derive(); pb->fun(0.01);//error C2668: 'fun' : ambiguous call to overloaded function delete pb; return 0; }
好了,我们再来分析一下例8-2。
n 例8-2中,我们也为派生类重载了一个函数版本:void fun(double d) ,同时覆盖了基类的虚函数,我们再来具体来分析一下,基类共有几个函数,派生类共有几个函数:
类型
基类
派生类
Vtable部分
void fun(int i)
void fun(int i)
静态部分
void fun(double d)
从表中我们可以看到,派生类的vtable中函数指针指向的是自己的重写的虚函数地址。
我们再来分析一下以下三句代码
Base *pb = new Derive();
pb->fun(1);//Derive::fun(int i)
pb->fun((double)0.01);//Derive::fun(int i)
第一句不必多说了,第二句,理所当然调用派生类的虚函数版本,第三句,嘿,感觉又怪怪的,其实呀,C++程序很笨的了,在运行时,埋头闯进派生类的vtable表中,只眼一看,靠,竞然没有想要的版本,真是想不通,基类指针为什么不四处转转再找找呢?呵呵,原来是眼力有限,基类年纪这么老了,想必肯定是老花了,它那双眼睛看得到的仅是自己的非Vtable部分(即静态部分)和自己要管理的Vtable部分,派生类的void fun(double d)那么远,看不到呀!再说了,派生类什么都要管,难道派生类没有自己的一点权力吗?哎,不吵了,各自管自己的吧^_^
唉!你是不是要叹气了,基类指针能进行多态调用,但是始终不能进行派生类的重载调用啊(参考例6)~~~
再来看看例9,
本例的效果同例6,异曲同工。想必你理解了上面的这些例子后,这个也是小Kiss了。
小结:
重载overload是根据函数的参数列表来选择要调用的函数版本,而多态是根据运行时对象的实际类型来选择要调用的虚virtual函数版本,多态的实现是通过派生类对基类的虚virtual函数进行覆盖override来实现的,若派生类没有对基类的虚virtual函数进行覆盖override的话,则派生类会自动继承基类的虚virtual函数版本,此时无论基类指针指向的对象是基类型还是派生类型,都会调用基类版本的虚virtual函数;如果派生类对基类的虚virtual函数进行覆盖override的话,则会在运行时根据对象的实际类型来选择要调用的虚virtual函数版本,例如基类指针指向的对象类型为派生类型,则会调用派生类的虚virtual函数版本,从而实现多态。
使用多态的本意是要我们在基类中声明函数为virtual,并且是要在派生类中覆盖override基类的虚virtual函数版本,注意,此时的函数原型与基类保持一致,即同名同参数类型;如果你在派生类中新添加函数版本,你不能通过基类指针动态调用派生类的新的函数版本,这个新的函数版本只作为派生类的一个重载版本。还是同一句话,重载只有在当前类中有效,不管你是在基类重载的,还是在派生类中重载的,两者互不牵连。如果明白这一点的话,在例6、例9中,我们也会对其的输出结果顺利地理解。
重载是静态联编的,多态是动态联编的。进一步解释,重载与指针实际指向的对象类型无关,多态与指针实际指向的对象类型相关。若基类的指针调用派生类的重载版本,C++编绎认为是非法的,C++编绎器只认为基类指针只能调用基类的重载版本,重载只在当前类的名字空间作用域内有效,继承会失去重载的特性,当然,若此时的基类指针调用的是一个虚virtual函数,那么它还会进行动态选择基类的虚virtual函数版本还是派生类的虚virtual函数版本来进行具体的操作,这是通过基类指针实际指向的对象类型来做决定的,所以说重载与指针实际指向的对象类型无关,多态与指针实际指向的对象类型相关。
最后阐明一点,虚virtual函数同样可以进行重载,但是重载只能是在当前自己名字空间作用域内有效
到底创建了几个String对象?
我们首先来看一段代码:
Java代码
String str=new String("abc");
紧接着这段代码之后的往往是这个问题,那就是这行代码究竟创建了几个String对象呢?相信大家对这道题并不陌生,答案也是众所周知的,2个。接下来我们就从这道题展开,一起回顾一下与创建String对象相关的一些JAVA知识。
我们可以把上面这行代码分成String str、=、"abc"和new String()四部分来看待。String str只是定义了一个名为str的String类型的变量,因此它并没有创建对象;=是对变量str进行初始化,将某个对象的引用(或者叫句柄)赋值给它,显然也没有创建对象;现在只剩下new String("abc")了。那么,new String("abc")为什么又能被看成"abc"和new String()呢?我们来看一下被我们调用了的String的构造器:
Java代码
public String(String original) {
//other code ...
}
大家都知道,我们常用的创建一个类的实例(对象)的方法有以下两种:
使用new创建对象。
调用Class类的newInstance方法,利用反射机制创建对象。
我们正是使用new调用了String类的上面那个构造器方法创建了一个对象,并将它的引用赋值给了str变量。同时我们注意到,被调用的构造器方法接受的参数也是一个String对象,这个对象正是"abc"。由此我们又要引入另外一种创建String对象的方式的讨论——引号内包含文本。
这种方式是String特有的,并且它与new的方式存在很大区别。
Java代码
String str="abc";
毫无疑问,这行代码创建了一个String对象。
Java代码
String a="abc";
String b="abc";
那这里呢?答案还是一个。
Java代码
String a="ab"+"cd";
再看看这里呢?答案仍是一个。有点奇怪吗?说到这里,我们就需要引入对字符串池相关知识的回顾了。
在JAVA虚拟机(JVM)中存在着一个字符串池,其中保存着很多String对象,并且可以被共享使用,因此它提高了效率。由于String类是final的,它的值一经创建就不可改变,因此我们不用担心String对象共享而带来程序的混乱。字符串池由String类维护,我们可以调用intern()方法来访问字符串池。
我们再回头看看String a="abc";,这行代码被执行的时候,JAVA虚拟机首先在字符串池中查找是否已经存在了值为"abc"的这么一个对象,它的判断依据是String类equals(Object obj)方法的返回值。如果有,则不再创建新的对象,直接返回已存在对象的引用;如果没有,则先创建这个对象,然后把它加入到字符串池中,再将它的引用返回。因此,我们不难理解前面三个例子中头两个例子为什么是这个答案了。
对于第三个例子:
Java代码
String a="ab"+"cd";
由于常量的值在编译的时候就被确定了。在这里,"ab"和"cd"都是常量,因此变量a的值在编译时就可以确定。这行代码编译后的效果等同于:
Java代码
String a="abcd";
因此这里只创建了一个对象"abcd",并且它被保存在字符串池里了。
现在问题又来了,是不是所有经过“+”连接后得到的字符串都会被添加到字符串池中呢?我们都知道“==”可以用来比较两个变量,它有以下两种情况:
如果比较的是两个基本类型(char,byte,short,int,long,float,double,boolean),则是判断它们的值是否相等。
如果表较的是两个对象变量,则是判断它们的引用是否指向同一个对象。
下面我们就用“==”来做几个测试。为了便于说明,我们把指向字符串池中已经存在的对象也视为该对象被加入了字符串池:
Java代码
public class StringTest { public static void main(String[] args) { String a = "ab";// 创建了一个对象,并加入字符串池中 System.out.println("String a = \"ab\";"); String b = "cd";// 创建了一个对象,并加入字符串池中 System.out.println("String b = \"cd\";"); String c = "abcd";// 创建了一个对象,并加入字符串池中 String d = "ab" + "cd"; // 如果d和c指向了同一个对象,则说明d也被加入了字符串池 if (d == c) { System.out.println("\"ab\"+\"cd\" 创建的对象 \"加入了\" 字符串池中"); } // 如果d和c没有指向了同一个对象,则说明d没有被加入字符串池 else { System.out.println("\"ab\"+\"cd\" 创建的对象 \"没加入\" 字符串池中"); } String e = a + "cd"; // 如果e和c指向了同一个对象,则说明e也被加入了字符串池 if (e == c) { System.out.println(" a +\"cd\" 创建的对象 \"加入了\" 字符串池中"); } // 如果e和c没有指向了同一个对象,则说明e没有被加入字符串池 else { System.out.println(" a +\"cd\" 创建的对象 \"没加入\" 字符串池中"); } String f = "ab" + b; // 如果f和c指向了同一个对象,则说明f也被加入了字符串池 if (f == c) { System.out.println("\"ab\"+ b 创建的对象 \"加入了\" 字符串池中"); } // 如果f和c没有指向了同一个对象,则说明f没有被加入字符串池 else { System.out.println("\"ab\"+ b 创建的对象 \"没加入\" 字符串池中"); } String g = a + b; // 如果g和c指向了同一个对象,则说明g也被加入了字符串池 if (g == c) { System.out.println(" a + b 创建的对象 \"加入了\" 字符串池中"); } // 如果g和c没有指向了同一个对象,则说明g没有被加入字符串池 else { System.out.println(" a + b 创建的对象 \"没加入\" 字符串池中"); } } }
- 上一篇:模拟打印机排队打印效果
- 下一篇:Java实现软件运行时启动信息窗口的方法