C++: Controle de Cópia

Última atualização: 2026-08-26

Na Lição 29, aprendemos construtores avançados.

Mas se você quiser copiar objetos, atribuir objetos ou mover objetos, o C++ possui um conjunto de regras de controle de cópia.

Compreender essas regras é essencial para escrever classes corretas.


1. Regra dos Três/Cinco

(1) 1.1 O que é a Regra dos Três/Cinco?

Se sua classe precisa de uma implementação personalizada de qualquer uma das funções a seguir, então você deve implementar personalizadamente todas as funções relacionadas:

Versão Funções a Gerenciar Descrição
Regra dos Três (C++98) 1. Destrutorbr2. Construtor de cópiabr3. Operador de atribuição por cópia Necessário quando há memória dinâmica
Regra dos Cinco (C++11) Regra dos Três +br4. Construtor de movimentaçãobr5. Operador de atribuição por movimentação C++11 introduziu semântica de movimentação

(2) 1.2 Por que a Regra dos Três/Cinco?

Se você precisa personalizar uma delas, significa que a classe possui recursos que exigem tratamento especial (como memória dinâmica, identificadores de arquivo).

Exemplo: Consequências de Não Seguir a Regra dos Três (Dificuldade ⭐⭐)

▶ Exemplo 2: Exemplo de Código (Dificuldade ⭐)

CPP
#include <iostream>
#include <cstring>

class BadString {
private:
 char* data;
 
public:
 // Constructor
 BadString(const char* s) {
 data = new char[strlen(s) + 1];
 strcpy(data, s);
 }
 
 // ❌ No custom copy constructor, copy assignment operator, or destructor
 // The compiler will generate defaults (shallow copy)
};

int main() {
 BadString s1("Hello");
 BadString s2 = s1; // ❌ Shallow copy: s1.data and s2.data point to the same memory
 
 // When main ends, s2 is destructed first (freeing data)
 // Then s1 is destructed (freeing data again, but it's already been freed) → Crash!
 
 return 0;
}
▶ Experimente

Saída:

TEXT 📖 Somente leitura
Hello
Hello

Consequência: Falha do programa (dupla liberação da mesma memória).

Correção: Siga a Regra dos Três — implemente personalizadamente todas as três funções.



2. Operador de Atribuição por Cópia*

(1) 2.1 Uso Básico

CPP
#include <iostream>
#include <cstring>

class MyString {
private:
 char* data;
 
public:
 // Constructor
 MyString(const char* s) {
 data = new char[strlen(s) + 1];
 strcpy(data, s);
 }
 
 // Copy constructor
 MyString(const MyString& other) {
 data = new char[strlen(other.data) + 1];
 strcpy(data, other.data);
 }
 
 // Copy assignment operator
 MyString& operator=(const MyString& other) {
 if (this != &other) { // Prevent self-assignment
 delete data; // Free old memory
 data = new char[strlen(other.data) + 1];
 strcpy(data, other.data);
 }
 return *this;
 }
 
 // Destructor
 ~MyString() {
 delete data;
 }
};

int main() {
 MyString s1("Hello");
 MyString s2("World");
 
 s2 = s1; // ✅ Calls the copy assignment operator
 
 return 0;
}

💡 Ponto-chave: O operador de atribuição por cópia deve verificar auto-atribuição (if (this != &other)).



3. Semântica de Movimentação (C++11)*

(1) 3.1 Por que Precisamos de Movimentação?

Se você tem um objeto temporário (prestes a ser destruído), copiar seu conteúdo é desperdício — é melhor apenas "roubar" seus recursos.

Exemplo: Usando Movimentação para Evitar Cópias Desnecessárias (Dificuldade ⭐⭐)

TEXT 📖 Somente leitura
#include <iostream>
#include <cstring>

class MyString {
private:
 char* data;
 
public:
 // Move constructor (C++11)
 MyString(MyString&& other) noexcept {
 data = other.data; // Steal the resource
 other.data = nullptr; // Set other to null
 }
 
 // Move assignment operator (C++11)
 MyString& operator=(MyString&& other) noexcept {
 if (this != &other) {
 delete data; // Free old resource
 data = other.data; // Steal the resource
 other.data = nullptr; // Set other to null
 }
 return *this;
 }
};

int main() {
 MyString s1("Hello");
 MyString s2 = std::move(s1); // ✅ Calls the move constructor
 // Now s1.data is nullptr, s2.data points to the original memory
 
 return 0;
}

💡 Dica: std::move() converte um lvalue em uma referência rvalue, acionando o construtor de movimentação ou o operador de atribuição por movimentação.



4. = default e = delete*

(1) 4.1 Usando = default para Deixar o Compilador Gerar Implementações Padrão*

Se sua classe não precisa de lógica especial de cópia, você pode usar = default para deixar o compilador gerar os padrões.

CPP
#include <iostream>
#include <string>

class Student {
private:
 std::string name;
 int age;
 
public:
 // Let the compiler generate default copy constructor, copy assignment operator, and destructor
 Student(const Student& other) = default;
 Student& operator=(const Student& other) = default;
 ~Student() = default;
};

int main() {
 Student s1;
 Student s2 = s1; // ✅ Calls the default copy constructor
 
 return 0;
}

💡 Dica: Se uma classe possui apenas tipos STL (std::string, std::vector, etc.), usar = default é suficiente.


(2) 4.2 Usando = delete para Proibir a Cópia*

Se você não quer que uma classe seja copiada (por exemplo, padrão singleton), pode usar = delete para proibi-la.

CPP
#include <iostream>

class Singleton {
private:
 Singleton() {} // Private constructor
 
public:
 // Prohibit copying
 Singleton(const Singleton&) = delete;
 Singleton& operator=(const Singleton&) = delete;
 
 static Singleton& getInstance() {
 static Singleton instance;
 return instance;
 }
};

int main() {
 Singleton& s = Singleton::getInstance();
 // Singleton s2 = s; // ❌ Compile error: copy constructor is deleted
 
 return 0;
}


5. Prática: Classe MyString Completa*

▶ Exemplo 1: Implementando uma Classe MyString Completa (Dificuldade ⭐⭐⭐)

CPP 📖 Somente leitura
#include <iostream>
#include <cstring>

class MyString {
private:
 char* data;
 int length;
 
public:
 // Constructor
 MyString(const char* s = "") {
 length = strlen(s);
 data = new char[length + 1];
 strcpy(data, s);
 }
 
 // Copy constructor
 MyString(const MyString& other) {
 length = other.length;
 data = new char[length + 1];
 strcpy(data, other.data);
 }
 
 // Move constructor
 MyString(MyString&& other) noexcept {
 data = other.data;
 length = other.length;
 other.data = nullptr;
 other.length = 0;
 }
 
 // Copy assignment operator
 MyString& operator=(const MyString& other) {
 if (this != &other) {
 delete data;
 length = other.length;
 data = new char[length + 1];
 strcpy(data, other.data);
 }
 return *this;
 }
 
 // Move assignment operator
 MyString& operator=(MyString&& other) noexcept {
 if (this != &other) {
 delete data;
 data = other.data;
 length = other.length;
 other.data = nullptr;
 other.length = 0;
 }
 return *this;
 }
 
 // Destructor
 ~MyString() {
 delete data;
 }
 
 // Other member functions
 void print() const {
 if (data) std::cout << data;
 }
 
 int size() const {
 return length;
 }
};

int main() {
 MyString s1("Hello");
 MyString s2 = s1; // Copy constructor
 
 s1.print(); // Hello
 std::cout << std::endl;
 
 s2.print(); // Hello
 std::cout << std::endl;
 
 return 0;
}
59 linhas de lógica (limite de 40, somente leitura)

Saída:

TEXT 📖 Somente leitura
(program output)

▶ Exemplo 3: Exemplo de Cópia Profunda (Dificuldade ⭐)

CPP
#include <iostream>

class Buffer {
    int* data;
    int size;
public:
    Buffer(int s) : size(s), data(new int[s]) {}
    ~Buffer() { delete[] data; }

    // Deep copy constructor
    Buffer(const Buffer& other) : size(other.size), data(new int[other.size]) {
        for (int i = 0; i < size; i++) data[i] = other.data[i];
    }

    int& operator[](int i) { return data[i]; }
};

int main() {
    Buffer b1(3);
    b1[0] = 1; b1[1] = 2; b1[2] = 3;

    Buffer b2 = b1; // Deep copy
    b2[0] = 100;

    std::cout << "b1[0]=" << b1[0] << ", b2[0]=" << b2[0] << std::endl;

    return 0;
}
▶ Experimente

Saída:

TEXT 📖 Somente leitura
b1[0]=1, b2[0]=100
💡 Dica: A cópia profunda copia o conteúdo apontado pelo ponteiro, não apenas o valor do ponteiro. Modificar a cópia não afeta o objeto original.


P: Quando devo definir um construtor de cópia personalizado? R: Quando a classe possui membros ponteiro (apontando para memória dinâmica).

Se a classe possui apenas tipos básicos (int, double) ou tipos STL (std::string, std::vector), não é necessário um construtor de cópia personalizado — o gerado pelo compilador funciona corretamente (tipos STL implementam cópia profunda por si mesmos).

P: O que =default e =delete fazem? R: > - = default: deixa o compilador gerar a implementação padrão (cópia membro a membro) > - = delete: proíbe uma função (por exemplo, proibir a cópia)

Usar = default é recomendado em vez de escrever lógica de cópia manualmente (se nenhuma lógica especial for necessária).

P: O que acontece se eu não escrever um construtor de cópia? R: O compilador gera automaticamente um construtor de cópia padrão (cópia membro a membro).

Mas se a classe possui membros ponteiro, o padrão realizará uma cópia superficial (copia apenas o valor do ponteiro, não o conteúdo para o qual ele aponta), causando dupla liberação de memória.



❓ Perguntas Frequentes

P: Quando preciso definir um construtor de cópia personalizado? R: Quando uma classe gerencia memória dinâmica ou recursos (por exemplo, membros ponteiro), você deve implementar cópia profunda, caso contrário a cópia superficial padrão causará dupla liberação. P: Qual a diferença entre um construtor de movimentação e um construtor de cópia? R: Um construtor de cópia copia recursos (cópia profunda), enquanto um construtor de movimentação transfere a propriedade dos recursos (cópia superficial), o que é mais eficiente.


📖 Resumo


📝 Exercícios

  1. Básico (Dificuldade ⭐): Defina uma classe Point com dois membros: x e y.

  2. Siga a Regra dos Três — defina um construtor de cópia personalizado, operador de atribuição por cópia e destrutor

  3. Teste a cópia em main

  4. Intermediário (Dificuldade ⭐⭐): Defina uma classe DynamicArray (array dinâmico) com:

  5. int* data (apontando para um array no heap)

  6. int size (tamanho do array)

Siga a Regra dos Cinco — implemente todas as funções membro especiais.

  1. Desafio (Dificuldade ⭐⭐⭐): Estenda a classe DynamicArray adicionando:
  2. void push_back(int x): adiciona um elemento ao final do array (se o espaço for insuficiente, dobre a capacidade)
  3. void pop_back(): remove o último elemento

6. 🚀 Próximos Passos*

Agora que você aprendeu controle de cópia e semântica de movimentação, vamos avançar para Herança (Lição 31) — criando relações "pai-filho" entre classes para reutilização de código!

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%