- 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?
c++
copy-constructor
assignment-operator
c++-faq
rule-of-three
fredoverflow
fonte
fonte
c++-faq
wiki tag antes de votar para fechar .Respostas:
Introdução
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:
(Se você está intrigado com a
name(name), age(age)
peça, isso é chamado de lista de inicializadores de membros .)Funções-membro especiais
O que significa copiar um
person
objeto? Amain
função mostra dois cenários distintos de cópia. A inicializaçãoperson b(a);
é realizada pelo construtor de cópia . Seu trabalho é construir um novo objeto com base no estado de um objeto existente. A atribuiçãob = 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:
Por padrão, copiar um objeto significa copiar seus membros:
Definições implícitas
As funções-membro especiais definidas implicitamente
person
são assim:Cópia em membros é exatamente o que queremos neste caso:
name
eage
são copiados, para obter umperson
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 doperson
destruidor:Gerenciando recursos
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. Aperson
classe pode ter se parecido com isto: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 viab
.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.Definições explícitas
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:
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áriox = x
. Sem essa verificação,delete[] name
excluiria a matriz que contém a cadeia de origem , porque quando você escrevex = x
, ambasthis->name
ethat.name
contêm o mesmo ponteiro.Exceção de segurança
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: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.
Recursos não copiáveis
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:Como alternativa, você pode herdar
boost::noncopyable
ou declarar como excluído (no C ++ 11 e acima):A regra de três
À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 :
(Infelizmente, essa "regra" não é imposta pelo padrão C ++ ou por qualquer compilador que eu conheça.)
A regra dos cinco
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:
A regra do zero
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.
Adendo
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 umstd::string
membro com a alternativa complicada e propensa a erros usando aechar*
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.fonte
A Regra dos Três é uma regra de ouro para C ++, basicamente dizendo
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.)
fonte
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.
fonte
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ê.
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.
fonte
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:
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.
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 é.
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 avisocar2
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.
fonte
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.
Declarar o construtor de cópias e o operador de atribuição de cópias como especificador de acesso privado.
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
fonte
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
fonte
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:
fonte