Qu'est-ce que le polymorphisme ? Quel est son mécanisme de mise en œuvre ? Quelle est la différence entre la surcharge et la réécriture ? Ce sont les quatre concepts très importants que nous allons revoir cette fois-ci : l'héritage, le polymorphisme, la surcharge et l'écrasement.
héritage
En termes simples, l'héritage consiste à générer un nouveau type basé sur un type existant en ajoutant de nouvelles méthodes ou en redéfinissant des méthodes existantes (comme indiqué ci-dessous, cette méthode est appelée réécriture). L'héritage est l'une des trois caractéristiques de base de l'orientation objet - encapsulation, héritage et polymorphisme. Chaque classe que nous écrivons lors de l'utilisation de JAVA hérite, car dans le langage JAVA, la classe java.lang.Object est la classe de base la plus fondamentale ( ou classe parent ou super classe) de toutes les classes. Si une classe nouvellement définie que nous définissons ne spécifie pas explicitement de quelle classe de base elle hérite, alors JAVA héritera par défaut de la classe Object.
Nous pouvons diviser les classes en JAVA en trois types suivants :
Classe : une classe définie à l'aide de la classe et ne contient pas de méthodes abstraites.
Classe abstraite : classe définie à l'aide d'une classe abstraite, qui peut ou non contenir des méthodes abstraites.
Interface : une classe définie à l'aide de l'interface.
Les règles d'héritage suivantes existent entre ces trois types :
Les classes peuvent étendre des classes, des classes abstraites et implémenter des interfaces.
Les classes abstraites peuvent hériter (étendre) des classes, elles peuvent hériter (étendre) des classes abstraites et elles peuvent hériter (implémenter) des interfaces.
Les interfaces ne peuvent qu'étendre les interfaces.
Veuillez noter que les différents mots-clés extends et Implements utilisés dans chaque cas d'héritage dans les trois règles ci-dessus ne peuvent pas être remplacés à volonté. Comme nous le savons tous, une fois qu'une classe ordinaire hérite d'une interface, elle doit implémenter toutes les méthodes définies dans cette interface, sinon elle ne peut être définie que comme une classe abstraite. La raison pour laquelle je n'utilise pas ici le terme « implémentation » pour le mot-clé Implements est que, conceptuellement, il représente également une relation d'héritage, et dans le cas de la classe abstraite Implements interface, il n'est pas nécessaire d'implémenter cette définition d'interface Any. méthode, il est donc plus raisonnable d’utiliser l’héritage.
Les trois règles ci-dessus respectent également les contraintes suivantes :
Les classes et les classes abstraites ne peuvent hériter que d'au plus une classe, ou d'au plus une classe abstraite, et ces deux situations s'excluent mutuellement, c'est-à-dire qu'elles héritent soit d'une classe, soit d'une classe abstraite.
Lorsque les classes, classes abstraites et interfaces héritent d’interfaces, elles ne sont pas limitées par le nombre. En théorie, elles peuvent hériter d’un nombre illimité d’interfaces. Bien entendu, pour une classe, elle doit implémenter toutes les méthodes définies dans toutes les interfaces dont elle hérite.
Lorsqu'une classe abstraite hérite d'une classe abstraite ou implémente une interface, elle peut partiellement, complètement ou complètement ne pas implémenter les méthodes abstraites de la classe abstraite parent ou les interfaces définies dans l'interface de la classe parent.
Lorsqu'une classe hérite d'une classe abstraite ou implémente une interface, elle doit implémenter toutes les méthodes abstraites de la classe abstraite parent ou toutes les interfaces définies dans l'interface de la classe parent.
L'avantage que l'héritage apporte à notre programmation est la réutilisation (réutilisation) des classes originales. Tout comme la réutilisation des modules, la réutilisation des classes peut améliorer notre efficacité de développement. En fait, la réutilisation des modules est l'effet superposé de la réutilisation d'un grand nombre de classes. En plus de l'héritage, nous pouvons également utiliser la composition pour réutiliser les classes. La soi-disant combinaison consiste à définir la classe d'origine comme attribut de la nouvelle classe et à réaliser la réutilisation en appelant les méthodes de la classe d'origine dans la nouvelle classe. S'il n'y a pas de relation incluse entre le type nouvellement défini et le type original, c'est-à-dire à partir d'un concept abstrait, les choses représentées par le type nouvellement défini ne font pas partie des choses représentées par le type original, comme les personnes jaunes. C'est un type d'être humain, et il existe une relation entre eux, inclure et être inclus, donc à l'heure actuelle, la combinaison est un meilleur choix pour parvenir à la réutilisation. L'exemple suivant est un exemple simple de la combinaison :
public class Sub { private Parent p = new Parent(); public void doSomething() { // Réutiliser la méthode p.method() de la classe Parent ; // autre code } } class Parent { public void method() { / / fais quelque chose ici } }
Bien entendu, afin de rendre le code plus efficace, nous pouvons également l'initialiser lorsque nous avons besoin d'utiliser le type d'origine (comme Parent p).
L'utilisation de l'héritage et de la combinaison pour réutiliser les classes originales est un modèle de développement incrémentiel. L'avantage de cette méthode est qu'il n'est pas nécessaire de modifier le code d'origine, elle n'apportera donc pas de nouveaux bogues au code d'origine et ce n'est pas nécessaire. re-tester en raison de modifications du code original, ce qui est évidemment bénéfique pour notre développement. Par conséquent, si nous maintenons ou transformons un système ou un module original, surtout lorsque nous n'en avons pas une compréhension approfondie, nous pouvons choisir le modèle de développement incrémental, qui peut non seulement améliorer considérablement notre efficacité de développement, mais également éviter les risques causés par modifications au code original.
Polymorphisme
Le polymorphisme est un autre concept de base important. Comme mentionné ci-dessus, c'est l'une des trois caractéristiques fondamentales de l'orientation objet. Qu’est-ce que le polymorphisme exactement ? Examinons d’abord l’exemple suivant pour vous aider à comprendre :
//Interface d'interface de voiture Car { // Nom de la voiture String getName(); // Obtenez le prix de la voiture int getPrice(); } // Classe BMW BMW implémente Car { public String getName() { return "BMW" } public int; getPrice() { return 300000; } } // Classe CheryQQ CheryQQ implémente Car { public String getName() { return "CheryQQ" } public int getPrice() ; { return 20000; } } // Magasin de vente de voitures public class CarShop { // Revenu des ventes de voitures private int money = 0; // Vendre une voiture public void sellCar(Car car) { System.out.println("Modèle de voiture : " + car.getName() + " Prix unitaire : " + car.getPrice()); // Augmenter les revenus de la vente de voitures += car.getPrice( } // Revenu total des ventes de voitures public int getMoney() { return money; } public static void main(String[] args) { CarShop aShop = new CarShop(); // Vendre une BMW aShop.sellCar(new BMW()); .sellCar(new CheryQQ()); System.out.println("Revenu total : " + aShop.getMoney());
Résultats en cours d'exécution :
Modèle : BMW Prix unitaire : 300 000
Modèle : CheryQQ Prix unitaire : 20 000
Revenu total : 320 000
L'héritage est la base de la réalisation du polymorphisme. Littéralement compris, le polymorphisme est un type (tous deux de type voiture) affichant plusieurs états (le nom de BMW est BMW et le prix est de 300 000 ; le nom de Chery est CheryQQ et le prix est de 2 000). L'association d'un appel de méthode au sujet (c'est-à-dire l'objet ou la classe) auquel appartient la méthode est appelée liaison, qui est divisée en deux types : liaison anticipée et liaison tardive. Leurs définitions sont expliquées ci-dessous :
Liaison anticipée : liaison avant l'exécution du programme, implémentée par le compilateur et l'éditeur de liens, également appelée liaison statique. Par exemple, les méthodes statiques et les méthodes finales. Notez que les méthodes privées sont également incluses ici car elles sont implicitement finales.
Liaison tardive : liaison selon le type de l'objet au moment de l'exécution, implémentée par le mécanisme d'appel de méthode, elle est donc également appelée liaison dynamique, ou liaison d'exécution. Toutes les méthodes, à l'exception de la liaison anticipée, sont des liaisons tardives.
Le polymorphisme est mis en œuvre sur le mécanisme de liaison tardive. L'avantage que nous apporte le polymorphisme est qu'il élimine la relation de couplage entre les classes et facilite l'extension du programme. Par exemple, dans l'exemple ci-dessus, pour ajouter un nouveau type de voiture à vendre, il vous suffit de laisser la classe nouvellement définie hériter de la classe Car et d'implémenter toutes ses méthodes sans apporter de modifications au code d'origine. ) de la classe CarShop Method peut gérer de nouveaux modèles de voitures. Le nouveau code est le suivant :
// Classe Santana Car Santana implémente Car { public String getName() { return "Santana" } public int getPrice() { return 80000 } }
Surcharge et remplacement
La surcharge et la réécriture sont deux concepts pour les méthodes. Avant de clarifier ces deux concepts, comprenons d'abord quelle est la structure de la méthode (le nom anglais est signature, certains sont traduits par "signature", bien qu'il soit utilisé plus largement, mais cette traduction ne l'est pas. précis). La structure fait référence à la structure de composition d'une méthode, incluant spécifiquement le nom et les paramètres de la méthode, couvrant le nombre, le type et l'ordre d'apparition des paramètres, mais n'inclut pas le type de valeur de retour de la méthode, les modificateurs d'accès et les modifications. tel que le symbole abstrait, statique et final. Par exemple, les deux méthodes suivantes ont la même structure :
public void method(int i, String s) { // faire quelque chose } public String method(int i, String s) { // faire quelque chose }
Ces deux méthodes sont avec des configurations différentes :
public void method(int i, String s) { // faire quelque chose } public void method(String s, int i) { // faire quelque chose }
Après avoir compris le concept de Gestalt, examinons la surcharge et la réécriture. Veuillez consulter leurs définitions :
Overriding, le nom anglais overriding, signifie qu'en cas d'héritage, une nouvelle méthode avec la même structure que la méthode de la classe de base est définie dans la sous-classe, appelée sous-classe remplaçant la méthode de la classe de base. C'est une étape nécessaire pour parvenir au polymorphisme.
La surcharge, le nom anglais « surcharge », fait référence à la définition de plusieurs méthodes portant le même nom mais des structures différentes dans la même classe. Dans une même classe, il n’est pas permis de définir plus d’une méthode du même type.
Considérons une question intéressante : les constructeurs peuvent-ils être surchargés ? La réponse est bien sûr oui, nous faisons souvent cela dans la programmation réelle. En fait, le constructeur est également une méthode. Le nom du constructeur est le nom de la méthode, les paramètres du constructeur sont les paramètres de la méthode et sa valeur de retour est une instance de la classe nouvellement créée. Cependant, le constructeur ne peut pas être remplacé par une sous-classe, car une sous-classe ne peut pas définir un constructeur du même type que la classe de base.
Surcharge, remplacement, polymorphisme et masquage de fonctions
On constate souvent que certains débutants en C++ ont une vague compréhension de la surcharge, de l'écrasement, du polymorphisme et du masquage de fonctions. J'écrirai ici certaines de mes propres opinions, dans l'espoir d'aider les débutants en C++ à dissiper leurs doutes.
Avant de comprendre la relation complexe et subtile entre la surcharge, l'écrasement, le polymorphisme et le masquage de fonctions, nous devons d'abord revoir les concepts de base tels que la surcharge et la couverture.
Tout d’abord, regardons un exemple très simple pour comprendre ce qu’est le masquage de fonction.
#include <iostream>en utilisant l'espace de noms 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; //La phrase suivante est fausse, elle est donc bloquée //d.fun();erreur C2660 : 'fun' : la fonction ne prend pas 0 paramètre d.fun(1); Derive *pd =new Derive(); //La phrase suivante est fausse, elle est donc bloquée //pd->fun();erreur C2660 : 'fun' : la fonction ne prend pas 0 paramètres pd->fun(1); delete pd return 0;}
/*Les fonctions dans différentes portées hors espace de noms ne constituent pas une surcharge. Les sous-classes et les classes parentes sont deux portées différentes.
Dans cet exemple, les deux fonctions se trouvent dans des portées différentes, elles ne sont donc pas surchargées, sauf si la portée est une portée d'espace de noms. */
Dans cet exemple, la fonction n'est ni surchargée ni remplacée, mais masquée.
Les cinq exemples suivants expliquent spécifiquement ce qu'est la dissimulation.
Exemple 1
#include <iostream>en utilisant l'espace de noms std;class Basic{public: void fun(){cout << "Base::fun()" << endl;}//surcharge 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();//Correct, la classe dérivée n'a pas de déclaration de fonction avec le même nom que la classe de base, alors toutes les fonctions surchargées avec le même nom dans la classe de base sera utilisée comme fonctions candidates. d.fun(1);//Correct, la classe dérivée n'a pas de déclaration de fonction avec le même nom que la classe de base, alors toutes les fonctions surchargées portant le même nom dans la classe de base seront utilisées comme fonctions candidates. renvoie 0 ;}
Exemple 2
#include <iostream>en utilisant l'espace de noms std;class Basic{public: void fun(){cout << "Base::fun()" << endl;}//surcharge void fun(int i){cout << "Base ::fun(int i)" << endl;}//overload};class Derive :public Basic{public: //Nouvelle version de la fonction, toutes les versions surchargées de la classe de base sont bloquées, ici, nous l'appelons hide function hide // S'il existe une déclaration d'une fonction portant le même nom que la classe de base dans la classe dérivée, la fonction portant le même nom dans la classe de base ne sera pas utilisée comme fonction candidate, même si la classe de base a plusieurs versions de fonctions surchargées avec différentes listes de paramètres. 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); //La phrase suivante est fausse, elle est donc bloquée //d.fun();erreur C2660 : 'fun' : la fonction le fait ne prend pas 0 paramètres renvoie 0 ;}
Exemple 3
#include <iostream>en utilisant l'espace de noms std;class Basic{public: void fun(){cout << "Base::fun()" << endl;}//surcharge void fun(int i){cout << "Base ::fun(int i)" << endl;}//overload};class Derive :public Basic{public: //Remplace l'une des versions de fonction de la classe de base de remplacement. De même, toutes les versions surchargées de la classe de base sont caché. // S'il existe une déclaration d'une fonction portant le même nom que la classe de base dans la classe dérivée, la fonction portant le même nom dans la classe de base ne sera pas utilisée comme fonction candidate, même si la classe de base a plusieurs versions de fonctions surchargées avec différentes listes de paramètres. void fun(){cout << "Derive::fun()" << endl;} void fun2(){cout << "Derive::fun2()" << endl;}};int main(){ Dériver d; d.fun(); //La phrase suivante est fausse, elle est donc bloquée //d.fun(1);erreur C2660 : 'fun' : la fonction ne prend pas 1 paramètre renvoie 0 ;}
Exemple 4
#include <iostream>en utilisant l'espace de noms std;class Basic{public: void fun(){cout << "Base::fun()" << endl;}//surcharge 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();//Corriger d.fun(1); //Corriger le retour 0;}/*Résultat de sortie Derive::fun()Base::fun(int i)Appuyez sur n'importe quelle touche pour continuer*/
Exemple 5
#include <iostream>en utilisant l'espace de noms std;class Basic{public: void fun(){cout << "Base::fun()" << endl;}//surcharge 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(){ Dérive d; .fun();//Corriger d.fun(1);//Corriger d.fun(1,2);//Corriger le retour 0;}/*Résultat de sortie Base::fun()Base::fun(int i)Derive::fun(int i,int j)Appuyez sur n'importe quelle touche pour continuer*/
Bon, faisons d'abord un petit résumé des caractéristiques entre surcharge et écrasement.
Caractéristiques de la surcharge :
n le même périmètre (dans la même classe) ;
n Le nom de la fonction est le même mais les paramètres sont différents ;
n Le mot-clé virtual est facultatif.
Le remplacement signifie qu'une fonction de classe dérivée couvre une fonction de classe de base. Les caractéristiques du remplacement sont :
n portées différentes (situées respectivement dans les classes dérivées et les classes de base) ;
n Le nom de la fonction et les paramètres sont les mêmes ;
n Les fonctions de la classe de base doivent avoir le mot-clé virtual. (S'il n'y a pas de mot-clé virtuel, cela s'appelle un masquage caché)
Si la classe de base a plusieurs versions surchargées d'une fonction et que vous remplacez (remplacez) une ou plusieurs versions de fonction dans la classe de base dans la classe dérivée, ou en ajoutez de nouvelles dans la version de fonction de la classe dérivée (même nom de fonction, paramètres différents) , alors toutes les versions surchargées de la classe de base sont bloquées, que nous appelons ici cachées. Ainsi, en général, lorsque vous souhaitez utiliser une nouvelle version de fonction dans une classe dérivée et que vous souhaitez utiliser la version de fonction de la classe de base, vous devez remplacer toutes les versions surchargées de la classe de base dans la classe dérivée. Si vous ne souhaitez pas remplacer la version de fonction surchargée de la classe de base, vous devez utiliser l'exemple 4 ou l'exemple 5 pour déclarer explicitement la portée de l'espace de noms de la classe de base.
En fait, le compilateur C++ estime qu'il n'y a aucune relation entre les fonctions portant le même nom de fonction et des paramètres différents. Il s'agit simplement de deux fonctions non liées. C'est juste que le langage C++ a introduit les concepts de surcharge et d'écrasement afin de simuler le monde réel et de permettre aux programmeurs de traiter les problèmes du monde réel de manière plus intuitive. La surcharge s'effectue dans la même étendue d'espace de noms, tandis que la substitution s'effectue dans des étendues d'espace de noms différentes. Par exemple, la classe de base et la classe dérivée sont deux étendues d'espace de noms différentes. Pendant le processus d'héritage, si une classe dérivée porte le même nom qu'une fonction de classe de base, la fonction de classe de base sera masquée. Bien entendu, la situation évoquée ici est qu’il n’y a pas de mot-clé virtuel devant la fonction de classe de base. Nous discuterons séparément de la situation lorsqu'il existe un mot-clé virtuel.
Une classe héritée remplace une version de fonction de la classe de base pour créer sa propre interface fonctionnelle. À l'heure actuelle, le compilateur C++ pense que puisque vous souhaitez maintenant utiliser l'interface réécrite de la classe dérivée, l'interface de ma classe de base ne vous sera pas fournie (vous pouvez bien sûr utiliser la méthode de déclaration explicite de la portée de l'espace de noms, voir [Bases du C++] Surcharge, écrasement, polymorphisme et masquage de fonctions (1)). Il ignore que l'interface de votre classe de base présente des caractéristiques de surcharge. Si vous souhaitez continuer à conserver la fonctionnalité de surcharge dans la classe dérivée, fournissez vous-même la fonctionnalité de surcharge d'interface. Par conséquent, dans une classe dérivée, tant que le nom de la fonction est le même, la version de la fonction de la classe de base sera impitoyablement bloquée. Dans le compilateur, le masquage est implémenté via la portée de l'espace de noms.
Par conséquent, pour conserver la version surchargée de la fonction de classe de base dans la classe dérivée, vous devez remplacer toutes les versions surchargées de la classe de base. La surcharge n'est valable que dans la classe actuelle et l'héritage perdra les caractéristiques de la surcharge de fonctions. En d’autres termes, si vous souhaitez placer la fonction surchargée de la classe de base dans la classe dérivée héritée, vous devez la réécrire.
"Caché" signifie ici que la fonction de la classe dérivée bloque la fonction de la classe de base du même nom. Faisons également un bref résumé des règles spécifiques :
n Si la fonction de la classe dérivée porte le même nom que la fonction de la classe de base, mais que les paramètres sont différents. A ce moment, si la classe de base ne possède pas le mot-clé virtual, les fonctions de la classe de base seront masquées. (Veillez à ne pas la confondre avec la surcharge. Bien que la fonction avec le même nom et des paramètres différents doive être appelée surcharge, elle ne peut pas être comprise ici comme une surcharge car la classe dérivée et la classe de base ne sont pas dans la même portée d'espace de noms. Ceci est compris comme se cacher)
n Si la fonction de la classe dérivée porte le même nom que la fonction de la classe de base, mais que les paramètres sont différents. À ce stade, si la classe de base possède le mot-clé virtual, les fonctions de la classe de base seront implicitement héritées dans la vtable de la classe dérivée. À ce stade, la fonction dans la vtable de classe dérivée pointe vers l'adresse de fonction de la version de la classe de base. Dans le même temps, cette nouvelle version de fonction est ajoutée à la classe dérivée en tant que version surchargée de la classe dérivée. Mais lorsque le pointeur de classe de base implémente la méthode de fonction d'appel polymorphe, cette nouvelle version de fonction de classe dérivée sera masquée.
n Si la fonction de la classe dérivée a le même nom que la fonction de la classe de base et que les paramètres sont également les mêmes, mais que la fonction de la classe de base n'a pas le mot-clé virtuel. A cette époque, les fonctions de la classe de base sont masquées. (Attention à ne pas le confondre avec la couverture, qui s'entend ici comme dissimulation).
n Si la fonction de la classe dérivée a le même nom que la fonction de la classe de base et que les paramètres sont également les mêmes, mais que la fonction de la classe de base a le mot-clé virtual. A ce moment, les fonctions de la classe de base ne seront pas « masquées ». (Ici, il faut le comprendre comme une couverture ^_^).
Interlude : Lorsqu'il n'y a pas de mot-clé virtuel devant la fonction de classe de base, nous devons le réécrire plus facilement. Lorsqu'il y a le mot-clé virtuel, il est plus raisonnable de l'appeler override. J'espère aussi que. tout le monde peut mieux comprendre le C++. Quelque chose de subtil. Sans plus tarder, illustrons avec un exemple.
Exemple 6
#include <iostream>using namespace std; class Base{public: virtual void fun() { cout << "Base::fun()" << endl;//surcharge 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;}//surcharge}; int main(){ Base *pb = new Derive(); pb->fun(); pb->fun(1); //La phrase suivante est fausse, elle est donc bloquée //pb->fun(1,2); la fonction virtuelle ne peut pas être surchargée, erreur C2661 : 'fun' : aucune fonction surchargée ne prend 2 paramètres cout << endl; pd = new Derive(); pd->fun(1); pd->fun(1,2);//surcharge delete pb; return 0;}/*
Résultats de sortie
Dérive ::fun()
Dérive ::fun(int i)
Dérive ::fun()
Dérive ::fun(int i)
Dérive ::fun(int i,int j)
Appuyez sur n'importe quelle touche pour continuer
*/
Exemple 7-1
#include <iostream> en utilisant l'espace de noms std; class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< class Derive : public Base{}; int main(){ Base *pb = new Derive(); pb->fun(1);//Base::fun(int i) delete pb;}
Exemple 7-2
#include <iostream> en utilisant l'espace de noms std; class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< class Derive : public Base{public: void fun(double d){ cout <<"Derive::fun(double d)"<< endl } }; pb->fun(1);//Base::fun(int i) pb->fun((double)0.01);//Base::fun(int i) delete pb; return 0;}
Exemple 8-1
#include <iostream> en utilisant l'espace de noms std; class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< class Derive : public Base{public: void fun(int i){ cout <<"Derive::fun(int i)"<< int main(){ Base *pb = new Derive(); pb->fun(1);//Derive::fun(int i) delete pb;}
Exemple 8-2
#include <iostream> en utilisant l'espace de noms std; class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< 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;}
Exemple 9
#include <iostream> en utilisant l'espace de noms 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 pb->fun(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();//Derive::fun(int i) // surcharge pd->fun('a');//Derive::fun(char c) //surcharge pd->fun(0.01);//Derive::fun(double d) supprimer pb ; supprimer pd ; renvoyer 0 ;}
Les exemples 7-1 et 8-1 sont faciles à comprendre. J'ai mis ces deux exemples ici pour que chacun puisse faire une comparaison et aider tout le monde à mieux comprendre :
n Dans l'exemple 7-1, la classe dérivée ne couvre pas la fonction virtuelle de la classe de base. À ce stade, l'adresse pointée par le pointeur de fonction dans la vtable de la classe dérivée est l'adresse de fonction virtuelle de la classe de base.
n Dans l'exemple 8-1, la classe dérivée remplace la fonction virtuelle de la classe de base. À ce stade, l'adresse pointée par le pointeur de fonction dans la table virtuelle de la classe dérivée est l'adresse de la propre fonction virtuelle remplacée de la classe dérivée.
Les exemples 7-2 et 8-2 semblent un peu étranges. En fait, si vous les comparez selon les principes ci-dessus, la réponse sera claire :
n Dans l'exemple 7-2, nous avons surchargé une version de fonction pour la classe dérivée : void fun(double d) En fait, ce n'est qu'une dissimulation. Analysons-le spécifiquement. Il existe plusieurs fonctions dans la classe de base et plusieurs fonctions dans la classe dérivée :
type classe de base classe dérivée
Section Vtable
vider le plaisir (int i)
Pointe vers la version de classe de base de la fonction virtuelle void fun(int i)
partie statique
vider le plaisir (double d)
Analysons à nouveau les trois lignes de code suivantes :
Base *pb = new Derive();
pb->fun(1);//Base::fun(int i)
pb->fun((double)0.01);//Base::fun(int i)
La première phrase est la clé.Le pointeur de la classe de base pointe vers l'objet de la classe dérivée.Nous savons qu'il s'agit d'un appel polymorphe.Le pointeur de la classe de base au moment de l'exécution s'avère être un objet de classe dérivée. le type de l'objet d'exécution, donc allez d'abord dans la vtable de la classe dérivée pour trouver la version de la fonction virtuelle de la classe dérivée. On constate que la classe dérivée ne couvre pas la fonction virtuelle de la classe de base. La classe dérivée ne fait qu'un pointeur vers l'adresse de la fonction virtuelle de la classe de base, il est donc naturel d'appeler la version classe de base de la fonction virtuelle. Dans la dernière phrase, le programme recherche toujours la vtable de la classe dérivée et constate qu'il n'y a aucune fonction virtuelle de cette version, il doit donc revenir en arrière et appeler sa propre fonction virtuelle.
Il convient également de mentionner ici que si la classe de base a plusieurs fonctions virtuelles à ce moment-là, le programme affichera « Appel peu clair » lors de la compilation du programme. Les exemples sont les suivants
#include <iostream> en utilisant l'espace de noms std; class Base{public: virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl; <"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);//erreur C2668 : 'fun' : appel ambigu à la fonction surchargée delete pb ; return 0 ;}
Bon, analysons à nouveau l'exemple 8-2.
n Dans l'exemple 8-2, nous avons également surchargé une version de fonction pour la classe dérivée : void fun(double d), et avons également couvert la fonction virtuelle de la classe de base. Analysons-la en détail. Il existe plusieurs fonctions dans la classe de base. , et la classe dérivée a plusieurs fonctions. La classe a plusieurs fonctions :
type classe de base classe dérivée
Section Vtable
vider le plaisir (int i)
vider le plaisir (int i)
partie statique
vider le plaisir (double d)
Dans le tableau, nous pouvons voir que le pointeur de fonction dans la vtable de la classe dérivée pointe vers sa propre adresse de fonction virtuelle remplacée.
Analysons à nouveau les trois lignes de code suivantes :
Base *pb = new Derive();
pb->fun(1);//Derive::fun(int i)
pb->fun((double)0.01);//Derive::fun(int i)
Il n'est pas nécessaire d'en dire plus sur la première phrase. La deuxième phrase consiste à appeler la version de fonction virtuelle de la classe dérivée comme une évidence. La troisième phrase, hé, ça fait encore bizarre. En fait, les programmes C++ sont très. stupide. Lors de l'exécution, ils se plongent dans la table vtable de la classe dérivée, je viens de la regarder et j'ai réalisé qu'il n'y avait pas de version que je voulais, je n'arrive vraiment pas à la comprendre. , pourquoi les pointeurs de la classe de base ne regardent-ils pas autour de eux et ne le recherchent-ils pas ? Haha, il s'avère que la classe de base est si vieille, donc elle doit être presbyte. Partie Vtable (c'est-à-dire la partie statique) et la partie Vtable que vous souhaitez gérer, le vide de la classe dérivée fun(double d) est si loin, on ne le voit pas ! En plus, la classe dérivée doit s'occuper de tout, la classe dérivée n'a-t-elle pas son propre pouvoir ? prends soin d'eux ^_^
Hélas ! Allez-vous soupirer ? Le pointeur de classe de base peut effectuer des appels polymorphes, mais il ne peut jamais effectuer d'appels surchargés aux classes dérivées (voir exemple 6)~~~
Regardons à nouveau l'exemple 9,
L'effet de cet exemple est le même que celui de l'exemple 6, avec le même objectif. Je crois qu'après avoir compris les exemples ci-dessus, c'est aussi un petit baiser.
résumé:
La surcharge sélectionne la version de la fonction à appeler en fonction de la liste des paramètres de la fonction, tandis que le polymorphisme sélectionne la version de la fonction virtuelle à appeler en fonction du type réel de l'objet d'exécution. Le polymorphisme est implémenté via des classes dérivées vers les classes de base. La fonction est implémentée en la remplaçant.Si la classe dérivée ne remplace pas la fonction virtuelle virtuelle de la classe de base, la classe dérivée héritera automatiquement de la fonction virtuelle virtuelle de la classe de base. Version de fonction.À ce stade, peu importe si l'objet pointé par le pointeur de classe de base est un type de base ou un type dérivé, la fonction virtuelle virtuelle de la version de la classe de base sera appelée si la classe dérivée remplace la fonction virtuelle virtuelle ; de la classe de base, il sera appelé au moment de l'exécution selon Le type réel de l'objet est utilisé pour sélectionner la version de la fonction virtuelle virtuelle à appeler. Par exemple, si le type d'objet pointé par le pointeur de la classe de base est un type dérivé. , la version virtuelle de la fonction virtuelle de la classe dérivée sera appelée, réalisant ainsi le polymorphisme.
L'intention initiale de l'utilisation du polymorphisme est de déclarer la fonction comme virtuelle dans la classe de base et de remplacer la version de la fonction virtuelle virtuelle de la classe de base dans la classe dérivée. Notez que le prototype de fonction à ce stade est cohérent avec la classe de base, c'est-à-dire le même nom et le même type de paramètre ; si vous ajoutez une nouvelle version de fonction à la classe dérivée, vous ne pouvez pas appeler dynamiquement la nouvelle version de fonction de la classe dérivée via le pointeur de classe de base. en tant que version surchargée de la classe dérivée. Toujours la même phrase, la surcharge n'est valable que dans la classe actuelle. Que vous la surchargez dans une classe de base ou une classe dérivée, les deux ne sont pas liées l'une à l'autre. Si nous comprenons cela, nous pouvons également comprendre avec succès les résultats des exemples 6 et 9.
La surcharge est liée statiquement, le polymorphisme est lié dynamiquement. Pour expliquer plus en détail, la surcharge n'a rien à voir avec le type d'objet vers lequel pointe réellement le pointeur, et le polymorphisme est lié au type d'objet vers lequel pointe réellement le pointeur. Si un pointeur de classe de base appelle une version surchargée d'une classe dérivée, le compilateur C++ considère cela comme illégal. Le compilateur C++ pense uniquement que le pointeur de classe de base ne peut appeler que la version surchargée de la classe de base et que la surcharge ne fonctionne que dans l'espace de noms. de la classe actuelle. Valable dans le domaine, l'héritage perdra la fonctionnalité de surcharge. Bien sûr, si le pointeur de classe de base appelle à ce moment une fonction virtuelle, Ensuite, il sélectionnera également dynamiquement la version de la fonction virtuelle virtuelle de la classe de base ou la version de la fonction virtuelle virtuelle de la classe dérivée pour effectuer des opérations spécifiques. Ceci est déterminé par le type d'objet réellement pointé par le pointeur de la classe de base, donc la surcharge et les pointeurs. Le type d'objet vers lequel pointe le pointeur n'a rien à voir avec cela ; le polymorphisme est lié au type d'objet vers lequel pointe le pointeur.
Enfin, pour clarifier, les fonctions virtuelles virtuelles peuvent également être surchargées, mais la surcharge ne peut être efficace que dans le cadre de l'espace de noms actuel. Combien d'objets String ont été créés ?
Regardons d'abord un morceau de code :
Code Java
Chaîne str=nouvelle Chaîne("abc");
Ce code est souvent suivi de la question suivante : combien d'objets String sont créés par cette ligne de code ? Je pense que tout le monde connaît cette question et la réponse est bien connue, 2. Ensuite, nous partirons de cette question et passerons en revue certaines connaissances JAVA liées à la création d'objets String.
Nous pouvons diviser la ligne de code ci-dessus en quatre parties : String str, =, "abc" et new String(). String str définit uniquement une variable de type String nommée str, elle ne crée donc pas d'objet ; = initialise la variable str et lui attribue une référence (ou un handle) à un objet, et ne crée évidemment pas d'objet ; maintenant seulement nouveau. Il reste String("abc"). Alors, pourquoi new String("abc") peut-il être considéré comme "abc" et new String() ? Jetons un coup d'œil au constructeur String que nous avons appelé :
Code Java
chaîne publique (chaîne originale) {
//autre code...
}
Comme nous le savons tous, il existe deux méthodes couramment utilisées pour créer des instances (objets) d'une classe :
Utilisez new pour créer des objets.
Appelez la méthode newInstance de la classe Class et utilisez le mécanisme de réflexion pour créer l'objet.
Nous avons utilisé new pour appeler la méthode constructeur ci-dessus de la classe String afin de créer un objet et d'attribuer sa référence à la variable str. En même temps, nous avons remarqué que le paramètre accepté par la méthode constructeur appelée est également un objet String, et cet objet est exactement "abc". À partir de là, nous devons introduire une autre façon de créer un objet String : du texte contenu entre guillemets.
Cette méthode est unique à String et elle est très différente de la nouvelle méthode.
Code Java
Chaîne str="abc";
Il ne fait aucun doute que cette ligne de code crée un objet String.
Code Java
Chaîne a="abc";
Chaîne b="abc";
Et ici ? La réponse est toujours une.
Code Java
Chaîne a="ab"+"cd" ;
Et ici ? La réponse est toujours une. Un peu bizarre ? À ce stade, nous devons introduire un examen des connaissances liées aux pools de chaînes.
Il existe un pool de chaînes dans la machine virtuelle JAVA (JVM), qui stocke de nombreux objets String et peut être partagé, ce qui améliore l'efficacité. Puisque la classe String est finale, sa valeur ne peut pas être modifiée une fois créée, nous n'avons donc pas à nous soucier de la confusion du programme causée par le partage d'objets String. Le pool de chaînes est géré par la classe String et nous pouvons appeler la méthode intern() pour accéder au pool de chaînes.
Revenons à String a="abc";. Lorsque cette ligne de code est exécutée, la machine virtuelle JAVA recherche d'abord dans le pool de chaînes pour voir si un tel objet avec la valeur "abc" existe déjà. La valeur de retour de la méthode de classe String equals(Object obj). Si tel est le cas, aucun nouvel objet ne sera créé et une référence à l'objet existant sera renvoyée directement ; sinon, l'objet sera d'abord créé, puis ajouté au pool de chaînes, puis sa référence sera renvoyée. Par conséquent, il ne nous est pas difficile de comprendre pourquoi les deux premiers des trois exemples précédents ont cette réponse.
Pour le troisième exemple :
Code Java
Chaîne a="ab"+"cd" ;
Parce que la valeur de la constante est déterminée au moment de la compilation. Ici, "ab" et "cd" sont des constantes, donc la valeur de la variable a peut être déterminée au moment de la compilation. L'effet compilé de cette ligne de code est équivalent à :
Code Java
Chaîne a="abcd";
Par conséquent, un seul objet « abcd » est créé ici et il est enregistré dans le pool de chaînes.
Maintenant, la question revient : toutes les chaînes obtenues après la connexion « + » seront-elles ajoutées au pool de chaînes ? Nous savons tous que "==" peut être utilisé pour comparer deux variables. Il se présente dans les deux situations suivantes :
Si deux types de base (char, byte, short, int, long, float, double, boolean) sont comparés, on juge si leurs valeurs sont égales.
Si le tableau compare deux variables d'objet, il est jugé si leurs références pointent vers le même objet.
Ensuite, nous utiliserons "==" pour faire quelques tests. Pour faciliter l'explication, nous considérons le pointage vers un objet qui existe déjà dans le pool de chaînes comme l'objet ajouté au pool de chaînes :
Code Java
public class StringTest { public static void main(String[] args) { String a = "ab";// Créez un objet et ajoutez-le au pool de chaînes System.out.println("String a = /"ab/" ; "); String b = "cd";// Un objet est créé et ajouté au pool de chaînes System.out.println("String b = /"cd/";"); String c = "abcd"; // Un objet est créé et ajouté au pool de chaînes String d = "ab" + "cd"; // Si d et c pointent vers le même objet, cela signifie que d a également été ajouté au pool de chaînes if (d == c ) { System.out.println("/"ab/"+/"cd/" L'objet créé/" est ajouté à/" le pool de chaînes" } // Si d et c ne pointent pas vers le même object, Cela signifie que d n'a pas été ajouté au pool de chaînes else { System.out.println("/"ab/"+/"cd/" L'objet créé/"n'est pas ajouté/" au pool de chaînes"); } String e = a + "cd"; c Pointe vers le même objet, cela signifie que e a également été ajouté au pool de chaînes if (e == c) { System.out.println(" a +/"cd/" L'objet créé/"joined/" string milieu de la piscine"); } // Si e et c ne pointent pas vers le même objet, cela signifie que e n'a pas été ajouté au pool de chaînes sinon { System.out.println(" a +/"cd/" objet créé/"non ajouté/" au string pool" ); } String f = "ab" + b; // Si f et c pointent vers le même objet, cela signifie que f a également été ajouté au pool de chaînes if (f == c) { System.out .println("/ Objet créé par "ab/"+ b /"Ajouté/" au pool de chaînes"); } // Si f et c ne pointent pas vers le même objet, cela signifie que f n'a pas été ajouté au pool de chaînes else { System.out.println("/" ab/" + b objet créé/"non ajouté/" au pool de chaînes"); } String g = a + b; // Si g et c pointent vers le même objet, cela signifie que g a également été ajouté au pool de chaînes if ( g == c) { System.out.println(" a + b L'objet créé/"ajouté/" au pool de chaînes"); } // Si g et c ne pointent pas vers le même objet, cela signifie que g n'a pas été ajouté au pool de chaînes else { System.out.println (" a + b objet créé/"non ajouté/" au pool de chaînes");
Les résultats en cours d'exécution sont les suivants :
Chaîne a = "ab" ;
Chaîne b = "cd" ;
L'objet créé par "ab"+"cd" est "rejoint" dans le pool de chaînes
L'objet créé par un + "cd" n'est "pas ajouté" au pool de chaînes.
L'objet créé par "ab" + b n'est "pas ajouté" au pool de chaînes.
L'objet créé par a + b n'est "pas ajouté" au pool de chaînes. D'après les résultats ci-dessus, nous pouvons facilement voir que seuls les nouveaux objets générés en utilisant des connexions "+" entre les objets String créés à l'aide de guillemets pour inclure du texte seront ajoutés. .dans le pool de chaînes. Pour toutes les expressions de connexion "+" contenant des objets créés en mode nouveau (y compris null), les nouveaux objets qu'elles génèrent ne seront pas ajoutés au pool de chaînes, et nous n'entrerons pas dans les détails à ce sujet.
Mais il y a une situation qui requiert notre attention. Veuillez regarder le code ci-dessous :
Code Java
public class StringStaticTest { // Constante A public static final String A = "ab"; // Constante B public static final String B = "cd"; public static void main(String[] args) { // Utiliser deux constantes +Connect pour initialiser s String s = A + B; String t = "abcd"; if (s == t) { System.out.println("s est égal à t, ce sont le même objet" } else) { System.out.println("s n'est pas égal à t, ce n'est pas le même objet" } } }
Le résultat de l'exécution de ce code est le suivant :
s est égal à t. C’est le même objet. Pourquoi ? La raison en est que pour une constante, sa valeur est fixe, elle peut donc être déterminée au moment de la compilation, alors que la valeur d'une variable ne peut être déterminée qu'au moment de l'exécution, car cette variable peut être appelée par différentes méthodes, donc peut provoquer le valeur à changer. Dans l'exemple ci-dessus, A et B sont des constantes et leurs valeurs sont fixes, donc la valeur de s est également fixe et elle est déterminée lors de la compilation de la classe. C'est à dire :
Code Java
Chaîne s=A+B ;
Équivalent à :
Code Java
Chaîne s="ab"+"cd" ;
Permettez-moi de modifier légèrement l'exemple ci-dessus et de voir ce qui se passe :
Code Java
public class StringStaticTest { // Constante A public static final String A ; // Constante B public static final String B ; // Initialisez s en connectant les deux constantes avec + String s = A + B; String t = "abcd" if (s == t) { System.out.println("s est égal à t, ce sont le même objet"); } else { System.out.println("s n'est pas égal à t, ce n'est pas le même objet" } } }
Le résultat de son opération est le suivant :
s n'est pas égal à t. Ce ne sont pas les mêmes objets mais ont été légèrement modifiés. Le résultat est exactement le contraire de l'exemple précédent. Analysons-le à nouveau. Bien que A et B soient définis comme des constantes (ne peuvent être affectées qu’une seule fois), elles ne sont pas affectées immédiatement. Avant que la valeur de s ne soit calculée, le moment où ils sont attribués et la valeur qui leur est attribuée sont tous des variables. Par conséquent, A et B se comportent comme une variable avant de se voir attribuer une valeur. Ensuite, les s ne peuvent pas être déterminés au moment de la compilation, mais ne peuvent être créés qu'au moment de l'exécution.
Étant donné que le partage d'objets dans le pool de chaînes peut améliorer l'efficacité, nous encourageons tout le monde à créer des objets String en incluant du texte entre guillemets. En fait, c'est ce que nous utilisons souvent en programmation.
Jetons ensuite un coup d'œil à la méthode intern(), qui est définie comme suit :
Code Java
public native String stagiaire ();
Il s'agit d'une méthode native. Lors de l'appel de cette méthode, la machine virtuelle JAVA vérifie d'abord si un objet avec une valeur égale à l'objet existe déjà dans le pool de chaînes. Si tel est le cas, elle renvoie une référence à l'objet dans le pool de chaînes. Sinon, elle crée d'abord un objet. objet dans le pool de chaînes. Objet String avec la même valeur, puis renvoie sa référence.
Regardons ce code :
Code Java
public class StringInternTest { public static void main(String[] args) { // Utilisez un tableau de caractères pour initialiser a pour éviter qu'un objet avec la valeur "abcd" existe déjà dans le pool de chaînes avant la création de a String a = new String ( new char[] { 'a', 'b', 'c', 'd' }); String b = a.intern(); if (b == a) { System.out.println("b a été ajouté au pool de chaînes, aucun nouvel objet n'a été créé"); } else { System.out.println("b n'a pas été ajouté au pool de chaînes, aucun nouvel objet n'a été créé"); } } }
Résultats en cours d'exécution :
b n'a pas été ajouté au pool de chaînes et un nouvel objet a été créé. Si la méthode intern() de la classe String ne trouve pas d'objet avec la même valeur, elle ajoute l'objet actuel au pool de chaînes puis renvoie son objet. référence, puis b et a Pointe vers le même objet ; sinon, l'objet pointé par b est nouvellement créé par la machine virtuelle JAVA dans le pool de chaînes, mais sa valeur est la même que a. Le résultat de l'exécution du code ci-dessus ne fait que confirmer ce point.
Enfin, parlons du stockage des objets String dans la machine virtuelle JAVA (JVM) et de la relation entre le pool de chaînes et le tas et la pile. Examinons d'abord la différence entre tas et pile :
Pile : enregistre principalement les types de base (ou types intégrés) (char, byte, short, int, long, float, double, boolean) et les références d'objets peuvent être partagées, et leur vitesse est la deuxième plus rapide que celle d'enregistrement. tas.
Heap : utilisé pour stocker des objets.
Lorsque nous examinons le code source de la classe String, nous constaterons qu'elle possède un attribut value, qui stocke la valeur de l'objet String. Le type est char[], ce qui montre également qu'une chaîne est une séquence de caractères.
Lors de l'exécution de String a="abc";, la machine virtuelle JAVA crée trois valeurs de caractères 'a', 'b' et 'c' dans la pile, puis crée un objet String dans le tas, sa valeur (value ) est un tableau de trois valeurs char qui vient d'être créée sur la pile {'a', 'b', 'c'} Enfin, l'objet String nouvellement créé sera ajouté au pool de chaînes. Si nous exécutons ensuite le code String b=new String("abc"); puisque "abc" a été créé et enregistré dans le pool de chaînes, la machine virtuelle JAVA créera uniquement un nouvel objet String dans le tas, mais son value correspond aux trois valeurs de type char « a », « b » et « c » créées sur la pile lors de l'exécution de la ligne de code précédente.
À ce stade, nous sommes déjà assez clairs sur la question de savoir pourquoi String str=new String("abc") soulevée au début de cet article crée deux objets.