- Que signifie copier un objet ?
- Que sont le constructeur de copie et l' opérateur d'affectation de copie ?
- Quand dois-je les déclarer moi-même?
- Comment empêcher la copie de mes objets?
Réponses:
C ++ traite les variables de types définis par l'utilisateur avec une sémantique de valeur . Cela signifie que les objets sont implicitement copiés dans divers contextes, et nous devons comprendre ce que signifie réellement "copier un objet".
Prenons un exemple simple:
class person
{
std::string name;
int age;
public:
person(const std::string& name, int age) : name(name), age(age)
{
}
};
int main()
{
person a("Bjarne Stroustrup", 60);
person b(a); // What happens here?
b = a; // And here?
}
(Si la partie vous laisse perplexe name(name), age(age)
, cela s'appelle une liste d'initialisation de membre .)
Que signifie copier un person
objet? La main
fonction affiche deux scénarios de copie distincts. L'initialisation person b(a);
est effectuée par le constructeur de copie . Son travail consiste à construire un nouvel objet en fonction de l'état d'un objet existant. L'affectation b = a
est effectuée par l' opérateur d'affectation de copie . Son travail est généralement un peu plus compliqué, car l'objet cible est déjà dans un état valide qui doit être traité.
Puisque nous n'avons déclaré ni le constructeur de copie ni l'opérateur d'affectation (ni le destructeur) nous-mêmes, ceux-ci sont implicitement définis pour nous. Citation de la norme:
Le constructeur de copie et l'opérateur d'affectation de copie, [...] et le destructeur sont des fonctions membres spéciales. [ Remarque : L'implémentation déclarera implicitement ces fonctions membres pour certains types de classe lorsque le programme ne les déclare pas explicitement. L'implémentation les définira implicitement si elles sont utilisées. [...] note de fin ] [n3126.pdf section 12 §1]
Par défaut, copier un objet signifie copier ses membres:
Le constructeur de copie défini implicitement pour une classe non-union X effectue une copie membre par membre de ses sous-objets. [n3126.pdf section 12.8 §16]
L'opérateur d'affectation de copie défini implicitement pour une classe non-union X effectue l'affectation de copie par membre de ses sous-objets. [n3126.pdf section 12.8 §30]
Les fonctions membres spéciales définies implicitement pour person
ressemblent à ceci:
// 1. copy constructor
person(const person& that) : name(that.name), age(that.age)
{
}
// 2. copy assignment operator
person& operator=(const person& that)
{
name = that.name;
age = that.age;
return *this;
}
// 3. destructor
~person()
{
}
La copie par membre est exactement ce que nous voulons dans ce cas:
name
et age
est copiée, nous obtenons donc un person
objet indépendant et indépendant . Le destructeur défini implicitement est toujours vide. C'est également très bien dans ce cas puisque nous n'avons acquis aucune ressource dans le constructeur. Les destructeurs des membres sont implicitement appelés une fois le person
destructeur terminé:
Après avoir exécuté le corps du destructeur et détruit tous les objets automatiques alloués dans le corps, un destructeur pour la classe X appelle les destructeurs des membres directs de X [n3126.pdf 12.4 §6]
Alors, quand devrions-nous déclarer explicitement ces fonctions membres spéciales? Lorsque notre classe gère une ressource , c'est-à-dire lorsqu'un objet de la classe est responsable de cette ressource. Cela signifie généralement que la ressource est acquise dans le constructeur (ou transmise au constructeur) et libérée dans le destructeur.
Remontons le temps au C ++ pré-standard. Il n'y avait rien de tel std::string
, et les programmeurs étaient amoureux des pointeurs. La person
classe aurait pu ressembler à ceci:
class person
{
char* name;
int age;
public:
// the constructor acquires a resource:
// in this case, dynamic memory obtained via new[]
person(const char* the_name, int the_age)
{
name = new char[strlen(the_name) + 1];
strcpy(name, the_name);
age = the_age;
}
// the destructor must release this resource via delete[]
~person()
{
delete[] name;
}
};
Aujourd'hui encore, les gens écrivent des cours dans ce style et ont des ennuis: " J'ai poussé une personne dans un vecteur et maintenant j'ai des erreurs de mémoire folles! " Rappelez-vous que par défaut, copier un objet signifie copier ses membres, mais copier le name
membre simplement copie un pointeur, pas le tableau de caractères vers lequel il pointe! Cela a plusieurs effets désagréables:
a
peuvent être observées via b
.b
détruit, se a.name
trouve un pointeur suspendu.a
est détruit, la suppression du pointeur pendant entraîne un comportement indéfini .name
indiqué avant l'affectation, tôt ou tard, vous obtiendrez des fuites de mémoire partout.Étant donné que la copie par membre n'a pas l'effet souhaité, nous devons définir explicitement le constructeur de copie et l'opérateur d'affectation de copie pour effectuer des copies complètes du tableau de caractères:
// 1. copy constructor
person(const person& that)
{
name = new char[strlen(that.name) + 1];
strcpy(name, that.name);
age = that.age;
}
// 2. copy assignment operator
person& operator=(const person& that)
{
if (this != &that)
{
delete[] name;
// This is a dangerous point in the flow of execution!
// We have temporarily invalidated the class invariants,
// and the next statement might throw an exception,
// leaving the object in an invalid state :(
name = new char[strlen(that.name) + 1];
strcpy(name, that.name);
age = that.age;
}
return *this;
}
Notez la différence entre l'initialisation et l'affectation: nous devons supprimer l'ancien état avant de l'affecter name
pour éviter les fuites de mémoire. Nous devons également nous protéger contre l'auto-affectation du formulaire x = x
. Sans cette vérification, delete[] name
supprimerait le tableau contenant la source de chaîne, parce que quand vous écrivez x = x
, à la fois this->name
et that.name
contiennent le même pointeur.
Malheureusement, cette solution échouera si new char[...]
lève une exception en raison de l'épuisement de la mémoire. Une solution possible consiste à introduire une variable locale et à réorganiser les instructions:
// 2. copy assignment operator
person& operator=(const person& that)
{
char* local_name = new char[strlen(that.name) + 1];
// If the above statement throws,
// the object is still in the same state as before.
// None of the following statements will throw an exception :)
strcpy(local_name, that.name);
delete[] name;
name = local_name;
age = that.age;
return *this;
}
Cela prend également en charge l'auto-affectation sans vérification explicite. Une solution encore plus robuste à ce problème est l' idiome de copie et d'échange , mais je n'entrerai pas dans les détails de la sécurité des exceptions ici. J'ai seulement mentionné des exceptions pour faire le point suivant: Écrire des classes qui gèrent des ressources est difficile.
Certaines ressources ne peuvent pas ou ne doivent pas être copiées, comme les descripteurs de fichiers ou les mutex. Dans ce cas, déclarez simplement le constructeur de copie et l'opérateur d'affectation de copie comme private
sans donner de définition:
private:
person(const person& that);
person& operator=(const person& that);
Alternativement, vous pouvez les hériter boost::noncopyable
ou les déclarer supprimés (en C ++ 11 et supérieur):
person(const person& that) = delete;
person& operator=(const person& that) = delete;
Parfois, vous devez implémenter une classe qui gère une ressource. (Ne gérez jamais plusieurs ressources dans une seule classe, cela ne fera que causer de la douleur.) Dans ce cas, rappelez-vous la règle des trois :
Si vous devez déclarer vous-même explicitement le destructeur, le constructeur de copie ou l'opérateur d'affectation de copie, vous devez probablement déclarer explicitement les trois.
(Malheureusement, cette "règle" n'est pas appliquée par la norme C ++ ou par tout compilateur que je connaisse.)
À partir de C ++ 11, un objet possède 2 fonctions membres spéciales supplémentaires: le constructeur de déplacement et l'affectation de déplacement. La règle des cinq États met également en œuvre ces fonctions.
Un exemple avec les signatures:
class person
{
std::string name;
int age;
public:
person(const std::string& name, int age); // Ctor
person(const person &) = default; // Copy Ctor
person(person &&) noexcept = default; // Move Ctor
person& operator=(const person &) = default; // Copy Assignment
person& operator=(person &&) noexcept = default; // Move Assignment
~person() noexcept = default; // Dtor
};
La règle de 3/5 est également appelée la règle de 0/3/5. La partie zéro de la règle indique que vous êtes autorisé à n'écrire aucune des fonctions membres spéciales lors de la création de votre classe.
La plupart du temps, vous n'avez pas besoin de gérer vous-même une ressource, car une classe existante telle que std::string
déjà le fait pour vous. Comparez simplement le code simple utilisant un std::string
membre à l'alternative alambiquée et sujette aux erreurs en utilisant un char*
et vous devriez être convaincu. Tant que vous restez à l'écart des membres du pointeur brut, la règle de trois ne concernera probablement pas votre propre code.
La règle des trois est une règle de base pour C ++, en disant essentiellement
Si votre classe a besoin de
- un constructeur de copie ,
- un opérateur d'affectation ,
- ou un destructeur ,
définis de manière explicite, il faudra alors probablement les trois .
La raison en est que les trois sont généralement utilisés pour gérer une ressource, et si votre classe gère une ressource, elle doit généralement gérer la copie ainsi que la libération.
S'il n'y a pas de bonne sémantique pour copier la ressource gérée par votre classe, alors envisagez d'interdire la copie en déclarant (pas en définissant ) le constructeur de copie et l'opérateur d'affectation comme private
.
(Notez que la nouvelle version à venir de la norme C ++ (qui est C ++ 11) ajoute la sémantique de déplacement à C ++, ce qui changera probablement la règle des trois. Cependant, je sais trop peu de choses à ce sujet pour écrire une section C ++ 11 à propos de la règle de trois.)
boost::noncopyable
). Cela peut aussi être beaucoup plus clair. Je pense que C ++ 0x et la possibilité de "supprimer" des fonctions pourraient aider ici, mais j'ai oublié la syntaxe: /
noncopyable
partie de la librairie standard, je ne considère pas cela comme une grande amélioration. (Oh, et si vous avez oublié la syntaxe de suppression, vous avez oublié plus que je n'ai jamais connu. :)
)
La loi des trois grands est celle indiquée ci-dessus.
Un exemple simple, en anglais simple, du type de problème qu'il résout:
Destructeur non par défaut
Vous avez alloué de la mémoire dans votre constructeur et vous devez donc écrire un destructeur pour le supprimer. Sinon, vous provoquerez une fuite de mémoire.
Vous pourriez penser que c'est du travail accompli.
Le problème sera, si une copie est faite de votre objet, alors la copie pointera vers la même mémoire que l'objet d'origine.
Une fois que l'un d'eux supprime la mémoire de son destructeur, l'autre aura un pointeur vers une mémoire invalide (c'est ce qu'on appelle un pointeur suspendu) lorsqu'il essaie de l'utiliser, les choses vont devenir poilues.
Par conséquent, vous écrivez un constructeur de copie afin qu'il alloue aux nouveaux objets leurs propres morceaux de mémoire à détruire.
Opérateur d'affectation et constructeur de copie
Vous avez alloué de la mémoire dans votre constructeur à un pointeur membre de votre classe. Lorsque vous copiez un objet de cette classe, l'opérateur d'affectation par défaut et le constructeur de copie copient la valeur de ce pointeur de membre vers le nouvel objet.
Cela signifie que le nouvel objet et l'ancien objet pointeront vers le même morceau de mémoire, donc lorsque vous le modifiez dans un objet, il sera également modifié pour l'autre objet. Si un objet supprime cette mémoire, l'autre continuera d'essayer de l'utiliser - eek.
Pour résoudre ce problème, vous écrivez votre propre version du constructeur de copie et de l'opérateur d'affectation. Vos versions allouent une mémoire distincte aux nouveaux objets et copient les valeurs vers lesquelles pointe le premier pointeur plutôt que son adresse.
Fondamentalement, si vous avez un destructeur (pas le destructeur par défaut), cela signifie que la classe que vous avez définie dispose d'une allocation de mémoire. Supposons que la classe soit utilisée à l'extérieur par un code client ou par vous.
MyClass x(a, b);
MyClass y(c, d);
x = y; // This is a shallow copy if assignment operator is not provided
Si MyClass n'a que quelques membres de type primitif, un opérateur d'affectation par défaut fonctionnerait, mais s'il a des membres de pointeur et des objets qui n'ont pas d'opérateurs d'affectation, le résultat serait imprévisible. Par conséquent, nous pouvons dire que s'il y a quelque chose à supprimer dans le destructeur d'une classe, nous pourrions avoir besoin d'un opérateur de copie en profondeur, ce qui signifie que nous devrions fournir un constructeur de copie et un opérateur d'affectation.
Que signifie copier un objet? Il existe plusieurs façons de copier des objets - parlons des 2 types auxquels vous faites probablement référence - la copie profonde et la copie superficielle.
Puisque nous sommes dans un langage orienté objet (ou du moins le supposons), disons que vous avez un morceau de mémoire alloué. Puisqu'il s'agit d'un langage OO, nous pouvons facilement faire référence aux morceaux de mémoire que nous allouons car ce sont généralement des variables primitives (ints, chars, octets) ou des classes que nous avons définies qui sont faites de nos propres types et primitives. Disons donc que nous avons une classe de voitures comme suit:
class Car //A very simple class just to demonstrate what these definitions mean.
//It's pseudocode C++/Javaish, I assume strings do not need to be allocated.
{
private String sPrintColor;
private String sModel;
private String sMake;
public changePaint(String newColor)
{
this.sPrintColor = newColor;
}
public Car(String model, String make, String color) //Constructor
{
this.sPrintColor = color;
this.sModel = model;
this.sMake = make;
}
public ~Car() //Destructor
{
//Because we did not create any custom types, we aren't adding more code.
//Anytime your object goes out of scope / program collects garbage / etc. this guy gets called + all other related destructors.
//Since we did not use anything but strings, we have nothing additional to handle.
//The assumption is being made that the 3 strings will be handled by string's destructor and that it is being called automatically--if this were not the case you would need to do it here.
}
public Car(const Car &other) // Copy Constructor
{
this.sPrintColor = other.sPrintColor;
this.sModel = other.sModel;
this.sMake = other.sMake;
}
public Car &operator =(const Car &other) // Assignment Operator
{
if(this != &other)
{
this.sPrintColor = other.sPrintColor;
this.sModel = other.sModel;
this.sMake = other.sMake;
}
return *this;
}
}
Une copie profonde est si nous déclarons un objet puis créons une copie complètement séparée de l'objet ... nous nous retrouvons avec 2 objets dans 2 ensembles de mémoire complètement.
Car car1 = new Car("mustang", "ford", "red");
Car car2 = car1; //Call the copy constructor
car2.changePaint("green");
//car2 is now green but car1 is still red.
Faisons maintenant quelque chose d'étrange. Disons que car2 est mal programmé ou intentionnellement destiné à partager la mémoire réelle de car1. (C'est généralement une erreur de le faire et dans les classes, c'est généralement la couverture dont il est question.) Imaginez que chaque fois que vous posez des questions sur car2, vous résolvez vraiment un pointeur vers l'espace mémoire de car1 ... c'est plus ou moins ce qu'une copie superficielle est.
//Shallow copy example
//Assume we're in C++ because it's standard behavior is to shallow copy objects if you do not have a constructor written for an operation.
//Now let's assume I do not have any code for the assignment or copy operations like I do above...with those now gone, C++ will use the default.
Car car1 = new Car("ford", "mustang", "red");
Car car2 = car1;
car2.changePaint("green");//car1 is also now green
delete car2;/*I get rid of my car which is also really your car...I told C++ to resolve
the address of where car2 exists and delete the memory...which is also
the memory associated with your car.*/
car1.changePaint("red");/*program will likely crash because this area is
no longer allocated to the program.*/
Donc, quelle que soit la langue dans laquelle vous écrivez, faites très attention à ce que vous voulez dire quand il s'agit de copier des objets, car la plupart du temps vous voulez une copie complète.
Que sont le constructeur de copie et l'opérateur d'affectation de copie? Je les ai déjà utilisés ci-dessus. Le constructeur de copie est appelé lorsque vous tapez du code tel que Car car2 = car1;
Essentiellement si vous déclarez une variable et l'affectez sur une seule ligne, c'est alors que le constructeur de copie est appelé. L'opérateur d'affectation est ce qui se passe lorsque vous utilisez un signe égal - car2 = car1;
. L'avis car2
n'est pas déclaré dans la même déclaration. Les deux morceaux de code que vous écrivez pour ces opérations sont probablement très similaires. En fait, le modèle de conception typique a une autre fonction que vous appelez pour tout définir une fois que vous êtes convaincu que la copie / affectation initiale est légitime - si vous regardez le code long que j'ai écrit, les fonctions sont presque identiques.
Quand dois-je les déclarer moi-même? Si vous n'écrivez pas de code à partager ou à produire d'une manière ou d'une autre, vous n'avez vraiment besoin de les déclarer que lorsque vous en avez besoin. Vous devez être conscient de ce que fait votre langage de programme si vous choisissez de l'utiliser «par accident» et n'en avez pas créé - c'est-à-dire que vous obtenez le compilateur par défaut. J'utilise rarement des constructeurs de copie par exemple, mais les remplacements d'opérateurs d'affectation sont très courants. Saviez-vous que vous pouvez également remplacer ce que signifie addition, soustraction, etc.?
Comment empêcher la copie de mes objets? Remplacer toutes les façons d'allouer de la mémoire à votre objet avec une fonction privée est un début raisonnable. Si vous ne voulez vraiment pas que les gens les copient, vous pouvez le rendre public et alerter le programmeur en lançant une exception et en ne copiant pas l'objet.
Quand dois-je les déclarer moi-même?
La règle de trois stipule que si vous déclarez
alors vous devez déclarer les trois. Il est né de l'observation que la nécessité de reprendre le sens d'une opération de copie provenait presque toujours de la classe effectuant une sorte de gestion des ressources, et cela impliquait presque toujours que
quelle que soit la gestion des ressources effectuée dans une opération de copie, elle devait probablement l'être dans l'autre opération de copie et
le destructeur de classe participerait également à la gestion de la ressource (la libérant généralement). La ressource classique à gérer était la mémoire, et c'est pourquoi toutes les classes Standard Library qui gèrent la mémoire (par exemple, les conteneurs STL qui effectuent la gestion dynamique de la mémoire) déclarent toutes «les trois grands»: à la fois des opérations de copie et un destructeur.
Une conséquence de la règle de trois est que la présence d'un destructeur déclaré par l'utilisateur indique qu'une copie simple par membre est peu susceptible d'être appropriée pour les opérations de copie dans la classe. Cela, à son tour, suggère que si une classe déclare un destructeur, les opérations de copie ne devraient probablement pas être générées automatiquement, car elles ne feraient pas la bonne chose. Au moment où C ++ 98 a été adopté, l'importance de ce raisonnement n'était pas pleinement appréciée, donc en C ++ 98, l'existence d'un destructeur déclaré par l'utilisateur n'a eu aucun impact sur la volonté des compilateurs de générer des opérations de copie. Cela continue d'être le cas en C ++ 11, mais uniquement parce que restreindre les conditions dans lesquelles les opérations de copie sont générées casserait trop de code hérité.
Comment empêcher la copie de mes objets?
Déclarez le constructeur de copie et l'opérateur d'affectation de copie en tant que spécificateur d'accès privé.
class MemoryBlock
{
public:
//code here
private:
MemoryBlock(const MemoryBlock& other)
{
cout<<"copy constructor"<<endl;
}
// Copy assignment operator.
MemoryBlock& operator=(const MemoryBlock& other)
{
return *this;
}
};
int main()
{
MemoryBlock a;
MemoryBlock b(a);
}
Dans C ++ 11 et suivants, vous pouvez également déclarer le constructeur de copie et l'opérateur d'affectation supprimés
class MemoryBlock
{
public:
MemoryBlock(const MemoryBlock& other) = delete
// Copy assignment operator.
MemoryBlock& operator=(const MemoryBlock& other) =delete
};
int main()
{
MemoryBlock a;
MemoryBlock b(a);
}
De nombreuses réponses existantes concernent déjà le constructeur de copie, l'opérateur d'affectation et le destructeur. Cependant, dans le post C ++ 11, l'introduction de la sémantique de mouvement peut étendre cela au-delà de 3.
Récemment, Michael Claisse a donné une conférence qui touche à ce sujet: http://channel9.msdn.com/events/CPP/C-PP-Con-2014/The-Canonical-Class
La règle de trois en C ++ est un principe fondamental de la conception et du développement de trois exigences selon lesquelles s'il existe une définition claire dans l'une des fonctions membres suivantes, le programmeur doit définir les deux autres fonctions membres ensemble. A savoir les trois fonctions membres suivantes sont indispensables: destructeur, constructeur de copie, opérateur d'affectation de copie.
Le constructeur de copie en C ++ est un constructeur spécial. Il est utilisé pour créer un nouvel objet, qui est le nouvel objet équivalent à une copie d'un objet existant.
L'opérateur d'affectation de copie est un opérateur d'affectation spécial qui est généralement utilisé pour spécifier un objet existant à d'autres du même type d'objet.
Il y a des exemples rapides:
// default constructor
My_Class a;
// copy constructor
My_Class b(a);
// copy constructor
My_Class c = a;
// copy assignment operator
b = a;
c++-faq
wiki du tag avant de voter pour fermer .