Chapitre 2 – La copie des objets
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.).
2 La copie des objets
2.1 Que signifie copier un objet ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.3 Que copie réellement le compilateur ? . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.4 Une classe contenant un pointeur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2026 – C++ Partie III
AccueilQue signifie copier un objet ? Nous avons déjà rencontré à de nombreuses reprises la copie de variables simples. Par exemple : Après ces instructions, les variables a et b contiennent toutes les deux la valeur 5, mais elles constituent deux variables distinctes. Modifier ensuite b ne modifie pas a : La valeur de a a donc servi à initialiser b, mais les deux variables sont ensuite indépendantes. Considérons maintenant une classe très simple représentant un point du plan : Point(double xInitial, double yInitial); double getX() const; double getY() const; void setX(double nouvelleValeur); void setY(double nouvelleValeur); 2026 – C++ Partie III Son constructeur initialise simplement les deux coordonnées : Point::Point(double xInitial, double yInitial) Créons maintenant un objet A : et utilisons cet objet pour en créer un second : Nous n’avons pourtant défini aucun constructeur permettant explicitement de construire B à partir Le programme est néanmoins parfaitement valide. Nous avons uniquement défini le constructeur Point(double xInitial, double yInitial); Comment le compilateur peut-il alors exécuter l’instruction suivante ? Le compilateur C++ peut générer automatiquement certaines fonctions spéciales nécessaires au fonctionnement d’une classe. Parmi celles-ci se trouve le constructeur de copie. Lorsque nous écrivons un nouvel objet B est construit à partir de l’objet A. Dans notre classe Point, le constructeur de copie généré automatiquement effectue une copie des membres de l’objet. Nous obtenons donc conceptuellement : x = 2.0 −→ x = 2.0 y = 5.0 −→ y = 5.0 Les deux objets possèdent les mêmes valeurs immédiatement après la copie, mais restent deux objets Nous pouvons le vérifier en modifiant uniquement l’objet B :
Accueil2026 – C++ Partie III std::cout << A.getX() << std::endl; std::cout << B.getX() << std::endl; Le programme affiche : La modification de B n’a donc eu aucun effet sur A. • Lorsqu’un nouvel objet est construit à partir d’un objet existant de la même classe, une opération de copie est effectuée. • Si aucun constructeur de copie particulier n’a été défini par le programmeur, le compilateur peut en générer un automatiquement. • Pour une classe simple comme Point, cette copie des membres produit deux objets indépendants possédant initialement les mêmes valeurs. Les deux écritures suivantes ne correspondent pas à la même opération : construit un nouvel objet B à partir de A. suppose que B existe déjà et lui affecte les valeurs provenant de A. Nous distinguerons donc soigneusement, dans la suite de ce chapitre, construction par copie et affectation par copie. Que copie réellement le compilateur ? Dans le cas de la classe Point, la copie automatique semble produire exactement le résultat souhaité. Mais cette situation est-elle toujours aussi simple ? Pour répondre à cette question, nous devons examiner plus précisément ce que signifie la copie des membres d’un objet. Une classe contenant un pointeur Pour comprendre ce qui peut rendre la copie d’un objet plus délicate, considérons maintenant une classe volontairement très simple.
Accueil2026 – C++ Partie III La classe EntierDynamique représente une valeur entière. Cependant, contrairement à une classe qui contiendrait directement un membre de type int, la valeur sera placée dans une zone de mémoire allouée dynamiquement. La classe contient donc un pointeur : class EntierDynamique EntierDynamique(int valeurInitiale); int getValeur() const; void setValeur(int nouvelleValeur); Lors de la construction de l’objet, une zone mémoire permettant de stocker un int est réservée dynamiquement. L’adresse de cette zone est conservée dans le membre valeur. EntierDynamique::EntierDynamique(int valeurInitiale) *valeur = valeurInitiale; Lorsque l’objet disparaît, la zone mémoire qu’il possède doit être libérée. EntierDynamique::~EntierDynamique() La lecture et la modification de l’entier nécessitent un déréférencement du pointeur : int EntierDynamique::getValeur() const void EntierDynamique::setValeur(int nouvelleValeur) *valeur = nouvelleValeur;
Accueil2026 – C++ Partie III Considérons le programme suivant : EntierDynamique A(5); EntierDynamique B = A; std::cout << A.getValeur() << std::endl; std::cout << B.getValeur() << std::endl; Quelles valeurs pensez-vous que le programme affichera ? Les objets A et B sont-ils indépendants comme l’étaient les deux objets de la classe Point ? Réponse : L’exécution s’arrête avec une erreur parce que les deux objets A et B possèdent après la copie la même adresse dans leur membre valeur. A la fin de main le programme essaie de détruire deux fois la même variable valeur. Pour remédier à cette erreur on va construire un nouveau constructeur qui aura comme paramètre un objet de type EntierDynamique. Ce constructeur est : EntierDynamique::EntierDynamique( const EntierDynamique& autre) *valeur = *autre.valeur; valeur int réserve un nouvel int et place son adresse dans le pointeur valeur du nouvel objet. ∗valeur = ∗autre.valeur; fait deux référencements.
Accueil2026 – C++ Partie III • La classe EntierDynamique possède maintenant deux constructeurs de signatures différentes : EntierDynamique(int valeurInitiale); EntierDynamique(const EntierDynamique& autre); Il s’agit d’une surcharge de constructeurs. Le constructeur appelé dépend du type de l’argument utilisé lors de la construction de l’objet. EntierDynamique A(5); appelle le constructeur recevant un int. Celui-ci réserve dynamiquement une zone mémoire permettant de stocker un entier et conserve son adresse dans le membre valeur. EntierDynamique B = A; construit un nouvel objet à partir d’un objet existant de la même classe. Elle fait appel au constructeur de copie : EntierDynamique(const EntierDynamique& autre); • Le paramètre autre est passé par référence constante. La référence évite de devoir effectuer une copie pour transmettre l’objet au constructeur de copie, et const garantit que l’objet source ne sera pas modifié. • Dans notre constructeur de copie, EntierDynamique::EntierDynamique( const EntierDynamique& autre) *valeur = *autre.valeur; une nouvelle zone mémoire est réservée pour le nouvel objet. La valeur contenue dans la zone mémoire de l’objet source est ensuite copiée dans cette nouvelle zone. • Les deux objets possèdent donc des allocations différentes contenant initialement la même valeur. Il s’agit d’une copie profonde (deep copy ). • Une méthode d’une classe peut accéder aux membres privés d’un autre objet de la même classe. L’expression est donc parfaitement valide même si valeur est déclaré private. • Lors d’une copie profonde, il faut distinguer soigneusement la valeur copiée de l’adresse. Les deux objets contiennent initialement la même valeur, mais leurs pointeurs contiennent des adresses différentes.
Accueil2026 – C++ Partie III L’affectation par copie Nous avons étudié la construction d’un nouvel objet à partir d’un objet existant : EntierDynamique A(5); EntierDynamique B = A; Au moment de l’instruction EntierDynamique B = A; l’objet B n’existe pas encore donc il doit être construit, c’est donc le constructeur de copie qui est La situation est différente dans le programme suivant : EntierDynamique A(5); EntierDynamique B(12); Lorsque l’instruction est exécutée, les deux objets existent déjà. Il ne s’agit donc plus de construire B, mais de lui affecter Cette opération est appelée affectation par copie. Avant l’affectation, nous avons deux objets indépendants : B.valeur −→ [ 12 ] L’objet B possède donc déjà sa propre zone de mémoire dynamique. Lorsqu’une méthode non statique est appelée, elle agit nécessairement sur un objet particulier. Considérons par exemple : EntierDynamique A(5); Lors de l’exécution de la méthode setValeur(), celle-ci doit savoir sur quel objet elle doit agir. Le C++ met pour cela automatiquement à sa disposition un pointeur particulier nommé this. Ce pointeur contient l’adresse de l’objet sur lequel la méthode est actuellement exécutée. void EntierDynamique::setValeur(int nouvelleValeur) *valeur = nouvelleValeur; pourrait également être écrite :
Accueil2026 – C++ Partie III void EntierDynamique::setValeur(int nouvelleValeur) *(this->valeur) = nouvelleValeur; Dans la première écriture, l’utilisation de this est simplement implicite. Le mot-clé this est un pointeur. Si une méthode est appelée par l’objet A, nous pouvons conceptuellement écrire : this = adresse de A. désigne donc l’adresse de l’objet courant. Comme pour tout pointeur, l’opérateur de déréférencement permet d’accéder à l’objet situé à cette désigne donc l’objet courant lui-même. Puisque this est un pointeur, l’accès à un membre de l’objet courant peut s’écrire avec l’opérateur Cette expression désigne le membre valeur de l’objet pointé par this. Elle est équivalente à : Les parenthèses sont nécessaires car l’opérateur . possède une priorité supérieure à l’opérateur de déréférencement ∗. • Dans toute méthode non statique, this est un pointeur vers l’objet sur lequel la méthode • this représente donc l’adresse de l’objet courant, tandis que ∗this représente l’objet permet d’accéder à un membre de l’objet courant. • Dans la plupart des méthodes, l’utilisation explicite de this n’est pas nécessaire. • Le pointeur this devient particulièrement utile lorsqu’il est nécessaire de connaître ou de comparer l’adresse de l’objet courant.
Accueil2026 – C++ Partie III EntierDynamique& EntierDynamique::operator=( const EntierDynamique& autre) *valeur = *autre.valeur; Ce code permet d’effectuer A=A ! • Le constructeur de copie intervient lorsqu’un nouvel objet est créé à partir d’un objet • L’opérateur d’affectation intervient lorsque l’objet destination existe déjà. • Une affectation profonde doit libérer l’ancienne ressource avant d’en créer une nouvelle. permet d’éviter les problèmes liés à l’auto-affectation. • L’opérateur d’affectation retourne généralement ∗this, c’est-à-dire l’objet courant.
AccueilP3-Chap-02- La copie des objets
AccueilTermes à ajouter au glossaire