Héritage en C ++: débutant, intermédiaire, avancé

Cet article décrit l'héritage à trois niveaux: débutant, intermédiaire et avancé. Expert no. Et pas un mot sur SOLID. Honnêtement.


Débutant


Qu'est-ce que l'héritage?


L'héritage est l'un des principes fondamentaux de la POO. Selon elle, une classe peut utiliser les variables et les méthodes d'une autre classe comme les siennes.


Une classe qui hérite de données est appelée une sous-classe, une classe dérivée ou une classe enfant. La classe dont les données ou les méthodes sont héritées est appelée une super classe, une classe de base ou une classe parente. Les termes «parent» et «enfant» sont extrêmement utiles pour comprendre l'héritage. Lorsqu'un enfant reçoit les caractéristiques de ses parents, la classe dérivée reçoit les méthodes et les variables de la classe de base.


L'héritage est utile car il vous permet de structurer et de réutiliser du code, qui à son tour peut accélérer considérablement le processus de développement. Malgré cela, l'héritage doit être utilisé avec prudence, car la plupart des modifications apportées à la superclasse affecteront toutes les sous-classes, ce qui peut entraîner des conséquences imprévues.


Dans cet exemple, la méthode turn_on() et la variable serial_number pas serial_number déclarées ou définies dans la sous-classe Computer . Cependant, ils peuvent être utilisés car ils sont hérités de la classe de base.


Remarque importante : les variables et méthodes privées ne peuvent pas être héritées.


 #include <iostream> using namespace std; class Device { public: int serial_number = 12345678; void turn_on() { cout << "Device is on" << endl; } private: int pincode = 87654321; }; class Computer: public Device {}; int main() { Computer Computer_instance; Computer_instance.turn_on(); cout << "Serial number is: " << Computer_instance.serial_number << endl; // cout << "Pin code is: " << Computer_instance.pincode << endl; // will cause compile time error return 0; } 

Types d'héritage


Il existe plusieurs types d'héritage en C ++:


  • public - les données publiques ( public ) et protégées ( protected ) sont héritées sans changer le niveau d'accès à celles-ci;
  • protégé ( protected ) - toutes les données héritées deviennent protégées;
  • privé - toutes les données héritées deviennent privées.

Pour la classe de base Device , le niveau d'accès aux données ne change pas, mais comme la classe dérivée Computer hérite des données privées, les données deviennent privées pour la classe Computer .


 #include <iostream> using namespace std; class Device { public: int serial_number = 12345678; void turn_on() { cout << "Device is on" << endl; } }; class Computer: private Device { public: void say_hello() { turn_on(); cout << "Welcome to Windows 95!" << endl; } }; int main() { Device Device_instance; Computer Computer_instance; cout << "\t Device" << endl; cout << "Serial number is: "<< Device_instance.serial_number << endl; Device_instance.turn_on(); // cout << "Serial number is: " << Computer_instance.serial_number << endl; // Computer_instance.turn_on(); // will cause compile time error cout << "\t Computer" << endl; Computer_instance.say_hello(); return 0; } 

La classe Computer utilise désormais la méthode turn_on() comme n'importe quelle méthode privée: turn_on() peut être appelée à partir de la classe, mais essayer de l'appeler directement à partir de main entraînera une erreur au moment de la compilation. Pour la classe de base Device , la méthode turn_on() restée publique et peut être appelée depuis main .


Constructeurs et destructeurs


En C ++, les constructeurs et destructeurs ne sont pas hérités. Cependant, ils sont appelés lorsque la classe enfant initialise son objet. Les constructeurs sont appelés hiérarchiquement les uns après les autres, en commençant par la classe de base et en terminant par la dernière classe dérivée. Les destructeurs sont appelés dans l'ordre inverse.


Remarque importante: cet article ne couvre pas les destructeurs virtuels. Des informations supplémentaires sur ce sujet peuvent être trouvées, par exemple, dans cet article sur le Habr .


 #include <iostream> using namespace std; class Device { public: // constructor Device() { cout << "Device constructor called" << endl; } // destructor ~Device() { cout << "Device destructor called" << endl; } }; class Computer: public Device { public: Computer() { cout << "Computer constructor called" << endl; } ~Computer() { cout << "Computer destructor called" << endl; } }; class Laptop: public Computer { public: Laptop() { cout << "Laptop constructor called" << endl; } ~Laptop() { cout << "Laptop destructor called" << endl; } }; int main() { cout << "\tConstructors" << endl; Laptop Laptop_instance; cout << "\tDestructors" << endl; return 0; } 

Constructeurs: Device -> Computer -> Laptop .
Destructeurs: Laptop -> Computer -> Device .


Héritage multiple


L'héritage multiple se produit lorsqu'une sous-classe a deux ou plusieurs superclasses. Dans cet exemple, la classe Laptop hérite à la fois de Monitor et Computer en même temps.


 #include <iostream> using namespace std; class Computer { public: void turn_on() { cout << "Welcome to Windows 95" << endl; } }; class Monitor { public: void show_image() { cout << "Imagine image here" << endl; } }; class Laptop: public Computer, public Monitor {}; int main() { Laptop Laptop_instance; Laptop_instance.turn_on(); Laptop_instance.show_image(); return 0; } 

Problèmes d'héritage multiples


L'héritage multiple nécessite une conception soignée, car il peut entraîner des conséquences imprévues. La plupart de ces conséquences sont causées par l'ambiguïté de l'héritage. Dans cet exemple, Laptop hérite de la méthode turn_on() des deux parents et la méthode à appeler n'est pas claire.


 #include <iostream> using namespace std; class Computer { private: void turn_on() { cout << "Computer is on." << endl; } }; class Monitor { public: void turn_on() { cout << "Monitor is on." << endl; } }; class Laptop: public Computer, public Monitor {}; int main() { Laptop Laptop_instance; // Laptop_instance.turn_on(); // will cause compile time error return 0; } 

Malgré le fait que les données privées ne soient pas héritées, il est impossible de résoudre l'héritage ambigu en changeant le niveau d'accès aux données en privé. Lors de la compilation, une recherche de méthode ou de variable a d'abord lieu, puis vérifie le niveau d'accès à ces dernières.


Intermédiaire


Problème de losange



Le problème du diamant est un problème classique dans les langues qui prennent en charge l'héritage multiple. Ce problème se produit lorsque les classes B et C héritent de A et que la classe D hérite de B et C


Par exemple, les classes A , B et C définissent la méthode print_letter() . Si print_letter() sera appelé par la classe D , la méthode à appeler n'est pas claire - une méthode de classe A , B ou C Différentes langues ont différentes approches pour résoudre des problèmes en forme de diamant. En C ++, la solution au problème est laissée à la discrétion du programmeur.

Le problème en forme de losange est principalement un problème de conception, et il devrait être fourni au stade de la conception. Au stade du développement, il peut être résolu comme suit:


  • appeler la méthode d'une superclasse spécifique;
  • désigner l'objet de la sous-classe comme un objet d'une super-classe particulière;
  • remplacer la méthode problématique dans la dernière classe enfant (dans le code, turn_on() dans la sous-classe Laptop ).

 #include <iostream> using namespace std; class Device { public: void turn_on() { cout << "Device is on." << endl; } }; class Computer: public Device {}; class Monitor: public Device {}; class Laptop: public Computer, public Monitor { /* public: void turn_on() { cout << "Laptop is on." << endl; } // uncommenting this function will resolve diamond problem */ }; int main() { Laptop Laptop_instance; // Laptop_instance.turn_on(); // will produce compile time error // if Laptop.turn_on function is commented out // calling method of specific superclass Laptop_instance.Monitor::turn_on(); // treating Laptop instance as Monitor instance via static cast static_cast<Monitor&>( Laptop_instance ).turn_on(); return 0; } 

Si la méthode turn_on() pas été remplacée dans Laptop, l'appel à Laptop_instance.turn_on() entraînera une erreur de compilation. Un objet Laptop peut accéder turn_on() deux turn_on() méthode turn_on() : Device:Computer:Laptop.turn_on() et Device:Monitor:Laptop.turn_on() .


Le problème du diamant: constructeurs et destructeurs


Comme en C ++, lorsque l'objet de la classe enfant est initialisé, les constructeurs de toutes les classes parentes sont appelés, un autre problème se pose: le constructeur de la classe de base Device sera appelé deux fois.


 #include <iostream> using namespace std; class Device { public: Device() { cout << "Device constructor called" << endl; } }; class Computer: public Device { public: Computer() { cout << "Computer constructor called" << endl; } }; class Monitor: public Device { public: Monitor() { cout << "Monitor constructor called" << endl; } }; class Laptop: public Computer, public Monitor {}; int main() { Laptop Laptop_instance; return 0; } 

Héritage virtuel


L'héritage virtuel empêche plusieurs objets de classe de base d'apparaître dans la hiérarchie d'héritage. Ainsi, le constructeur de la classe de base Device ne sera appelé qu'une seule fois et un appel à la méthode turn_on() sans la turn_on() dans la classe enfant ne provoquera pas d'erreur de compilation.


 #include <iostream> using namespace std; class Device { public: Device() { cout << "Device constructor called" << endl; } void turn_on() { cout << "Device is on." << endl; } }; class Computer: virtual public Device { public: Computer() { cout << "Computer constructor called" << endl; } }; class Monitor: virtual public Device { public: Monitor() { cout << "Monitor constructor called" << endl; } }; class Laptop: public Computer, public Monitor {}; int main() { Laptop Laptop_instance; Laptop_instance.turn_on(); return 0; } 

Remarque : l'héritage virtuel dans les classes Computer et Monitor n'autorisera pas l'héritage rhomboïde si la classe enfant Laptop n'hérite pas virtuellement de la classe Device ( class Laptop: public Computer, public Monitor, public Device {}; ).


Classe abstraite


En C ++, une classe dans laquelle existe au moins une méthode virtuelle pure est considérée comme abstraite. Si la méthode virtuelle n'est pas remplacée dans la classe enfant, le code ne sera pas compilé. De plus, en C ++, il est impossible de créer un objet d'une classe abstraite - une tentative provoquera également une erreur lors de la compilation.


 #include <iostream> using namespace std; class Device { public: void turn_on() { cout << "Device is on." << endl; } virtual void say_hello() = 0; }; class Laptop: public Device { public: void say_hello() { cout << "Hello world!" << endl; } }; int main() { Laptop Laptop_instance; Laptop_instance.turn_on(); Laptop_instance.say_hello(); // Device Device_instance; // will cause compile time error return 0; } 

Interface


C ++, contrairement à certains langages OOP, ne fournit pas de mot clé distinct pour désigner une interface. Cependant, l'implémentation de l'interface est possible en créant une classe abstraite pure - une classe dans laquelle il n'y a que des déclarations de méthode. Ces classes sont également souvent appelées classes de base abstraites (ABC).


 #include <iostream> using namespace std; class Device { public: virtual void turn_on() = 0; }; class Laptop: public Device { public: void turn_on() { cout << "Device is on." << endl; } }; int main() { Laptop Laptop_instance; Laptop_instance.turn_on(); // Device Device_instance; // will cause compile time error return 0; } 

Avancé


Bien que l'héritage soit un principe fondamental de la POO, il doit être utilisé avec prudence. Il est important de penser que tout code qui sera utilisé est susceptible d'être modifié et peut être utilisé d'une manière qui n'est pas évidente pour le développeur.


Héritage d'une classe implémentée ou partiellement implémentée


Si l'héritage ne provient pas d'une interface (une pure classe abstraite dans le contexte de C ++), mais d'une classe dans laquelle il existe des implémentations, il convient de considérer que l'héritier est connecté à la classe parente par la connexion la plus proche possible. La plupart des modifications apportées à la classe parent peuvent affecter l'héritier, ce qui peut entraîner un comportement inattendu. De tels changements dans le comportement de l'héritier ne sont pas toujours évidents - une erreur peut se produire dans le code déjà testé et fonctionnel. Cette situation est exacerbée par la présence d'une hiérarchie de classes complexe. Il faut toujours se rappeler que le code peut être modifié non seulement par la personne qui l'a écrit, et les chemins d'héritage évidents pour l'auteur peuvent ne pas être pris en compte par ses collègues.


En revanche, il convient de noter que l'héritage de classes partiellement implémentées présente un avantage indéniable. Les bibliothèques et les frameworks fonctionnent souvent comme suit: ils fournissent à l'utilisateur une classe abstraite avec plusieurs méthodes virtuelles et plusieurs implémentées. Ainsi, la plus grande quantité de travail a déjà été effectuée - la logique complexe a déjà été écrite et l'utilisateur ne peut personnaliser la solution prête à l'emploi qu'en fonction de ses besoins.


Interface


L'héritage d'une interface (pure classe abstraite) présente l'héritage comme une opportunité de structurer le code et de protéger l'utilisateur. Étant donné que l'interface décrit le travail que fera la classe d'implémentation, mais ne décrit pas comment, tout utilisateur de l'interface est protégé contre les modifications de la classe qui implémente cette interface.


Interface: Exemple d'utilisation


Tout d'abord, il convient de noter que l'exemple est étroitement lié au concept de polymorphisme, mais sera considéré dans le contexte de l'héritage d'une pure classe abstraite.


Une application qui exécute une logique métier abstraite doit être configurée à partir d'un fichier de configuration distinct. À un stade précoce de développement, le formatage de ce fichier de configuration n'était pas entièrement formé. Passer l'analyse de fichiers derrière une interface offre plusieurs avantages.


Le manque de clarté concernant le formatage du fichier de configuration ne ralentit pas le processus de développement du programme principal. Deux développeurs peuvent travailler en parallèle - l'un sur la logique métier et l'autre sur l'analyseur. Puisqu'ils interagissent via cette interface, chacun d'eux peut fonctionner de manière indépendante. Cette approche facilite le codage des tests unitaires avec du code, car les tests nécessaires peuvent être écrits à l'aide de mock pour cette interface.


De plus, lors de la modification du format de fichier de configuration, la logique métier de l'application n'est pas affectée. La seule chose qui nécessite une transition complète d'un formatage à un autre est d'écrire une nouvelle implémentation de la classe abstraite déjà existante (classe analyseur). De plus, le retour au format de fichier d'origine nécessite un travail minimal - remplacer un analyseur existant par un autre.


Conclusion


L'héritage offre de nombreux avantages, mais doit être soigneusement conçu pour éviter les problèmes qu'il présente. Dans le contexte de l'héritage, C ++ fournit une large gamme d'outils qui ouvre une tonne de possibilités au programmeur.


Et SOLID est bon.

Source: https://habr.com/ru/post/fr445948/


All Articles