Les bases du C++: Pointeurs, Références, Héritage, Polymorphisme

Comprendre les bases en C++

L’objectif de cet article est de comprendre de nombreux termes complexes en informatique, liés à la POO (Programmation Orientée Objet).

La POO permet d’élaborer des programmes complexes de façon simple et structurée, en C++, Java, et dans d’autres langages de programmation (PHP, Python, etc..).

Par exemple, la POO permet de créer des programmes de gestion (de ses comptes en banque, d’un parking, de salariés dans une entreprise, etc), ou encore de réaliser des jeux (comme le puissance 4, Pokémon, les échecs, etc). Il existe plein d’autres applications à la POO.

Voici le sommaire de l’article afin de s’y retrouver (en C++) :

  • 1) L’architecture d’un programme (.h, .cpp, main.cpp)
  • 2) L’abstraction (dans le .h)
  • 3) L’encapsulation (dans le .h)
  • 4) Les méthodes et attributs (dans le .h)
  • 5) Le constructeur
  • 6) L’accesseur
  • 7) Premier programme en POO
  • 8) Héritage
  • 9) Polymorphisme
  • 10) Pointeurs et Références

Si le programme ci-dessous vous semble impossible à comprendre, ne vous inquiétez pas ; une fois arrivé au point numéro 7, il vous sera facile de le décrypter, et même de l’améliorer !

Remarque : Si vous n’avez pas téléchargé d’IDE (environnement de développement) pour compiler (faire tourner) vos programmes en C++ sur votre ordinateur, et n’avez pas envie de le faire, il est toujours possible de coder gratuitement sur codeboard.io (inscription gratuite, et ne demande que 1 minute).

1) L’architecture d’un programme en POO

Un programme informatique en POO est composé de trois parties :

  • Un header (.h), qui est un fichier dans lequel on déclare les variables et fonctions.
  • Un fichier (.cpp), qui est un fichier dans lequel on définit les fonctions.
  • Un main (main.cpp), qui est un fichier dans lequel on exécute le programme.

Remarque :

  • On déclare les variables avec des types : int age, string nom.
  • On les définit en rajoutant un égal : int age = 20, string nom = Jack.
  • On déclare les fonctions avec des types de retour : int compteur(), void afficher().
  • On les définit en rajoutant des accolades : void afficher(){cout << « Salut » << endl;}

Le type int s’utilise avec les chiffres, le type string pour les mots (chaîne de caractères).

2) L’abstraction (dans le .h)

L’abstraction correspond à la création d’une classe, qui facilite l’utilisation d’objets.

Un objet (aussi appelé instance) est un mélange de plusieurs variables. Par exemple, un Chat est une instance, qui est composée de variables (âge, couleur de poils, race).

Une classe regroupe toutes les variables et fonctions relatives à l’objet. Par exemple, si l’objet est le Chat, sa classe sera Animal. Dans cette classe Animal, on pourra aussi y intégrer d’autres animaux comme le Chien, à condition que les variables et fonctions peuvent être utilisées pour décrire aussi bien le Chat que le Chien.

Dans le programme, on trouve l’objet dans le fichier main (Animal chat (« nom », « race »)).

3) L’encapsulation (dans le .h)

L’encapsulation permet de masquer les informations complexes à l’utilisateur, et de lui permettre d’accéder uniquement aux classes qui peuvent l’intéresser. On utilise généralement trois types d’accès :

  • Public autorise l’accès à toutes les fonctions de n’importe quelle classe.
  • Private autorise l’accès seulement aux fonctions de la classe mère.
  • Protected autorise l’accès seulement aux fonctions de la classe mère et filles.

L’accès Protected sera davantage développé au point 8, concernant l’héritage.

public, protected, private

4) Les méthodes et attributs (dans le .h)

  • Les méthodes sont les fonctions que l’on déclare dans la partie public.
  • Les attributs sont les variables que l’on déclare dans la partie private.

Par défaut, tous les éléments d’une classe sont en private.

Par exemple :

  • Dans public, on déclare une méthode (fonction) : void afficher();
  • Dans private, on déclare un attribut (variable) : string m_nom;

La variable m_nom se trouvera en private, et la variable nom dans le main.

5) Le constructeur (.h, .cpp, main.cpp)

Le constructeur permet d’initialiser l’objet (Chat) de la classe (Animal).

Trois règles pour le constructeur :

  • Un constructeur a toujours le même nom que la classe.
  • Un constructeur ne renvoie jamais rien (pas de return à l’intérieur).
  • Un constructeur par défaut est un constructeur qui n’a aucun paramètre.

Par exemple :

Dans le header, dans la classe Animal, en public, on déclare le constructeur (avec deux paramètres) :

  • Animal (string nom, string race);

Dans le fichier.cpp, on définit le constructeur (lié à la classe Animal) :

  • Animal::Animal(string nom, string race)
  • {m_nom = nom;
  • m_race = race;}

Remarque : les (::) sont des opérateurs de résolution de portée permettant de lier la fonction à la classe.

Dans le main, on initialise quatre objets :

  • Animal Chat1 (« Chat », « Abyssin »);
  • Animal Chat2 (« Chat », « Persan »);
  • Animal Chien1 (« Chien », « Rottweiler »);
  • Animal Chien2 (« Chien », « Labrador »);

Remarque : Un constructeur peut avoir un nombre illimité de paramètres, ici nous n’en avons mis que deux pour chaque objet, mais il peut y en avoir beaucoup plus, d’où l’utilité d’un constructeur.

6) L’accesseur (.h, .cpp)

L’accesseur est une fonction getter permettant d’accéder aux attributs, et de les retourner (de les donner), il est complémentaire du constructeur (qui ne retourne rien).

Par exemple :

Dans le header, dans la classe Animal, en public, on déclare les accesseurs :

  • string getNom();
  • string getRace();

Astuce : afin d’éviter de mettre un void devant un get, au lieu d’un int ou d’un string, il est possible de penser à la liaison : string get , par la lettre g.

Dans le fichier.cpp, on définit les accesseurs (liés à la classe Animal) :

  • string Animal::getNom()
  • {return m_nom;}
  • string Animal::getRace()
  • {return m_race;}

Remarque : Contrairement au constructeur, il est nécessaire de créer plusieurs accesseurs, un pour chaque attribut.

7) Premier programme en POO

Pour que le programme fonctionne correctement, il est à chaque fois nécessaire d’y inclure la bibliothèque <iostream> (liée au using namespace std;) , ainsi que <string> pour utiliser les string (types pour chaîne de caractères), en haut de chaque fichier.

Dans le header, on réalise l’abstraction (création de la classe Animal), puis l’encapsulation où on déclare les méthodes (fonctions) dans public et les attributs (variables) dans private.

Dans le fichier.cpp, tout en haut du code, on doit inclure le header (.h) afin de pouvoir définir les fonctions et variables qui ont été déclarées dans le header. Chaque fonction qui est définie dans le fichier.cpp doit être liée à la classe Animal par un opérateur de résolution de portée (Animal::afficher(){…}).

Dans le main.cpp, tout en haut du code, on doit une nouvelle fois inclure le header (.h), qui nous permettra d’avoir accès aux variables et fonctions afin d’exécuter le programme.

Une fois toutes ces étapes réalisées, on compile le programme, et on obtient :

Et à ce moment là, on se dit : Tout ce baratin pour n’afficher que ça !?

Oui, mais l’objectif de ce programme était de comprendre tout le vocabulaire essentiel en POO, maintenant qu’il est mieux compris, il pourra être utilisé afin de créer des programmes plus complexes (mais bien organisés), comme par exemple :

  • Une simulation en C++ d’un trafic routier :

  • Un puissance 4 :

Toutefois, le vocabulaire en POO vu plus haut ne s’arrête pas là, et ne suffit pas à faire des programmes comme le puissance 4. Voici la deuxième partie de l’article :

8) Héritage

L’héritage consiste à créer des sous classes (filles) liées à une classe mère.

Par exemple : Si la classe mère est Animal, on peut créer deux classes filles : Domestique et Sauvage.

Le code de base que nous allons améliorer avec l’héritage est celui vu précédemment :

Tout d’abord, voici l’architecture du début et de la fin des 7 fichiers utiles à l’héritage :

Puisque les fichiers sont séparés, il y a plusieurs compilations ; il est donc nécessaire d’ajouter des balises de garde : #ifndef , #define (au début des headers) et #endif (à la fin), qui permettent d’éviter des problèmes de compilation (causés par des inclusions multiples).

Et dans le main.cpp, on aura :

Ps : Si jamais vous êtes sur téléphone mobile, et que le code du main.cpp ne n’affiche pas, pensez à mettre la version ordinateur de votre mobile, pour voir les lignes de code.

L’objectif pour la réalisation du prochain programme relatif à l’héritage sera :

  • Avoir un attribut m_nom dans la classe Animal, utilisables dans les classes filles.
  • Avoir un attribut m_reclame dans la classe Domestique (qui consiste à afficher le nombre de fois où l’animal domestique a réclamé à manger).
  • Avoir un attribut m_attaque dans la classe Sauvage (qui consiste à afficher le nombre de fois où l’animal domestique a attaqué le camps).

Voici le programme en entier pour l’héritage :

(animal.h) :

Remarque : Quand vous allez recopier le programme pour le compiler sur votre ordinateur, n’oubliez pas le point-virgule après la classe : class Animal {}; (souvent source d’erreurs).

(animal.cpp) :

(domestique.h) :

Remarque : On relie la classe fille (Domestique) aux données publics de la classe mère (Animal) grâce à la ligne de code : class Domestique : public Animal {}

(domestique.cpp) :

(sauvage.h) :

Remarque : On relie la classe fille (Sauvage) aux données publics de la classe mère (Animal) grâce à la ligne de code : class Sauvage : public Animal {}

(sauvage.cpp) :

(main.cpp) :

En compilant le programme, on obtient le résultat suivant :

L’héritage est utile, et suffit dans de nombreux cas à réaliser des programmes, mais parfois il est nécessaire d’améliorer l’héritage, et cela avec du polymorphisme.

9) Polymorphisme

Le polymorphisme qui signifie « plusieurs formes », possède la capacité de prendre plusieurs formes ; il est lié à l’héritage.

Par exemple, la fonction membre afficher() dans la classe Animal est la même fonction afficher() présente dans les classes filles (Domestique et Sauvage), mais appliquée différemment (affichant un message différent à chaque fois) :

Ce pseudo-code (mieux rédigé par la suite), donne le résultat suivant :

Dans l’exemple ci-dessus, on utilise virtual void afficher() const (dans le header de la classe mère) qui permet d’utiliser le polymorphisme ; si nous n’avions pas mis le mot clef virtual, le polymorphisme n’aurait pas pu être réalisé, et la console aurait seulement affiché :

Sans le mot virtual devant la fonction afficher() de la classe mère, nous n’avons pas de polymorphisme ; on ne bénéficie donc pas de la précision des fonctions afficher() issues des classes filles, mais seulement celle de la classe mère.

Pour le polymorphisme, tout se joue dans deux fichiers : le header (contenant la classe mère) et le fichier main (contenant une fonction extérieure à la fonction main).

Voici le programme en entier pour le polymorphisme :

Remarque : les débuts et fins de fichiers (comprenant les balises et bibliothèques) sont à rajouter, et sont exactement les mêmes que pour l’héritage, vu juste au-dessus.

(animal.h) :

Remarque : virtual double dormir() const = 0; est une fonction d’initialisation appelée fonction virtuelle pure ; lorsqu’il y a un moins une fonction virtuelle pure dans la classe mère, celle-ci devient une classe abstraire, et n’est plus utilisable dans la fonction main. Cela n’est pas gênant puisque dans le polymorphisme on utilise essentiellement les classes filles.

(animal.cpp) :

(domestique.h) :

(domestique.cpp) :

(sauvage.h) :

(sauvage.cpp) :

(main.cpp) :

Voici le résultat de la compilation de ce programme :

Le polymorphisme permet donc l’accès aux fonctions des classes filles à partir d’une fonction (autre que le main) prenant en argument une instance de la class mère Animal, comme dans l’exemple suivant où l’instance (l’objet) est la lettre a :

Header (contenant la classe mère) :

  • virtual void afficher() const;

Fichier main (contenant la fonction) :

  • void rapport (const Animal &a)
  • {a.afficher();}

Le symbole & (esperluette) devant l’instance a, est une référence, qui permet de réaliser le polymorphisme. Si l’on ne la met pas, il n’est pas possible de compiler le programme.

10) Pointeurs et Références

Voici les définitions des références et pointeurs, puis deux exemples visuels, pour enfin comprendre et apprendre à les utiliser :

  • Un pointeur est une variable qui permet de stocker l’adresse d’une autre variable.
  • Une référence est une variable qui permet d’accéder à l’adresse d’une autre variable.

Pour exécuter un programme, il faut que celui-ci soit stocké en mémoire. Cette mémoire est constituée de nombreuses petites cases possédant chacune un numéro qui correspond à l’adresse, et chaque adresse peut être occupée par une variable. Par exemple :

  • Pour int x, on réserve une case (adresse) pour la variable x.
  • Pour int x = 1, on attribut à la case (adresse) la valeur 1.
  • Pour void changer(int x){x = 10}, rien ne se passe.

Il faut donc utiliser un pointeur :

  • Pour int *x, on réserve une case pour la variable *x.
  • Pour int *x = new int(1), on attribut à la case la valeur 1.
  • Pour void changer(int *x){*x = 10}, la valeur 1 est remplacée par 10.

Sans pointeur :

Avec pointeur :

Remarque :  Pour afficher l’adresse de la variable il suffit de lui ajouter un & devant le nom de la variable, par exemple &x , de la mettre dans un cout, et cela renvoie l’adresse : 012FFC5C (cette valeur change à chaque exécution du programme).

Concrètement, un pointeur permet de modifier des données à partir d’une fonction, ce qui n’est pas possible sans pointeur.

Exemple :

On crée 3 fonctions différentes, une avec un pointeur, une avec une référence, et une sans rien. Ces trois fonctions sont censée attribuer à la variable x à la valeur 10.

Dans le main, on initialise toutes les variables (a, *b, et c) à la valeur 1, puis on fait appelle aux fonctions précédemment définies pour que la variable x passe de 1 à 10.

On remarque que pour la fonction rien(), la valeur de x est toujours égale à 1, en effet, on ne peut pas modifier les données à partir d’une fonction sans utiliser un pointeur ou une référence. Avec les fonctions pointeur() et reference(), la valeur de x passe bien de 1 à 10.

Voici le programme complet :

Qui affiche :

Nous avons donc vu qu’un pointeur est une variable qui permet de stocker l’adresse d’une autre variable, et permet de modifier des données à partir d’une fonction. Mais l’utilité des pointeurs ne s’arrête pas là, ils sont aussi très utile lors de l’utilisation de plusieurs données dans un tableau.

Nous allons illustrer les pointeurs à l’aide des Pokémons. De nombreux Pokémon peuvent évoluer (se transformer en un nouveau Pokémon), par exemple, Pichu évolue en Pikachu qui évolue ensuite en Raichu.

Supposons que Pichu soit prénommé Sparky, lorsqu’il va évoluer il aura toujours son prénom, quand il sera un Pikachu, son dresseur l’appellera encore Sparky, de même quand il deviendra un Raichu.

En informatique, pour reproduire les évolutions de Pichu, nous avons donc besoin des pointeurs. Pour cela, on crée un tableau contenant Pichu, Pikachu et Raichu, ainsi que le pointeur *sparky dans les fonctions (sparky étant le prénom initial de Pichu).

Voici le programme :

Remarque : Si l’on n’utilise pas les pointeurs (en retirant les * devant sparky), il n’est plus possible d’afficher les évolutions.

Autre remarque : Tous les tableaux en C++ commencent par 0, c’est pour ça que le premier terme du tableau (à savoir Pichu) est donné par sparky[0].

Le programme affiche :

Si l’on souhaite ne pas prendre le risque de faire de confusions entre niveau 2 et case numéro 1 et 2 du tableau, on peut réaliser le remplacement suivant :

niveau_2(sparky[1]) par niveau_II(sparky[II])

Voici le programme amélioré :

Avec les notions vues en héritage, polymorphisme, liées aux pointeurs et références, il est désormais possible de réaliser des mini-jeux (puissance 4, Pokémons, jeu des échecs, etc..), des programmes de gestion (de ses comptes en banque, d’un parking, de salariés dans une entreprise, etc), et plein d’autres choses.

Nous espérons que ces quelques paragraphes sur les différents mots de vocabulaire en C++ (POO), vous auront permis de mieux comprendre ce langage informatique. Si vous avez des questions sur l’article, n’hésitez pas les poster en commentaire.

Adrien Verschaere
Les derniers articles par Adrien Verschaere (tout voir)

Laisser un commentaire