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 ⭐)
#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;
}
Saída:
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
#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 ⭐⭐)
#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.
#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.
#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 ⭐⭐⭐)
#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;
}
Saída:
(program output)
▶ Exemplo 3: Exemplo de Cópia Profunda (Dificuldade ⭐)
#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;
}
Saída:
b1[0]=1, b2[0]=100
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
=defaulte=deletefazem? 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
- Construtor de cópia: inicializa um novo objeto a partir de um objeto do mesmo tipo
- Operador de atribuição por cópia: sobrecarga do operador =
- Regra dos Três/Cinco: se você personalizar um, normalmente precisa de todos
- Cópia superficial vs cópia profunda: membros ponteiro devem usar cópia profunda
- Funções delete evitam cópias: = delete
📝 Exercícios
-
Básico (Dificuldade ⭐): Defina uma classe
Pointcom dois membros: x e y. -
Siga a Regra dos Três — defina um construtor de cópia personalizado, operador de atribuição por cópia e destrutor
-
Teste a cópia em
main -
Intermediário (Dificuldade ⭐⭐): Defina uma classe
DynamicArray(array dinâmico) com: -
int* data(apontando para um array no heap) -
int size(tamanho do array)
Siga a Regra dos Cinco — implemente todas as funções membro especiais.
- Desafio (Dificuldade ⭐⭐⭐):
Estenda a classe
DynamicArrayadicionando: void push_back(int x): adiciona um elemento ao final do array (se o espaço for insuficiente, dobre a capacidade)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!