Concepts de base et allocation dynamique
Classe et objetEncapsulationConstructeur et destructeurListe d'initialisationPile et tasnew / deleteTableaux dynamiques
Fil conducteur de tout le site : l'atelier de robotique des TP (batterie, moteurs, capteurs, pinces, robot). Chaque notion est illustrée avec ces classes, puis exercée avec des questions du type de celles des intras passés.
1. Une classe décrit, un objet existe
Une classe est un plan : elle décrit les attributs (les données) et les méthodes (les fonctions membres) de ses objets. Un objet (ou instance) est une variable construite d'après ce plan. Deux moteurs construits séparément ont chacun leur nom, leur masse et leur charge : modifier l'un ne modifie pas l'autre.
- Interface d'une classe : l'ensemble de ses membres publics (ce que le reste du programme peut utiliser).
- Implémentation : le code des méthodes et les attributs privés (le « comment »).
- Encapsulation : l'état d'un objet n'est manipulable que par son interface. Les attributs sont donc privés et modifiés uniquement par des méthodes, ce qui empêche n'importe qui de mettre l'objet dans un état incohérent (une charge de moteur à 250 %, par exemple).
Par convention à Polytechnique (guide de codage) : attributs privés avec un suffixe _, noms de classes en CamelCase, méthodes en camelCase, accesseurs getX() et mutateurs setX(). La déclaration va dans un fichier d'en-tête .h, la définition des méthodes dans le .cpp correspondant, dans le même ordre que dans le .h (exigence des TP).
Moteur::getNom: l'opérateur de portée::dit « la méthode getNom de la classe Moteur ». Sans lui, on définirait une fonction libre du même nom.#pragma onceévite d'inclure deux fois le même en-tête (équivalent des gardes#ifndef MOTEUR_H).- Dans le
.hon écritstd::string(jamaisusing namespace std;dans un en-tête : il s'imposerait à tous les fichiers qui l'incluent).
2. Constructeurs et destructeur
Le constructeur porte le nom de la classe, n'a pas de type de retour et s'exécute automatiquement à la création de l'objet. Le destructeur (~Moteur()) s'exécute automatiquement à sa destruction : il sert à libérer ce que l'objet possède (mémoire dynamique, fichier ouvert).
| Type de constructeur | Signature | Quand il est appelé |
|---|---|---|
| Par défaut | Moteur(); | Moteur m;, Moteur t[10];, new Moteur[5], vector<Moteur> v(3). Fourni automatiquement par le compilateur seulement si la classe ne déclare aucun constructeur. |
| Par paramètres | Moteur(const string& nom, double m, double p); | Moteur m("roue", 2, 40);, new Moteur("roue", 2, 40). Des valeurs par défaut (double p = 0) permettent à un même constructeur de servir de constructeur par défaut. |
| Par copie | Moteur(const Moteur& autre); | Moteur m2 = m1;, Moteur m2(m1);, passage par valeur, retour par valeur. Fourni automatiquement (copie membre à membre). Détails au module 4. |
La liste d'initialisation
Entre la signature du constructeur et son corps, après :, on initialise les attributs. C'est la forme attendue dans les TP (pénalité de style sinon) et la seule possible pour les attributs constants, les références et les attributs objets sans constructeur par défaut.
Chaque attribut est construit une seule fois, directement avec la bonne valeur.
Les attributs sont d'abord construits par défaut, puis affectés. Plus lent, et impossible pour const, les références ou une classe de base.
Les attributs sont initialisés dans l'ordre de leur déclaration dans la classe, pas dans l'ordre de la liste. Écrire la liste dans un autre ordre déclenche l'avertissement -Wreorder (donc une pénalité au TP) et peut donner un attribut initialisé à partir d'un autre pas encore construit. Règle simple : même ordre dans la liste que dans le .h.
Quand le destructeur est-il appelé ?
- À la sortie de la portée (accolade fermante, fin de fonction) pour une variable locale, dans l'ordre inverse de construction.
- Au
deletepour un objet dynamique. - À la destruction d'un objet qui le contient (attribut, élément d'un vecteur).
- À la fin d'une expression pour un objet temporaire (par exemple
f(Moteur("x", 1, 1))).
3. Accesseurs, mutateurs et méthodes
Comme les attributs sont privés, on fournit des accesseurs (getMasseKg(), en lecture seule, donc const, voir module 2) et des mutateurs (setCharge()) qui valident la nouvelle valeur. Un mutateur peut retourner bool pour signaler un refus (convention des TP : setCharge retourne false pour une charge hors de [0, 1], sans modifier l'objet).
Une méthode a accès à tous les membres de l'objet sur lequel elle est appelée, y compris les privés, et aux membres privés des autres objets de la même classe (la visibilité est par classe, pas par objet). C'est ce qui permettra d'écrire autre.nom_ dans un constructeur de copie.
4. Pile et tas : allocation automatique ou dynamique
| Allocation automatique (pile) | Allocation dynamique (tas) | |
|---|---|---|
| Syntaxe | Moteur m("roue", 2, 40); | Moteur* p = new Moteur("roue", 2, 40); |
| Taille décidée | à la compilation | à l'exécution (new int[n] avec n lu au clavier) |
| Durée de vie | jusqu'à la fin du bloc : destruction automatique | jusqu'au delete explicite ; sinon l'objet survit à la fonction (ou fuit) |
| Accès aux membres | m.afficher(cout) | p->afficher(cout), équivalent à (*p).afficher(cout) |
| Libération | automatique | delete p; pour un objet, delete[] t; pour un tableau |
Un pointeur contient une adresse. Moteur* p déclare un pointeur vers un Moteur ; &m donne l'adresse de m ; *p est l'objet pointé ; p->x équivaut à (*p).x. Un pointeur qui ne pointe rien doit valoir nullptr (jamais 0 ni NULL en C++ moderne). Déréférencer un pointeur nul ou pendant est un comportement indéfini (plantage typique).
Objets et tableaux dynamiques
Avec const int INTMAX = 10;, déclarer : (i) un tableau sur la pile de INTMAX objets AA : AA tableau1[INTMAX]; ; (ii) un tableau sur le tas de INTMAX objets AA : AA* tableau2 = new AA[INTMAX]; ; (iii) un tableau sur le tas de INTMAX pointeurs vers des AA alloués sur le tas : AA** tableau3 = new AA*[INTMAX]; puis tableau3[i] = new AA; pour chaque case.
Agrandir un tableau dynamique
Un tableau alloué avec new int[taille] a une taille fixe. Pour l'agrandir, on alloue un nouveau tableau plus grand, on copie les éléments, on libère l'ancien et on fait pointer la variable vers le nouveau. C'est exactement ce que std::vector fait pour vous (module 2).
Insertion à la position pos en conservant l'ordre : décaler les éléments de la fin vers pos (boucle décroissante, tab[i] = tab[i-1]), puis écrire tab[pos] = val. Suppression : décaler vers la gauche (boucle croissante, tab[i] = tab[i+1]). Il faut bien sûr qu'il reste de la capacité pour insérer.
5. Erreurs de mémoire à reconnaître
| Erreur | Exemple | Conséquence |
|---|---|---|
| Fuite de mémoire | p = new Moteur(...) deux fois sans delete entre les deux ; oublier delete avant la fin d'une fonction | mémoire jamais rendue ; le destructeur n'est jamais appelé |
| Pointeur pendant | utiliser p après delete p ; retourner l'adresse d'une variable locale | comportement indéfini (valeurs aléatoires, plantage) |
| Double libération | delete p; delete p; ; deux objets qui possèdent le même pointeur (copie superficielle, module 4) | plantage |
| Mauvais delete | delete t sur un tableau alloué avec new[] ; delete sur une adresse qui ne vient pas de new (variable locale) | comportement indéfini |
| Déréférencement nul | Moteur* p = nullptr; p->afficher(cout); | plantage immédiat |
Qui fait new fait delete : chaque objet dynamique a un seul propriétaire responsable de le libérer, exactement une fois. Dans le TP1, le robot possède sa batterie et son châssis (il les libère dans son destructeur) mais seulement utilise les moteurs, capteurs et pinces (c'est l'appelant qui les libère). Les pointeurs intelligents (module 3) automatisent cette règle.
6. Organisation des fichiers et compilation
Moteur.h: déclaration de la classe (interface).Moteur.cpp: définition des méthodes, qui inclut"Moteur.h".main.cpp: utilise la classe.- Chaque
.cppest compilé séparément en un fichier objet, puis l'éditeur de liens les assemble. Une méthode déclarée mais jamais définie donne une erreur « symbole externe non résolu » à l'édition des liens, pas à la compilation. - Les fonctions et classes génériques (module 7) font exception : leur définition reste dans le
.h. - Options imposées aux TP :
-std=c++20 -Wall -Wextra -pedantic-errors, zéro avertissement. Un avertissement courant :-Wreorder(liste d'initialisation dans le mauvais ordre), variable non utilisée, comparaison entreintetsize_t(-Wsign-compare) dans une boucle sur un vecteur.
7. À retenir
- Classe = plan (attributs privés + méthodes publiques) ; objet = instance. L'interface est l'ensemble des membres publics.
- Constructeur : même nom que la classe, pas de type de retour, liste d'initialisation dans l'ordre des attributs. Le constructeur par défaut implicite disparaît dès qu'on déclare un constructeur.
- Destructeur :
~Classe(), appelé automatiquement en fin de portée, audelete, ou avec l'objet qui contient l'attribut. Ordre inverse de la construction. - Pile : automatique, taille connue à la compilation. Tas :
new/delete,new[]/delete[], un seul propriétaire,nullptraprèsdelete. - Agrandir un tableau = nouveau bloc, copie,
delete[]de l'ancien, réaffectation du pointeur.
Quiz rapide - Module 1
Une seule bonne réponse par question. Le meilleur score est mémorisé dans ce navigateur.
Qu'est-ce que l'interface d'une classe ?
- L'ensemble de ses attributs
- L'ensemble de ses membres publics
- Le contenu de son fichier .cpp
- Ses constructeurs seulement
Une classe déclare un seul constructeur Capteur(const string& nom, double masse);. Laquelle de ces lignes ne compile pas ?
Capteur c("temp", 0.5);Capteur* p = new Capteur("temp", 0.5);Capteur tableau[3];Capteur c = Capteur("temp", 0.5);
Dans quel ordre les attributs sont-ils initialisés par la liste d'initialisation ?
- Dans l'ordre de leur déclaration dans la classe
- Dans l'ordre où ils apparaissent dans la liste
- Dans l'ordre alphabétique
- Dans l'ordre inverse de la liste
-Wreorder ; la règle des TP est d'écrire la liste dans le même ordre que les attributs.Quand le destructeur d'une variable locale est-il appelé ?
- Jamais, sauf si on appelle
delete - Seulement à la fin du programme
- Quand on appelle explicitement
~Classe() - Automatiquement à la sortie de son bloc
Que fait delete p; quand p pointe un objet alloué avec new ?
- Il met
pànullptr - Il libère la mémoire sans appeler le destructeur
- Il appelle le destructeur puis libère la mémoire ;
pgarde l'ancienne adresse - Il détruit aussi la variable
p
delete p, le pointeur contient toujours l'adresse de la mémoire libérée (pointeur pendant). D'où la bonne pratique p = nullptr; juste après.Quelle différence entre delete et delete[] ?
- Aucune, ce sont des synonymes
delete[]libère un tableau alloué avecnew[]et appelle le destructeur de chaque élémentdelete[]libère plusieurs pointeurs à la foisdeleteest réservé aux types fondamentaux
new T[n] doit être libéré avec delete[], qui détruit les n objets. Utiliser delete sur un tableau est un comportement indéfini.Quelle déclaration réserve, sur le tas, un tableau de 10 pointeurs vers des objets AA ?
AA** t = new AA*[10];AA* t = new AA[10];AA t[10];AA* t[10] = new AA;
new AA*[10] alloue 10 pointeurs (non initialisés) ; le résultat est un pointeur vers un pointeur, AA**. Chaque case devra ensuite recevoir son propre new AA.Quel est l'affichage de ce programme ?
abc~c~b~aab~bc~a~cab~bc~c~aabc~a~b~c
b est détruit dès la fin de son bloc. À la fin de main, c (construit en dernier) est détruit avant a.Pourquoi garder les attributs privés ?
- Pour économiser de la mémoire
- Pour que l'état de l'objet ne soit modifiable que par ses méthodes, qui peuvent valider les valeurs
- Parce que le compilateur l'exige
- Pour accélérer l'exécution
setCharge refuse une charge hors de [0, 1]).Quelle affirmation sur la pile et le tas est vraie ?
- Les objets du tas sont détruits automatiquement à la fin de la fonction
- La taille d'un tableau sur la pile peut être décidée à l'exécution
- Un objet sur la pile doit être libéré avec
delete - Un objet sur le tas survit à la fonction qui l'a créé tant qu'on ne fait pas
delete
Dans une méthode de la classe Moteur, l'expression autre.nom_ (où autre est un autre Moteur) :
- Compile : la visibilité privée est par classe, pas par objet
- Ne compile pas :
nom_est privé - Compile seulement si
autreest passé par valeur - Compile seulement dans le constructeur
Ce code contient une erreur de mémoire. Laquelle ?
- Double libération
- Pointeur pendant
- Fuite de mémoire : l'objet « p1 » n'est jamais libéré
- Aucune, le code est correct
new écrase la seule adresse de « p1 ». Plus personne ne peut la libérer : fuite.Que vaut un pointeur juste après int* p; dans une fonction ?
nullptr- Une valeur indéterminée (garbage)
0- L'adresse de la première variable locale
int* p = nullptr;.Combien de fois le destructeur de Moteur est-il appelé par delete[] parc; si parc = new Moteur[4] ?
- 0 fois
- 1 fois
- Cela dépend du compilateur
- 4 fois
delete[] détruit chaque élément du tableau (4 appels à ~Moteur) avant de libérer le bloc.Où doit-on définir le corps de double Moteur::getMasseKg() const dans un TP ?
- Dans Moteur.cpp, à la même position que sa déclaration dans Moteur.h
- Dans main.cpp
- Obligatoirement dans Moteur.h
- N'importe où, l'ordre n'a pas d'importance pour la note
Exercices à développement (à faire sur papier ou dans un éditeur)
Résolvez avant de déplier l'indice ou la solution. Les énoncés marqués « Intra » viennent des séances de révision d'intras passés (A2018, H2019).
Écrire la classe Capteur
Un capteur a un nom, une masse en kg (positive ou nulle), une puissance en W et un état actif ou inactif (inactif à la construction). Il consomme sa puissance seulement lorsqu'il est actif. Écrivez la déclaration (Capteur.h) et la définition (Capteur.cpp) avec : un constructeur par paramètres (liste d'initialisation), les accesseurs getNom, getMasseKg, estActif, getConsommationW, le mutateur setActif(bool) et afficher(std::ostream&).
Indice
Quatre attributs : std::string nom_; double masseKg_; double puissanceW_; bool actif_;. La consommation est une expression conditionnelle : actif_ ? puissanceW_ : 0.0. Tous les accesseurs sont const.
Solution
Points à vérifier : liste d'initialisation dans l'ordre des attributs ; const sur tout ce qui ne modifie pas l'objet ; définitions dans le même ordre que les déclarations ; std:: dans l'en-tête.
Constructeurs, destructeurs et portées
Donnez l'affichage exact de ce programme.
Indice
Dans f(), l'objet dynamique « d » n'est jamais libéré : son destructeur ne s'affiche jamais (fuite, deux fois).
Solution
aconstruit (+a). Appel de f : +f, +d, « | », puis fin de f : seulelocaleest détruite (-f). « d » fuit.- +p puis -p immédiatement au delete.
- Bloc : +b, f() de nouveau (+f +d | -f), accolade fermante : -b.
- « fin », puis fin de main : -a. Les deux objets « d » ne sont jamais détruits.
Insérer dans un tableau dynamique
Écrivez la fonction void inserer(int* tab, int taille, int val, int pos) qui insère val à l'indice pos d'un tableau contenant taille éléments valides, en décalant les éléments suivants pour conserver l'ordre. On suppose que la capacité allouée est suffisante (au moins taille + 1). Écrivez ensuite supprimer(int* tab, int taille, int pos).
Indice
Pour insérer, décalez de la fin vers pos (boucle décroissante) sinon vous écrasez des valeurs. Pour supprimer, décalez de pos vers la fin (boucle croissante).
Solution
Avec un std::vector<int>&, l'insertion devient tab.push_back(tab.back()); puis la même boucle de décalage à partir de tab.size() - 1 ; la suppression se termine par tab.pop_back();. Ou simplement tab.insert(tab.begin() + pos, val) et tab.erase(tab.begin() + pos) (module 2).
Compter les blocs mémoire
En considérant que la classe Poney possède tous les constructeurs nécessaires, combien de blocs mémoire contenant un objet Poney sont alloués par ce programme ?
Indice
Une affectation (= entre deux objets existants) ne crée pas d'objet ; elle copie dans un bloc déjà alloué. Un objet temporaire Poney() occupe un bloc le temps de l'expression.
Solution
11 blocs.
poney1: 1 ;groupePoney[6]: 6 ; le temporairePoney()affecté à la case 1 : 1 ;poney2: 1 ;poney3: 1 (le temporaire est élidé directement dans poney3) ;new Poney(): 1. Total 11.- Les affectations
groupePoney[0] = poney1,[2] = poney2,[3] = poney3,[4] = *poney4copient le contenu dans des blocs existants : aucun nouveau bloc. - Remarque : le bloc de
poney4n'est jamais libéré (fuite).
Trouver les erreurs de mémoire
Ce code compile. Identifiez chaque erreur de gestion de mémoire et corrigez-la.
Solution
creerMoteurretourne l'adresse d'une variable locale, détruite à la sortie de la fonction : pointeur pendant. Correction :return new Moteur("temp", 1, 10);(et l'appelant faitdelete), ou retourner l'objet par valeur.new Moteur[3]exige un constructeur par défaut (ici il n'existe pas : erreur de compilation en réalité) et doit être libéré avecdelete[] t, pasdelete t.betcpointent le même objet :delete b; delete c;est une double libération. Un seuldelete, puis les deux pointeurs ànullptr.a->afficher(cout)utilise un pointeur pendant.
Constructeurs et valeurs par défaut
La classe Batterie a un nom, une masse, une tension (V), une capacité totale (Wh), une énergie restante (Wh, égale à la capacité au départ) et un courant maximal (A). Écrivez un constructeur par paramètres qui permette aussi de construire une batterie sans argument (nom vide, toutes les valeurs à 0). Puis écrivez getPuissanceMaxW() et getTempsRestant(double puissanceW) : 0 si la puissance est négative ou dépasse le maximum, l'infini (INFINITY de <cmath>) si elle est nulle, sinon énergie restante / puissance.
Solution
Grâce aux valeurs par défaut, Batterie b; et Batterie t[3]; compilent. energieWh_ est initialisé à partir du paramètre capaciteWh, pas de l'attribut capaciteWh_ (qui, lui, est bien déjà construit puisqu'il est déclaré avant, mais la règle d'ordre rend la version par paramètre plus sûre).