- Cosa significa copiare un oggetto ?
- Cosa sono il costruttore di copie e l' operatore di assegnazione delle copie ?
- Quando devo dichiararli io stesso?
- Come posso impedire che i miei oggetti vengano copiati?
Risposte:
Il C ++ tratta variabili di tipi definiti dall'utente con semantica di valore . Ciò significa che gli oggetti vengono implicitamente copiati in vari contesti e dovremmo capire cosa significa effettivamente "copiare un oggetto".
Consideriamo un semplice esempio:
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?
}
(Se sei perplesso dalla name(name), age(age)
parte, questo si chiama a elenco di inizializzatori del membro .)
Cosa significa copiare un person
oggetto? La main
funzione mostra due scenari di copia distinti. L'inizializzazione person b(a);
viene eseguita dal costruttore della copia . Il suo compito è costruire un nuovo oggetto basato sullo stato di un oggetto esistente. L'assegnazione b = a
viene eseguita dall'operatore di assegnazione della copia . Il suo lavoro è generalmente un po 'più complicato, perché l'oggetto target è già in uno stato valido che deve essere affrontato.
Poiché non abbiamo dichiarato né il costruttore di copie né l'operatore di assegnazione (né il distruttore), questi sono implicitamente definiti per noi. Citazione dalla norma:
Il [...] costruttore di copie e l'operatore di assegnazione delle copie, [...] e il distruttore sono funzioni speciali dei membri. [ Nota : l'implementazione dichiarerà implicitamente queste funzioni membro per alcuni tipi di classe quando il programma non le dichiara esplicitamente. L'implementazione li definirà implicitamente se vengono utilizzati. [...] nota finale ] [n3126.pdf sezione 12 §1]
Per impostazione predefinita, copiare un oggetto significa copiare i suoi membri:
Il costruttore di copie definito implicitamente per una classe X non sindacale esegue una copia membro dei suoi oggetti secondari. [n3126.pdf sezione 12.8 §16]
L'operatore di assegnazione copia implicitamente definito per una classe X non sindacale esegue l'assegnazione di copia membro dei suoi oggetti secondari. [n3126.pdf sezione 12.8 §30]
Le funzioni membro speciali implicitamente definite per person
assomigliano a questo:
// 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 copia a livello di membro è esattamente ciò che vogliamo in questo caso:
name
e age
vengono copiati, quindi otteniamo un person
oggetto indipendente e indipendente . Il distruttore definito implicitamente è sempre vuoto. Anche in questo caso va bene, dal momento che non abbiamo acquisito risorse nel costruttore. I distruttori dei membri vengono chiamati implicitamente dopo che il person
distruttore è finito:
Dopo aver eseguito il corpo del distruttore e aver distrutto qualsiasi oggetto automatico allocato all'interno del corpo, un distruttore per la classe X chiama i distruttori per i membri diretti [...] di X [n3126.pdf 12.4 §6]
Quindi, quando dovremmo dichiarare esplicitamente quelle funzioni di membro speciale? Quando la nostra classe gestisce una risorsa , cioè quando un oggetto della classe è responsabile di quella risorsa. Questo di solito significa che la risorsa viene acquisita nel costruttore (o passata nel costruttore) e rilasciata nel distruttore.
Torniamo indietro nel tempo al C ++ pre-standard. Non c'era niente di simile std::string
e i programmatori erano innamorati dei puntatori. La person
classe avrebbe potuto apparire così:
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;
}
};
Ancora oggi, le persone scrivono ancora lezioni in questo stile e si mettono nei guai: " Ho spinto una persona in un vettore e ora ho errori di memoria pazzi! " Ricorda che per impostazione predefinita, copiare un oggetto significa copiare i suoi membri, ma copiare name
semplicemente il membro copia un puntatore, non l'array di caratteri a cui punta! Ciò ha diversi effetti spiacevoli:
a
possono essere osservate tramiteb
.b
distrutto,a.name
è un puntatore penzolante.a
viene distrutto, l'eliminazione del puntatore penzolante produce un comportamento indefinito .name
indicava prima del compito, prima o poi otterrai perdite di memoria in tutto il luogo.Poiché la copia membro non ha l'effetto desiderato, è necessario definire esplicitamente il costruttore della copia e l'operatore di assegnazione della copia per eseguire copie profonde della matrice di caratteri:
// 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;
}
Nota la differenza tra inizializzazione e assegnazione: dobbiamo eliminare il vecchio stato prima di assegnare a name
per evitare perdite di memoria. Inoltre, dobbiamo proteggere dall'autoassegnazione del modulo x = x
. Senza quel controllo, delete[] name
eliminerebbe l'array contenente la stringa di origine , perché quando scrivi x = x
, entrambi this->name
e that.name
contengono lo stesso puntatore.
Sfortunatamente, questa soluzione fallirà se new char[...]
genera un'eccezione a causa dell'esaurimento della memoria. Una possibile soluzione è quella di introdurre una variabile locale e riordinare le istruzioni:
// 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;
}
Questo si occupa anche di autoassegnazione senza un controllo esplicito. Una soluzione ancora più efficace a questo problema è il linguaggio copia-e-scambia , ma non entrerò nei dettagli della sicurezza delle eccezioni qui. Ho citato solo le eccezioni per evidenziare quanto segue: Scrivere classi che gestiscono le risorse è difficile.
Alcune risorse non possono o non devono essere copiate, come handle di file o mutex. In tal caso, dichiarare semplicemente il costruttore della copia e l'operatore di assegnazione della copia come private
senza fornire una definizione:
private:
person(const person& that);
person& operator=(const person& that);
In alternativa, puoi ereditarli boost::noncopyable
o dichiararli come eliminati (in C ++ 11 e versioni successive):
person(const person& that) = delete;
person& operator=(const person& that) = delete;
A volte è necessario implementare una classe che gestisce una risorsa. (Non gestire mai più risorse in una singola classe, questo provocherà solo dolore.) In tal caso, ricorda la regola di tre :
Se devi dichiarare esplicitamente tu stesso il distruttore, il costruttore della copia o l'operatore di assegnazione della copia, probabilmente dovrai dichiarare esplicitamente tutti e tre.
(Sfortunatamente, questa "regola" non è applicata dallo standard C ++ o da qualsiasi compilatore di cui sia a conoscenza.)
Da C ++ 11 in poi, un oggetto ha 2 funzioni membro speciali extra: il costruttore di spostamento e l'assegnazione di spostamento. La regola di cinque stati per implementare anche queste funzioni.
Un esempio con le firme:
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 regola di 3/5 viene anche definita regola del 0/3/5. La parte zero della regola indica che durante la creazione della classe è consentito non scrivere alcuna funzione del membro speciale.
Il più delle volte, non è necessario gestire una risorsa da soli, perché una classe esistente come std::string
già lo fa per te. Basta confrontare il semplice codice usando un std::string
membro con l'alternativa contorta e soggetta a errori usando un char*
e dovresti essere convinto. Finché stai lontano dai membri puntatori non elaborati, è improbabile che la regola del tre riguardi il tuo codice.
La Regola dei tre è una regola empirica per il C ++, in pratica dice
Se la tua classe ne ha bisogno
- un costruttore di copie ,
- un operatore di incarico ,
- o un distruttore ,
definito esplicitamente, quindi probabilmente avrà bisogno di tutti e tre .
La ragione di ciò è che tutti e tre sono generalmente usati per gestire una risorsa e se la tua classe gestisce una risorsa, di solito deve gestire sia la copia che la liberazione.
Se non esiste una semantica valida per la copia della risorsa gestita dalla tua classe, considera di vietare la copia dichiarando (non definendo ) il costruttore della copia e l'operatore di assegnazione come private
.
(Si noti che la nuova versione imminente dello standard C ++ (che è C ++ 11) aggiunge la semantica di spostamento al C ++, che probabilmente cambierà la Regola di Tre. Tuttavia, ne so troppo poco per scrivere una sezione C ++ 11 sulla Regola dei Tre.)
boost::noncopyable
). Può anche essere molto più chiaro. Penso che C ++ 0x e la possibilità di "eliminare" le funzioni potrebbero aiutare qui, ma ho dimenticato la sintassi: /
noncopyable
che non faccia parte della libreria standard, non lo considero molto un miglioramento. (Oh, e se hai dimenticato la sintassi di cancellazione, hai dimenticato mor ethan che io abbia mai conosciuto. :)
)
La legge dei tre grandi è come sopra specificato.
Un semplice esempio, in parole povere, del tipo di problema che risolve:
Distruttore non predefinito
Hai allocato memoria nel tuo costruttore e quindi devi scrivere un distruttore per eliminarlo. Altrimenti causerai una perdita di memoria.
Potresti pensare che questo sia un lavoro fatto.
Il problema sarà che, se viene creata una copia del tuo oggetto, la copia punterà alla stessa memoria dell'oggetto originale.
Una volta, uno di questi elimina la memoria nel suo distruttore, l'altro avrà un puntatore alla memoria non valida (questo si chiama puntatore penzolante) quando cerca di usarlo le cose diventeranno pelose.
Pertanto, si scrive un costruttore di copie in modo che alloca nuovi oggetti i propri pezzi di memoria da distruggere.
Operatore di assegnazione e costruttore di copie
Hai allocato memoria nel costruttore a un puntatore membro della tua classe. Quando si copia un oggetto di questa classe, l'operatore di assegnazione predefinito e il costruttore della copia copieranno il valore di questo puntatore membro sul nuovo oggetto.
Ciò significa che il nuovo oggetto e l'oggetto vecchio indicheranno lo stesso pezzo di memoria, quindi quando lo si modifica in un oggetto verrà modificato anche per l'altro oggetto. Se un oggetto cancella questa memoria, l'altro continuerà a provare a usarla - eek.
Per risolvere questo problema, scrivi la tua versione del costruttore della copia e dell'operatore di assegnazione. Le versioni allocano memoria separata per i nuovi oggetti e copiano i valori a cui punta il primo puntatore anziché il suo indirizzo.
Fondamentalmente se hai un distruttore (non il distruttore predefinito) significa che la classe che hai definito ha qualche allocazione di memoria. Supponiamo che la classe sia utilizzata all'esterno da un codice client o dall'utente.
MyClass x(a, b);
MyClass y(c, d);
x = y; // This is a shallow copy if assignment operator is not provided
Se MyClass ha solo alcuni membri tipizzati primitivi, un operatore di assegnazione predefinito funzionerebbe, ma se avesse alcuni membri del puntatore e oggetti che non hanno operatori di assegnazione, il risultato sarebbe imprevedibile. Quindi possiamo dire che se c'è qualcosa da cancellare nel distruttore di una classe, potremmo aver bisogno di un operatore di copia profonda, il che significa che dovremmo fornire un costruttore di copia e un operatore di assegnazione.
Cosa significa copiare un oggetto? Ci sono alcuni modi in cui puoi copiare gli oggetti - parliamo dei 2 tipi a cui ti riferisci molto probabilmente - copia profonda e copia superficiale.
Dato che siamo in un linguaggio orientato agli oggetti (o almeno lo stiamo assumendo), supponiamo che abbiate un pezzo di memoria allocato. Dato che è un linguaggio OO, possiamo facilmente riferirci a blocchi di memoria che allociamo perché di solito sono variabili primitive (ints, caratteri, byte) o classi che abbiamo definito che sono fatte dei nostri tipi e primitive. Quindi diciamo che abbiamo una classe di auto come segue:
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;
}
}
Una copia profonda è se dichiariamo un oggetto e quindi creiamo una copia completamente separata dell'oggetto ... finiamo con 2 oggetti in 2 set di memoria completamente.
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.
Ora facciamo qualcosa di strano. Supponiamo che car2 sia programmato in modo errato o intenzionalmente inteso a condividere la memoria effettiva di cui è composto car1. (Di solito è un errore farlo e in classe è di solito la coperta di cui è discusso.) Fai finta che ogni volta che chiedi di car2, stai davvero risolvendo un puntatore allo spazio di memoria di car1 ... è più o meno una copia superficiale è.
//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.*/
Quindi, indipendentemente dalla lingua in cui stai scrivendo, fai molta attenzione a cosa intendi quando si tratta di copiare oggetti perché la maggior parte delle volte vuoi una copia approfondita.
Cosa sono il costruttore di copie e l'operatore di assegnazione delle copie? Li ho già usati sopra. Il costruttore della copia viene chiamato quando si digita un codice come Car car2 = car1;
Essenzialmente se si dichiara una variabile e la si assegna su una riga, in quel momento viene chiamato il costruttore della copia. L'operatore di assegnazione è ciò che accade quando si utilizza un segno di uguale - car2 = car1;
. L'avviso car2
non è dichiarato nella stessa dichiarazione. I due blocchi di codice che scrivi per queste operazioni sono probabilmente molto simili. In effetti il tipico modello di progettazione ha un'altra funzione che chiami per impostare tutto una volta che sei soddisfatto la copia / assegnazione iniziale è legittima - se guardi il codice a mano lunga che ho scritto, le funzioni sono quasi identiche.
Quando devo dichiararli io stesso? Se non stai scrivendo codice che deve essere condiviso o per la produzione in qualche modo, devi davvero dichiararli solo quando ne hai bisogno. È necessario essere consapevoli di ciò che fa il linguaggio del programma se si sceglie di utilizzarlo "per caso" e non ne è stato creato uno, ovvero si ottiene il compilatore predefinito. Uso raramente i costruttori di copie per esempio, ma le sostituzioni degli operatori di assegnazione sono molto comuni. Sapevi che puoi scavalcare anche il significato di addizione, sottrazione, ecc.?
Come posso impedire che i miei oggetti vengano copiati? Sostituire tutti i modi in cui ti è permesso allocare memoria per il tuo oggetto con una funzione privata è un inizio ragionevole. Se davvero non vuoi che le persone li copino, potresti renderlo pubblico e avvisare il programmatore lanciando un'eccezione e anche non copiare l'oggetto.
Quando devo dichiararli io stesso?
La Regola del Tre afferma che se dichiari una di a
allora dovresti dichiarare tutti e tre. Nacque dall'osservazione che la necessità di assumere il significato di un'operazione di copia derivava quasi sempre dalla classe che eseguiva una sorta di gestione delle risorse, e ciò implicava quasi sempre che
qualunque sia stata la gestione delle risorse in un'operazione di copia probabilmente doveva essere fatta nell'altra operazione di copia e
il distruttore di classe parteciperebbe anche alla gestione della risorsa (di solito rilasciandola). La risorsa classica da gestire era la memoria, ed è per questo che tutte le classi della libreria standard che gestiscono la memoria (ad esempio, i contenitori STL che eseguono la gestione dinamica della memoria) dichiarano tutti "i tre grandi": entrambe le operazioni di copia e un distruttore.
Una conseguenza della Regola dei tre è che la presenza di un distruttore dichiarato dall'utente indica che è improbabile che una copia saggia dei membri semplici sia appropriata per le operazioni di copia nella classe. Ciò, a sua volta, suggerisce che se una classe dichiara un distruttore, probabilmente le operazioni di copia non dovrebbero essere generate automaticamente, perché non farebbero la cosa giusta. Al momento dell'adozione di C ++ 98, il significato di questa linea di ragionamento non era pienamente apprezzato, quindi in C ++ 98 l'esistenza di un utente dichiarato distruttore non ha avuto alcun impatto sulla volontà dei compilatori di generare operazioni di copia. Questo continua ad essere il caso di C ++ 11, ma solo perché limitando le condizioni in cui vengono generate le operazioni di copia si romperebbe troppo codice legacy.
Come posso impedire che i miei oggetti vengano copiati?
Dichiara costruttore di copia e operatore di assegnazione copia come identificatore di accesso privato.
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);
}
A partire da C ++ 11 puoi anche dichiarare il costruttore di copie e l'operatore di assegnazione cancellati
class MemoryBlock
{
public:
MemoryBlock(const MemoryBlock& other) = delete
// Copy assignment operator.
MemoryBlock& operator=(const MemoryBlock& other) =delete
};
int main()
{
MemoryBlock a;
MemoryBlock b(a);
}
Molte delle risposte esistenti toccano già il costruttore della copia, l'operatore di assegnazione e il distruttore. Tuttavia, nel post C ++ 11, l'introduzione del movimento semantico può espandere questo oltre 3.
Recentemente Michael Claisse ha tenuto un discorso che tocca questo argomento: http://channel9.msdn.com/events/CPP/C-PP-Con-2014/The-Canonical-Class
La regola del tre in C ++ è un principio fondamentale della progettazione e dello sviluppo di tre requisiti secondo cui se esiste una chiara definizione in una delle seguenti funzioni membro, il programmatore dovrebbe definire insieme le altre due funzioni membri. In particolare, sono indispensabili le seguenti tre funzioni membro: distruttore, costruttore della copia, operatore di assegnazione della copia.
Il costruttore di copie in C ++ è un costruttore speciale. Viene utilizzato per creare un nuovo oggetto, che è il nuovo oggetto equivalente a una copia di un oggetto esistente.
L'operatore di assegnazione copia è un operatore di assegnazione speciale che viene solitamente utilizzato per specificare un oggetto esistente ad altri dello stesso tipo di oggetto.
Ci sono esempi rapidi:
// default constructor
My_Class a;
// copy constructor
My_Class b(a);
// copy constructor
My_Class c = a;
// copy assignment operator
b = a;
c++-faq
tag wiki prima di votare per chiudere .