Heim  >  Artikel  >  Java  >  Einführung in Vererbung, Polymorphismus, Überladung und Umschreiben in Java

Einführung in Vererbung, Polymorphismus, Überladung und Umschreiben in Java

高洛峰
高洛峰Original
2017-01-19 13:54:101665Durchsuche

Was ist Polymorphismus? Was ist der Implementierungsmechanismus? Was ist der Unterschied zwischen Überladen und Umschreiben? Dies sind die vier sehr wichtigen Konzepte, die wir dieses Mal besprechen werden: Vererbung, Polymorphismus, Überladung und Überschreiben.

Vererbung

Einfach ausgedrückt basiert die Vererbung auf einem vorhandenen Typ, indem neue Methoden hinzugefügt oder vorhandene Methoden neu definiert werden (diese Methode wird als Überschreiben bezeichnet), um einen neuen Typ zu generieren . Vererbung ist eines der drei Grundmerkmale der Objektorientierung – Kapselung, Vererbung und Polymorphismus. Jede Klasse, die wir bei Verwendung von JAVA schreiben, ist eine Vererbung, da in der JAVA-Sprache die Klasse java.lang.Object die grundlegendste Basisklasse ist ( Wenn eine neu definierte Klasse, die wir definieren, nicht explizit angibt, von welcher Basisklasse sie erbt, erbt JAVA standardmäßig von der Object-Klasse.

Wir können Klassen in JAVA in die folgenden drei Typen unterteilen:

Klasse: eine Klasse, die mithilfe der Klasse definiert wird und keine abstrakten Methoden enthält.
Abstrakte Klasse: Eine mithilfe einer abstrakten Klasse definierte Klasse, die abstrakte Methoden enthalten kann oder nicht.
Schnittstelle: Über die Schnittstelle definierte Klasse.

Die folgenden Vererbungsregeln bestehen zwischen diesen drei Typen:

Klassen können Klassen erben (erweitern), abstrakte Klassen erben (erweitern) und Schnittstellen erben (implementieren).
Abstrakte Klassen können Klassen erben (erweitern), abstrakte Klassen erben (erweitern) und Schnittstellen erben (implementieren).
Schnittstellen können Schnittstellen nur erweitern.

Bitte beachten Sie, dass die verschiedenen Schlüsselwörter „extends“ und „implements“, die in den einzelnen Vererbungsfällen in den oben genannten drei Regeln verwendet werden, nicht beliebig ersetzt werden können. Wie wir alle wissen, muss eine gewöhnliche Klasse, nachdem sie eine Schnittstelle geerbt hat, alle in dieser Schnittstelle definierten Methoden implementieren, andernfalls kann sie nur als abstrakte Klasse definiert werden. Der Grund, warum ich hier nicht den Begriff „Implementierung“ für das Schlüsselwort „implementiert“ verwende, liegt darin, dass es konzeptionell auch eine Vererbungsbeziehung darstellt und im Fall der abstrakten Klasse „implementiert“ diese Schnittstellendefinition nicht unbedingt implementiert werden muss Methode, daher ist es sinnvoller, die Vererbung zu verwenden.

Die oben genannten drei Regeln erfüllen auch die folgenden Einschränkungen:

Sowohl Klassen als auch abstrakte Klassen können höchstens eine Klasse erben oder höchstens eine abstrakte Klasse erben, und diese beiden Situationen bedingen einander exklusiv. Das heißt, sie erben entweder eine Klasse oder eine abstrakte Klasse.
Klassen, abstrakte Klassen und Schnittstellen sind nicht durch die Anzahl der Schnittstellen eingeschränkt, die sie erben. Theoretisch können sie eine unbegrenzte Anzahl von Schnittstellen erben. Natürlich muss eine Klasse alle Methoden implementieren, die in allen von ihr geerbten Schnittstellen definiert sind.
Wenn eine abstrakte Klasse eine abstrakte Klasse erbt oder eine Schnittstelle implementiert, kann sie die abstrakte Methode der übergeordneten abstrakten Klasse oder die in der Schnittstelle der übergeordneten Klasse definierte Schnittstelle teilweise, vollständig oder vollständig nicht implementieren.
Wenn eine Klasse eine abstrakte Klasse erbt oder eine Schnittstelle implementiert, muss sie alle abstrakten Methoden der übergeordneten abstrakten Klasse oder alle in der Schnittstelle der übergeordneten Klasse definierten Schnittstellen implementieren.

Der Vorteil, den die Vererbung für unsere Programmierung mit sich bringt, ist die Wiederverwendung (Wiederverwendung) der ursprünglichen Klassen. Genau wie die Wiederverwendung von Modulen kann die Wiederverwendung von Klassen unsere Entwicklungseffizienz verbessern. Tatsächlich ist die Wiederverwendung von Modulen der überlagerte Effekt der Wiederverwendung einer großen Anzahl von Klassen. Zusätzlich zur Vererbung können wir auch Komposition verwenden, um Klassen wiederzuverwenden. Die sogenannte Kombination besteht darin, die ursprüngliche Klasse als Attribut der neuen Klasse zu definieren und eine Wiederverwendung zu erreichen, indem die Methoden der ursprünglichen Klasse in der neuen Klasse aufgerufen werden. Wenn keine Beziehung zwischen dem neu definierten Typ und dem ursprünglichen Typ besteht, d. h. ausgehend von einem abstrakten Konzept, gehören die durch den neu definierten Typ dargestellten Dinge nicht zu den Dingen, die durch den ursprünglichen Typ dargestellt werden, z. B. gelbe Menschen Es handelt sich um eine Art Mensch, und es besteht eine Beziehung zwischen ihnen, einschließlich Einbeziehung und Einbeziehung. Daher ist eine Kombination zu diesem Zeitpunkt die bessere Wahl, um eine Wiederverwendung zu erreichen. Das folgende Beispiel ist ein einfaches Beispiel für die Kombinationsmethode:

public class Sub { 
  private Parent p = new Parent(); 
  
  public void doSomething() { 
    // 复用Parent类的方法 
    p.method(); 
    // other code 
  } 
} 
  
class Parent { 
  public void method() { 
    // do something here 
  } 
}

Um den Code effizienter zu gestalten, können wir bei Bedarf natürlich auch den Originaltyp (z. B. Parent p) initialisieren Benutze es.

Die Verwendung von Vererbung und Kombination zur Wiederverwendung von Originalklassen ist ein inkrementelles Entwicklungsmodell. Der Vorteil dieser Methode besteht darin, dass der Originalcode nicht geändert werden muss und daher keine Auswirkungen auf den Originalcode hat. Aufgrund von Änderungen am Originalcode ist kein erneuter Test erforderlich, was für unsere Entwicklung offensichtlich von Vorteil ist. Wenn wir also ein ursprüngliches System oder Modul warten oder transformieren, insbesondere wenn wir kein umfassendes Verständnis davon haben, können wir das inkrementelle Entwicklungsmodell wählen, das nicht nur unsere Entwicklungseffizienz erheblich verbessern, sondern auch die dadurch verursachten Risiken vermeiden kann Änderungen am Originalcode.

Polymorphismus

Polymorphismus ist ein weiteres wichtiges Grundkonzept. Wie oben erwähnt, ist es eines der drei Grundmerkmale der Objektorientierung. Was genau ist Polymorphismus? Schauen wir uns zum besseren Verständnis das folgende Beispiel an:

//汽车接口 
interface Car { 
  // 汽车名称 
  String getName(); 
  
  // 获得汽车售价 
  int getPrice(); 
} 
  
// 宝马 
class BMW implements Car { 
  public String getName() { 
    return "BMW"; 
  } 
  
  public int getPrice() { 
    return 300000; 
  } 
} 
  
// 奇瑞QQ 
class CheryQQ implements Car { 
  public String getName() { 
    return "CheryQQ"; 
  } 
  
  public int getPrice() { 
    return 20000; 
  } 
} 
  
// 汽车出售店 
public class CarShop { 
  // 售车收入 
  private int money = 0; 
  
  // 卖出一部车 
  public void sellCar(Car car) { 
    System.out.println("车型:" + car.getName() + " 单价:" + car.getPrice()); 
    // 增加卖出车售价的收入 
    money += car.getPrice(); 
  } 
  
  // 售车总收入 
  public int getMoney() { 
    return money; 
  } 
  
  public static void main(String[] args) { 
    CarShop aShop = new CarShop(); 
    // 卖出一辆宝马 
    aShop.sellCar(new BMW()); 
    // 卖出一辆奇瑞QQ 
    aShop.sellCar(new CheryQQ()); 
    System.out.println("总收入:" + aShop.getMoney()); 
  } 
}

Laufergebnisse:

Modell: BMW Stückpreis: 300000
Modell: CheryQQ Stückpreis: 20000
Gesamtumsatz: 320.000

继承是多态得以实现的基础。从字面上理解,多态就是一种类型(都是Car类型)表现出多种状态(宝马汽车的名称是BMW,售价是300000;奇瑞汽车的名称是CheryQQ,售价是2000)。将一个方法调用同这个方法所属的主体(也就是对象或类)关联起来叫做绑定,分前期绑定和后期绑定两种。下面解释一下它们的定义:

前期绑定:在程序运行之前进行绑定,由编译器和连接程序实现,又叫做静态绑定。比如static方法和final方法,注意,这里也包括private方法,因为它是隐式final的。
后期绑定:在运行时根据对象的类型进行绑定,由方法调用机制实现,因此又叫做动态绑定,或者运行时绑定。除了前期绑定外的所有方法都属于后期绑定。

多态就是在后期绑定这种机制上实现的。多态给我们带来的好处是消除了类之间的耦合关系,使程序更容易扩展。比如在上例中,新增加一种类型汽车的销售,只需要让新定义的类继承Car类并实现它的所有方法,而无需对原有代码做任何修改,CarShop类的sellCar(Car car)方法就可以处理新的车型了。新增代码如下:

// 桑塔纳汽车 
class Santana implements Car { 
  public String getName() { 
    return "Santana"; 
  } 
  
  public int getPrice() { 
    return 80000; 
  } 
}

重载(overloading)和重写(overriding)

重载和重写都是针对方法的概念,在弄清楚这两个概念之前,我们先来了解一下什么叫方法的型构(英文名是signature,有的译作“签名”,虽然它被使用的较为广泛,但是这个翻译不准确的)。型构就是指方法的组成结构,具体包括方法的名称和参数,涵盖参数的数量、类型以及出现的顺序,但是不包括方法的返回值类型,访问权限修饰符,以及abstract、static、final等修饰符。比如下面两个就是具有相同型构的方法:

public void method(int i, String s) { 
  // do something 
} 
  
public String method(int i, String s) { 
  // do something 
}

而这两个就是具有不同型构的方法: 

public void method(int i, String s) { 
  // do something 
} 
  
public void method(String s, int i) { 
  // do something 
}

了解完型构的概念后我们再来看看重载和重写,请看它们的定义:

重写,英文名是overriding,是指在继承情况下,子类中定义了与其基类中方法具有相同型构的新方法,就叫做子类把基类的方法重写了。这是实现多态必须的步骤。
重载,英文名是overloading,是指在同一个类中定义了一个以上具有相同名称,但是型构不同的方法。在同一个类中,是不允许定义多于一个的具有相同型构的方法的。

我们来考虑一个有趣的问题:构造器可以被重载吗?答案当然是可以的,我们在实际的编程中也经常这么做。实际上构造器也是一个方法,构造器名就是方法名,构造器参数就是方法参数,而它的返回值就是新创建的类的实例。但是构造器却不可以被子类重写,因为子类无法定义与基类具有相同型构的构造器。

重载、覆盖、多态与函数隐藏

经常看到C++的一些初学者对于重载、覆盖、多态与函数隐藏的模糊理解。在这里写一点自己的见解,希望能够C++初学者解惑。

要弄清楚重载、覆盖、多态与函数隐藏之间的复杂且微妙关系之前,我们首先要来回顾一下重载覆盖等基本概念。

首先,我们来看一个非常简单的例子,理解一下什么叫函数隐藏hide。

#include <iostream>
using namespace std;
class Base{
public:
  void fun() { cout << "Base::fun()" << endl; }
};
class Derive : public Base{
public:
  void fun(int i) { cout << "Derive::fun()" << endl; }
};
int main()
{
  Derive d;
     //下面一句错误,故屏蔽掉
  //d.fun();error C2660: &#39;fun&#39; : function does not take 0 parameters
  d.fun(1);
     Derive *pd =new Derive();
     //下面一句错误,故屏蔽掉
     //pd->fun();error C2660: &#39;fun&#39; : function does not take 0 parameters
     pd->fun(1);
     delete pd;
  return 0;
}

/*在不同的非命名空间作用域里的函数不构成重载,子类和父类是不同的两个作用域。
在本例中,两个函数在不同作用域中,故不够成重载,除非这个作用域是命名空间作用域。*/
在这个例子中,函数不是重载overload,也不是覆盖override,而是隐藏hide。

接下来的5个例子具体说明一下什么叫隐藏

例1

#include <iostream>
using namespace std;
class Basic{
public:
     void fun(){cout << "Base::fun()" << endl;}//overload
     void fun(int i){cout << "Base::fun(int i)" << endl;}//overload
};
class Derive :public Basic{
public:
     void fun2(){cout << "Derive::fun2()" << endl;}
};
int main()
{
     Derive d;
     d.fun();//正确,派生类没有与基类同名函数声明,则基类中的所有同名重载函数都会作为候选函数。
     d.fun(1);//正确,派生类没有与基类同名函数声明,则基类中的所有同名重载函数都会作为候选函数。
     return 0;
}

例2

#include <iostream>
using namespace std;
class Basic{
public:
     void fun(){cout << "Base::fun()" << endl;}//overload
     void fun(int i){cout << "Base::fun(int i)" << endl;}//overload
};
class Derive :public Basic{
public:
     //新的函数版本,基类所有的重载版本都被屏蔽,在这里,我们称之为函数隐藏hide
  //派生类中有基类的同名函数的声明,则基类中的同名函数不会作为候选函数,即使基类有不同的参数表的多个版本的重载函数。
     void fun(int i,int j){cout << "Derive::fun(int i,int j)" << endl;}
     void fun2(){cout << "Derive::fun2()" << endl;}
};
int main()
{
     Derive d;
     d.fun(1,2);
     //下面一句错误,故屏蔽掉
     //d.fun();error C2660: &#39;fun&#39; : function does not take 0 parameters
     return 0;
}

例3

#include <iostream>
using namespace std;
class Basic{
public:
     void fun(){cout << "Base::fun()" << endl;}//overload
     void fun(int i){cout << "Base::fun(int i)" << endl;}//overload
};
class Derive :public Basic{
public:
     //覆盖override基类的其中一个函数版本,同样基类所有的重载版本都被隐藏hide
  //派生类中有基类的同名函数的声明,则基类中的同名函数不会作为候选函数,即使基类有不同的参数表的多个版本的重载函数。
     void fun(){cout << "Derive::fun()" << endl;}
     void fun2(){cout << "Derive::fun2()" << endl;}
};
int main()
{
     Derive d;
     d.fun();
     //下面一句错误,故屏蔽掉
     //d.fun(1);error C2660: &#39;fun&#39; : function does not take 1 parameters
     return 0;
}

例4

#include <iostream>
using namespace std;
class Basic{
public:
     void fun(){cout << "Base::fun()" << endl;}//overload
     void fun(int i){cout << "Base::fun(int i)" << endl;}//overload
};
class Derive :public Basic{
public:
     using Basic::fun;
     void fun(){cout << "Derive::fun()" << endl;}
     void fun2(){cout << "Derive::fun2()" << endl;}
};
int main()
{
     Derive d;
     d.fun();//正确
     d.fun(1);//正确
     return 0;
}
/*
输出结果
Derive::fun()
Base::fun(int i)
Press any key to continue
*/

例5

#include <iostream>
using namespace std;
class Basic{
public:
     void fun(){cout << "Base::fun()" << endl;}//overload
     void fun(int i){cout << "Base::fun(int i)" << endl;}//overload
};
class Derive :public Basic{
public:
     using Basic::fun;
     void fun(int i,int j){cout << "Derive::fun(int i,int j)" << endl;}
     void fun2(){cout << "Derive::fun2()" << endl;}
};
int main()
{
     Derive d;
     d.fun();//正确
     d.fun(1);//正确
     d.fun(1,2);//正确
     return 0;
}
/*
输出结果
Base::fun()
Base::fun(int i)
Derive::fun(int i,int j)
Press any key to continue
*/

好了,我们先来一个小小的总结重载与覆盖两者之间的特征

重载overload的特征:

n          相同的范围(在同一个类中);
n          函数名相同参数不同;
n          virtual 关键字可有可无。

覆盖override是指派生类函数覆盖基类函数,覆盖的特征是:

n          不同的范围(分别位于派生类与基类);
n          函数名和参数都相同;
n          基类函数必须有virtual 关键字。(若没有virtual 关键字则称之为隐藏hide)

如果基类有某个函数的多个重载(overload)版本,而你在派生类中重写(override)了基类中的一个或多个函数版本,或是在派生类中重新添加了新的函数版本(函数名相同,参数不同),则所有基类的重载版本都被屏蔽,在这里我们称之为隐藏hide。所以,在一般情况下,你想在派生类中使用新的函数版本又想使用基类的函数版本时,你应该在派生类中重写基类中的所有重载版本。你若是不想重写基类的重载的函数版本,则你应该使用例4或例5方式,显式声明基类名字空间作用域。
事实上,C++编译器认为,相同函数名不同参数的函数之间根本没有什么关系,它们根本就是两个毫不相关的函数。只是C++语言为了模拟现实世界,为了让程序员更直观的思维处理现实世界中的问题,才引入了重载和覆盖的概念。重载是在相同名字空间作用域下,而覆盖则是在不同的名字空间作用域下,比如基类和派生类即为两个不同的名字空间作用域。在继承过程中,若发生派生类与基类函数同名问题时,便会发生基类函数的隐藏。当然,这里讨论的情况是基类函数前面没有virtual 关键字。在有virtual 关键字关键字时的情形我们另做讨论。
继承类重写了基类的某一函数版本,以产生自己功能的接口。此时C++编绎器认为,你现在既然要使用派生类的自己重新改写的接口,那我基类的接口就不提供给你了(当然你可以用显式声明名字空间作用域的方法,见[C++基础]重载、覆盖、多态与函数隐藏(1))。而不会理会你基类的接口是有重载特性的。若是你要在派生类里继续保持重载的特性,那你就自己再给出接口重载的特性吧。所以在派生类里,只要函数名一样,基类的函数版本就会被无情地屏蔽。在编绎器中,屏蔽是通过名字空间作用域实现的。

所以,在派生类中要保持基类的函数重载版本,就应该重写所有基类的重载版本。重载只在当前类中有效,继承会失去函数重载的特性。也就是说,要把基类的重载函数放在继承的派生类里,就必须重写。

这里“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,具体规则我们也来做一小结:

n          如果派生类的函数与基类的函数同名,但是参数不同。此时,若基类无virtual关键字,基类的函数将被隐藏。(注意别与重载混淆,虽然函数名相同参数不同应称之为重载,但这里不能理解为重载,因为派生类和基类不在同一名字空间作用域内。这里理解为隐藏)
n          如果派生类的函数与基类的函数同名,但是参数不同。此时,若基类有virtual关键字,基类的函数将被隐式继承到派生类的vtable中。此时派生类vtable中的函数指向基类版本的函数地址。同时这个新的函数版本添加到派生类中,作为派生类的重载版本。但在基类指针实现多态调用函数方法时,这个新的派生类函数版本将会被隐藏。
n          如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏。(注意别与覆盖混淆,这里理解为隐藏)。
n          如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数有virtual关键字。此时,基类的函数不会被“隐藏”。(在这里,你要理解为覆盖哦^_^)。

插曲:基类函数前没有virtual关键字时,我们要重写更为顺口些,在有virtual关键字时,我们叫覆盖更为合理些,戒此,我也希望大家能够更好的理解C++中一些微妙的东西。费话少说,我们举例说明吧。

例6

#include <iostream>
using namespace std;
  
class Base{
public:
     virtual void fun() { cout << "Base::fun()" << endl; }//overload
  virtual void fun(int i) { cout << "Base::fun(int i)" << endl; }//overload
};
  
class Derive : public Base{
public:
     void fun() { cout << "Derive::fun()" << endl; }//override
  void fun(int i) { cout << "Derive::fun(int i)" << endl; }//override
     void fun(int i,int j){ cout<< "Derive::fun(int i,int j)" <<endl;}//overload
};
  
int main()
{
 Base *pb = new Derive();
 pb->fun();
 pb->fun(1);
 //下面一句错误,故屏蔽掉
 //pb->fun(1,2);virtual函数不能进行overload,error C2661: &#39;fun&#39; : no overloaded function takes 2 parameters
  
 cout << endl;
 Derive *pd = new Derive();
 pd->fun();
 pd->fun(1);
 pd->fun(1,2);//overload
  
 delete pb;
 delete pd;
 return 0;
}
/*

输出结果

 
Derive::fun()
Derive::fun(int i)
 
Derive::fun()
Derive::fun(int i)
Derive::fun(int i,int j)
Press any key to continue
*/

例7-1

#include <iostream> 
using namespace std;
  
class Base{
public:
     virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl; }
};
  
class Derive : public Base{};
  
int main()
{
     Base *pb = new Derive();
     pb->fun(1);//Base::fun(int i)
     delete pb;
     return 0;
}

例7-2

#include <iostream>
using namespace std;
  
class Base{
public:
     virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl; }
};
  
class Derive : public Base{
public:
  void fun(double d){ cout <<"Derive::fun(double d)"<< endl; }
};
  
int main()
{
     Base *pb = new Derive();
     pb->fun(1);//Base::fun(int i)
     pb->fun((double)0.01);//Base::fun(int i)
     delete pb;
     return 0;
}

  

例8-1

#include <iostream>
using namespace std;
  
class Base{
public:
     virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl; }
};
  
class Derive : public Base{
public:
     void fun(int i){ cout <<"Derive::fun(int i)"<< endl; }
};
  
int main()
{
     Base *pb = new Derive();
     pb->fun(1);//Derive::fun(int i)
     delete pb;
     return 0;
}

例8-2

#include <iostream>
using namespace std;
  
class Base{
public:
     virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl; }
};
  
class Derive : public Base{
public:
     void fun(int i){ cout <<"Derive::fun(int i)"<< endl; }
     void fun(double d){ cout <<"Derive::fun(double d)"<< endl; }    
};
  
int main()
{
     Base *pb = new Derive();
     pb->fun(1);//Derive::fun(int i)
     pb->fun((double)0.01);//Derive::fun(int i)
     delete pb;
     return 0;
}

     

例9

#include <iostream>
using namespace std;
  
class Base{
public:
     virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl; }
 
};
class Derive : public Base{
public:
    void fun(int i){ cout <<"Derive::fun(int i)"<< endl; }
    void fun(char c){ cout <<"Derive::fun(char c)"<< endl; }
    void fun(double d){ cout <<"Derive::fun(double d)"<< endl; }    
};
int main()
{
     Base *pb = new Derive();
     pb->fun(1);//Derive::fun(int i)
     pb->fun(&#39;a&#39;);//Derive::fun(int i)
     pb->fun((double)0.01);//Derive::fun(int i)
  
     Derive *pd =new Derive();
     pd->fun(1);//Derive::fun(int i)
     //overload
     pd->fun(&#39;a&#39;);//Derive::fun(char c)    
     //overload
     pd->fun(0.01);//Derive::fun(double d)    
  
     delete pb;
     delete pd;
     return 0;
}

 

例7-1和例8-1很好理解,我把这两个例子放在这里,是让大家作一个比较摆了,也是为了帮助大家更好的理解:
n          例7-1中,派生类没有覆盖基类的虚函数,此时派生类的vtable中的函数指针指向的地址就是基类的虚函数地址。
n          例8-1中,派生类覆盖了基类的虚函数,此时派生类的vtable中的函数指针指向的地址就是派生类自己的重写的虚函数地址。
在例7-2和8-2看起来有点怪怪,其实,你按照上面的原则对比一下,答案也是明朗的:
n          例7-2中,我们为派生类重载了一个函数版本:void fun(double d)  其实,这只是一个障眼法。我们具体来分析一下,基类共有几个函数,派生类共有几个函数:
类型
 基类
 派生类
 
Vtable部分
 void fun(int i)
 指向基类版的虚函数void fun(int i)
 
静态部分
  
 void fun(double d)
 

 
我们再来分析一下以下三句代码
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: &#39;fun&#39; : ambiguous call to overloaded function
     delete pb;
     return 0;
}

好了,我们再来分析一下例8-2。

n          例8-2中,我们也为派生类重载了一个函数版本:void fun(double d)  ,同时覆盖了基类的虚函数,我们再来具体来分析一下,基类共有几个函数,派生类共有几个函数:

Typ
Basisklasse
Abgeleitete Klasse

Vtable-Teil
void fun(int i)
void fun(int i)

Statischer Teil

void fun(double d)
Aus der Tabelle können wir ersehen, dass der Funktionszeiger in der vtable der abgeleiteten Klasse auf seine eigene umgeschriebene virtuelle Funktionsadresse zeigt.

Lassen Sie uns die folgenden drei Codezeilen analysieren

Base *pb = new Derive();
pb->fun(1);//Derive::fun(int i )
pb->fun((double)0.01);//Derive::fun(int i)

Mehr muss man zum ersten Satz nicht sagen Es ist natürlich, die virtuelle Funktion der abgeleiteten Klasse aufzurufen. Der dritte Satz fühlt sich tatsächlich wieder seltsam an. Beim Ausführen bin ich in die vtable-Tabelle der abgeleiteten Klasse eingetaucht Habe es mir angesehen. Verdammt, ich kann wirklich nicht herausfinden, welche Version ich haben möchte. Haha dass meine Sehkraft so alt ist, dass ihre Augen nur den Nicht-Vtable-Teil (d. h. den statischen Teil) und den Vtable-Teil sehen können d) der abgeleiteten Klasse ist so weit entfernt, dass man sie nicht sehen kann! Außerdem muss sich die abgeleitete Klasse nicht um alles kümmern. Hey, hör auf zu streiten , jeder kümmert sich um seine eigenen Angelegenheiten^_^

Leider! Basisklassenzeiger können polymorphe Aufrufe durchführen, aber abgeleitete Klassen können niemals durchgeführt werden (siehe Beispiel 6)~~~

Schauen wir uns noch einmal Beispiel 9 an
Die Wirkung dieses Beispiels ist die gleiche wie bei Beispiel 6, mit dem gleichen Ansatz, aber dem gleichen Ziel. Ich glaube, nachdem Sie die obigen Beispiele verstanden haben, ist dies auch ein kleiner Kuss.
Zusammenfassung:

Durch Überladen wird die aufzurufende Funktionsversion basierend auf der Parameterliste der Funktion ausgewählt, während durch Polymorphismus die aufzurufende virtuelle Funktionsversion basierend auf dem tatsächlichen Typ des Laufzeitobjekts ausgewählt wird Der Status wird erreicht, indem die virtuelle virtuelle Funktion der Basisklasse durch die abgeleitete Klasse überschrieben wird. Wenn die abgeleitete Klasse die virtuelle virtuelle Funktion der Basisklasse nicht überschreibt, erbt die abgeleitete Klasse automatisch die virtuelle virtuelle Funktionsversion der Basisklasse Unabhängig davon, ob das Objekt, auf das der Basisklassenzeiger zeigt, ein Basistyp oder ein abgeleiteter Typ ist, wird die virtuelle virtuelle Funktion der Basisklassenversion aufgerufen Die Basisklasse wird zur Laufzeit entsprechend dem Objekttyp aufgerufen. Der tatsächliche Typ wird verwendet, um die aufzurufende virtuelle Funktionsversion auszuwählen. Wenn der Objekttyp, auf den der Basisklassenzeiger zeigt, beispielsweise ein abgeleiteter Typ ist, wird der tatsächliche Typ verwendet Die virtuelle Funktionsversion der abgeleiteten Klasse wird aufgerufen, wodurch Polymorphismus erreicht wird.

Die ursprüngliche Absicht der Verwendung von Polymorphismus besteht darin, die Funktion in der Basisklasse als virtuell zu deklarieren und die virtuelle virtuelle Funktionsversion der Basisklasse in der abgeleiteten Klasse zu überschreiben. Beachten Sie, dass der Funktionsprototyp und die Basisklasse bei Behalten Sie diesmal die Konsistenz bei, d. h. den gleichen Namen und Parametertyp. Wenn Sie der abgeleiteten Klasse eine neue Funktionsversion hinzufügen, können Sie die neue Funktionsversion der abgeleiteten Klasse nicht dynamisch über den Basisklassenzeiger aufrufen dient nur als überladene Version der abgeleiteten Klasse. Immer noch derselbe Satz: Überladung ist nur in der aktuellen Klasse gültig. Unabhängig davon, ob Sie sie in einer Basisklasse oder einer abgeleiteten Klasse überladen, stehen beide in keinem Zusammenhang. Wenn wir dies verstehen, können wir auch die Ausgabeergebnisse in den Beispielen 6 und 9 erfolgreich verstehen.

Überladung ist statisch verknüpft und Polymorphismus ist dynamisch verknüpft. Um es weiter zu erklären: Überladung hat nichts mit der Art des Objekts zu tun, auf das der Zeiger tatsächlich zeigt, und Polymorphismus hängt mit der Art des Objekts zusammen, auf das der Zeiger tatsächlich zeigt. Wenn ein Basisklassenzeiger eine überladene Version einer abgeleiteten Klasse aufruft, betrachtet der C++-Compiler dies als illegal. Der C++-Compiler geht nur davon aus, dass der Basisklassenzeiger nur die überladene Version der Basisklasse aufrufen kann und die Überladung nur im Namespace funktioniert Wenn der Basisklassenzeiger zu diesem Zeitpunkt eine virtuelle Funktion aufruft, wählt er natürlich auch dynamisch die virtuelle virtuelle Funktionsversion der Basisklasse aus Die virtuelle Funktionsversion der abgeleiteten Klasse wird durch den Objekttyp bestimmt, auf den der Basisklassenzeiger tatsächlich zeigt. Überladung hat also nichts mit dem Objekttyp zu tun, auf den der Zeiger tatsächlich zeigt, und auch nicht mit dem Polymorphismus hängt mit der Art des Objekts zusammen, auf das der Zeiger tatsächlich zeigt.

Abschließend sei noch darauf hingewiesen, dass virtuelle virtuelle Funktionen auch überladen werden können, die Überladung kann jedoch nur im Rahmen des aktuellen Namespace gültig sein.

Wie viele String-Objekte wurden erstellt?
Schauen wir uns zunächst einen Code an:
Java-Code
String str=new String("abc");
Was diesem Code folgt, ist oft die Frage, was genau das ist Codezeile? Wie viele String-Objekte wurden erstellt? Ich glaube, jeder kennt diese Frage und die Antwort ist bekannt: 2. Als Nächstes beginnen wir mit dieser Frage und überprüfen einige JAVA-Kenntnisse im Zusammenhang mit der Erstellung von String-Objekten.
我们可以把上面这行代码分成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  创建的对象 \"没加入\" 字符串池中");
    }
  }
}

运行结果如下: 
String a = "ab";
String b = "cd";
"ab"+"cd" 创建的对象 "加入了" 字符串池中
a  +"cd" 创建的对象 "没加入" 字符串池中
"ab"+ b   创建的对象 "没加入" 字符串池中
a  + b   创建的对象 "没加入" 字符串池中
从上面的结果中我们不难看出,只有使用引号包含文本的方式创建的String对象之间使用“+”连接产生的新对象才会被加入字符串池中。对于所有包含new方式新建对象(包括null)的“+”连接表达式,它所产生的新对象都不会被加入字符串池中,对此我们不再赘述。 
但是有一种情况需要引起我们的注意。请看下面的代码: 
Java代码 

public class StringStaticTest {
  // 常量A
  public static final String A = "ab";
  
  // 常量B
  public static final String B = "cd";
  
  public static void main(String[] args) {
    // 将两个常量用+连接对s进行初始化
    String s = A + B;
    String t = "abcd";
    if (s == t) {
      System.out.println("s等于t,它们是同一个对象");
    } else {
      System.out.println("s不等于t,它们不是同一个对象");
    }
  }
}

这段代码的运行结果如下: 
s等于t,它们是同一个对象
这又是为什么呢?原因是这样的,对于常量来讲,它的值是固定的,因此在编译期就能被确定了,而变量的值只有到运行时才能被确定,因为这个变量可以被不同的方法调用,从而可能引起值的改变。在上面的例子中,A和B都是常量,值是固定的,因此s的值也是固定的,它在类被编译时就已经确定了。也就是说: 
Java代码 
String s=A+B;  
等同于: 
Java代码 
String s="ab"+"cd";  
我对上面的例子稍加改变看看会出现什么情况: 
Java代码 

public class StringStaticTest {
  // 常量A
  public static final String A;
  
  // 常量B
  public static final String B;
  
  static {
    A = "ab";
    B = "cd";
  }
  
  public static void main(String[] args) {
    // 将两个常量用+连接对s进行初始化
    String s = A + B;
    String t = "abcd";
    if (s == t) {
      System.out.println("s等于t,它们是同一个对象");
    } else {
      System.out.println("s不等于t,它们不是同一个对象");
    }
  }
}

 

它的运行结果是这样: 
s不等于t,它们不是同一个对象
只是做了一点改动,结果就和刚刚的例子恰好相反。我们再来分析一下。A和B虽然被定义为常量(只能被赋值一次),但是它们都没有马上被赋值。在运算出s的值之前,他们何时被赋值,以及被赋予什么样的值,都是个变数。因此A和B在被赋值之前,性质类似于一个变量。那么s就不能在编译期被确定,而只能在运行时被创建了。 
由于字符串池中对象的共享能够带来效率的提高,因此我们提倡大家用引号包含文本的方式来创建String对象,实际上这也是我们在编程中常采用的。 
接下来我们再来看看intern()方法,它的定义如下: 
Java代码 
public native String intern();  
这是一个本地方法。在调用这个方法时,JAVA虚拟机首先检查字符串池中是否已经存在与该对象值相等对象存在,如果有则返回字符串池中对象的引用;如果没有,则先在字符串池中创建一个相同值的String对象,然后再将它的引用返回。 
我们来看这段代码: 
Java代码 

public class StringInternTest {
  public static void main(String[] args) {
    // 使用char数组来初始化a,避免在a被创建之前字符串池中已经存在了值为"abcd"的对象
    String a = new String(new char[] { &#39;a&#39;, &#39;b&#39;, &#39;c&#39;, &#39;d&#39; });
    String b = a.intern();
    if (b == a) {
      System.out.println("b被加入了字符串池中,没有新建对象");
    } else {
      System.out.println("b没被加入字符串池中,新建了对象");
    }
  }
}

运行结果: 
b没被加入字符串池中,新建了对象
如果String类的intern()方法在没有找到相同值的对象时,是把当前对象加入字符串池中,然后返回它的引用的话,那么b和a指向的就是同一个对象;否则b指向的对象就是JAVA虚拟机在字符串池中新建的,只是它的值与a相同罢了。上面这段代码的运行结果恰恰印证了这一点。 
最后我们再来说说String对象在JAVA虚拟机(JVM)中的存储,以及字符串池与堆(heap)和栈(stack)的关系。我们首先回顾一下堆和栈的区别: 
栈(stack):主要保存基本类型(或者叫内置类型)(char、byte、short、int、long、float、double、boolean)和对象的引用,数据可以共享,速度仅次于寄存器(register),快于堆。
堆(heap):用于存储对象。
我们查看String类的源码就会发现,它有一个value属性,保存着String对象的值,类型是char[],这也正说明了字符串就是字符的序列。 
当执行String a="abc";时,JAVA虚拟机会在栈中创建三个char型的值'a'、'b'和'c',然后在堆中创建一个String对象,它的值(value)是刚才在栈中创建的三个char型值组成的数组{'a','b','c'},最后这个新创建的String对象会被添加到字符串池中。如果我们接着执行String b=new String("abc");代码,由于"abc"已经被创建并保存于字符串池中,因此JAVA虚拟机只会在堆中新创建一个String对象,但是它的值(value)是共享前一行代码执行时在栈中创建的三个char型值值'a'、'b'和'c'。 
说到这里,我们对于篇首提出的String str=new String("abc")为什么是创建了两个对象这个问题就已经相当明了了。

更多Java中继承、多态、重载和重写介绍相关文章请关注PHP中文网!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn