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.
Nachlass
Einfach ausgedrückt besteht die Vererbung darin, einen neuen Typ basierend auf einem vorhandenen Typ zu generieren, indem neue Methoden hinzugefügt oder vorhandene Methoden neu definiert werden (wie unten erläutert, wird diese Methode als Umschreiben bezeichnet). 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 einteilen:
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: Eine Klasse, die mithilfe der Schnittstelle definiert wird.
Zwischen diesen drei Typen gelten die folgenden Vererbungsregeln:
Klassen können Klassen erweitern, Klassen abstrahieren und Schnittstellen implementieren.
Abstrakte Klassen können Klassen erben (erweitern), sie können abstrakte Klassen erben (erweitern) und sie können 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 außerdem die folgenden Einschränkungen:
Sowohl Klassen als auch abstrakte Klassen können höchstens eine Klasse oder höchstens eine abstrakte Klasse erben. Diese beiden Situationen schließen sich gegenseitig aus, dh sie erben entweder eine Klasse oder eine abstrakte Klasse.
Wenn Klassen, abstrakte Klassen und Schnittstellen Schnittstellen erben, sind sie nicht durch die Anzahl beschränkt. 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 es sein, dass sie die abstrakten Methoden der übergeordneten abstrakten Klasse oder die in der Schnittstelle der übergeordneten Klasse definierten Schnittstellen teilweise, vollständig oder vollständig nicht implementiert.
Wenn eine Klasse eine abstrakte Klasse erbt oder eine Schnittstelle implementiert, muss sie alle abstrakten Methoden der übergeordneten abstrakten Klasse oder alle in der übergeordneten Klassenschnittstelle 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 zwischen ihnen besteht eine Beziehung, einschließlich Einbeziehung und Einbeziehung. Daher ist die Kombination derzeit die bessere Wahl, um eine Wiederverwendung zu erreichen. Das folgende Beispiel ist ein einfaches Beispiel für die Kombination:
public class Sub { private Parent p = new Parent(); public void doSomething() { // Wiederverwendung der Methode p.method() der Parent-Klasse // anderer Code } } class Parent { public void method() { / / hier etwas machen } }
Um den Code effizienter zu gestalten, können wir ihn natürlich auch initialisieren, wenn wir den Originaltyp verwenden müssen (z. B. Parent p).
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 neuen Fehler entstehen Aufgrund von Änderungen am Originalcode wird der Test erneut durchgeführt, was offensichtlich für unsere Entwicklung von Vorteil ist. Wenn wir also ein ursprüngliches System oder Modul warten oder umwandeln, 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 zunächst das folgende Beispiel an:
//Auto-Schnittstellenschnittstelle Car { // Autoname String getName(); // BMW-Klasse BMW implementiert Car { public String getName() { return "BMW"; getPrice() { return 300000; } } // CheryQQ-Klasse CheryQQ implementiert Car { public String getName() { return "CheryQQ"} public int getPrice(); { return 20000; } } // Autoverkaufsshop public class CarShop { // Autoverkaufseinkommen private int money = 0; // Auto verkaufen public void sellCar(Car car) { System.out.println("Automodell: " + car.getName() + " Stückpreis: " + car.getPrice()); // Erhöhen Sie die Einnahmen aus Autoverkäufen. += car.getPrice(} // Gesamteinnahmen aus Autoverkäufen public int getMoney() { return money; } public static void main(String[] args) { CarShop aShop = new CarShop(); // Verkaufe einen BMW aShop.sellCar(new BMW()); .sellCar(new CheryQQ()); System.out.println("Gesamtumsatz: " + aShop.getMoney());
Laufergebnisse:
Modell: BMW Stückpreis: 300.000
Modell: CheryQQ Stückpreis: 20.000
Gesamtumsatz: 320.000
Vererbung ist die Grundlage für die Realisierung von Polymorphismus. Im wörtlichen Sinne ist Polymorphismus ein Typ (beide Autotypen), der mehrere Zustände aufweist (der Name von BMW ist BMW und der Preis beträgt 300.000; der Name von Chery ist CheryQQ und der Preis beträgt 2.000). Das Verknüpfen eines Methodenaufrufs mit dem Subjekt (d. h. dem Objekt oder der Klasse), zu dem die Methode gehört, wird als Bindung bezeichnet und in zwei Typen unterteilt: frühe Bindung und späte Bindung. Ihre Definitionen werden im Folgenden erläutert:
Frühe Bindung: Bindung vor der Programmausführung, implementiert durch Compiler und Linker, auch statische Bindung genannt. Zum Beispiel statische Methoden und endgültige Methoden. Beachten Sie, dass hier auch private Methoden enthalten sind, da sie implizit endgültig sind.
Späte Bindung: Bindung entsprechend dem Objekttyp zur Laufzeit, implementiert durch den Methodenaufrufmechanismus, daher wird sie auch als dynamische Bindung oder Laufzeitbindung bezeichnet. Alle Methoden außer der frühen Bindung sind späte Bindung.
Polymorphismus basiert auf dem Mechanismus der späten Bindung. Der Vorteil des Polymorphismus besteht darin, dass er die Kopplungsbeziehung zwischen Klassen eliminiert und die Erweiterung des Programms erleichtert. Um beispielsweise im obigen Beispiel einen neuen Autotyp zum Verkauf hinzuzufügen, müssen Sie die neu definierte Klasse nur die Klasse „Car“ erben lassen und alle ihre Methoden implementieren, ohne Änderungen am ursprünglichen Code „SellCar(Car)“ vorzunehmen ) der CarShop-Klassenmethode kann mit neuen Automodellen umgehen. Der neue Code lautet wie folgt:
// Santana Car class Santana implementiert Car { public String getName() { return "Santana" } public int getPrice() { return 80000;
Überladen und Überschreiben
Überladen und Umschreiben sind beide Konzepte für Methoden. Bevor wir diese beiden Konzepte klären, wollen wir zunächst die Struktur der Methode verstehen (der englische Name lautet „Signatur“, einige werden mit „Signatur“ übersetzt, obwohl er allgemeiner verwendet wird, diese Übersetzung jedoch nicht). genau). Struktur bezieht sich auf die Zusammensetzungsstruktur einer Methode, insbesondere einschließlich des Namens und der Parameter der Methode, einschließlich der Anzahl, Art und Reihenfolge des Auftretens der Parameter, jedoch nicht des Rückgabewerttyps der Methode, Zugriffsmodifikatoren und Modifikationen wie abstraktes, statisches und endgültiges Symbol. Beispielsweise haben die folgenden beiden Methoden die gleiche Struktur:
public void method(int i, String s) { // etwas tun } public String method(int i, String s) { // etwas tun }
Bei diesen beiden handelt es sich um Methoden mit unterschiedlichen Konfigurationen:
public void method(int i, String s) { // etwas tun } public void method(String s, int i) { // etwas tun }
Nachdem wir das Konzept der Gestalt verstanden haben, werfen wir einen Blick auf das Überladen und Umschreiben. Bitte schauen Sie sich ihre Definitionen an:
Überschreiben, der englische Name ist überschreibend, bedeutet, dass im Fall der Vererbung eine neue Methode mit derselben Struktur wie die Methode in der Basisklasse in der Unterklasse definiert wird, die als Unterklasse bezeichnet wird, die die Methode der Basisklasse überschreibt. Dies ist ein notwendiger Schritt, um Polymorphismus zu erreichen.
Overloading, der englische Name ist Overloading, bezieht sich auf die Definition von mehr als einer Methode mit demselben Namen, aber unterschiedlichen Strukturen in derselben Klasse. In derselben Klasse ist es nicht erlaubt, mehr als eine Methode mit demselben Typ zu definieren.
Betrachten wir eine interessante Frage: Können Konstruktoren überladen werden? Die Antwort lautet natürlich: Ja, wir machen das oft in der tatsächlichen Programmierung. Tatsächlich ist der Konstruktor auch eine Methode. Der Konstruktorname ist der Methodenname, die Konstruktorparameter sind die Methodenparameter und sein Rückgabewert ist eine Instanz der neu erstellten Klasse. Der Konstruktor kann jedoch nicht von einer Unterklasse überschrieben werden, da eine Unterklasse keinen Konstruktor mit demselben Typ wie die Basisklasse definieren kann.
Überladen, Überschreiben, Polymorphismus und Funktionsverstecken
Man sieht oft, dass einige C++-Anfänger ein vages Verständnis von Überladung, Überschreiben, Polymorphismus und Funktionsverstecken haben. Ich werde hier einige meiner eigenen Meinungen schreiben, in der Hoffnung, C++-Anfängern dabei zu helfen, ihre Zweifel auszuräumen.
Bevor wir die komplexe und subtile Beziehung zwischen Überladung, Überschreiben, Polymorphismus und Funktionsverstecken verstehen, müssen wir zunächst grundlegende Konzepte wie Überladung und Abdeckung überprüfen.
Schauen wir uns zunächst ein sehr einfaches Beispiel an, um zu verstehen, was das Verstecken von Funktionen ist.
#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; //Der folgende Satz ist falsch, daher ist er blockiert //d.fun();error C2660 : 'fun': Funktion akzeptiert keine 0 Parameter d.fun(1); Derive *pd =new Derive(); //Der folgende Satz ist falsch, daher ist er blockiert //pd->fun();error C2660: 'fun': Funktion benötigt keine 0 Parameter pd->fun(1); delete pd;}
/*Funktionen in verschiedenen Nicht-Namespace-Bereichen stellen keine Überladung dar. Unterklassen und übergeordnete Klassen sind zwei verschiedene Bereiche.
In diesem Beispiel befinden sich die beiden Funktionen in unterschiedlichen Bereichen, sodass sie nicht überlastet werden, es sei denn, der Bereich ist ein Namespace-Bereich. */
In diesem Beispiel wird die Funktion nicht überladen oder überschrieben, sondern ausgeblendet.
Die nächsten fünf Beispiele erklären konkret, was Verstecken ist.
Beispiel 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();//Richtig, die abgeleitete Klasse hat keine Funktionsdeklaration mit demselben Namen wie die Basisklasse, dann alle überladenen Funktionen mit demselben Namen in Die Basisklasse wird als Kandidatenfunktion verwendet. d.fun(1);//Richtig, die abgeleitete Klasse hat keine Funktionsdeklaration mit demselben Namen wie die Basisklasse, dann werden alle überladenen Funktionen mit demselben Namen in der Basisklasse als Kandidatenfunktionen verwendet. 0 zurückgeben;}
Beispiel 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: //Neue Funktionsversion, alle überladenen Versionen der Basisklasse werden blockiert, hier nennen wir es hide function hide //Wenn in der abgeleiteten Klasse eine Funktion mit demselben Namen wie die Basisklasse deklariert ist, wird die Funktion mit demselben Namen in der Basisklasse nicht als Kandidatenfunktion verwendet, selbst wenn die Basisklasse mehrere Versionen hat von überladenen Funktionen mit unterschiedlichen Parameterlisten. 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); //Der folgende Satz ist falsch, daher ist er blockiert //d.fun();error C2660: 'fun' : Funktion funktioniert Nehmen Sie keine 0-Parameter an 0 zurückgeben;}
Beispiel 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: //Eine der Funktionsversionen der Override-Basisklasse überschreiben. Ebenso sind alle überladenen Versionen der Basisklasse versteckt. //Wenn in der abgeleiteten Klasse eine Funktion mit demselben Namen wie die Basisklasse deklariert ist, wird die Funktion mit demselben Namen in der Basisklasse nicht als Kandidatenfunktion verwendet, selbst wenn die Basisklasse mehrere Versionen hat von überladenen Funktionen mit unterschiedlichen Parameterlisten. void fun(){cout << "Derive::fun()" << endl;} void fun2(){cout << "Derive::fun2()" << endl;}};int main(){ Derive d; d.fun(); //Der folgende Satz ist falsch, daher ist er blockiert //d.fun(1);Fehler C2660: 'fun': Funktion akzeptiert nicht 1 Parameter return 0;}
Beispiel 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.fun();//Correct d.fun(1); //Korrekte Rückgabe 0;}/*Ergebnis ausgeben Derive::fun()Base::fun(int i)Drücken Sie eine beliebige Taste, um fortzufahren*/
Beispiel 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; .fun();//Correct d.fun(1);//Correct d.fun(1,2);//Rect return 0;}/*Ergebnis ausgeben Base::fun()Base::fun(int i)Derive::fun(int i,int j)Drücken Sie eine beliebige Taste, um fortzufahren*/
Okay, lassen Sie uns zunächst eine kleine Zusammenfassung der Merkmale zwischen Überladung und Überschreibung erstellen.
Merkmale der Überlastung:
n im gleichen Bereich (in derselben Klasse);
n Der Funktionsname ist derselbe, aber die Parameter sind unterschiedlich.
n Das Schlüsselwort virtual ist optional.
Override bedeutet, dass eine abgeleitete Klassenfunktion eine Basisklassenfunktion abdeckt. Die Merkmale von Override sind:
n verschiedene Bereiche (in abgeleiteten Klassen bzw. Basisklassen);
n Der Funktionsname und die Parameter sind gleich;
n Basisklassenfunktionen müssen das Schlüsselwort virtual haben. (Wenn kein virtuelles Schlüsselwort vorhanden ist, wird es als verstecktes Verstecken bezeichnet.)
Wenn die Basisklasse über mehrere überladene Versionen einer Funktion verfügt und Sie eine oder mehrere Funktionsversionen in der Basisklasse in der abgeleiteten Klasse überschreiben (überschreiben) oder neue in der Funktionsversion der abgeleiteten Klasse hinzufügen (gleicher Funktionsname, unterschiedliche Parameter) , dann werden alle überladenen Versionen der Basisklasse blockiert, was wir hier versteckt nennen. Wenn Sie also eine neue Funktionsversion in einer abgeleiteten Klasse und die Funktionsversion der Basisklasse verwenden möchten, sollten Sie im Allgemeinen alle überladenen Versionen in der Basisklasse in der abgeleiteten Klasse überschreiben. Wenn Sie die überladene Funktionsversion der Basisklasse nicht überschreiben möchten, sollten Sie Beispiel 4 oder Beispiel 5 verwenden, um den Namespacebereich der Basisklasse explizit zu deklarieren.
Tatsächlich geht der C++-Compiler davon aus, dass es keine Beziehung zwischen Funktionen mit demselben Funktionsnamen und unterschiedlichen Parametern gibt. Es handelt sich lediglich um zwei nicht verwandte Funktionen. Es ist nur so, dass die C++-Sprache die Konzepte des Überladens und Überschreibens eingeführt hat, um die reale Welt zu simulieren und es Programmierern zu ermöglichen, mit realen Problemen intuitiver umzugehen. Das Überladen erfolgt im selben Namespace-Bereich, während das Überschreiben in unterschiedlichen Namespace-Bereichen erfolgt. Basisklasse und abgeleitete Klasse sind beispielsweise zwei unterschiedliche Namespace-Bereiche. Wenn während des Vererbungsprozesses eine abgeleitete Klasse denselben Namen wie eine Basisklassenfunktion hat, wird die Basisklassenfunktion ausgeblendet. Die hier diskutierte Situation ist natürlich, dass vor der Basisklassenfunktion kein virtuelles Schlüsselwort steht. Wir werden die Situation, in der ein virtuelles Schlüsselwort vorhanden ist, separat besprechen.
Eine geerbte Klasse überschreibt eine Funktionsversion der Basisklasse, um eine eigene funktionale Schnittstelle zu erstellen. Zu diesem Zeitpunkt geht der C++-Compiler davon aus, dass Ihnen die Schnittstelle meiner Basisklasse nicht zur Verfügung gestellt wird, da Sie jetzt die neu geschriebene Schnittstelle der abgeleiteten Klasse verwenden möchten (natürlich können Sie die Methode verwenden, den Namespace-Bereich explizit zu deklarieren). siehe [C++-Grundlagen] Überladen, Überschreiben, Polymorphismus und Ausblenden von Funktionen (1)). Es ignoriert, dass die Schnittstelle Ihrer Basisklasse Überlastungseigenschaften aufweist. Wenn Sie die Überladungsfunktion in der abgeleiteten Klasse weiterhin beibehalten möchten, geben Sie die Schnittstellenüberladungsfunktion selbst an. Daher wird in einer abgeleiteten Klasse die Funktionsversion der Basisklasse rücksichtslos blockiert, solange der Funktionsname derselbe ist. Im Compiler wird die Maskierung über den Namespace-Bereich implementiert.
Um die überladene Version der Basisklassenfunktion in der abgeleiteten Klasse beizubehalten, sollten Sie daher alle überladenen Versionen der Basisklasse überschreiben. Überladung ist nur in der aktuellen Klasse gültig, und durch die Vererbung gehen die Eigenschaften einer Funktionsüberladung verloren. Mit anderen Worten: Wenn Sie die überladene Funktion der Basisklasse in die geerbte abgeleitete Klasse einfügen möchten, müssen Sie sie neu schreiben.
„Versteckt“ bedeutet hier, dass die Funktion der abgeleiteten Klasse die gleichnamige Basisklassenfunktion blockiert. Lassen Sie uns auch eine kurze Zusammenfassung der spezifischen Regeln erstellen:
n Wenn die Funktion der abgeleiteten Klasse denselben Namen wie die Funktion der Basisklasse hat, die Parameter jedoch unterschiedlich sind. Wenn die Basisklasse zu diesem Zeitpunkt nicht über das Schlüsselwort virtual verfügt, werden die Funktionen der Basisklasse ausgeblendet. (Achten Sie darauf, es nicht mit Überladung zu verwechseln. Obwohl die Funktion mit demselben Namen und unterschiedlichen Parametern als Überladung bezeichnet werden sollte, kann sie hier nicht als Überladung verstanden werden, da sich die abgeleitete Klasse und die Basisklasse nicht im selben Namespace-Bereich befinden. Dies ist als Verstecken verstanden)
n Wenn die Funktion der abgeleiteten Klasse denselben Namen wie die Funktion der Basisklasse hat, die Parameter jedoch unterschiedlich sind. Wenn die Basisklasse zu diesem Zeitpunkt das Schlüsselwort virtual hat, werden die Funktionen der Basisklasse implizit in die vtable der abgeleiteten Klasse geerbt. Zu diesem Zeitpunkt zeigt die Funktion in der vtable der abgeleiteten Klasse auf die Funktionsadresse der Basisklassenversion. Gleichzeitig wird diese neue Funktionsversion als überladene Version der abgeleiteten Klasse zur abgeleiteten Klasse hinzugefügt. Wenn der Basisklassenzeiger jedoch eine polymorphe Aufruffunktionsmethode implementiert, wird diese neue abgeleitete Klassenfunktionsversion ausgeblendet.
n Wenn die Funktion der abgeleiteten Klasse denselben Namen wie die Funktion der Basisklasse hat und auch die Parameter identisch sind, die Basisklassenfunktion jedoch nicht über das Schlüsselwort virtual verfügt. Zu diesem Zeitpunkt sind die Funktionen der Basisklasse ausgeblendet. (Achten Sie darauf, es nicht mit Abdeckung zu verwechseln, die hier als Verstecken verstanden wird.)
n Wenn die Funktion der abgeleiteten Klasse denselben Namen wie die Funktion der Basisklasse hat und auch die Parameter identisch sind, die Basisklassenfunktion jedoch das Schlüsselwort virtual hat. Zu diesem Zeitpunkt werden die Funktionen der Basisklasse nicht „versteckt“. (Hier muss man es als Berichterstattung verstehen ^_^).
Zwischenspiel: Wenn vor der Basisklassenfunktion kein virtuelles Schlüsselwort steht, müssen wir es reibungsloser umschreiben. Wenn es ein virtuelles Schlüsselwort gibt, ist es sinnvoller, dies nicht zu tun Jeder kann C++ besser verstehen. Lassen Sie uns dies ohne weitere Umschweife anhand eines Beispiels veranschaulichen.
Beispiel 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}; 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); //Der folgende Satz ist falsch und daher blockiert //pb->fun(1,2); virtuelle Funktion kann nicht überladen werden, Fehler C2661: 'fun': Keine überladene Funktion benötigt 2 Parameter cout << endl; pd = new Derive(); pd->fun(1);//overload delete pb;
Ergebnisse ausgeben
Ableiten::fun()
Ableiten::fun(int i)
Ableiten::fun()
Ableiten::fun(int i)
Ableiten::fun(int i,int j)
Drücken Sie eine beliebige Taste, um fortzufahren
*/
Beispiel 7-1
#include <iostream> using namespace std; class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl }}; int main(){ Base *pb = new Derive(); pb->fun(1);//Base::fun(int i) delete pb;}
Beispiel 7-2
#include <iostream> using namespace std; class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl }}; void fun(double d){ cout <<"Derive::fun(double d)"<< endl main(){ Base *pb = new Derive(); pb->fun(1);//Base::fun(int i) pb->fun((double)0.01);//Base::fun(int i) delete pb;}
Beispiel 8-1
#include <iostream> using namespace std; class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl }}; void fun(int i){ cout <<"Derive::fun(int i)"<< endl }}; pb->fun(1);//Derive::fun(int i) delete pb;}
Beispiel 8-2
#include <iostream> using namespace std; class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl }}; 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;}
Beispiel 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(int i)"<< endl; } void fun(double d){ cout <<"Derive::fun(double d)"<< endl; } };int main(){ Base *pb = new Derive(1); //Derive::fun(int i) pb->fun('a');//Derive::fun(int i) pb->fun((double)0.01);//Derive::fun(int i) Derive *pd =new Derive(); pd->fun(1);//Derive::fun(int i) // überladen pd->fun('a');//Derive::fun(char c) //überladen pd->fun(0.01);//Derive::fun(double d) pb löschen; pd löschen; return 0;}
Die Beispiele 7-1 und 8-1 sind leicht zu verstehen. Ich habe diese beiden Beispiele hier aufgeführt, damit jeder einen Vergleich anstellen kann und jeder es besser versteht:
n In Beispiel 7-1 deckt die abgeleitete Klasse die virtuelle Funktion der Basisklasse nicht ab. Zu diesem Zeitpunkt ist die Adresse, auf die der Funktionszeiger in der vtable der abgeleiteten Klasse zeigt, die virtuelle Funktionsadresse der Basisklasse.
n In Beispiel 8-1 überschreibt die abgeleitete Klasse die virtuelle Funktion der Basisklasse. Zu diesem Zeitpunkt ist die Adresse, auf die der Funktionszeiger in der vtable der abgeleiteten Klasse zeigt, die Adresse der eigenen überschriebenen virtuellen Funktion der abgeleiteten Klasse.
Die Beispiele 7-2 und 8-2 sehen etwas seltsam aus. Wenn man sie jedoch nach den oben genannten Prinzipien vergleicht, wird die Antwort klar sein:
n In Beispiel 7-2 haben wir eine Funktionsversion für die abgeleitete Klasse überladen: void fun(double d) Tatsächlich ist dies nur eine Vertuschung. Lassen Sie es uns genauer analysieren. Es gibt mehrere Funktionen in der Basisklasse und mehrere Funktionen in der abgeleiteten Klasse:
Typ Basisklasse abgeleitete Klasse
Vtable-Abschnitt
void fun(int i)
Zeigt auf die Basisklassenversion der virtuellen Funktion void fun(int i)
statischer Teil
Leerer Spaß (Doppel-D)
Lassen Sie uns die folgenden drei Codezeilen noch einmal analysieren:
Basis *pb = new Derive();
pb->fun(1);//Base::fun(int i)
pb->fun((double)0.01);//Base::fun(int i)
Der erste Satz ist der Schlüssel auf das Objekt der abgeleiteten Klasse. Wir wissen, dass es sich um einen polymorphen Aufruf handelt Gehen Sie also zuerst zur vtable der abgeleiteten Klasse, um die virtuelle Funktionsversion der abgeleiteten Klasse zu finden. Es wird festgestellt, dass die abgeleitete Klasse die virtuelle Funktion der Basisklasse nicht abdeckt Die abgeleitete Klasse erstellt nur einen Zeiger auf die Adresse der virtuellen Funktion der Basisklasse, daher ist es selbstverständlich, die Basisklassenversion der virtuellen Funktion aufzurufen. Im letzten Satz sucht das Programm immer noch nach der vtable der abgeleiteten Klasse und stellt fest, dass es überhaupt keine virtuelle Funktion dieser Version gibt, sodass es zurückgehen und seine eigene virtuelle Funktion aufrufen muss.
Erwähnenswert ist hier auch, dass das Programm beim Kompilieren des Programms „Unklarer Aufruf“ anzeigt, wenn die Basisklasse zu diesem Zeitpunkt über mehrere virtuelle Funktionen verfügt. Beispiele sind wie folgt
#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' : mehrdeutiger Aufruf der überladenen Funktion delete pb; return 0;}
Okay, analysieren wir Beispiel 8-2 noch einmal.
n In Beispiel 8-2 haben wir auch eine Funktionsversion für die abgeleitete Klasse überladen: void fun(double d) und auch die virtuelle Funktion der Basisklasse behandelt. Es gibt mehrere Funktionen in der Basisklasse , und die abgeleitete Klasse hat mehrere Funktionen. Die Klasse hat mehrere Funktionen:
Typ Basisklasse abgeleitete Klasse
Vtable-Abschnitt
void fun(int i)
void fun(int i)
statischer Teil
Leerer Spaß (Doppel-D)
Aus der Tabelle können wir ersehen, dass der Funktionszeiger in der vtable der abgeleiteten Klasse auf die eigene überschriebene virtuelle Funktionsadresse zeigt.
Lassen Sie uns die folgenden drei Codezeilen noch einmal analysieren:
Basis *pb = new Derive();
pb->fun(1);//Derive::fun(int i)
pb->fun((double)0.01);//Derive::fun(int i)
Zum ersten Satz muss nicht mehr gesagt werden. Der zweite Satz besteht darin, die virtuelle Funktionsversion der abgeleiteten Klasse aufzurufen. Hey, es fühlt sich tatsächlich wieder seltsam an Dumm, sie tauchen in die vtable-Tabelle der abgeleiteten Klasse ein, ich habe sie mir nur angeschaut und festgestellt, dass es keine Version gibt, die ich wirklich nicht herausfinden kann. , warum schauen sich die Zeiger der Basisklasse nicht um? Der Vtable-Teil (also der statische Teil) und der Vtable-Teil, den Sie verwalten möchten, sind die Leerzeichen der abgeleiteten Klasse fun(double d) ist so weit weg, dass man es nicht sehen kann! Außerdem muss sich die abgeleitete Klasse um alles kümmern. Hey, kein Streit mehr, das kann jeder Passen Sie auf sich auf^_^
Ach, werden Sie seufzen? Der Basisklassenzeiger kann polymorphe Aufrufe durchführen, aber niemals überladene Aufrufe an abgeleitete Klassen (siehe Beispiel 6)~~~
Schauen wir uns noch einmal Beispiel 9 an:
Die Wirkung dieses Beispiels ist die gleiche wie die von Beispiel 6, mit demselben Zweck. 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 Polymorphismus die aufzurufende virtuelle Funktionsversion basierend auf dem tatsächlichen Typ des Laufzeitobjekts auswählt. Polymorphismus wird durch abgeleitete Klassen in Basisklassen implementiert Die Funktion wird durch Überschreiben implementiert. Wenn die abgeleitete Klasse die virtuelle virtuelle Funktion der Basisklasse nicht überschreibt, erbt die abgeleitete Klasse automatisch die virtuelle virtuelle Funktion der Basisklasse. Funktionsversion: 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 Der tatsächliche Typ des Objekts wird zur Auswahl der aufzurufenden virtuellen Funktionsversion verwendet. Wenn der Objekttyp, auf den der Basisklassenzeiger zeigt, beispielsweise ein abgeleiteter Typ ist, wird er zur Laufzeit aufgerufen , wird die virtuelle virtuelle Funktionsversion der abgeleiteten Klasse 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 zu diesem Zeitpunkt mit der Basisklasse übereinstimmt. Das heißt, derselbe Name und derselbe Parametertyp. Wenn Sie der abgeleiteten Klasse eine neue Funktionsversion hinzufügen, können Sie die neue Funktionsversion nicht dynamisch über den Basisklassenzeiger aufrufen 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 gebunden, Polymorphismus ist dynamisch gebunden. 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, geht bei der Vererbung die Überladungsfunktion verloren. Anschließend wird auch dynamisch die virtuelle virtuelle Funktionsversion der Basisklasse oder die virtuelle virtuelle Funktionsversion der abgeleiteten Klasse ausgewählt, um bestimmte Operationen auszuführen. Dies wird durch den Objekttyp bestimmt, auf den der Basisklassenzeiger tatsächlich zeigt, also Überladung und Zeiger Der Objekttyp, auf den der Zeiger tatsächlich zeigt, hat nichts damit zu tun; der Polymorphismus hängt mit dem Objekttyp zusammen, auf den der Zeiger tatsächlich zeigt.
Zur Verdeutlichung: Virtuelle virtuelle Funktionen können zwar auch überladen werden, die Überladung kann jedoch nur im Rahmen des aktuellen Namespace wirksam sein. Wie viele String-Objekte wurden erstellt?
Schauen wir uns zunächst einen Code an:
Java-Code
String str=new String("abc");
Auf diesen Code folgt oft die Frage, wie viele String-Objekte durch diese Codezeile erstellt werden? 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.
Wir können die obige Codezeile in vier Teile unterteilen: String str, =, „abc“ und new String(). String str definiert nur eine String-Typ-Variable mit dem Namen str, erstellt also kein Objekt; = initialisiert die Variable str und weist ihr eine Referenz (oder ein Handle) zu und erstellt offensichtlich kein Objekt ;Jetzt nur neu String("abc") bleibt übrig. Warum kann also new String(„abc“) als „abc“ und new String() betrachtet werden? Werfen wir einen Blick auf den String-Konstruktor, den wir aufgerufen haben:
Java-Code
public String(String original) {
//anderer Code...
}
Wie wir alle wissen, gibt es zwei häufig verwendete Methoden zum Erstellen von Instanzen (Objekten) einer Klasse:
Verwenden Sie new, um Objekte zu erstellen.
Rufen Sie die newInstance-Methode der Class-Klasse auf und verwenden Sie den Reflexionsmechanismus, um das Objekt zu erstellen.
Wir haben new verwendet, um die obige Konstruktormethode der String-Klasse aufzurufen, um ein Objekt zu erstellen und seine Referenz der str-Variablen zuzuweisen. Gleichzeitig haben wir festgestellt, dass der von der aufgerufenen Konstruktormethode akzeptierte Parameter ebenfalls ein String-Objekt ist und dieses Objekt genau „abc“ ist. Daraus müssen wir eine andere Möglichkeit zum Erstellen eines String-Objekts einführen – Text in Anführungszeichen.
Diese Methode ist einzigartig für String und unterscheidet sich stark von der neuen Methode.
Java-Code
String str="abc";
Es besteht kein Zweifel, dass diese Codezeile ein String-Objekt erstellt.
Java-Code
String a="abc";
String b="abc";
Was ist hier? Die Antwort ist immer noch eine.
Java-Code
String a="ab"+"cd";
Was ist hier? Die Antwort ist immer noch eine. Ein bisschen seltsam? An dieser Stelle müssen wir einen Überblick über das Wissen über String-Pools geben.
In der JAVA Virtual Machine (JVM) gibt es einen String-Pool, der viele String-Objekte speichert und gemeinsam genutzt werden kann, wodurch die Effizienz verbessert wird. Da die String-Klasse endgültig ist, kann ihr Wert nach der Erstellung nicht mehr geändert werden, sodass wir uns keine Sorgen über Programmverwirrungen machen müssen, die durch die gemeinsame Nutzung von String-Objekten entstehen. Der String-Pool wird von der String-Klasse verwaltet, und wir können die Methode intern() aufrufen, um auf den String-Pool zuzugreifen.
Schauen wir uns String a="abc"; an. Wenn diese Codezeile ausgeführt wird, sucht die virtuelle JAVA-Maschine zunächst im String-Pool, um festzustellen, ob ein solches Objekt mit dem Wert „abc“ bereits vorhanden ist Der Rückgabewert der String-Klasse ist die Methode equal(Object obj). Wenn dies der Fall ist, wird kein neues Objekt erstellt und eine Referenz auf das vorhandene Objekt wird direkt zurückgegeben. Andernfalls wird das Objekt zuerst erstellt, dann dem String-Pool hinzugefügt und dann seine Referenz zurückgegeben. Daher ist es für uns nicht schwer zu verstehen, warum die ersten beiden der vorherigen drei Beispiele diese Antwort haben.
Zum dritten Beispiel:
Java-Code
String a="ab"+"cd";
Weil der Wert der Konstante zur Kompilierungszeit bestimmt wird. Hier sind „ab“ und „cd“ Konstanten, sodass der Wert der Variablen a zur Kompilierungszeit bestimmt werden kann. Der kompilierte Effekt dieser Codezeile entspricht:
Java-Code
String a="abcd";
Daher wird hier nur ein Objekt „abcd“ erstellt und im String-Pool gespeichert.
Jetzt stellt sich erneut die Frage: Werden alle nach der „+“-Verbindung erhaltenen Zeichenfolgen zum Zeichenfolgenpool hinzugefügt? Wir alle wissen, dass „==“ zum Vergleichen zweier Variablen verwendet werden kann. Es gibt die folgenden zwei Situationen:
Wenn zwei Grundtypen (char, byte, short, int, long, float, double, boolean) verglichen werden, wird beurteilt, ob ihre Werte gleich sind.
Wenn die Tabelle zwei Objektvariablen vergleicht, wird beurteilt, ob ihre Referenzen auf dasselbe Objekt verweisen.
Als nächstes werden wir „==“ verwenden, um ein paar Tests durchzuführen. Der Einfachheit halber betrachten wir den Verweis auf ein Objekt, das bereits im String-Pool vorhanden ist, als das Objekt, das dem String-Pool hinzugefügt wird:
Java-Code
public class StringTest { public static void main(String[] args) { String a = "ab";// Erstellen Sie ein Objekt und fügen Sie es dem String-Pool hinzu System.out.println("String a = /"ab/" ; "); String b = "cd";// Ein Objekt wird erstellt und dem String-Pool hinzugefügt System.out.println("String b = /"cd/";"); String c = "abcd"; // Ein Objekt wird erstellt und zum String-Pool hinzugefügt. String d = „ab“ + „cd“; // Wenn d und c auf dasselbe Objekt zeigen, bedeutet dies, dass d auch zum String-Pool hinzugefügt wurde, wenn (d == c ) { System.out.println("/"ab/"+/"cd/" Das erstellte Objekt/" wird zum String-Pool hinzugefügt" } // Wenn d und c nicht auf dasselbe zeigen Objekt, Dies bedeutet, dass d nicht zum String-Pool hinzugefügt wurde, sonst { System.out.println("/"ab/"+/"cd/" Das erstellte Objekt/"wird nicht zum String-Pool hinzugefügt"); } String e = a + "cd"; c Zeigt auf dasselbe Objekt, was bedeutet, dass e auch zum String-Pool hinzugefügt wurde if (e == c) { System.out.println(" a +/"cd/" Das erstellte Objekt/"joined/" string pool middle"); } // Wenn e und c nicht auf dasselbe Objekt verweisen, bedeutet dies, dass e nicht zum String-Pool hinzugefügt wurde, sonst { System.out.println(" ein +/"cd/" erstelltes Objekt/"nicht hinzugefügt/" zum string pool" ); } String f = "ab" + b; // Wenn f und c auf dasselbe Objekt zeigen, bedeutet dies, dass f auch zum String-Pool hinzugefügt wurde if (f == c) { System.out .println("/ Objekt erstellt von „ab/“+ b /"Added/" to the string pool"); } // Wenn f und c nicht auf dasselbe Objekt zeigen, bedeutet das, dass f nicht zum String-Pool hinzugefügt wurde else { System.out.println("/" ab/" + b erstelltes Objekt/"nicht hinzugefügt/" zum String-Pool"); } String g = a + b; // Wenn g und c auf dasselbe Objekt zeigen, bedeutet dies, dass g auch zum hinzugefügt wurde String-Pool if ( g == c) { System.out.println(" a + b Das erstellte Objekt/"hinzugefügt/" zum String-Pool"); } // Wenn g und c nicht auf dasselbe Objekt zeigen, bedeutet dies, dass g nicht zum String-Pool hinzugefügt wurde else { System.out.println ("a + b erstelltes Objekt/"nicht hinzugefügt/" zum String-Pool");
Die Laufergebnisse sind wie folgt:
Zeichenfolge a = "ab";
String b = "cd";
Das durch „ab“+„cd“ erstellte Objekt wird in den String-Pool „eingefügt“.
Das durch + „cd“ erstellte Objekt wird dem String-Pool „nicht hinzugefügt“.
Das durch „ab“+b erstellte Objekt wird dem String-Pool „nicht hinzugefügt“.
Das durch a + b erstellte Objekt wird dem String-Pool „nicht hinzugefügt“. Aus den obigen Ergebnissen können wir leicht erkennen, dass nur neue Objekte hinzugefügt werden, die durch die Verwendung von „+“-Verbindungen zwischen String-Objekten generiert werden, die mithilfe von Anführungszeichen erstellt wurden, um Text einzuschließen . im String-Pool. Bei allen „+“-Verbindungsausdrücken, die im neuen Modus erstellte Objekte enthalten (einschließlich Null), werden die von ihnen generierten neuen Objekte nicht zum String-Pool hinzugefügt, und wir werden nicht näher darauf eingehen.
Aber es gibt eine Situation, die unsere Aufmerksamkeit erfordert. Bitte schauen Sie sich den folgenden Code an:
Java-Code
public class StringStaticTest { // Konstante A public static final String A = "ab"; // Konstante B public static final String B = "cd"; // Zwei Konstanten verwenden +Connect zum Initialisieren von s String s = A + B; String t = "abcd"; if (s == t) { System.out.println("s equal t, they are the same object" }); { System.out.println("s ist nicht gleich t, sie sind nicht dasselbe Objekt"} } }
Das Ergebnis der Ausführung dieses Codes ist wie folgt:
s ist gleich t. Sie sind das gleiche Objekt. Der Grund dafür ist, dass der Wert einer Konstanten fest ist, sodass er zur Kompilierungszeit bestimmt werden kann, während der Wert einer Variablen nur zur Laufzeit bestimmt werden kann, da diese Variable mit verschiedenen Methoden aufgerufen werden kann Wert zu ändern. Im obigen Beispiel sind A und B Konstanten und ihre Werte sind fest, sodass auch der Wert von s fest ist und beim Kompilieren der Klasse bestimmt wird. Das heißt:
Java-Code
Zeichenfolge s=A+B;
Entspricht:
Java-Code
String s="ab"+"cd";
Lassen Sie mich das obige Beispiel leicht ändern und sehen, was passiert:
Java-Code
public class StringStaticTest { // Constant A public static final String A; // Constant B public static final String B; // Initialisiere s, indem du die beiden Konstanten mit + String s = A + B verbindest; String t = "abcd"; System.out.println("s ist gleich t, sie sind das gleiche Objekt"); } else { System.out.println("s ist nicht gleich t, sie sind nicht das gleiche Objekt");
Das Ergebnis seiner Operation ist folgendes:
s ist nicht gleich t. Sie sind nicht dasselbe Objekt, sondern wurden leicht verändert. Das Ergebnis ist genau das Gegenteil des Beispiels. Analysieren wir es noch einmal. Obwohl A und B als Konstanten definiert sind (nur einmal zugewiesen werden können), werden sie nicht sofort zugewiesen. Bevor der Wert von s berechnet wird, wann sie zugewiesen werden und welcher Wert ihnen zugewiesen wird, sind alles Variablen. Daher verhalten sich A und B wie eine Variable, bevor ihnen ein Wert zugewiesen wird. Dann können s nicht zur Kompilierzeit ermittelt, sondern erst zur Laufzeit erstellt werden.
Da die gemeinsame Nutzung von Objekten im String-Pool die Effizienz verbessern kann, empfehlen wir jedem, String-Objekte zu erstellen, indem er Text in Anführungszeichen setzt. Tatsächlich verwenden wir dies häufig in der Programmierung.
Schauen wir uns als Nächstes die Methode intern() an, die wie folgt definiert ist:
Java-Code
öffentlicher nativer String intern();
Dies ist eine native Methode. Beim Aufruf dieser Methode prüft die virtuelle JAVA-Maschine zunächst, ob im String-Pool bereits ein Objekt mit einem dem Objekt entsprechenden Wert vorhanden ist. Wenn dies nicht der Fall ist, erstellt sie zunächst einen Objekt im String-Pool mit demselben Wert und gibt dann seine Referenz zurück.
Schauen wir uns diesen Code an:
Java-Code
public class StringInternTest { public static void main(String[] args) { // Verwenden Sie ein char-Array, um a zu initialisieren, um zu vermeiden, dass ein Objekt mit dem Wert „abcd“ bereits im String-Pool vorhanden ist, bevor a erstellt wird. String a = neuer String ( new char[] { 'a', 'b', 'c', 'd' }); String b = a.intern(); if (b == a) { System.out.println("b wurde zum String-Pool hinzugefügt, es wurde kein neues Objekt erstellt"); } else { System.out.println("b wurde nicht zum String-Pool hinzugefügt, es wurde kein neues Objekt erstellt"); } } }
Laufergebnisse:
b wurde nicht zum String-Pool hinzugefügt und es wurde ein neues Objekt erstellt. Wenn die intern()-Methode der String-Klasse kein Objekt mit demselben Wert findet, fügt sie das aktuelle Objekt dem String-Pool hinzu und gibt es dann zurück Referenz, dann b und a Zeigt auf dasselbe Objekt; andernfalls wird das Objekt, auf das b zeigt, von der virtuellen JAVA-Maschine im String-Pool neu erstellt, sein Wert ist jedoch derselbe wie a. Das laufende Ergebnis des obigen Codes bestätigt diesen Punkt lediglich.
Lassen Sie uns abschließend über die Speicherung von String-Objekten in der JAVA Virtual Machine (JVM) und die Beziehung zwischen dem String-Pool und dem Heap und Stack sprechen. Schauen wir uns zunächst den Unterschied zwischen Heap und Stack an:
Stapel: Speichert hauptsächlich Basistypen (oder integrierte Typen) (char, byte, short, int, long, float, double, boolean) und ist schneller als andere Haufen.
Heap: Wird zum Speichern von Objekten verwendet.
Wenn wir uns den Quellcode der String-Klasse ansehen, werden wir feststellen, dass er über ein Wertattribut verfügt, das den Wert des String-Objekts speichert. Der Typ ist char[], was auch zeigt, dass eine Zeichenfolge eine Folge von Zeichen ist.
Beim Ausführen von String a="abc"; erstellt die virtuelle JAVA-Maschine drei Zeichenwerte „a“, „b“ und „c“ im Stapel und erstellt dann ein String-Objekt im Heap, dessen Wert (Wert) ist. ist ein Array aus drei gerade auf dem Stapel erstellten Zeichenwerten {'a', 'b', 'c'}. Schließlich wird das neu erstellte String-Objekt dem String-Pool hinzugefügt. Wenn wir dann den String b=new String("abc");-Code ausführen, erstellt die virtuelle JAVA-Maschine nur ein neues String-Objekt im Heap, da „abc“ erstellt und gespeichert wurde value sind die drei char-Typwerte „a“, „b“ und „c“, die auf dem Stapel erstellt wurden, als die vorherige Codezeile ausgeführt wurde.
An diesem Punkt sind wir uns bereits ganz im Klaren über die Frage, warum String str=new String("abc"), der am Anfang dieses Artikels aufgeworfen wurde, zwei Objekte erzeugt.