Quelle est la copie d'un objet signifie? Quelles sont les constructeur de copie et l' opérateur d'assignation de copie? Quand dois-je déclarer moi-même? Comment puis-je empêcher mes objets d'être copié?
Réponses
Trop de publicités?Introduction
C++ traite des variables de types définis par l'utilisateur avec la valeur sémantique. Cela signifie que les objets sont implicitement copiés dans des contextes divers, et nous devons comprendre ce qu'est "la copie d'un objet" signifie réellement.
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 vous êtes intrigué par l' name(name), age(age)
partie,
ceci est appelé un initialiseur de membre de la liste.)
Spécial des fonctions de membre du
Que signifie le fait de copier un person
objet?
L' main
fonction montre deux distincts de la copie de scénarios.
L'initialisation person b(a);
est effectuée par le constructeur de copie.
Son travail consiste à construire une nouvelle objet en fonction de l'état d'un objet existant.
La cession b = a
est effectué par l' opérateur d'assignation de copie.
Son emploi est généralement un peu plus compliqué,
parce que l'objet cible est déjà dans un état valide qui doit être traitée.
Depuis, nous avons déclaré, ni le constructeur de copie ni de l'opérateur d'affectation (ni le destructeur) nous-mêmes, ces sont définis implicitement pour nous. Citation de la norme:
[...] Le constructeur de copie et l'opérateur d'assignation de copie, [...] et le destructeur sont spéciales des fonctions membres. [ Note: La mise en œuvre implicitement déclarer ces fonctions de membre pour certains types de classe lorsque le programme ne prévoit pas explicitement de les déclarer. La mise en œuvre implicitement définir si elles sont utilisées. [...] la note de fin] [n3126.pdf de l'article 12 §1]
Par défaut, la copie d'un objet signifie copie de ses membres:
La implicitement défini constructeur de copie pour une non-union de la classe X effectue une memberwise copie de ses sous-objets. [n3126.pdf article 12.8 §16]
La implicitement défini par l'opérateur d'assignation de copie pour une non-union de la classe X effectue memberwise copie de l'assignation de ses sous-objets. [n3126.pdf article 12.8 §30]
Implicite définitions
La implicitement défini spécial des fonctions membres d' person
ressembler à 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()
{
}
Memberwise la copie est exactement ce que nous voulons dans ce cas:
name
et age
sont copiés, nous obtenons donc un autonome, indépendante person
objet.
La implicitement défini destructeur est toujours vide.
C'est aussi très bien dans ce cas, puisque nous n'avons pas acquérir toutes les ressources dans le constructeur.
Les membres destructeurs sont implicitement appelée après l' person
destructeur est fini:
Après l'exécution du corps du destructeur et en détruisant tout automatique des objets alloués dans le corps, un destructeur d'une classe de X appelle les destructeurs de X en direct [...] les membres de l' [n3126.pdf 12.4 §6]
La gestion des ressources
Alors, quand doit-on déclarer ces fonctions de membre explicitement? Lors de notre classe gère une ressource, qui est, lorsqu'un objet de la classe est responsable de cette ressource. Cela signifie généralement la ressource est acquis dans le constructeur (ou passé dans le constructeur) et publié dans le destructeur.
Revenons en arrière dans le temps pour la pré-norme C++.
Il n'y a pas une telle chose comme std::string
, et les programmeurs étaient dans l'amour avec des pointeurs.
L' person
classe pourrait 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;
}
};
Même aujourd'hui, les gens continuent à écrire des classes dans ce style et avoir des ennuis:
"J'ai poussé une personne dans un vecteur et maintenant je me fou les erreurs de mémoire!"
Rappelez-vous que par défaut, la copie d'un objet signifie copie de ses membres,
mais la copie de l' name
membre ne fait que copier un pointeur, pas le tableau de caractères il points de!
Ce qui a plusieurs effets désagréables:
- Les modifications par
a
peut être observée parb
. - Une fois
b
est détruit,a.name
est un bancales pointeur. - Si
a
est détruit, la suppression de la balançant pointeur rendements comportement indéfini. - Depuis la cession ne tiennent pas compte de ce
name
a souligné devant la mission, tôt ou tard, vous obtiendrez des fuites de mémoire dans tous les sens.
Des définitions explicites
Depuis memberwise la copie ne pas avoir l'effet désiré, on doit définir le constructeur de copie et l'opérateur d'assignation de copie de faire explicitement profonde des copies de la matrice 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 affectation:
nous devons abattre l'ancien état avant de l'affecter à l' name
à empêcher les fuites de mémoire.
Aussi, nous devons nous protéger contre l'auto-assignation de la forme x = x
.
Sans cette vérification, delete[] name
aurait pour effet de supprimer le tableau contenant la sourcede la chaîne,
parce que quand vous écrivez x = x
, les deux this->name
et that.name
contiennent le même pointeur.
Exception de sécurité
Malheureusement, cette solution va échouer si new char[...]
déclenche une exception en raison de l'épuisement de la mémoire.
Une solution possible est d'introduire une variable locale et de réorganiser les états:
// 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;
}
Ceci prend également soin de soi-affectation, sans une vérification explicite. Encore plus robuste solution à ce problème est la copie-et-swap idiome, mais je ne vais pas entrer dans les détails de l'exception de sécurité ici. Je ne exceptions mentionnées à faire le point suivant: l'Écriture de classes qui gèrent des ressources est dur.
Noncopyable ressources
Certaines ressources ne peut pas ou ne doit pas être copié, comme les handles de fichiers ou de mutex.
Dans ce cas, il suffit de déclarer le constructeur de copie et l'opérateur d'assignation de copie en tant que private
sans en donner une définition:
private:
person(const person& that);
person& operator=(const person& that);
Alternativement, vous pouvez hériter boost::noncopyable
ou de les déclarer comme étant supprimés (C++0x):
person(const person& that) = delete;
person& operator=(const person& that) = delete;
La règle de trois
Parfois, vous avez besoin pour mettre en œuvre une classe qui gère une ressource. (Ne jamais administrer de multiples ressources dans une seule classe, cela ne fera que conduire à la douleur.) Dans ce cas, n'oubliez pas la règle de trois:
Si vous avez besoin de déclarer explicitement le destructeur, constructeur de copie ou copie de l'opérateur d'assignation de vous-même, vous avez probablement besoin de déclarer explicitement tous les trois d'entre eux.
(Malheureusement, cette "règle" n'est pas appliquée par la norme C++ ou n'importe quel compilateur j'en suis conscient.)
Conseils
La plupart du temps, vous n'avez pas besoin de gérer une ressource de vous-même,
parce qu'une classe existante comme std::string
déjà fait pour vous.
Il suffit de comparer le simple code à l'aide d'un std::string
membre
à l'compliquée et sujette à erreur de rechange à l'aide d'un char*
et vous devriez en être convaincu.
Aussi longtemps que vous restez loin de pointeur brut membres, la règle de trois est peu probable que la préoccupation de votre propre code.
La Règle de Trois est une règle de pouce pour le C++, ce qui revient à dire
Si votre classe a besoin de
- un constructeur de copie,
- un opérateur d'affectation,
- ou un destructeur,
définit explicitement, alors il est susceptible d'avoir besoin tous les trois d'entre eux.
Les raisons pour cela est que tous les trois d'entre eux sont généralement utilisés pour gérer une ressource, et si votre classe gère une ressource, il est généralement nécessaire de gérer la copie libérer.
Si il n'y a pas de bonne sémantique de copie de la ressource de votre classe gère, puis envisager d'interdire la copie en déclarant (pas la définition) le constructeur de copie et l'opérateur d'affectation en tant que private
.
(Notez que la prochaine nouvelle version de la norme C++ (actuellement généralement désigné comme C++0x ou C++1x) ajoute la sémantique de déplacement de C++, qui sera susceptible de changer la Règle de Trois. Cependant, je sais trop peu sur cette fonction pour écrire un C++1x section à propos de la Règle de Trois.)
La loi des trois grands de l'est, comme spécifié ci-dessus.
Un exemple simple, dans un anglais simple, le genre de problème qu'il résout:
Vous avez alloué de la mémoire de votre constructeur et si vous avez besoin d'écrire un destructeur de le supprimer. Sinon vous allez provoquer une fuite de mémoire.
Vous pourriez penser que c'est du travail fait.
Le problème sera, si une copie de votre objet, puis la copie de point à la même mémoire que l'objet d'origine.
Une fois, un de ces supprime la mémoire, c'est destructeur, l'autre aura un pointeur vers une mémoire non valide (ce qui est appelé un bancales pointeur) lorsqu'il tente d'utiliser les choses vont s'arracher les cheveux.
Donc, vous écrivez un constructeur de copie, de sorte que, il alloue de nouveaux objets de leurs propres morceaux de mémoire pour les détruire.
Le principe s'étend à d'autres ressources et de l'opérateur d'affectation.
En gros si vous avez un destructeur (pas le destructeur par défaut), cela signifie que la classe que vous avez défini en a certains d'allocation de mémoire. Supposons que la classe est utilisé à l'extérieur par certains 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 Maclasse a seulement quelques primitives tapé les membres d'une affectation par défaut de l'opérateur pourrait fonctionner, mais si elle a un peu pointeur membres et les objets qui n'ont opérateurs d'affectation le résultat serait imprévisible. Nous pouvons donc dire que si il y a quelque chose à supprimer, dans le destructeur d'une classe, nous aurions besoin d'une copie de l'opérateur qui signifie que nous devons fournir une copie ctor et opérateur d'affectation.
La règle de trois (aussi connu comme la Loi de La Grande Trois ou Les Trois) est une règle de base en C++, qui prétend que si une classe définit l'une des opérations suivantes il devrait probablement définir explicitement tous les trois[1]:
-destructeur
-constructeur de copie
-opérateur d'affectation
Ces trois fonctions sont spéciaux des fonctions membres. Si l'une de ces fonctions est utilisé sans d'abord être déclaré par le programmeur, il sera implicitement mis en œuvre par le compilateur par défaut de la sémantique d'exécution de cette opération sur tous les membres de la classe.
Destructeur de Détruire tous les membres de l'objet
Constructeur de copie - Construire tous les membres de l'objet à partir de l'équivalent de membres dans le constructeur de copie du paramètre
Opérateur d'affectation - Affecter tous les membres de l'objet à partir de l'équivalent membres de l'opérateur d'affectation du paramètre
La Règle de Trois prétend que si l'une de ces personnes devant être définies par le programmeur, cela signifie que l'généré par le compilateur de la version ne correspond pas aux besoins de la classe, dans un cas, et il ne sera probablement pas de place dans les autres cas.