Chapitre 3 – L’héritage
Document réservé
Vous consultez actuellement la présentation publique de ce chapitre.
Les documents PDF complets, comprenant les développements théoriques, les exemples détaillés et les exercices, sont disponibles sur demande.
Pour obtenir un accès, contactez-moi via la page Contact en indiquant les domaines qui vous intéressent (C++, ESP-IDF, électronique, etc.).
3.2 Une classe commune : Vecteur ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.3 Pourquoi ne pas utiliser l’héritage ? . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.4 Un exemple plus naturel : les matrices . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.5.1 Stocker les coefficients . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.5.2 Comment stocker les coefficients . . . . . . . . . . . . . . . . . . . . . . . . .
3.5.3 Construire et détruire une matrice . . . . . . . . . . . . . . . . . . . . . . . . .
3.5.4 Repérer un coefficient dans la mémoire . . . . . . . . . . . . . . . . . . . . . .
3.5.5 Accéder aux coefficients . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.5.7 Modifier un coefficient . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.5.8 Multiplication matricielle . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.5.10 Affectation par copie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.7 Une première classe dérivée : MatriceCarree . . . . . . . . . . . . . . . . . . . . . .
3.7.1 Une classe qui n’est pas encore utilisable . . . . . . . . . . . . . . . . . . . . .
3.8 Le constructeur d’une classe dérivée . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.8.1 La classe de base est construite en premier . . . . . . . . . . . . . . . . . . .
3.8.2 Utiliser les méthodes héritées . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.9 private et protected dans l’héritage . . . . . . . . . . . . . . . . . . . . . . . . . .
3.9.1 Tentative d’accès depuis la classe dérivée . . . . . . . . . . . . . . . . . . . .
3.9.2 Le niveau d’accès protected . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.9.3 Un cas où protected peut être utile . . . . . . . . . . . . . . . . . . . . . . .
3.10 Enrichir la classe dérivée . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.10.1 Calcul du déterminant par développement de cofacteurs . . . . . . . . . . . .
3.10.2 Construire une matrice mineure . . . . . . . . . . . . . . . . . . . . . . . . . .
3.10.3 Calcul récursif du déterminant . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.11 Que reçoit réellement une classe dérivée ? . . . . . . . . . . . . . . . . . . . . . . . . .
3.11.1 Les fonctions membres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.11.2 Les données de la classe de base . . . . . . . . . . . . . . . . . . . . . . . . .
3.11.3 Le cas particulier des constructeurs . . . . . . . . . . . . . . . . . . . . . . . .
3.11.4 Les opérations de copie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.12 Destruction d’un objet dérivé . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.13 Une classe dérivée peut être utilisée comme sa classe de base . . . . . . . . . . . . .
3.14 L’héritage ne fonctionne pas dans les deux sens . . . . . . . . . . . . . . . . . . . . .
3.14.1 Le type et l’état sont deux notions différentes . . . . . . . . . . . . . . . . . .
3.14.2 Construire un nouvel objet spécialisé . . . . . . . . . . . . . . . . . . . . . . .
3.15 L’héritage sur plusieurs niveaux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2026 – C++ Partie II
3.15.1 Une chaîne de constructeurs . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.15.2 Des fonctionnalités héritées sur plusieurs niveaux . . . . . . . . . . . . . . . .
3.15.3 Une propriété qui doit rester vraie . . . . . . . . . . . . . . . . . . . . . . . . .
3.16 Préserver la symétrie de la matrice . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
AccueilDans le chapitre précédent, nous avons construit une classe représentant les vecteurs libres du plan (déplacements libres). Nous souhaitons maintenant étendre notre bibliothèque aux vecteurs de l’espace R3 . Dans une base orthonormée de l’espace R3 , un vecteur possède trois composantes. Par exemple, u = 3 = (2, 3, 6)T . Les deux premières composantes correspondent à celles d’un vecteur du plan. La troisième est associée à un nouvel axe, généralement noté z . En appliquant les principes d’encapsulation étudiés précédemment, nous pouvons construire une nouvelle classe possédant trois membres privés. Code 3.1 – Vecteur3D.h Vecteur3D(double xInitial, double getX() const; double getY() const; double getZ() const; 2026 – C++ Partie II Vecteur3D additionner(const Vecteur3D& autre) const; double norme() const; double produitScalaire(const Vecteur3D& autre) const; // Precondition : le vecteur ne doit pas etre nul. Vecteur3D vecteurUnitaire() const; Le constructeur initialise les trois composantes du vecteur. Vecteur3D::Vecteur3D(double xInitial, Les getters permettent de consulter séparément chacune des composantes. double Vecteur3D::getX() const double Vecteur3D::getY() const double Vecteur3D::getZ() const L’addition de deux vecteurs de l’espace s’effectue composante par composante : (x 1 , y 1 , z 1 )T + (x 2 , y 2 , z 2 )T = (x 1 + x 2 , y 1 + y 2 , z 1 + z 2 )T . La méthode additionner() construit et retourne le vecteur résultant. Vecteur3D Vecteur3D::additionner( const Vecteur3D& autre) const
Accueil2026 – C++ Partie II La norme d’un vecteur de l’espace est donnée par La fonction std::sqrt() étant utilisée dans l’implémentation, le fichier Vecteur3D.cpp doit inclure le fichier d’en-tête <cmath>. double Vecteur3D::norme() const return std::sqrt(x * x + y * y + z * z); Dans une base orthonormée, le produit scalaire de deux vecteurs de l’espace est défini par u · v = xu x v + y u y v + zu z v . Son implémentation est donc : double Vecteur3D::produitScalaire( const Vecteur3D& autre) const Enfin, le vecteur unitaire associé à un vecteur non nul est obtenu en divisant chacune de ses composantes par sa norme. Vecteur3D Vecteur3D::vecteurUnitaire() const Comme pour la classe Vecteur2D, la méthode vecteurUnitaire() possède une précondition : le vecteur qui l’appelle ne doit pas être nul. Le fichier source complet commence donc par les inclusions suivantes : Code 3.2 – Vecteur3D.cpp #include « Vecteur3D.h » // Definitions des methodes de Vecteur3D
Accueil2026 – C++ Partie II Nous pouvons maintenant créer et utiliser des vecteurs de l’espace : Vecteur3D u(2.0, 3.0, 6.0); Vecteur3D v(1.0, -1.0, 2.0); Vecteur3D somme = u.additionner(v); double longueur = u.norme(); double produit = u.produitScalaire(v); Les résultats obtenus sont : u + v = (3, 2, 8)T , u · v = 2 · 1 + 3 · (−1) + 6 · 2 = 11. Puisque Vecteur2D existe déjà, pourrait-on déclarer Vecteur3D comme une classe dérivée de Cette relation serait mathématiquement incorrecte. Un vecteur quelconque de l’espace n’est pas un vecteur du plan. Seuls les vecteurs dont la troisième composante est nulle peuvent être identifiés à des vecteurs du plan. Les classes Vecteur2D et Vecteur3D devront plutôt dériver d’une classe plus générale représentant la notion de vecteur. Comparez les classes Vecteur2D et Vecteur3D. Quelles parties de leur interface et de leur implémentation ont dû être réécrites ? Les deux classes possèdent notamment : • des composantes ; • un constructeur ; • une méthode d’addition ; • une méthode calculant la norme ; • une méthode calculant le produit scalaire ; • une méthode retournant un vecteur unitaire.
Accueil2026 – C++ Partie II Seul le nombre de composantes et, par conséquent, le détail de certains calculs changent. La répétition observée entre les deux classes n’est pas une erreur commise par le programmeur. Elle fait apparaître une propriété commune qui n’avait pas encore été représentée dans notre bibliothèque : unVecteur2D et un Vecteur3D sont tous deux des vecteurs. • Un vecteur de l’espace possède trois composantes. • La classe Vecteur3D reprend de nombreuses caractéristiques de la classe Vecteur2D. • Un Vecteur3D n’est pas un cas particulier deVecteur2D. • Les deux classes peuvent être considérées comme des cas particuliers d’une classe plus Une classe commune : Vecteur ? Nous avons maintenant construit deux classes distinctes : • Vecteur2D, qui représente les vecteurs libres du plan ; • Vecteur3D, qui représente les vecteurs libres de l’espace. Ces deux classes ne décrivent pas le même ensemble mathématique. Un vecteur quelconque de R3 n’est pas un vecteur de R2 , et il serait donc incorrect de considérer Vecteur3D comme une extension En revanche, les deux classes représentent manifestement le même type d’objet mathématique général : un vecteur. Elles possèdent également plusieurs caractéristiques communes : • elles possèdent un certain nombre de composantes ; • elles peuvent être additionnées à un vecteur de même dimension ; • elles possèdent une norme ; • elles permettent de calculer un produit scalaire ; • elles peuvent être associées à un vecteur unitaire lorsqu’elles sont non nulles. La répétition observée entre les classes Vecteur2D et Vecteur3D nous indique donc qu’une notion plus générale manque encore dans notre bibliothèque. Nous allons essayer d’introduire une nouvelle classe appelée Vecteur, chargée de représenter les caractéristiques communes à tous les vecteurs que nous souhaitons manipuler. Les classes Vecteur2D et Vecteur3D deviendront alors deux spécialisations de cette classe générale.
Accueil2026 – C++ Partie II La classe Vecteur ne représente pas ici un vecteur de dimension déterminée. Elle sert à exprimer ce que les classes Vecteur2D et Vecteur3D ont en commun. Nous verrons progressivement quelles informations et quelles opérations peuvent réellement être placées dans cette classe générale. Proposition pour la classe Vecteur : Code 3.3 – Vecteur.h int nombreComposantes; Vecteur(int nombre); int getNombreComposantes() const; La classe Vecteur contient pour l’instant une seule information commune aux différentes représentations : le nombre de composantes. Un objet de type Vecteur2D possède deux composantes, tandis qu’un objet de type Vecteur3D en Le constructeur de la classe Vecteur est placé dans la partie protected. Il pourra ainsi être utilisé par les classes qui hériteront de Vecteur, mais il ne pourra pas être appelé directement depuis la Nous disposons maintenant d’une classe générale représentant une propriété commune à plusieurs classes. Il reste à établir une relation entre cette classe Vecteur et les classes Vecteur2D et Cette relation est précisément ce que permet l’héritage. Pourquoi ne pas utiliser l’héritage ? Nous avons constaté que les classes Vecteur2D et Vecteur3D possèdent de nombreux points communs. Il pourrait donc sembler naturel de chercher à factoriser ces éléments dans une classe plus
Accueil2026 – C++ Partie II générale appelée Vecteur. Cette idée est séduisante, mais elle rencontre rapidement une difficulté fondamentale : les opérations communes aux deux classes ne sont pas nécessairement les mêmes opérations du point de vue de leur implémentation. Considérons par exemple l’addition. Dans le plan, R2 × R2 −→ R2 , alors que dans l’espace, R3 × R3 −→ R3 . Les deux opérations portent le même nom et obéissent au même principe mathématique, mais elles agissent sur des objets différents et produisent des résultats de types différents. Il en va de même pour le produit scalaire : La norme ou la construction d’un vecteur unitaire présentent une difficulté semblable : leur définition générale est commune, mais leur calcul dépend du nombre de composantes et de la représentation Nous pourrions certainement construire une classe Vecteur capable de représenter des vecteurs de dimension quelconque. Cela nécessiterait cependant d’introduire dès maintenant des mécanismes supplémentaires pour gérer le nombre et le stockage des composantes. Nous nous éloignerions alors de notre objectif actuel, qui est de comprendre le principe de l’héritage. Il existe surtout une raison plus importante : l’héritage doit traduire une relation naturelle entre les objets représentés. Or un Vecteur3D n’est pas une spécialisation de Vecteur2D, pas plus qu’un Vecteur2D n’est une spécialisation de Vecteur3D. Les ressemblances entre deux classes ne suffisent donc pas à justifier une relation d’héritage. • Deux classes peuvent posséder des méthodes semblables sans qu’une relation d’héritage • L’héritage doit traduire une véritable relation de spécialisation entre les classes. • Il ne faut pas modifier artificiellement la représentation d’un problème uniquement pour pouvoir utiliser l’héritage. • Lorsqu’une hiérarchie de classes paraît artificielle, il est souvent préférable de chercher Les vecteurs nous ont ainsi permis de découvrir une première règle essentielle de conception en programmation orientée objet : avant de se demander comment utiliser l’héritage, il faut se demander s’il est réellement justifié.
Accueil2026 – C++ Partie II Pour étudier son fonctionnement, nous allons donc nous tourner vers une famille d’objets mathématiques présentant une véritable relation de spécialisation : les matrices. Un exemple plus naturel : les matrices L’étude des classes Vecteur2D et Vecteur3D nous a montré qu’une ressemblance entre deux classes ne suffit pas à justifier une relation d’héritage. Pour introduire ce mécanisme, nous allons donc nous tourner vers une autre famille d’objets mathématiques : les matrices. Contrairement aux vecteurs de dimensions différentes, certaines familles de matrices possèdent naturellement une relation de spécialisation. Toute matrice carrée est une matrice. De même, toute matrice symétrique est une matrice carrée. Nous pouvons écrire : {matrices symétriques} ⊂ {matrices carrées} ⊂ {matrices}. Cette relation mathématique suggère directement une hiérarchie de classes : Dans ce cas, la relation exprimée par l’héritage possède une signification mathématique précise : • une MatriceCarree est une Matrice ; • une MatriceSymetrique est une MatriceCarree. Nous allons donc construire progressivement cette hiérarchie, en commençant par la classe la plus générale : Matrice. Une matrice quelconque est notamment caractérisée par son nombre de lignes et son nombre de colonnes. Une matrice de m lignes et n colonnes contient Nous pouvons donc commencer la définition de notre classe par les deux membres suivants : Matrice(int lignes, int colonnes);
Accueil2026 – C++ Partie II int getNombreLignes() const; int getNombreColonnes() const; Une difficulté nouvelle apparaît : le nombre de coefficients que devra contenir un objet Matrice n’est pas nécessairement connu lors de l’écriture du programme. Nous aurons donc besoin de pouvoir réserver de la mémoire au moment de la création de Cette technique, appelée allocation dynamique de mémoire, est étudiée en détail dans la Partie III de ce cours. Stocker les coefficients Nous savons maintenant qu’il est possible de réserver dynamiquement une zone de mémoire dont la taille est déterminée pendant l’exécution du programme. Une matrice de m lignes et n colonnes possède m × n coefficients. Nous pouvons donc stocker tous ces coefficients dans un tableau dynamique de double contenant exactement La classe Matrice peut alors être complétée de la manière suivante : double* coefficients; Matrice(int lignes, int colonnes); int getNombreLignes() const; int getNombreColonnes() const; Comment stocker les coefficients Les coefficients d’une matrice forment naturellement un tableau à deux dimensions. En mémoire, nous pouvons cependant les stocker dans un unique tableau linéaire de double. Nous ajoutons donc à la classe un pointeur vers la zone mémoire contenant les coefficients :
Accueil2026 – C++ Partie II double* coefficients; Matrice(int lignes, int colonnes); int getNombreLignes() const; int getNombreColonnes() const; double getCoefficient(int ligne, int colonne) const; void setCoefficient(int ligne, Le membre coefficients ne contient pas les coefficients eux-mêmes. Il contient l’adresse du premier élément d’une zone de mémoire réservée dynamiquement. Pour une matrice possédant m lignes et n colonnes, cette zone devra contenir exactement valeurs de type double. Construire et détruire une matrice Le constructeur reçoit le nombre de lignes et de colonnes. Il initialise ces deux membres puis réserve la mémoire nécessaire aux coefficients. Matrice::Matrice(int lignes, int colonnes) : nombreLignes(lignes), nombreColonnes(colonnes), new double[lignes * colonnes]{} new double[lignes * colonnes]{} double* coefficients; coefficients = new double[lignes * colonnes]{}; L’accolade vide initialise les coefficients à zéro.
Accueil2026 – C++ Partie II La mémoire réservée dynamiquement doit être libérée lorsque l’objet Matrice disparaît. Cette opération est effectuée dans le destructeur : delete[] coefficients; // coefficient = nullptr; // inutile ici Après delete ou delete[], mettre le pointeur à nullptr n’est utile si le pointeur continue d’exister et peut encore être utilisé. Dans un destructeur, c’est généralement inutile puisque le pointeur membre disparaît immédiatement avec l’objet. Le constructeur réserve donc la ressource nécessaire à l’objet, tandis que le destructeur la restitue lorsqu’elle n’est plus utilisée. Cette première version de la classe est volontairement limitée. Un objet possédant une ressource dynamique ne peut pas être copié correctement avec les mécanismes de copie par défaut du C++. Nous éviterons donc pour l’instant de copier les La copie profonde, le constructeur de copie et l’opérateur d’affectation seront étudiés dans la Repérer un coefficient dans la mémoire Considérons une matrice possédant m lignes et n colonnes. Ses coefficients peuvent être rangés ligne après ligne dans le tableau dynamique : En mémoire, ils sont disposés dans l’ordre : a 00 , a 01 , . . . , a 0,n−1 , a 10 , a 11 , . . . , a m−1,n−1 . Le coefficient situé à la ligne i et à la colonne j possède alors l’indice Nous pouvons donc écrire : double Matrice::getCoefficient( return coefficients[
Accueil2026 – C++ Partie II ligne * nombreColonnes + colonne void Matrice::setCoefficient( ligne * nombreColonnes + colonne Les dimensions de la matrice sont obtenues de manière classique : int Matrice::getNombreLignes() const return nombreLignes; int Matrice::getNombreColonnes() const return nombreColonnes; A.setCoefficient(0, 0, 2.0); A.setCoefficient(0, 1, 5.0); A.setCoefficient(1, 2, 7.0); << A.getCoefficient(1, 2) Accéder aux coefficients Les coefficients de la matrice sont stockés dans un tableau dynamique unidimensionnel. Pour accéder à un coefficient donné, nous devons donc établir une correspondance entre sa position dans la matrice et sa position dans le tableau. Considérons par exemple une matrice possédant 2 lignes et 3 colonnes : Les coefficients sont stockés ligne après ligne dans le tableau dynamique : coefficient a00 a01 a02 a10 a11 a12
Accueil2026 – C++ Partie II Pour une matrice possédant nombreColonnes colonnes, le coefficient situé à la ligne ligne et à la colonne colonne se trouve donc à l’indice ligne * nombreColonnes + colonne. Par exemple, dans notre matrice à trois colonnes, le coefficient a12 se trouve à l’indice Nous pouvons utiliser cette relation pour construire les méthodes permettant de lire et de modifier La méthode getCoefficient() reçoit le numéro de la ligne et de la colonne du coefficient recherché : double Matrice::getCoefficient( return coefficients[ ligne * nombreColonnes + colonne Modifier un coefficient La méthode setCoefficient() reçoit la position du coefficient ainsi que sa nouvelle valeur : void Matrice::setCoefficient( ligne * nombreColonnes + colonne Les méthodes getCoefficient() et setCoefficient() illustrent un avantage important de l’encapsulation. L’utilisateur de la classe manipule les coefficients à l’aide de leur ligne et de leur colonne. Il n’a pas besoin de savoir que ceux-ci sont en réalité stockés dans un tableau unidimensionnel. La classe Matrice se charge elle-même de convertir les deux indices en une position dans le
Accueil2026 – C++ Partie II Les méthodes getCoefficient() et setCoefficient() supposent que les indices fournis correspondent à une position existante dans la matrice. Pour une matrice possédant m lignes et n colonnes, ils doivent vérifier Nous ne mettons volontairement pas en place de contrôle supplémentaire dans cette première version de la classe. Multiplication matricielle Notre classe doit encore être pourvue de la multiplication matricielle. Voici son implémentation dans Matrice Matrice::multiplier( const Matrice& autre) const autre.nombreColonnes for (int i = 0; i < nombreLignes; ++i) for (int j = 0; j < autre.nombreColonnes; ++j) for (int k = 0; k < nombreColonnes; ++k) getCoefficient(i, k) autre.getCoefficient(k, j); resultat.setCoefficient(i, j, somme); La juste correspondance entre le nombre de colonnes de la première matrice et le nombre de ligne de la matrice en argument n’est pas vérifiée. Constructeur de copie Si on veut par exemple effecture l’opération
Accueil2026 – C++ Partie II il nous faut un constructeur par copie profonde afin que si A est détruite, B ne le soit pas également. On ne veut pas que la matrice A pointe vers les coefficients de la matrice B, on désire une construction en profondeur, une réplication exacte qui crée de nouveaux emplacements de mémoire pour la copie Ce constructeur est : Matrice::Matrice(const Matrice& autre) autre.nombreColonnes for (int i = 0; i < nombreLignes; ++i) for (int j = 0; j < nombreColonnes; ++j) autre.getCoefficient(i, j) Le code crée immédiatement une matrice possédant son propre tableau dynamique aux bonnes dimensions. Il ne reste ensuite qu’à recopier les coefficients. Affectation par copie Ici, contrairement au constructeur de copie, l’objet existe déjà. Il faut donc d’abord libérer son ancienne allocation, adapter ses dimensions, effectuer une nouvelle allocation puis recopier les coefficients. Si on a par exemple déjà les deux matrices A et B et qu’on veut faire Les deux matrices existant déjà, il faut s’assurer de détruire A pour lui affecter B et également prévoir le cas A = A par exemple. est ici indispensable à la correction de notre algorithme, pas simplement une optimisation. Si A = A, il dit simplement : source et destination sont le même objet ; il n’y a absolument rien à faire. C’est d’ailleurs maintenant que l’intérêt de this devient particulièrement évident : sans comparer this et &autre, notre opérateur d’affectation profonde détruirait lui-même sa source lors d’une Le code de cette surcharge d’affectation est : Matrice Matrice::multiplier( const Matrice& autre) const
Accueil2026 – C++ Partie II autre.nombreColonnes for (int i = 0; i < nombreLignes; ++i) for (int j = 0; j < autre.nombreColonnes; ++j) for (int k = 0; k < nombreColonnes; ++k) getCoefficient(i, k) autre.getCoefficient(k, j); resultat.setCoefficient(i, j, somme); avec la précondition nombreColonnes = autre.nombreLignes 1 Dans le fichier d’en-tête, les 3 méthodes que l’on vient d’ajouter s’écrivent : Matrice(int lignes, int colonnes); const Matrice& autre); Matrice(const Matrice& autre); const Matrice& autre) const; Matrice multiplier(const Matrice& autre) const; Classe Matrice de base La classe que nous venons de la construire, va maintenant devenir notre classe de base qui nous permet d’attaquer véritablement l’héritage. Le code de cette classe de base est : Code 3.4 – Fichier en-tête – Matrice.h (version 1.0) 1. On traitera l’interception d’exceptions plus loin dans le cour, pour l’instant on va au plus important.
Accueil2026 – C++ Partie II Created by Michel Semon on 11.08.2026. double∗ coefficients; Matrice(int lignes, int colonnes); Matrice(const Matrice& autre); Matrice& operator=(const Matrice& autre); int getNombreLignes() const; int getNombreColonnes() const; double getCoefficient(int ligne, int colonne) const; void setCoefficient(int ligne, Matrice multiplier(const Matrice& autre) const; void afficherMatrice() const; Code 3.5 – Fichier en-tête – Matrice.cpp (version 1.0) Created by Michel Semon on 11.08.2026.
Accueil2026 – C++ Partie II #include « Matrice.h » Matrice::Matrice(int lignes, int colonnes) : nombreLignes(lignes), nombreColonnes(colonnes), coefficients(new double[lignes ∗ colonnes]{}) (const Matrice& autre) : Matrice(autre.nombreLignes,autre. for (int i=0; i< nombreLignes;i++) for (int j=0; j<nombreColonnes; j++) { autre.getCoefficient(i, j) Matrice& Matrice::operator=(const Matrice& autre) delete[] coefficients; nombreLignes = autre.nombreLignes; nombreColonnes = autre.nombreColonnes; coefficients = new double[nombreLignes ∗ nombreColonnes]; for (int i=0; i<nombreLignes; ++i) { for (int j=0; j<nombreColonnes; j++) { this−>setCoefficient(i, j, autre.getCoefficient(i, j));
Accueil2026 – C++ Partie II delete[] coefficients; int Matrice::getNombreLignes() const return nombreLignes; int Matrice::getNombreColonnes() const return nombreColonnes; double Matrice::getCoefficient(int ligne, return coefficients[ ligne ∗ nombreColonnes + colonne void Matrice::setCoefficient(int ligne, ligne ∗ nombreColonnes + colonne // On suppose dans un premier temps que les dimensions sont adequate pour l’ operation de multiplication Matrice Matrice::multiplier(const Matrice& autre) const autre.nombreColonnes for (int i = 0; i < nombreLignes; i++) for (int j = 0; j < autre.nombreColonnes; j++) for (int k = 0; k < nombreColonnes; k++) getCoefficient(i, k)
Accueil2026 – C++ Partie II autre.getCoefficient(k, j); resultat.setCoefficient(i, j, somme); void Matrice::afficherMatrice() const for (int i=0; i<nombreLignes; i++) for (int j=0; j<nombreColonnes; j++) std::cout << getCoefficient(i, j) << » « ; std::cout << std::endl; Le fichier compressé contenant les codes de la classe Matrice v1.1 peut être téléchargé ici : Matrice La classe Matrice que nous venons de construire représente une matrice quelconque. Jusqu’à présent, nous l’avons étudiée comme une classe indépendante. Nous allons maintenant changer de point de vue. Certaines matrices possèdent des propriétés supplémentaires tout en restant des matrices. Une matrice carrée, par exemple, est une matrice dont le nombre de lignes est égal au nombre de colonnes. {matrices carrées} ⊂ {matrices}. Cette relation peut être traduite directement dans notre programme par une relation d’héritage. La classe Matrice va jouer le rôle de classe de base. Nous allons construire à partir d’elle une nouvelle classe MatriceCarree, appelée classe dérivée. L’objectif n’est pas de recopier dans MatriceCarree ce que nous avons déjà programmé dans Matrice. Au contraire, nous allons chercher à réutiliser les caractéristiques et les comportements déjà définis dans la classe de base. Une première classe dérivée : MatriceCarree Une matrice carrée est une matrice possédant le même nombre de lignes et de colonnes. Une matrice carrée d’ordre n possède donc n lignes et n colonnes. Puisqu’une matrice carrée est un cas particulier de matrice, nous pouvons représenter cette relation directement en C++ en déclarant MatriceCarree comme une classe dérivée de Matrice.
Accueil2026 – C++ Partie II Une première déclaration peut être extrêmement simple : #ifndef MATRICECARREE_H #define MATRICECARREE_H #include « Matrice.h » class MatriceCarree : public Matrice indique que la classe MatriceCarree dérive de la classe Matrice. • Matrice est la classe de base ; • MatriceCarree est la classe dérivée. La classe dérivée peut ainsi réutiliser une partie de ce qui a déjà été défini dans la classe de base. La classe MatriceCarree ne contient encore aucune déclaration entre ses accolades. Cela signifie-t-il qu’elle ne possède aucune fonctionnalité ? Non. La classe MatriceCarree dérive de Matrice. Elle dispose donc déjà des membres accessibles hérités de cette classe de base. Par exemple, une matrice carrée pourra utiliser les méthodes publiques déjà définies dans Matrice, Il n’est donc pas nécessaire de réécrire ces méthodes dans MatriceCarree.
Accueil2026 – C++ Partie II L’héritage ne signifie pas que la classe dérivée peut accéder librement à tous les membres de Les membres déclarés private dans Matrice restent inaccessibles directement depuis le code de MatriceCarree. Nous étudierons précisément les niveaux d’accès dans la suite du chapitre. Une classe qui n’est pas encore utilisable Notre classe MatriceCarree possède maintenant une relation d’héritage avec Matrice, mais nous rencontrons immédiatement un problème lorsque nous essayons de créer un objet : La classe de base Matrice ne possède pas de constructeur sans argument. Son constructeur exige Matrice(int lignes, int colonnes); Or une matrice carrée d’ordre n ne nécessite qu’une seule information : son ordre. Nous souhaiterions donc pouvoir écrire : et obtenir automatiquement une matrice possédant Le constructeur d’une classe dérivée Nous souhaitons pouvoir créer une matrice carrée en indiquant uniquement son ordre. Par exemple, doit créer une matrice possédant quatre lignes et quatre colonnes. Nous pouvons donc commencer par déclarer un constructeur dans la classe MatriceCarree : Code 3.6 – MatriceCarree.h #ifndef MATRICECARREE_H #define MATRICECARREE_H #include « Matrice.h » class MatriceCarree : public Matrice
Accueil2026 – C++ Partie II MatriceCarree(int ordre); Cependant, un objet de type MatriceCarree contient également toute la partie provenant de sa classe de base Matrice. Cette partie doit elle aussi être construite. Or le constructeur de Matrice attend deux arguments : Matrice(int lignes, int colonnes); Pour une matrice carrée d’ordre ordre, ces deux arguments sont identiques. Le constructeur de la classe dérivée peut donc appeler explicitement le constructeur de sa classe de base : Code 3.7 – MatriceCarree.cpp #include « MatriceCarree.h » MatriceCarree::MatriceCarree(int ordre) : Matrice(ordre, ordre) : Matrice(ordre, ordre) demande au constructeur de MatriceCarree de construire sa partie Matrice en appelant le constructeur Matrice(ordre, ordre); Ainsi, l’instruction provoque notamment l’appel du constructeur de Matrice avec La classe de base effectue alors exactement le travail qu’elle effectuait déjà pour une matrice quelconque : elle mémorise les dimensions et réserve dynamiquement la mémoire nécessaire aux coefficients. La classe de base est construite en premier Lorsqu’un objet d’une classe dérivée est créé, sa partie correspondant à la classe de base doit être construite avant la partie propre à la classe dérivée. Pour l’ordre est donc :
Accueil2026 – C++ Partie II 1. appel du constructeur de MatriceCarree ; 2. construction de la partie Matrice par Matrice(4, 4) ; 3. exécution du corps du constructeur de MatriceCarree. Dans notre exemple, le corps du constructeur de MatriceCarree est vide : Tout le travail nécessaire à la création de la matrice est déjà effectué par le constructeur de la classe Utiliser les méthodes héritées Nous pouvons maintenant créer une matrice carrée et utiliser directement certaines méthodes publiques définies dans Matrice : A.setCoefficient(0, 0, 1.0); A.setCoefficient(1, 1, 2.0); A.setCoefficient(2, 2, 3.0); A.afficherMatrice(); La classe MatriceCarree ne contient pourtant aucune définition de setCoefficient() ni de Ces méthodes proviennent de la classe de base Matrice. • Un objet d’une classe dérivée contient une partie correspondant à sa classe de base. • Lors de la construction d’un objet dérivé, la classe de base est construite avant l’exécution du corps du constructeur de la classe dérivée. • Le constructeur de la classe de base peut être appelé dans la liste d’initialisation du constructeur de la classe dérivée. MatriceCarree::MatriceCarree(int ordre) : Matrice(ordre, ordre) le constructeur de Matrice crée une matrice possédant ordre lignes et ordre colonnes. • Une classe dérivée peut utiliser les méthodes publiques héritées de sa classe de base sans avoir à les réécrire.
Accueil2026 – C++ Partie II private et protected dans l’héritage La classe Matrice contient actuellement les membres suivants : double* coefficients; Ces trois membres sont déclarés private. Nous avons déjà vu que cela interdit au code extérieur à la classe d’y accéder directement. L’apparition d’une classe dérivée pose maintenant une nouvelle question : une méthode de MatriceCarree peut-elle accéder directement aux membres privés de Matrice ? Tentative d’accès depuis la classe dérivée Ajoutons temporairement à MatriceCarree une méthode permettant d’obtenir son ordre : iint MatriceCarree::getOrdre() const return getNombreLignes(); Cette méthode ne compile pas. Bien que MatriceCarree dérive de Matrice, nombreLignes reste un membre privé de Matrice. Une classe dérivée ne possède donc pas un accès direct aux membres private de sa classe de base. Par contre, si l’on utilise la méthode public getNombreLignes cela fonctionne, notre méthode doit être par conséquent int MatriceCarree::getOrdre() const return nombreLignes; Le niveau d’accès protected Le C++ fournit un troisième niveau d’accès particulièrement utile dans le contexte de l’héritage : Un membre déclaré protected reste inaccessible au code extérieur à la classe, mais il devient directement accessible aux classes dérivées. Nous pourrions par exemple écrire :
Accueil2026 – C++ Partie II double* coefficients; Et je comparerais alors clairement les trois niveaux : | Accès au membre | ‘public‘ | ‘protected‘ | ‘private‘ | |————————| :——– 😐 :———— 😐 :——— 😐 | Classe elle-même | oui | | Classe dérivée | oui | | Code extérieur | oui | Mais faut-il réellement transformer nos membres en ‘protected‘ ? Il n’y a aucune raison de donner accès à nombreLignes et nombreColonne à la classe MatriceCarree car elle y a accès par les getters. Il faut par contre laisser double∗ coefficients; en statut privé afin que MatriceCarree ne puisse pas faire delete[] coefficients et compromettre toute la classe Matrice. • Un membre private n’est directement accessible que depuis la classe qui le déclare. • Une classe dérivée ne peut pas accéder directement aux membres private de sa classe • Un membre protected est accessible depuis la classe qui le déclare ainsi que depuis ses classes dérivées, mais reste inaccessible au code extérieur. • Un membre public est également accessible depuis le code extérieur. • L’héritage ne supprime donc pas l’encapsulation de la classe de base. • Il n’est pas nécessaire de déclarer un membre protected simplement parce qu’une classe doit servir de classe de base. Lorsque l’interface publique existante suffit, le membre peut Un cas où protected peut être utile Dans notre classe Matrice, les membres privés peuvent rester private car les classes dérivées disposent déjà des méthodes publiques nécessaires pour les utiliser.
Accueil2026 – C++ Partie II Il existe cependant des situations dans lesquelles une classe de base est conçue pour permettre à ses classes dérivées d’accéder directement à une partie de son état interne. Considérons une classe très simple représentant un compteur : Compteur(int valeurInitiale); int getValeur() const; Le membre valeur n’est pas public. Le code utilisant un objet Compteur ne peut donc pas le modifier directement. Nous pouvons cependant créer une classe dérivée représentant un compteur possédant une valeur class CompteurLimite : public Compteur void incrementerSiPossible(); La classe CompteurLimite doit pouvoir consulter et modifier directement la valeur du compteur tout en respectant la limite imposée. Puisque valeur est déclarée protected dans la classe de base, une méthode de la classe dérivée peut y accéder directement : void CompteurLimite::incrementerSiPossible() if (valeur < maximum)
Accueil2026 – C++ Partie II Une classe dérivée ne sert pas seulement à récupérer ce que possède sa classe de base ; elle peut ajouter des opérations qui n’ont de sens que pour la spécialisation qu’elle représente. C’est ce que l’on va étudier maintenant. Enrichir la classe dérivée La classe MatriceCarree possède déjà les fonctionnalités qu’elle peut utiliser à travers la classe Matrice. Elle peut notamment stocker et modifier ses coefficients, les afficher ou participer à une multiplication matricielle. Mais une classe dérivée peut également posséder des fonctionnalités qui lui sont propres. Considérons le déterminant, celui-ci n’est défini que pour les matrices carrées. Il ne serait donc pas cohérent d’ajouter une méthode double determinant() const; à la classe générale Matrice, puisqu’un objet de cette classe peut par exemple représenter une Cette méthode trouve en revanche naturellement sa place dans MatriceCarree : class MatriceCarree : public Matrice MatriceCarree(int ordre); double determinant() const; Nous obtenons ainsi une classe qui conserve les fonctionnalités générales d’une matrice tout en ajoutant une opération propre aux matrices carrées. La classe MatriceCarree ne contient encore aucune déclaration entre ses accolades. Cela signifie-t-il qu’elle ne possède aucune fonctionnalité ? Non. La classe MatriceCarree dérive de Matrice. Elle dispose donc déjà des membres accessibles hérités de cette classe de base. Par exemple, une matrice carrée pourra utiliser les méthodes publiques déjà définies dans Matrice,
Accueil2026 – C++ Partie II Il n’est donc pas nécessaire de réécrire ces méthodes dans MatriceCarree. L’héritage ne signifie pas que la classe dérivée peut accéder librement à tous les membres de Les membres déclarés private dans Matrice restent inaccessibles directement depuis le code de MatriceCarree. Nous étudierons précisément les niveaux d’accès dans la suite du chapitre. Calcul du déterminant par développement de cofacteurs Pour généraliser la méthode aux matrices carrées d’ordre supérieur à deux, nous allons utiliser le développement du déterminant par cofacteurs. Considérons une matrice carrée A d’ordre n . En développant son déterminant suivant la première ligne, nous obtenons (−1) j a 0 j det M 0 j , où M0 j désigne la matrice obtenue en supprimant de A la ligne 0 et la colonne j . La matrice M0 j est elle-même une matrice carrée, d’ordre n − 1. Son déterminant peut donc être calculé exactement de la même manière. Nous retrouvons ainsi une structure déjà étudiée dans ce cours : la récursivité. Construire une matrice mineure Pour calculer le déterminant par développement de cofacteurs, nous devons pouvoir construire une matrice obtenue en supprimant une ligne et une colonne de la matrice originale. Considérons par exemple Si nous supprimons la ligne 0 et la colonne 1, nous obtenons la matrice La matrice obtenue possède un ordre inférieur d’une unité à celui de la matrice initiale. Nous allons donc ajouter à la classe MatriceCarree une méthode privée : MatriceCarree mineur( int colonneSupprimee
Accueil2026 – C++ Partie II Cette méthode n’est pas destinée à être utilisée directement par le programme principal. Elle constitue un outil interne nécessaire au calcul du déterminant. Pour construire le mineur, il faut parcourir tous les coefficients de la matrice originale en ignorant la ligne et la colonne à supprimer. Comme les indices de la matrice originale ne correspondent plus directement à ceux de la matrice mineure, nous utiliserons deux couples d’indices : • i et j pour parcourir la matrice originale ; • ligneMineur et colonneMineur pour remplir la nouvelle matrice. La méthode peut être écrite de la manière suivante : MatriceCarree MatriceCarree::mineur( int colonneSupprimee int ordre = getNombreLignes(); MatriceCarree resultat(ordre – 1); int ligneMineur = 0; for (int i = 0; i < ordre; ++i) if (i == ligneSupprimee) int colonneMineur = 0; for (int j = 0; j < ordre; ++j) if (j == colonneSupprimee) resultat.setCoefficient(
Codes et programmes contenus dans le chapitre
Matrice v.1.0
Classe Matrice
Matrice v.2.0
Classes Matrice, Matrice Carree, Matrice Symetrique
README.pdf
Explications sur l’utilisation des codes