C++ Cópia e Movimento

Quando você escreve int b = a;, o C++ copia um número. Fácil. Mas o que acontece em Caderno b = a;, quando a é um objeto de uma classe sua? Quem faz a cópia? Copia o quê? Nesta aula você vai ver as duas funções que o C++ usa para copiar objetos, descobrir quando elas rodam (às vezes em lugares inesperados), conhecer o bug clássico da "cópia rasa" que derruba o programa e aprender as regras que os programadores C++ seguem para nunca cair nele.

Você vai aprender
  • O construtor de cópia Classe(const Classe&) e a atribuição de cópia operator=.
  • Em que linhas cada um é chamado: Classe b = a;, b = a;, passagem por valor, push_back.
  • A diferença entre cópia rasa e cópia profunda, e o erro double free que a cópia rasa causa.
  • A regra dos três, a regra dos cinco e a melhor de todas, a regra do zero.
  • Como proibir cópias com = delete, e o erro que o compilador mostra.
  • Onde entra o movimento (a aula completa é Semântica de Movimento).

Esta aula usa o que você viu em Construtores, Destrutores e RAII e Referências. Os programas foram compilados com o g++ 15 na máquina da Ana, na pasta ~/cpp, com g++ -std=c++23 -Wall -Wextra arquivo.cpp -o programa.


A ideia: xerox ou caderno compartilhado?

A Ana empresta o caderno de receitas para a Bia. Há dois jeitos:

  • Tirar uma xerox de cada página. A Bia pode rabiscar a cópia dela à vontade: o caderno da Ana continua igual.
  • Dar um bilhete dizendo "o caderno está na minha estante". Agora as duas usam o mesmo caderno: o que a Bia rabiscar, a Ana vai ver. E se as duas resolverem jogar "o seu" caderno fora, alguém vai tentar jogar fora um caderno que não existe mais.

Em C++, a xerox é a cópia profunda; o bilhete é a cópia rasa. Para tipos como int, std::string e std::vector, o C++ já faz a xerox sozinho. O perigo aparece quando a classe guarda um ponteiro cru (int*), que é exatamente um "bilhete" dizendo onde está a coisa.


1. As duas funções de cópia

Toda classe tem duas funções responsáveis por copiar. Se você não escrever, o compilador cria as duas, e elas copiam atributo por atributo.

FunçãoFormaQuando roda
Construtor de cópiaClasse(const Classe& outro)Quando um objeto novo nasce como cópia de outro.
Atribuição de cópiaClasse& operator=(const Classe& outro)Quando um objeto que já existe recebe o valor de outro.

Para ver quando cada uma roda, vamos escrevê-las nós mesmos, com um std::cout dentro:

#include <iostream>
#include <string>

class Caderno {
public:
    Caderno(std::string dono) : dono_{dono} {
        std::cout << "  [construtor] " << dono_ << '\n';
    }
    Caderno(const Caderno& outro) : dono_{outro.dono_} {
        std::cout << "  [cópia] copiei o caderno de " << dono_ << '\n';
    }
    Caderno& operator=(const Caderno& outro) {
        std::cout << "  [atribuição] " << dono_ << " recebe o de " << outro.dono_ << '\n';
        dono_ = outro.dono_;
        return *this;
    }
    ~Caderno() {
        std::cout << "  [destrutor] " << dono_ << '\n';
    }
    std::string dono() const { return dono_; }
private:
    std::string dono_;
};

void por_valor(Caderno c) {
    std::cout << "  dentro de por_valor: " << c.dono() << '\n';
}

void por_referencia(const Caderno& c) {
    std::cout << "  dentro de por_referencia: " << c.dono() << '\n';
}

int main() {
    std::cout << "1) criar a\n";
    Caderno a{"Ana"};
    std::cout << "2) Caderno b = a;\n";
    Caderno b = a;
    std::cout << "3) Caderno c{a};\n";
    Caderno c{a};
    std::cout << "4) Caderno d{\"Bia\"}; d = a;\n";
    Caderno d{"Bia"};
    d = a;
    std::cout << "5) por_valor(a)\n";
    por_valor(a);
    std::cout << "6) por_referencia(a)\n";
    por_referencia(a);
    std::cout << "7) fim do main\n";
}
Caderno(const Caderno& outro)
O construtor de cópia. Recebe o objeto original por referência constante: & para não copiar (copiar para poder copiar seria um círculo sem fim) e const porque copiar não deve mudar o original.
: dono_{outro.dono_}
Inicializa o atributo do objeto novo com o do original. Uma classe pode ler os atributos privados de outro objeto da mesma classe.
Caderno& operator=(const Caderno& outro)
A atribuição de cópia: é a função que roda no sinal = entre dois objetos que já existem. O nome operator= é a forma de "ensinar" um operador a uma classe (aula Sobrecarga de Operadores).
return *this;
Devolve o próprio objeto (this), para permitir x = y = z;, como com números.
$ g++ -std=c++23 -Wall -Wextra copia.cpp -o copia
$ ./copia
1) criar a
  [construtor] Ana
2) Caderno b = a;
  [cópia] copiei o caderno de Ana     « tem um "=", mas é construtor de cópia: b está nascendo
3) Caderno c{a};
  [cópia] copiei o caderno de Ana
4) Caderno d{"Bia"}; d = a;
  [construtor] Bia
  [atribuição] Bia recebe o de Ana    « d já existia: atribuição
5) por_valor(a)
  [cópia] copiei o caderno de Ana     « o parâmetro c é uma cópia nova
  dentro de por_valor: Ana
  [destrutor] Ana                     « e morre no fim da função
6) por_referencia(a)
  dentro de por_referencia: Ana       « nenhuma cópia!
7) fim do main
  [destrutor] Ana
  [destrutor] Ana
  [destrutor] Ana
  [destrutor] Ana

A pegadinha está no caso 2: Caderno b = a; tem um sinal de igual, mas não chama a atribuição. Como b está nascendo nessa linha, quem roda é o construtor de cópia. A regra para não errar:

Nasce ou já existia?
  • O objeto da esquerda está nascendo nesta linha (tem o tipo na frente)? Construtor de cópia.
  • O objeto da esquerda já existia? Atribuição de cópia (operator=).

Os casos 5 e 6 mostram por que, nas aulas de funções, o guia recomenda const Classe& para objetos (Por Valor × Por Referência): passar por valor fez uma cópia inteira (e uma destruição) só para ler o nome.

Mais dois lugares: return e push_back

Caderno criar() {
    Caderno novo{"Caio"};
    return novo;
}

int main() {
    std::cout << "1) Caderno x = criar();\n";
    Caderno x = criar();
    std::cout << "2) vector + push_back(x)\n";
    std::vector<Caderno> pilha;
    pilha.push_back(x);
    std::cout << "3) fim do main\n";
}
$ g++ -std=c++23 -Wall -Wextra retorno.cpp -o retorno
$ ./retorno
1) Caderno x = criar();
  [construtor] Caio                   « nenhuma cópia no return!
2) vector + push_back(x)
  [cópia] Caio                        « o vetor guarda uma cópia de x
3) fim do main
  [destrutor] Caio
  [destrutor] Caio

Devolver um objeto de uma função não copiou nada: o compilador construiu o novo direto no lugar de x. Isso se chama elisão de cópia (copy elision, "omitir a cópia"), e é por isso que devolver objetos por valor é barato em C++. Já o push_back guardou uma cópia de x: o vetor tem o seu próprio caderno.

Experimente: rode e confira cada linha. Depois troque por_valor(Caderno c) por por_valor(const Caderno& c) e veja a cópia e a destruição desaparecerem. Por último, acrescente a = a; no fim do main: quem roda?

#include <iostream>
#include <string>

class Caderno {
public:
    Caderno(std::string dono) : dono_{dono} {
        std::cout << "  [construtor] " << dono_ << '\n';
    }
    Caderno(const Caderno& outro) : dono_{outro.dono_} {
        std::cout << "  [cópia] " << dono_ << '\n';
    }
    Caderno& operator=(const Caderno& outro) {
        std::cout << "  [atribuição] " << dono_ << " recebe o de " << outro.dono_ << '\n';
        dono_ = outro.dono_;
        return *this;
    }
    ~Caderno() {
        std::cout << "  [destrutor] " << dono_ << '\n';
    }
    std::string dono() const { return dono_; }
private:
    std::string dono_;
};

void por_valor(Caderno c) {
    std::cout << "  lendo: " << c.dono() << '\n';
}

int main() {
    Caderno a{"Ana"};
    Caderno b = a;
    Caderno d{"Bia"};
    d = a;
    por_valor(a);
    std::cout << "fim do main\n";
}

2. Cópia rasa × cópia profunda

A cópia que o compilador gera copia atributo por atributo. Para um std::string, "copiar o atributo" chama a cópia da própria string, que faz uma xerox do texto. Tudo certo. Mas, para um ponteiro cru, "copiar o atributo" copia só o endereço: o bilhete, não o caderno. Veja uma classe que guarda notas num array criado com new[]:

#include <iostream>

class Notas {
public:
    Notas(int tamanho) : tamanho_{tamanho}, dados_{new int[tamanho]{}} {}
    ~Notas() { delete[] dados_; }

    int tamanho() const { return tamanho_; }
    void definir(int i, int valor) { dados_[i] = valor; }
    int pegar(int i) const { return dados_[i]; }
private:
    int tamanho_;
    int* dados_;
};

int main() {
    Notas ana{3};
    ana.definir(0, 7);

    Notas bia = ana;          // cópia feita pelo compilador
    bia.definir(0, 10);       // mudei só a Bia...

    std::cout << "Ana: " << ana.pegar(0) << '\n';
    std::cout << "Bia: " << bia.pegar(0) << '\n';
}
$ g++ -std=c++23 -Wall -Wextra rasa.cpp -o rasa
$ ./rasa
Ana: 10                  « a nota da Ana mudou sozinha!
Bia: 10
free(): double free detected in tcache 2
Abortado                   (imagem do núcleo gravada) ./rasa
$ echo $?
134

Dois problemas, nenhum aviso do compilador:

  1. Estado compartilhado. bia.dados_ recebeu o mesmo endereço de ana.dados_. As duas apontam para o mesmo array; mudar a Bia mudou a Ana.
  2. Double free ("liberação dupla"). No fim do main, bia morre e faz delete[] no array. Depois ana morre e faz delete[] no mesmo array, que já tinha sido devolvido. A biblioteca C percebeu (double free detected) e abortou o programa.

Com o AddressSanitizer (-fsanitize=address), o diagnóstico fica bem mais claro. A saída é longa; aqui vão as partes que importam:

$ g++ -std=c++23 -Wall -Wextra -g -fsanitize=address rasa.cpp -o rasa
$ ./rasa
Ana: 10
Bia: 10
=================================================================
==526964==ERROR: AddressSanitizer: attempting double-free on 0x799f015e0010 in thread T0:
    #0 0x7d7f02b2d46f in operator delete[](void*) ../../../../src/libsanitizer/asan/asan_new_delete.cpp:179
    #1 0x608446baf7eb in Notas::~Notas() /home/ana/cpp/rasa.cpp:6
    #2 0x608446baf578 in main /home/ana/cpp/rasa.cpp:25
                         « (linhas internas da biblioteca omitidas)
freed by thread T0 here:
    #0 0x7d7f02b2d46f in operator delete[](void*) ../../../../src/libsanitizer/asan/asan_new_delete.cpp:179
    #1 0x608446baf7eb in Notas::~Notas() /home/ana/cpp/rasa.cpp:6
    #2 0x608446baf56c in main /home/ana/cpp/rasa.cpp:25
                         « (…)
previously allocated by thread T0 here:
    #0 0x7d7f02b2c8cf in operator new[](unsigned long) ../../../../src/libsanitizer/asan/asan_new_delete.cpp:111
    #1 0x608446baf6ec in Notas::Notas(int) /home/ana/cpp/rasa.cpp:5
    #2 0x608446baf458 in main /home/ana/cpp/rasa.cpp:17
                         « (…)
SUMMARY: AddressSanitizer: double-free /home/ana/cpp/rasa.cpp:6 in Notas::~Notas()
==526964==ABORTING

Leia em três blocos: "attempting double-free" (tentativa de liberar duas vezes) no destrutor, linha 6; "freed by … here" (já tinha sido liberado aqui), no mesmo destrutor, chamado para o outro objeto; e "previously allocated" (alocado antes aqui), no construtor, linha 5, quando a Ana nasceu na linha 17. Só um array foi criado, e dois objetos tentaram devolvê-lo.

O conserto à moda antiga: cópia profunda

Para a cópia virar uma xerox, você escreve as duas funções de cópia à mão. Cada uma cria um array novo e copia os valores, um por um:

class Notas {
public:
    Notas(int tamanho) : tamanho_{tamanho}, dados_{new int[tamanho]{}} {}
    ~Notas() { delete[] dados_; }

    // construtor de cópia: memória NOVA, com os mesmos valores
    Notas(const Notas& outra)
        : tamanho_{outra.tamanho_}, dados_{new int[outra.tamanho_]} {
        for (int i = 0; i < tamanho_; ++i) {
            dados_[i] = outra.dados_[i];
        }
    }

    // atribuição de cópia: joga fora o antigo e copia o novo
    Notas& operator=(const Notas& outra) {
        if (this == &outra) return *this;      // a = a; não faz nada
        int* novos = new int[outra.tamanho_];
        for (int i = 0; i < outra.tamanho_; ++i) {
            novos[i] = outra.dados_[i];
        }
        delete[] dados_;
        dados_ = novos;
        tamanho_ = outra.tamanho_;
        return *this;
    }
    // ... tamanho, definir e pegar como antes
private:
    int tamanho_;
    int* dados_;
};
TrechoPor quê
dados_{new int[outra.tamanho_]}O objeto novo ganha o seu próprio array.
o forCopia os valores, um por um: a xerox.
if (this == &outra)Protege contra ana = ana;. Sem isso, o objeto apagaria os próprios dados antes de copiá-los.
new antes do delete[]Se o new falhar (falta de memória), o objeto continua com os dados antigos, inteiro.
$ g++ -std=c++23 -Wall -Wextra -g -fsanitize=address profunda.cpp -o profunda
$ ./profunda
Ana: 7                   « a Ana não foi afetada
Bia: 10
Caio: 5 (tamanho 3)      « caio = ana; depois caio.definir(0, 5);

Funciona, e o AddressSanitizer ficou quieto. Mas veja quanto código foi preciso para um array de inteiros, e quantos lugares para errar. Existe um jeito muito melhor.


3. Regra dos três, dos cinco e do zero

Regra dos três

Se a sua classe precisou de um destes três, quase certamente precisa dos três:

  1. destrutor (~Classe());
  2. construtor de cópia (Classe(const Classe&));
  3. atribuição de cópia (operator=(const Classe&)).

O motivo: se você escreveu um destrutor, é porque a classe cuida de um recurso "na mão" (o delete[]), e a cópia automática não sabe duplicar esse recurso. Foi exatamente o caso da Notas.

Regra dos cinco C++11

O C++11 acrescentou duas funções de movimento: o construtor de movimento Classe(Classe&&) e a atribuição de movimento operator=(Classe&&). Elas "roubam" o recurso de um objeto que vai morrer, em vez de copiá-lo, o que é muito mais rápido. A regra dos três virou a regra dos cinco: quem escreve uma das cinco deve pensar nas cinco. O movimento tem aula própria: Semântica de Movimento.

Regra do zero: a melhor

A recomendação moderna é não escrever nenhuma das cinco. Em vez de guardar um ponteiro cru e cuidar dele à mão, guarde um tipo que já sabe se copiar, mover e destruir (RAII, da aula anterior): std::vector, std::string, std::unique_ptr. A cópia automática, atributo por atributo, passa a fazer a coisa certa:

#include <iostream>
#include <vector>

class Notas {
public:
    Notas(int tamanho) : dados_(tamanho) {}

    int tamanho() const { return static_cast<int>(dados_.size()); }
    void definir(int i, int valor) { dados_[i] = valor; }
    int pegar(int i) const { return dados_[i]; }
private:
    std::vector<int> dados_;
};

int main() {
    Notas ana{3};
    ana.definir(0, 7);

    Notas bia = ana;
    bia.definir(0, 10);

    std::cout << "Ana: " << ana.pegar(0) << '\n';
    std::cout << "Bia: " << bia.pegar(0) << '\n';
}
$ g++ -std=c++23 -Wall -Wextra zero.cpp -o zero
$ ./zero
Ana: 7
Bia: 10

Sem destrutor, sem construtor de cópia, sem operator=, sem new: a classe ficou menor e correta. Repare no dados_(tamanho) com parênteses: para std::vector, (3) cria três elementos valendo zero, enquanto {3} criaria um elemento só, valendo 3 (detalhe da aula std::vector).

RegraQuando usar
ZeroQuase sempre. Atributos que já se cuidam (string, vector, unique_ptr…) e nenhuma das cinco funções escrita à mão.
CincoQuando você escreve uma classe que é o dono de um recurso cru (como a Arquivo da aula anterior). Escreva as cinco, ou proíba as que não fazem sentido.
TrêsO mesmo que a dos cinco, em código anterior ao C++11.

4. Proibir a cópia com = delete

Alguns objetos não deveriam ser copiados de jeito nenhum: um arquivo aberto, uma conexão com o banco, uma senha guardada na memória. Para proibir, declare as funções de cópia e escreva = delete no lugar do corpo C++11:

#include <iostream>
#include <string>

class Senha {
public:
    Senha(std::string texto) : texto_{texto} {}

    Senha(const Senha&) = delete;             // proíbe: Senha b = a;
    Senha& operator=(const Senha&) = delete;  // proíbe: b = a;

    bool confere(const std::string& tentativa) const { return tentativa == texto_; }
private:
    std::string texto_;
};

int main() {
    Senha minha{"gato123"};
    Senha copia = minha;
    std::cout << copia.confere("gato123") << '\n';
}
$ g++ -std=c++23 -Wall -Wextra proibido.cpp -o proibido
proibido.cpp: In function ‘int main()’:
proibido.cpp:18:19: error: use of deleted function ‘Senha::Senha(const Senha&)’
   18 |     Senha copia = minha;
      |                   ^~~~~
proibido.cpp:8:5: note: declared here
    8 |     Senha(const Senha&) = delete;             // proíbe: Senha b = a;
      |     ^~~~~
proibido.cpp:18:19: note: use ‘-fdiagnostics-all-candidates’ to display considered candidates
   18 |     Senha copia = minha;
      |                   ^~~~~
error: use of deleted function
"uso de função apagada": a linha 18 tentou usar uma função marcada com = delete.
‘Senha::Senha(const Senha&)’
Qual função: o construtor de cópia da Senha.
note: declared here
Onde ela foi apagada (linha 8). É o compilador mostrando que a proibição foi de propósito.
note: use ‘-fdiagnostics-all-candidates’
Uma dica: com essa opção, o g++ lista todas as funções que considerou. Pode ignorar.

O erro aparece na compilação, antes de o programa existir: muito melhor que um double free em produção. O Clang do editor diz o mesmo com outras palavras: call to deleted constructor of 'Senha'. Proibir a cópia também proíbe passar uma Senha por valor; use const Senha&.

= default: o contrário

Se você quer deixar claro que a cópia automática está boa, escreva Classe(const Classe&) = default;: "use a versão que o compilador gera". Pela regra do zero, normalmente nem isso é preciso.


Quando dá errado

"free(): double free detected in tcache 2"
$ ./rasa
Ana: 10
Bia: 10
free(): double free detected in tcache 2
Abortado                   (imagem do núcleo gravada) ./rasa

O que significa: a mesma memória foi liberada duas vezes. Quase sempre é uma classe com ponteiro cru e destrutor, mas com a cópia automática (rasa). tcache é um detalhe interno da biblioteca C; não ajuda a achar o erro. O código de saída é 134 (abortado).

Como consertar: regra do zero (troque o ponteiro cru por std::vector, std::string ou std::unique_ptr), ou escreva a cópia profunda, ou proíba a cópia com = delete. Para achar onde, compile com -g -fsanitize=address.

Mudei uma cópia e o original mudou junto

O que significa: é a outra cara da cópia rasa: os dois objetos dividem o mesmo recurso por um ponteiro. Pode acontecer sem nenhum erro visível, se a classe não tiver destrutor (e aí ela vaza memória).

Como consertar: o mesmo do cenário anterior.

"use of deleted function" ao passar para uma função

O que significa: a classe proíbe cópias (como Senha, std::unique_ptr ou std::ofstream), e a função recebe o parâmetro por valor, o que exigiria uma cópia.

Como consertar: receba por referência (const Senha&). Se a intenção é entregar o objeto para a função, use std::move (aula Semântica de Movimento).

O operator= apagou os próprios dados

O que significa: um operator= escrito à mão que faz delete[] dados_ antes de copiar quebra em a = a;: ele apaga o array e depois tenta copiar do array apagado. Isso também acontece de forma escondida, como em lista[i] = lista[j]; quando i == j.

Como consertar: teste if (this == &outra) return *this; e crie o array novo antes de apagar o antigo. Ou, melhor, regra do zero.


Missões

Missão 1 (regra do zero): esta Notas usa um ponteiro cru, e a cópia da Bia muda a nota da Ana. Troque o int* por um std::vector<int> (inclua <vector>), apague o destrutor e ajuste o construtor, até a saída ser Ana: 7 e Bia: 10.

#include <iostream>

class Notas {
public:
    Notas(int tamanho) : dados_{new int[tamanho]{}} {}
    ~Notas() { delete[] dados_; }

    void definir(int i, int valor) { dados_[i] = valor; }
    int pegar(int i) const { return dados_[i]; }
private:
    int* dados_;
};

int main() {
    Notas ana{3};
    ana.definir(0, 7);

    Notas bia = ana;
    bia.definir(0, 10);

    std::cout << "Ana: " << ana.pegar(0) << '\n';
    std::cout << "Bia: " << bia.pegar(0) << '\n';
}
#include <iostream>
#include <vector>

class Notas {
public:
    Notas(int tamanho) : dados_(tamanho) {}

    void definir(int i, int valor) { dados_[i] = valor; }
    int pegar(int i) const { return dados_[i]; }
private:
    std::vector<int> dados_;
};

int main() {
    Notas ana{3};
    ana.definir(0, 7);

    Notas bia = ana;
    bia.definir(0, 10);

    std::cout << "Ana: " << ana.pegar(0) << '\n';
    std::cout << "Bia: " << bia.pegar(0) << '\n';
}

Missão 2 (provoque o erro): uma Chave do cofre não pode ter cópias. Acrescente na classe as duas linhas com = delete que proíbem copiar e rode: a compilação deve falhar na linha Chave reserva = chave;, com um erro que fala de função deleted.

#include <iostream>
#include <string>

class Chave {
public:
    Chave(std::string codigo) : codigo_{codigo} {}
    // proíba a cópia aqui

    std::string codigo() const { return codigo_; }
private:
    std::string codigo_;
};

int main() {
    Chave chave{"cofre-42"};
    Chave reserva = chave;
    std::cout << reserva.codigo() << '\n';
}
#include <iostream>
#include <string>

class Chave {
public:
    Chave(std::string codigo) : codigo_{codigo} {}
    Chave(const Chave&) = delete;
    Chave& operator=(const Chave&) = delete;

    std::string codigo() const { return codigo_; }
private:
    std::string codigo_;
};

int main() {
    Chave chave{"cofre-42"};
    Chave reserva = chave;
    std::cout << reserva.codigo() << '\n';
}

Referência

CódigoO que roda
Classe b = a; ou Classe b{a};Construtor de cópia
b = a; (b já existe)Atribuição de cópia, operator=
f(a) com void f(Classe x)Construtor de cópia (para o parâmetro)
f(a) com void f(const Classe& x)Nada: é uma referência
return obj; de um objeto localNormalmente nada (elisão de cópia); senão, movimento
v.push_back(a);Construtor de cópia (o vetor guarda a sua própria cópia)
Classe(const Classe&) = delete;Proíbe a cópia: erro de compilação em quem tentar
Classe(const Classe&) = default;Pede a cópia automática, atributo por atributo

Exercícios

1. A assinatura

Complete o construtor de cópia da classe Produto:

Produto()

2. Proibir

Complete para proibir a atribuição de cópia da classe Conexao:

Conexao& operator=(const Conexao&) = ;

3. Quantas cópias?

Com a classe Caderno da seção 1 (que imprime [cópia]), quantas linhas [cópia] aparecem em: Caderno a{"Ana"}; Caderno b = a; Caderno c{"Caio"}; c = b;?

4. Uma classe guarda int* dados_ criado com new[] e tem destrutor com delete[], mas não tem construtor de cópia. O que acontece em Classe b = a; e no fim do programa?

5. O que diz a regra do zero?

6. Em qual linha roda a atribuição de cópia (e não o construtor de cópia)?


Resumo

  • O construtor de cópia Classe(const Classe&) roda quando um objeto nasce como cópia (inclusive em Classe b = a;, passagem por valor e push_back).
  • A atribuição de cópia operator= roda quando um objeto que já existe recebe outro.
  • A cópia automática copia atributo por atributo. Com ponteiro cru, isso é uma cópia rasa: estado compartilhado e double free.
  • Cópia profunda = memória nova + copiar os valores. Funciona, mas dá trabalho.
  • Regra dos três/cinco: escreveu uma das funções especiais, pense em todas. Regra do zero: prefira atributos RAII e não escreva nenhuma.
  • = delete proíbe a cópia com erro de compilação (use of deleted function).
  • Devolver objeto por valor costuma não copiar nada (elisão de cópia). O movimento, a outra metade desta história, está em Semântica de Movimento.