- O que significa copiar um objeto ?
- O que são o construtor de cópias e o operador de atribuição de cópias ?
- Quando preciso declará-los eu mesmo?
- Como posso impedir que meus objetos sejam copiados?
Respostas:
O C ++ trata variáveis de tipos definidos pelo usuário com semântica de valores . Isso significa que os objetos são copiados implicitamente em vários contextos, e devemos entender o que "copiar um objeto" realmente significa.
Vamos considerar um exemplo simples:
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 você está intrigado com a name(name), age(age)
peça, isso é chamado de lista de inicializadores de membros .)
O que significa copiar um person
objeto? A main
função mostra dois cenários distintos de cópia. A inicialização person b(a);
é realizada pelo construtor de cópia . Seu trabalho é construir um novo objeto com base no estado de um objeto existente. A atribuição b = a
é realizada pelo operador de atribuição de cópia . Seu trabalho geralmente é um pouco mais complicado, porque o objeto de destino já está em algum estado válido que precisa ser tratado.
Como não declaramos nem o construtor de cópias nem o operador de atribuição (nem o destruidor), eles são implicitamente definidos para nós. Citação do padrão:
O construtor de cópias e o operador de atribuição de cópias, [...] e o destruidor são funções-membro especiais. [ Nota : A implementação declarará implicitamente essas funções de membro para alguns tipos de classe quando o programa não as declarar explicitamente. A implementação os definirá implicitamente se eles forem usados. [...] nota final ] [n3126.pdf seção 12 §1]
Por padrão, copiar um objeto significa copiar seus membros:
O construtor de cópia definido implicitamente para uma classe não-união X executa uma cópia de membro de seus subobjetos. [n3126.pdf seção 12.8 §16]
O operador de atribuição de cópia definido implicitamente para uma classe não-união X executa a atribuição de cópia em membros dos seus subobjetos. [n3126.pdf seção 12.8 §30]
As funções-membro especiais definidas implicitamente person
são assim:
// 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()
{
}
Cópia em membros é exatamente o que queremos neste caso:
name
e age
são copiados, para obter um person
objeto independente e independente . O destruidor definido implicitamente está sempre vazio. Isso também é bom neste caso, pois não adquirimos nenhum recurso no construtor. Os destruidores dos membros são chamados implicitamente após a conclusão do person
destruidor:
Após executar o corpo do destruidor e destruir quaisquer objetos automáticos alocados dentro do corpo, um destruidor da classe X chama os destruidores dos membros diretos de X [n ]126.pdf 12.4 §6]
Então, quando devemos declarar explicitamente essas funções-membro especiais? Quando nossa classe gerencia um recurso , ou seja, quando um objeto da classe é responsável por esse recurso. Isso geralmente significa que o recurso é adquirido no construtor (ou passado para o construtor) e liberado no destruidor.
Vamos voltar no tempo ao C ++ pré-padrão. Não havia tal coisa std::string
, e os programadores estavam apaixonados por indicadores. A person
classe pode ter se parecido com isto:
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;
}
};
Ainda hoje, as pessoas ainda escrevem aulas nesse estilo e se metem em problemas: " Eu empurrei uma pessoa para um vetor e agora tenho erros de memória loucos! " Lembre-se de que, por padrão, copiar um objeto significa copiar seus membros, mas copiar o name
membro apenas copia um ponteiro, não a matriz de caracteres que ele aponta! Isso tem vários efeitos desagradáveis:
a
podem ser observadas via b
.b
destruído, a.name
é um ponteiro pendente.a
for destruído, a exclusão do ponteiro danificado gera um comportamento indefinido .name
apontado antes da tarefa, mais cedo ou mais tarde você terá vazamentos de memória por todo o lugar.Como a cópia em membros não tem o efeito desejado, devemos definir explicitamente o construtor de cópias e o operador de atribuição de cópias para fazer cópias profundas da matriz de caracteres:
// 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;
}
Observe a diferença entre inicialização e designação: precisamos desmembrar o estado antigo antes de designar name
para evitar vazamentos de memória. Além disso, temos que nos proteger contra a atribuição automática do formulário x = x
. Sem essa verificação, delete[] name
excluiria a matriz que contém a cadeia de origem , porque quando você escreve x = x
, ambas this->name
e that.name
contêm o mesmo ponteiro.
Infelizmente, esta solução falhará se new char[...]
lançar uma exceção devido ao esgotamento da memória. Uma solução possível é introduzir uma variável local e reordenar as instruções:
// 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;
}
Isso também cuida da auto-atribuição sem uma verificação explícita. Uma solução ainda mais robusta para esse problema é o idioma de copiar e trocar , mas não abordarei os detalhes de segurança de exceção aqui. Mencionei apenas exceções para enfatizar o seguinte: Escrever aulas que gerenciam recursos é difícil.
Alguns recursos não podem ou não devem ser copiados, como identificadores de arquivo ou mutexes. Nesse caso, simplesmente declare o construtor de cópias e o operador de atribuição de cópias como private
sem fornecer uma definição:
private:
person(const person& that);
person& operator=(const person& that);
Como alternativa, você pode herdar boost::noncopyable
ou declarar como excluído (no C ++ 11 e acima):
person(const person& that) = delete;
person& operator=(const person& that) = delete;
Às vezes, você precisa implementar uma classe que gerencia um recurso. (Nunca gerencie vários recursos em uma única classe, isso só causará problemas.) Nesse caso, lembre-se da regra de três :
Se você precisar declarar explicitamente o destruidor, o construtor de cópias ou o operador de atribuição de cópias, provavelmente precisará declarar explicitamente todos os três.
(Infelizmente, essa "regra" não é imposta pelo padrão C ++ ou por qualquer compilador que eu conheça.)
A partir do C ++ 11, um objeto possui 2 funções-membro especiais extras: o construtor de movimentação e a atribuição de movimentação. A regra dos cinco estados para implementar essas funções também.
Um exemplo com as assinaturas:
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
};
A regra de 3/5 também é conhecida como regra de 0/3/5. A parte zero da regra declara que você tem permissão para não escrever nenhuma das funções especiais de membro ao criar sua classe.
Na maioria das vezes, você não precisa gerenciar um recurso por conta própria, porque uma classe existente, como std::string
já faz isso por você. Basta comparar o código simples usando um std::string
membro com a alternativa complicada e propensa a erros usando ae char*
você deve estar convencido. Desde que você fique longe dos membros brutos do ponteiro, é improvável que a regra dos três diga respeito ao seu próprio código.
A Regra dos Três é uma regra de ouro para C ++, basicamente dizendo
Se a sua turma precisar de algum
- um construtor de cópias ,
- um operador de atribuição ,
- ou um destruidor ,
definido explicitamente, é provável que precise dos três .
A razão para isso é que os três geralmente são usados para gerenciar um recurso e, se sua classe gerencia um recurso, geralmente é necessário gerenciar a cópia e a liberação.
Se não houver uma boa semântica para copiar o recurso que sua classe gerencia, considere proibir a cópia declarando (não definindo ) o construtor de cópia e o operador de atribuição como private
.
(Observe que a próxima nova versão do padrão C ++ (que é C ++ 11) adiciona semântica de movimentação ao C ++, o que provavelmente mudará a Regra de Três. No entanto, eu sei muito pouco sobre isso para escrever uma seção do C ++ 11 sobre a Regra dos Três.)
boost::noncopyable
). Também pode ser muito mais claro. Eu acho que C ++ 0x e a possibilidade de "excluir" funções podem ajudar aqui, mas esqueci a sintaxe: /
noncopyable
parte da lib std, não considero isso uma grande melhoria. (Ah, e se você esqueceu a sintaxe eliminação, você esqueceu mor ethan que já conheci. :)
)
A lei dos três grandes é a especificada acima.
Um exemplo fácil, em inglês simples, do tipo de problema que ele resolve:
Destruidor não padrão
Você alocou memória no seu construtor e, portanto, precisa escrever um destruidor para excluí-lo. Caso contrário, você causará um vazamento de memória.
Você pode pensar que isso é trabalho feito.
O problema será que, se uma cópia for feita do seu objeto, a cópia apontará para a mesma memória que o objeto original.
Uma vez, uma delas exclui a memória em seu destruidor, a outra terá um ponteiro para a memória inválida (isso é chamado de ponteiro danificado) quando ela tenta usá-la, as coisas vão ficar peludas.
Portanto, você escreve um construtor de cópias para que ele aloque novos objetos que seus próprios pedaços de memória sejam destruídos.
Operador de atribuição e construtor de cópias
Você alocou memória no construtor para um ponteiro de membro da sua classe. Quando você copia um objeto dessa classe, o operador de atribuição padrão e o construtor de cópias copiam o valor desse ponteiro de membro para o novo objeto.
Isso significa que o novo objeto e o objeto antigo estarão apontando para o mesmo pedaço de memória; portanto, quando você o altera em um objeto, ele também é alterado para o outro objeto. Se um objeto excluir essa memória, o outro continuará tentando usá-la - por exemplo.
Para resolver isso, escreva sua própria versão do construtor de cópias e operador de atribuição. Suas versões alocam memória separada para os novos objetos e copiam os valores que o primeiro ponteiro está apontando, em vez de seu endereço.
Basicamente, se você tem um destruidor (não o destruidor padrão), isso significa que a classe que você definiu possui alguma alocação de memória. Suponha que a classe seja usada fora por algum código de cliente ou por você.
MyClass x(a, b);
MyClass y(c, d);
x = y; // This is a shallow copy if assignment operator is not provided
Se MyClass tiver apenas alguns membros de tipo primitivo, um operador de atribuição padrão funcionaria, mas se tiver alguns membros e objetos de ponteiro que não possuem operadores de atribuição, o resultado será imprevisível. Portanto, podemos dizer que, se houver algo a ser excluído no destruidor de uma classe, podemos precisar de um operador de cópia profunda, o que significa que devemos fornecer um construtor de cópias e um operador de atribuição.
O que significa copiar um objeto? Existem algumas maneiras de copiar objetos - vamos falar sobre os dois tipos aos quais você provavelmente se refere - cópia profunda e cópia superficial.
Como estamos em uma linguagem orientada a objetos (ou pelo menos estamos assumindo isso), digamos que você tenha um pedaço de memória alocado. Como é uma linguagem OO, podemos facilmente nos referir a pedaços de memória que alocamos porque geralmente são variáveis primitivas (ints, chars, bytes) ou classes que definimos que são feitas de nossos próprios tipos e primitivos. Então, digamos que temos uma classe de carro da seguinte maneira:
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;
}
}
Uma cópia profunda é se declararmos um objeto e, em seguida, criarmos uma cópia completamente separada do objeto ... acabaremos com 2 objetos em 2 conjuntos de memória 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.
Agora vamos fazer algo estranho. Digamos que o carro2 seja programado errado ou intencionalmente destinado a compartilhar a memória real da qual o carro1 é feito. (Geralmente, é um erro fazer isso e, nas aulas, geralmente é o cobertor discutido.) Finja que sempre que você pergunta sobre o car2, está realmente resolvendo um ponteiro para o espaço de memória do car1 ... isso é mais ou menos uma cópia superficial é.
//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.*/
Portanto, independentemente do idioma em que você está escrevendo, tenha muito cuidado com o que você quer dizer quando se trata de copiar objetos, porque na maioria das vezes você deseja uma cópia profunda.
O que são o construtor de cópias e o operador de atribuição de cópias? Eu já os usei acima. O construtor de cópia é chamado quando você digita um código como Car car2 = car1;
Essencialmente, se você declarar uma variável e atribuí-la em uma linha, é quando o construtor de cópia é chamado. O operador de atribuição é o que acontece quando você usa um sinal de igual-- car2 = car1;
. O aviso car2
não está declarado na mesma declaração. Os dois pedaços de código que você escreve para essas operações provavelmente são muito semelhantes. De fato, o padrão de design típico tem outra função que você chama para definir tudo quando estiver satisfeito com a cópia / atribuição inicial ser legítima - se você olhar para o código longhand que escrevi, as funções são quase idênticas.
Quando preciso declará-los eu mesmo? Se você não está escrevendo um código que deve ser compartilhado ou produzido de alguma maneira, você só precisará declará-lo quando precisar. Você precisa estar ciente do que a linguagem do seu programa faz se optar por usá-la 'por acidente' e não a criou - ou seja, você obtém o padrão do compilador. Eu raramente uso construtores de cópia, por exemplo, mas as substituições do operador de atribuição são muito comuns. Você sabia que pode substituir o que adição, subtração etc. também significam?
Como posso impedir que meus objetos sejam copiados? Substituir todas as maneiras pelas quais você pode alocar memória para seu objeto com uma função privada é um começo razoável. Se você realmente não quer que as pessoas as copiem, você pode torná-lo público e alertar o programador lançando uma exceção e também não copiando o objeto.
Quando preciso declará-los eu mesmo?
A Regra dos Três estabelece que, se você declarar algum
então você deve declarar todos os três. Surgiu da observação de que a necessidade de assumir o significado de uma operação de cópia quase sempre decorria da classe realizar algum tipo de gerenciamento de recursos, e isso quase sempre implicava que
qualquer gerenciamento de recursos que estivesse sendo feito em uma operação de cópia provavelmente precisava ser feito na outra operação de cópia e
o destruidor de classe também participaria do gerenciamento do recurso (geralmente liberando-o). O recurso clássico a ser gerenciado era a memória, e é por isso que todas as classes da Biblioteca Padrão que gerenciam a memória (por exemplo, os contêineres STL que executam o gerenciamento dinâmico de memória) declaram "os três grandes": operações de cópia e destruidor.
Uma conseqüência da Regra dos Três é que a presença de um destruidor declarado pelo usuário indica que é improvável que uma cópia simples de membro seja apropriada para as operações de cópia na classe. Isso, por sua vez, sugere que, se uma classe declara um destruidor, as operações de cópia provavelmente não devem ser geradas automaticamente, porque elas não fariam a coisa certa. No momento em que o C ++ 98 foi adotado, o significado dessa linha de raciocínio não era totalmente apreciado; portanto, no C ++ 98, a existência de um destruidor declarado pelo usuário não teve impacto na disposição dos compiladores em gerar operações de cópia. Esse continua sendo o caso no C ++ 11, mas somente porque restringir as condições sob as quais as operações de cópia são geradas quebraria muito código legado.
Como posso impedir que meus objetos sejam copiados?
Declarar o construtor de cópias e o operador de atribuição de cópias como especificador de acesso privado.
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);
}
No C ++ 11 em diante, você também pode declarar que o construtor de cópias e o operador de atribuição foram excluídos
class MemoryBlock
{
public:
MemoryBlock(const MemoryBlock& other) = delete
// Copy assignment operator.
MemoryBlock& operator=(const MemoryBlock& other) =delete
};
int main()
{
MemoryBlock a;
MemoryBlock b(a);
}
Muitas das respostas existentes já tocam no construtor de cópias, no operador de atribuição e no destruidor. No entanto, no pós C ++ 11, a introdução da semântica de movimentação pode expandir isso além de 3.
Recentemente, Michael Claisse fez uma palestra que aborda este tópico: http://channel9.msdn.com/events/CPP/C-PP-Con-2014/The-Canonical-Class
A regra de três no C ++ é um princípio fundamental do design e do desenvolvimento de três requisitos que, se houver uma definição clara em uma das seguintes funções de membro, o programador deverá definir as outras funções de dois membros juntos. Nomeadamente, as três funções-membro a seguir são indispensáveis: destruidor, construtor de cópias, operador de atribuição de cópias.
O construtor de cópias em C ++ é um construtor especial. É usado para criar um novo objeto, que é o novo objeto equivalente a uma cópia de um objeto existente.
O operador de atribuição de cópia é um operador de atribuição especial que geralmente é usado para especificar um objeto existente para outros do mesmo tipo de objeto.
Existem exemplos rápidos:
// 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 tag antes de votar para fechar .