Pointeur this, surcharge des opérateurs, constructeur de copie et opérateur d'affectation
thisAppels en cascadeOpérateur membre ou friendoperator<<++ préfixe et suffixeCopie superficielle et profondeRègle des trois
Le module le plus sollicité aux intras : « donnez l'implémentation de la surcharge de ... », « quelles fonctions membres manquent pour éviter la copie superficielle ? », « combien de fois le constructeur de copie est-il appelé ? ». Tout ce qu'il faut est ici.
1. Le pointeur this
Dans toute méthode, this est un pointeur vers l'objet courant (celui sur lequel la méthode a été appelée). Il est implicite : écrire nom_ revient à écrire this->nom_. On l'explicite dans trois situations :
- Conflit de noms : un paramètre porte le même nom qu'un attribut.
void setNom(string nom) { this->nom = nom; }. (Avec la convention du suffixe_, le conflit n'arrive pas :nom_ = nom;.) - Retourner l'objet courant :
return *this;dansoperator=,operator+=, pour permettre les appels en cascade(robot += moteur) += capteur;. - Comparer des adresses :
if (this != &autre)détecte l'auto-affectationa = a;.
Dans une méthode const, this est de type const Classe* : impossible de modifier *this.
2. Surcharge des opérateurs
Surcharger un opérateur, c'est définir une fonction nommée operatorop pour qu'une expression comme a + b ou cout << a ait un sens pour vos classes. Le compilateur traduit a + b en a.operator+(b) (fonction membre) ou operator+(a, b) (fonction libre).
- On ne peut pas inventer de nouveaux opérateurs ni changer leur priorité, leur associativité ou leur nombre d'opérandes (un opérateur unaire agit sur l'objet courant seul :
-a,++a,!a; un binaire a un opérande gauche et un droit). - Au moins un opérande doit être d'un type classe (on ne redéfinit pas
int + int). - Non surchargeables :
::,.,.*,?:,sizeof. - Une surcharge n'implique pas les autres : définir
==ne donne pas!=, définir+ne donne pas+=.
Fonction membre ou fonction amie (friend) ?
| Fonction membre | Fonction libre (friend si elle touche aux membres privés) | |
|---|---|---|
| Forme | Type operatorop(const Droite& d) const; : l'opérande gauche est *this | friend Type operatorop(const Gauche& g, const Droite& d); : les deux opérandes sont des paramètres |
| Quand | l'opérande gauche est un objet de la classe | l'opérande gauche est d'un autre type (ostream, int), ou pour que l'opérateur soit commutatif (2 * v et v * 2) |
| Obligatoirement membre | =, [], (), -> | |
| Obligatoirement libre | << et >> sur les flux (l'opérande gauche est le flux, pas notre objet) |
Une fonction friend est une fonction globale déclarée dans la classe avec le mot-clé friend : elle n'est pas membre (pas de this, pas de Classe:: dans sa définition), mais elle a accès aux membres privés. Sans friend, la fonction libre doit passer par les accesseurs publics.
Les signatures à connaître par cœur
+,-,*produisent une nouvelle valeur : retour par valeur, méthodeconst.=,+=,-=,++préfixe modifient l'objet : retourClasse&avecreturn *this;, jamaisconst.==,<,!=:bool,const.<<:ostream&(le même flux), pour enchaînercout << a << b << endl;.[]: une référence (double&) pour permettrev[0] = 5;, plus une versionconstqui retourneconst double&ou une copie. Le TP2 retourneconst shared_ptr<Moteur>&: on peut régler le moteur pointé, pas remplacer le pointeur.
Pourquoi les appels en cascade fonctionnent
cout << "x = " << x << endl; s'évalue de gauche à droite : operator<<(cout, "x = ") retourne cout (une référence vers le même flux), qui devient l'opérande gauche de l'appel suivant. C'est exactement comme écrire les trois instructions l'une après l'autre, en plus lisible. Si operator<< retournait void, le deuxième << n'aurait pas de flux à gauche : erreur de compilation. Même principe pour (robot += moteur) += capteur; et a = b = c;.
Pourquoi un paramètre int fictif pour le ++ suffixe ?
++mc et mc++ appellent tous deux un opérateur à un seul opérande : impossible de les distinguer par les arguments. Le langage impose donc que la version suffixe déclare un paramètre int jamais utilisé : mc++ est traduit en mc.operator++(0), ++mc en mc.operator++(). La version préfixe incrémente et retourne l'objet lui-même (par référence) ; la version suffixe sauvegarde une copie, incrémente via la préfixe, et retourne la copie (par valeur : l'ancienne valeur).
Trois écritures équivalentes
Surcharger << sans friend (intra H16)
Quand la classe n'a pas déclaré la fonction friend, on écrit une fonction libre qui n'utilise que l'interface publique :
3. Constructeur de copie et opérateur d'affectation
Le compilateur fournit par défaut un constructeur de copie et un opérateur d'affectation qui copient membre à membre. Pour une classe faite de string, vector, double et shared_ptr, c'est exactement ce qu'il faut (chaque membre sait se copier correctement : un vecteur copie ses éléments). Le problème surgit avec un pointeur brut vers une ressource possédée (composition par pointeur) : la copie membre à membre copie l'adresse, pas l'objet pointé.
- Modifier l'un des objets modifie l'autre (ils partagent le même bloc).
- La destruction du premier objet laisse le second avec un pointeur pendant.
- La destruction du second fait un double delete : plantage.
Les deux fonctions à écrire
Si une classe a besoin de l'un des trois (destructeur, constructeur de copie, opérateur d'affectation), elle a presque sûrement besoin des trois. Le marqueur : un new dont la classe est responsable. Si la copie n'a pas de sens (TP1), on l'interdit : Robot(const Robot&) = delete; Robot& operator=(const Robot&) = delete;. Si la copie par défaut convient, on peut l'écrire explicitement : = default. (En C++ moderne s'ajoutent le constructeur et l'affectation de déplacement, T(T&&) : règle des cinq. Le cours s'en tient aux trois.)
Quand le constructeur de copie est-il appelé ?
- Initialisation à partir d'un objet du même type :
Robot b(a);,Robot b = a;,Robot b{a};. - Passage d'un paramètre par valeur.
- Retour par valeur (sauf élision du compilateur : depuis C++17,
Robot r = creer();construit directementrquandcreerretourne un temporaire). - Insertion dans un conteneur par valeur :
v.push_back(a).
b = a; entre deux objets déjà construits appelle operator=, jamais le constructeur de copie. La ligne Robot b = a; est trompeuse : c'est une initialisation (constructeur de copie), pas une affectation.
Copie et affectation avec des pointeurs intelligents (TP2)
*autre.batterie_ déréférence le unique_ptr pour obtenir la Batterie, dont le constructeur de copie reproduit tous les attributs, y compris l'énergie restante (copier une batterie déchargée ne la remplit pas). Copier un robot ne duplique donc que ce qu'il possède en exclusivité ; les pièces partagées voient leur use_count augmenter.
4. À retenir
this: pointeur vers l'objet courant ;return *this;pour le chaînage,this != &autrecontre l'auto-affectation.- Membre quand l'opérande gauche est l'objet ; friend ou fonction libre quand il est d'un autre type (flux, scalaire) ou pour la commutativité ;
=,[],()sont obligatoirement membres ;<<obligatoirement libre. - Retours : valeur pour
+ - *(const),Classe&pour= += ++préfixe,boolpour les comparaisons,ostream&pour<<, référence pour[]. ++suffixe : paramètreintfictif, retourne l'ancienne valeur par valeur.- Copie superficielle = adresse copiée. Règle des trois : destructeur, constructeur de copie,
operator=(test d'auto-affectation, libérer, réallouer, copier,return *this). Avec vector, string, shared_ptr, la copie par défaut convient ; avec unique_ptr, il faut l'écrire ; avec un pointeur brut possédé, il faut l'écrire ou l'interdire.
Quiz rapide - Module 4
Qu'est-ce que this ?
- Une référence vers la classe
- Une copie de l'objet courant
- Un pointeur vers l'objet sur lequel la méthode est appelée
- Un pointeur vers le premier attribut
robot.afficher(), this == &robot à l'intérieur d'afficher. *this est l'objet lui-même.Peut-on surcharger operator<< (flux de sortie) comme fonction membre de la classe AA ?
- Oui, c'est même recommandé
- Non : l'opérande gauche est le flux, pas un AA ; il faut une fonction libre (friend ou non)
- Oui, si AA hérite de ostream
- Oui, mais seulement pour cout
AA::operator<<(ostream&) correspondrait à aa << cout. Pour cout << aa, la fonction doit être libre. Question vrai/faux de l'intra H16 : FAUX.Les surcharges de « objet AA + int » et « int + objet AA » sont toutes deux des fonctions membres de AA.
- Vrai
- Vrai si AA définit un constructeur à partir d'un int
- Faux : aucune des deux ne peut être membre
- Faux : seule « AA + int » peut être membre ; « int + AA » doit être une fonction libre (friend)
Quelle signature est correcte pour l'opérateur += de la classe Complexe, fonction membre ?
Complexe& operator+=(const Complexe& c);Complexe operator+=(const Complexe& c) const;void operator+=(Complexe c);friend Complexe& operator+=(Complexe& a, const Complexe& b);
Pourquoi operator<< retourne-t-il une référence vers le flux ?
- Pour éviter de copier l'objet affiché
- Pour permettre les appels en cascade : le résultat de
cout << asert d'opérande gauche à<< b - Parce que ostream n'a pas de destructeur
- Pour que l'opérateur soit commutatif
void interdirait cout << a << b.Quelle est la différence entre T& operator++() et T operator++(int) ?
- La première incrémente de 1, la seconde de la valeur de l'int
- La première est friend, la seconde membre
- La première est le ++ préfixe (retourne l'objet incrémenté par référence), la seconde le ++ suffixe (l'int fictif sert seulement à les distinguer ; retourne l'ancienne valeur par valeur)
- Aucune, ce sont des synonymes
mc++ devient mc.operator++(0). Le paramètre n'est jamais utilisé.Dans operator=, comment vérifier qu'on n'affecte pas un objet à lui-même ?
if (this != &autre): comparer l'adresse de l'objet courant à celle du paramètreif (*this != autre): comparer les contenusif (this != autre)- Ce n'est jamais nécessaire
operator!= et laisserait passer deux objets distincts de même valeur. Sans ce test, t = t; ferait delete[] sur les données avant de les copier : lecture d'une mémoire libérée.Quelles fonctions membres manquent à cette classe pour éviter la copie superficielle de attribut_ ?
- Un constructeur par défaut
- Un accesseur et un mutateur
- Seulement le constructeur de copie
- Le constructeur de copie et l'opérateur d'affectation
AA(const AA&) (appelé par uneFonction(deux), passage par valeur, et par les retours par valeur) et AA& operator=(const AA&) (appelé par un = deuxFonction();). Question de l'intra H16.Laquelle de ces lignes appelle l'opérateur d'affectation plutôt que le constructeur de copie ?
Robot b = a;b = a;(b déjà déclaré)Robot b(a);f(a);avecvoid f(Robot r)
operator=. Robot b = a; est une initialisation malgré le signe =.La classe Achats n'a qu'un attribut vector<Item> mesAchats_. Pour permettre liste2 = liste;, faut-il écrire un opérateur d'affectation ?
- Oui, obligatoirement, sinon erreur de compilation
- Oui, pour éviter la copie superficielle du vecteur
- Non : l'opérateur par défaut appelle
operator=de vector, qui copie les éléments en profondeur - Non, car un vecteur ne peut pas être copié
Pourquoi operator+ devrait-il être const et retourner par valeur ?
- Il crée un nouvel objet sans modifier ses opérandes ; retourner une référence pointerait vers une variable locale détruite
- Pour qu'il soit commutatif
- Parce que le compilateur l'exige
- Pour pouvoir l'enchaîner avec
<<
a + b ne change ni a ni b : const. Le résultat est un objet neuf, construit dans la fonction : par valeur.Que se passe-t-il lors de la copie d'un Robot du TP2 (batterie en unique_ptr, moteurs en vector de shared_ptr) avec le constructeur de copie décrit dans l'énoncé ?
- Les deux robots partagent la même batterie
- Les moteurs sont dupliqués
- Rien n'est copié, c'est une erreur
- La batterie est dupliquée (objet distinct) ; les moteurs sont partagés (mêmes objets, use_count augmente)
Quel est l'intérêt de l'idiome copie-et-échange (copy-and-swap) dans operator= ?
- Il évite d'écrire un constructeur de copie
- Il réutilise le constructeur de copie et le destructeur ; si la copie échoue, l'objet d'origine n'est pas modifié à moitié
- Il est plus rapide que la copie membre à membre
- Il rend l'opérateur commutatif
Quel doit être le type de retour de operator[] pour que v[0] = 5; compile ?
doubleconst double&double&bool
const pour les objets constants. Si on refuse l'écriture (intra H16 : retourner un Item par défaut hors bornes), retourner par valeur est un choix de conception valable.Une fonction friend d'une classe :
- N'est pas membre de la classe (pas de this) mais a accès à ses membres privés
- Est une méthode statique
- Doit être définie dans le .cpp avec
Classe:: - Ne peut accéder qu'aux membres publics
Classe::).Exercices à développement
Achats : operator[], operator+ et operator<<
Avec les classes Item et Achats du module 2 (attribut vector<Item> mesAchats_, accesseur public getItems()), donnez uniquement l'implémentation de :
- la surcharge de l'opérateur d'indexation
[], qui retourne l'item à l'indice demandé ; vous devez vérifier les bornes et retourner un Item par défaut si l'indice sort des bornes ; - la surcharge de l'opérateur
+, fonction membre, qui combine deux listes d'achats en une nouvelle liste ; - sans fonction amie, la surcharge de
<<qui affiche les items d'un Achats.
Solution
Avec un indice int, il faudrait tester index >= 0 && index < mesAchats_.size() (et convertir pour éviter -Wsign-compare). Déclarations correspondantes dans la classe : Item operator[](size_t index) const; et Achats operator+(const Achats& autre) const;.
AA : éviter la copie superficielle
Pour la classe AA du quiz (attribut string* attribut_, destructeur qui fait delete attribut_), écrivez les deux fonctions membres manquantes. Attention : attribut_ peut être nullptr.
Solution
Dans le main de l'énoncé, uneFonction(deux) (paramètre par valeur) et AA deuxFonction() (retour par valeur) appellent le constructeur de copie ; un = deuxFonction(); appelle l'opérateur d'affectation. Sans ces fonctions : double delete à la sortie de uneFonction.
Classe Complexe
Écrivez la classe Complexe (partie réelle et imaginaire, double) avec : constructeur (valeurs par défaut 0), += (méthode), + entre deux complexes (méthode const), * entre un double et un complexe dans les deux sens, ==, - unaire, et << au format 3 + 2i.
Solution
Variante sans friend pour + : Complexe operator+(const Complexe& a, const Complexe& b) { Complexe r(a); r += b; return r; } réutilise +=.
Copie d'un robot avec pointeurs intelligents
La classe Robot possède std::string nom_, std::unique_ptr<Batterie> batterie_, std::unique_ptr<Chassis> chassis_ et std::vector<std::shared_ptr<Moteur>> moteurs_. Écrivez le constructeur de copie (batterie et châssis dupliqués s'ils existent, moteurs partagés) et l'opérateur d'affectation avec l'idiome copie-et-échange. Pourquoi ne peut-on pas simplement écrire batterie_(autre.batterie_) ?
Solution
batterie_(autre.batterie_) tenterait de copier un unique_ptr : constructeur supprimé, erreur de compilation. Il faut construire une nouvelle batterie à partir de l'objet pointé, *autre.batterie_, avec le constructeur de copie de Batterie. L'opérateur ternaire garde nullptr si l'original n'a pas de batterie.
Qui est appelé ?
La classe Z affiche « ctor », « copie », « affect » ou « dtor » dans chacune de ses quatre fonctions spéciales (comme la classe P plus haut). Donnez l'affichage de :
Indice
Comptez les objets créés (chacun finira par un « dtor ») et distinguez initialisation (constructeur de copie) et affectation entre objets existants (operator=, même quand l'objet est affecté à lui-même).
Solution
Z a;ctor.Z b(a);copie.Z c = b;copie (initialisation, pas affectation).a = b;affect.parValeur(a): copie du paramètre, puis dtor à la sortie de la fonction.parRef(a): rien du tout.new Z(b): copie sur le tas ;delete p: dtor.c = c;: l'opérateur d'affectation est appelé (affect) ; c'est le testthis != &autreà l'intérieur qui évite de casser l'objet.- Fin de main : dtor c, dtor b, dtor a (ordre inverse de déclaration). Au total 5 objets créés, 5 détruits.
Pour l'examen, l'analyse attendue : une copie par passage par valeur, une par new Z(objet) ou Z x = objet, aucune par référence ; une affectation par = entre objets existants.
Personnage : égalité, affichage, incrémentation
La classe Personnage a string nom_, int niveau_, int experience_. Écrivez : operator== (même nom et même niveau), operator!= en réutilisant ==, operator<< (friend) qui affiche « nom (niveau N) », ++ préfixe (monte d'un niveau) et ++ suffixe, puis operator+=(int xp) qui ajoute de l'expérience et monte d'un niveau par tranche de 100 points. Montrez ensuite un appel en cascade.