Méthodes constantes, passage de paramètres, vecteurs
constPar valeur, par référence, par pointeurRéférence constanteRésolution de surchargestd::vectorBoucle for sur un intervalle
Trois questions à se poser pour chaque paramètre et chaque méthode : est-ce qu'on copie ? est-ce qu'on modifie ? est-ce qu'on promet de ne pas modifier ? Le mot-clé const répond à la troisième, et le compilateur vérifie la promesse.
1. Méthodes constantes
Une méthode déclarée avec const après sa liste de paramètres promet de ne pas modifier l'objet sur lequel elle est appelée. Dans son corps, l'objet courant est vu comme constant.
- modifier un attribut (
charge_ = 0;→ erreur « cannot assign to non-static data member within const member function ») ; - appeler une méthode non constante de l'objet courant ou de ses attributs objets ;
- retourner une référence (ou un pointeur) non constante vers l'objet lui-même ou vers un attribut (cela ouvrirait une porte de modification).
Elle peut par contre modifier ses paramètres et ses variables locales, même s'ils sont de la même classe : la constance porte sur *this seulement.
Pourquoi ? Un objet constant (const Moteur m(...)) et une référence constante (const Moteur& m, le mode de passage le plus courant) ne peuvent appeler que des méthodes constantes. Si getNom() n'est pas const, un paramètre const Robot& r ne peut même pas faire r.getNom(). Règle pratique : tout accesseur et toute méthode d'affichage ou de calcul est const.
- Une méthode
constpeut retourner une copie d'un attribut ou uneconst T&, jamais uneT&. Les deux versionsT& get()etconst T& get() constpeuvent coexister (surcharge sur la constance) : le compilateur choisit selon que l'objet est constant ou non. constfait partie de la signature : si le.hditconst, le.cppdoit le répéter, sinon vous définissez une autre méthode (erreur « out-of-line definition does not match »).- Un attribut
mutablepeut être modifié par une méthodeconst(rare, pour un cache ou un compteur d'accès).
2. Passage de paramètres
| Mode | Syntaxe | Copie ? | L'appelé peut modifier l'original ? | Quand l'utiliser |
|---|---|---|---|---|
| Par valeur | void f(Robot r) | oui (constructeur de copie) | non, il travaille sur la copie | types fondamentaux (int, double, bool), petits objets, ou quand on veut justement une copie à modifier |
| Par référence | void f(Robot& r) | non (alias) | oui | paramètre de sortie, objet à modifier (swap, flux ostream&) |
| Par référence constante | void f(const Robot& r) | non | non (seules les méthodes const sont utilisables) | le mode par défaut pour tout objet qu'on ne veut que lire : string, vector, classes du TP |
| Par pointeur | void f(Robot* r) | copie de l'adresse seulement | oui (via r->) | quand « pas d'objet » (nullptr) est une valeur possible ; transfert de propriété (TP1 : installerBatterie(Batterie*)) ; tableaux C |
| Par pointeur constant | void f(const Robot* r) | adresse | non | observation d'un objet optionnel |
Référence : on évite de copier l'objet (temps et mémoire ; une copie d'un vecteur de 10 000 éléments copie les 10 000). Constante : on empêche de modifier l'original, et on peut passer des temporaires et des objets constants. Utilisez const T& partout où vous auriez écrit T sans avoir besoin de modifier le paramètre.
Retourner une valeur
- Par valeur (
Robot creer()) : copie (ou déplacement) du résultat. Toujours correct, même pour une variable locale. - Par référence constante (
const string& getNom() const) : pas de copie ; valable tant que l'objet qui contient l'attribut existe. C'est le choix des accesseurs des TP pour les chaînes et les vecteurs (const vector<Moteur*>& getMoteurs() const: on donne accès au vecteur sans le copier, sans permettre de le modifier). - Par référence (
Robot& operator+=(...)retourne*this) : pour le chaînage d'appels (module 4). - Jamais une référence ou un pointeur vers une variable locale de la fonction : elle est détruite au
return.
Résolution de surcharge
Plusieurs fonctions peuvent porter le même nom si leurs paramètres diffèrent (nombre ou types). Le compilateur choisit d'après le type des arguments, à la compilation. Le type de retour ne compte pas. Un appel qui correspond aussi bien à deux surcharges est ambigu : erreur de compilation.
3. Pointeur ou référence ?
Référence T& | Pointeur T* | |
|---|---|---|
| Doit désigner un objet | toujours, dès sa déclaration (Robot& r = a;) | non : peut valoir nullptr |
| Peut changer de cible | non | oui (p = &b;) |
| Accès aux membres | r.getNom() | p->getNom() |
| Arithmétique | non | oui (p + 1, tableaux) |
| Dans le cours | paramètres et retours de fonctions | allocation dynamique, agrégation, tableaux, polymorphisme |
Attention aux deux sens du symbole & : dans une déclaration (Robot& r, void f(const Robot&)) il signifie « référence » ; dans une expression (&q) il signifie « adresse de ». De même * : « pointeur » dans une déclaration, « objet pointé » dans une expression.
4. Les vecteurs de la bibliothèque standard
std::vector<T> (en-tête <vector>) est un tableau dynamique qui gère lui-même sa mémoire : il grandit au besoin, se copie en profondeur et libère ses éléments dans son destructeur. Dans les TP, il remplace les tableaux new T[n].
v[i]ne vérifie rien : lire ou écrire hors de[0, size()[est un comportement indéfini, même si la capacité est plus grande.vector<int> v(9); v[9] = 7;est une erreur.push_backpeut réallouer : tous les pointeurs, références et itérateurs vers les éléments deviennent invalides. Ne gardez pas&v[i]pendant qu'on ajoute.- Passer un vecteur par valeur le copie en entier. Paramètres :
const vector<T>&(lecture) ouvector<T>&(modification). size()retourne unsize_t(non signé) :v.size() - 1sur un vecteur vide donne un nombre gigantesque, et comparer avec unintprovoque-Wsign-compare.- Un
vector<Moteur*>détruit ses pointeurs, pas les objets pointés : avec des pointeurs bruts, c'est à vous de faire lesdelete(ou d'utiliser des pointeurs intelligents, module 3).
Retirer un élément en conservant l'ordre (TP1 : retirerMoteur)
Avec les algorithmes de la STL (module 9), la recherche s'écrit auto it = find(moteurs_.begin(), moteurs_.end(), moteur); puis if (it != moteurs_.end()) moteurs_.erase(it);.
5. À retenir
constaprès la signature : la méthode ne modifie pas*this. Obligatoire pour qu'elle soit appelable sur un objet ou une référence constante. Accesseurs, affichages, calculs : toujoursconst.- Objets en paramètre :
const T&par défaut,T&pour modifier,T(valeur) pour les types simples ou quand on veut une copie,T*quand l'absence est possible ou pour transférer la propriété. - La surcharge est résolue à la compilation d'après les types des arguments ; référence et valeur sont ambiguës entre elles.
vector:push_back,size,[](non vérifié) ouat(vérifié),insert/eraseavecbegin() + i,pop_back,empty,clear; bouclefor (const T& x : v).
Quiz rapide - Module 2
Que signifie le const dans double getMasse() const; ?
- La valeur retournée ne peut pas être modifiée par l'appelant
- La méthode ne peut être appelée qu'une fois
- La méthode ne modifie pas l'objet sur lequel elle est appelée
- Les paramètres de la méthode sont constants
const après la signature porte sur *this. Un const devant le type de retour (const string& getNom() const) porterait, lui, sur la valeur retournée.Dans une fonction void f(const Robot& r), lequel de ces appels compile ?
r.setNom("x");(setNom n'est pas const)r.getNom();(getNom est const)r = Robot("y");- Tous les trois
Une méthode const peut-elle modifier une variable locale de type Robot déclarée dans son corps ?
- Oui : la constance ne porte que sur l'objet courant
- Non : tout Robot est constant dans une méthode const
- Seulement si la variable est déclarée
mutable - Seulement si Robot n'a pas d'attributs
Quel mode de passage choisir pour un paramètre std::string qu'on ne fait que lire ?
string sstring& sstring* sconst string& s
Combien de fois le constructeur de copie de Robot est-il appelé ?
- 2
- 3
- 4
- 5
Robot b = a (1), f(a) par valeur (2), Robot c(b) (3). g(a) passe par référence constante : aucune copie.Pourquoi ce code est-il dangereux ?
- Il ne compile pas
- La concaténation copie les chaînes
- Il retourne une référence vers une variable locale détruite à la sortie
- Les paramètres devraient être passés par valeur
string nomComplet(...)). Une référence vers un objet détruit est un pointeur pendant déguisé.Avec void tester(Jouet& p) et void tester(Jouet p) toutes deux déclarées, l'appel tester(q) avec Jouet q; :
- Ne compile pas : appel ambigu
- Appelle la version par référence
- Appelle la version par valeur
- Appelle les deux
Qu'affiche ce programme ?
- 9
- 11
- 12
- 10
v[i] = ... ne changent pas la taille (et v[9] = 7 est valide seulement parce que la taille est redevenue 10). Question de l'intra H15.Quelle différence entre v[i] et v.at(i) ?
- Aucune
atvérifie l'indice et lancestd::out_of_range;[]ne vérifie rien[]vérifie l'indice,atnonatretourne une copie,[]une référence
at est vérifié (plus lent, mais sûr).Pour parcourir un vector<Robot> robots sans copier chaque robot ni le modifier, on écrit :
for (Robot r : robots)for (Robot& r : robots)for (const Robot& r : robots)for (Robot* r : robots)
Que fait v.erase(v.begin() + 2); ?
- Retire l'élément d'indice 2 et décale les suivants vers la gauche
- Retire les 2 premiers éléments
- Retire l'élément de valeur 2
- Met l'élément d'indice 2 à zéro
erase prend un itérateur ; begin() + i désigne l'indice i. La taille diminue de 1 et l'ordre des autres est conservé.Que se passe-t-il quand push_back dépasse la capacité du vecteur ?
- Une exception est lancée
- Un bloc plus grand est alloué, les éléments y sont copiés, l'ancien bloc est libéré ; les anciens pointeurs vers les éléments deviennent invalides
- L'élément est refusé
- Le vecteur est vidé
Un vector<Capteur*> est détruit. Que deviennent les capteurs pointés ?
- Ils sont détruits automatiquement
- Ils sont copiés dans un autre vecteur
- Rien : seuls les pointeurs disparaissent, les objets restent alloués
- Le programme plante
delete, c'est une fuite. C'est la situation du TP1 (agrégation).Que vaut v.size() - 1 si v est vide ?
- -1
- 0
- Une exception
- Un très grand nombre positif (débordement non signé)
size() retourne un size_t, non signé : 0 - 1 déborde vers la valeur maximale. Écrire i + 1 < v.size() plutôt que i < v.size() - 1.Quelle signature permet à f d'accepter nullptr comme argument ?
void f(Batterie* b)void f(Batterie& b)void f(const Batterie& b)void f(Batterie b)
installerBatterie(nullptr) du TP1 pour retirer la batterie).Exercices à développement
Compter les appels au constructeur de copie
La classe Dragon est fonctionnelle et possède tous les constructeurs nécessaires. Combien de fois son constructeur de copie est-il appelé ?
Indice
Le constructeur de copie sert à créer un objet à partir d'un autre : déclaration avec initialisation, paramètre par valeur, retour par valeur. Une affectation entre deux objets existants appelle operator=, pas le constructeur de copie.
Solution
5 appels.
Dragon dragon2(dragon1);: 1.Dragon dragon3 = dragon1;: 2 (c'est une initialisation, pas une affectation).dragon3 = dragon2;: opérateur d'affectation, pas de copie.fonction1(dragon1);: paramètre par valeur, 3. (Dragon dragon2;à l'intérieur : constructeur par défaut.)fonction2(dragon1): le paramètre est une référence constante (0) ; lereturn *new Dragon()construit la valeur de retour par copie de l'objet dynamique : 4. Puisdragon4 = ...est une affectation. Au passage, l'objet créé parnewn'est jamais libéré : fuite.fonction1(dragon4);: 5.
Rendre la classe const-correcte
Ce code ne compile pas. Trouvez les erreurs liées à const et corrigez la déclaration de la classe pour que main fonctionne sans retirer aucun const de main.
Solution
afficherreçoit uneconst Pince&:getNometgetConsommationWdoivent êtreconst, sinon « member function not viable: 'this' argument has type const Pince ».setFermeedéclaréconstne peut pas écrirefermee_ = f. On retire leconst. Il reste appelable surmobile(non constant).
Achats::Ajout
Soit la fonction membre Ajout de la classe Achats, qui reçoit le nom de l'item (string) et la quantité (int) et l'insère dans le vecteur mesAchats_. Donnez uniquement l'implémentation de Ajout.
Solution
On construit un Item temporaire et push_back le copie dans le vecteur. (mesAchats_.emplace_back(nom, quantite); construit l'élément directement dans le vecteur.) Remarque : getItems() retourne le vecteur par valeur, donc une copie complète à chaque appel ; const vector<Item>& serait préférable.
Masse et consommation d'un robot
Le robot du TP1 possède Batterie* batterie_, Chassis* chassis_ (peuvent être nullptr) et des vecteurs vector<Moteur*> moteurs_, vector<Capteur*> capteurs_, vector<Pince*> pinces_. Écrivez double Robot::getMasse() const (somme des masses de toutes les pièces présentes) et double Robot::getConsommationW() const (somme des consommations des moteurs, capteurs et pinces ; la batterie et le châssis ne consomment pas). Chaque pièce a getMasseKg() et, sauf la batterie et le châssis, getConsommationW().
Solution
Les deux méthodes sont const (elles ne modifient pas le robot) et n'appellent que des méthodes const des pièces. Les boucles sur intervalle avec const Moteur* copient seulement le pointeur.
Choisir le bon mode de passage
Pour chaque fonction, proposez la signature la plus appropriée (valeur, référence, référence constante, pointeur) et justifiez en une phrase.
echanger: échange le contenu de deux robots.estPlusLourd: compare la masse de deux robots et retourne un booléen.installerBatterie(TP1) : donne au robot une batterie allouée avecnew, ou aucune.afficher: écrit la description d'un robot dans un flux de sortie.setCharge: règle la charge d'un moteur avec undouble.nomsDesMoteurs: retourne la liste des noms des moteurs d'un robot.
Solution
void echanger(Robot& a, Robot& b): on doit modifier les deux originaux.bool estPlusLourd(const Robot& a, const Robot& b): lecture seule, sans copier les robots.void Robot::installerBatterie(Batterie* batterie): « aucune » se représente parnullptr, et le pointeur matérialise le transfert de propriété (le robot fera ledelete).void Robot::afficher(std::ostream& out) const: un flux se passe toujours par référence (il n'est pas copiable, et écrire dedans le modifie) ; la méthode estconst.bool Moteur::setCharge(double charge): undoublese passe par valeur.vector<string> Robot::nomsDesMoteurs() const: un nouveau vecteur est construit dans la fonction, on le retourne par valeur (jamais par référence : il serait détruit).
Insertion triée et suppression
Écrivez void insererTrie(vector<int>& v, int val) qui insère val dans un vecteur déjà trié en ordre croissant, en gardant le tri, et void supprimerValeur(vector<int>& v, int val) qui retire toutes les occurrences de val.
Indice
Pour la suppression, attention à l'indice après un erase : l'élément suivant a pris la place de celui qu'on vient de retirer, il ne faut pas l'enjamber.
Solution
Version STL (module 9) : v.insert(lower_bound(v.begin(), v.end(), val), val); et v.erase(remove(v.begin(), v.end(), val), v.end());.