C++: Construtores Avançados
Última atualização: 2026-08-26
Na Aula 28, aprendemos o básico sobre construtores.
Mas em cenários do mundo real, você pode precisar copiar objetos, mover objetos, usar listas de inicialização para eficiência...
Nesta aula, aprenderemos o uso avançado de construtores.
1. Construtor Padrão
(1) 1.1 O que é um Construtor Padrão?
Um construtor padrão é um construtor que não recebe parâmetros.
#include <iostream>
#include <string>
class Student {
private:
std::string name;
int age;
public:
// Construtor padrão (sem parâmetros)
Student() {
name = "Desconhecido";
age = 0;
std::cout << "Construtor padrão chamado" << std::endl;
}
};
int main() {
Student s1; // ✅ Chama o construtor padrão
return 0;
}
💡 Ponto Importante: Se você não escrever nenhum construtor, o compilador gera automaticamente um construtor padrão (que não faz nada). Mas se você escrever qualquer construtor, o compilador não gerará mais automaticamente um construtor padrão.
(2) 1.2 Problema: Após Escrever um Construtor Parametrizado, a Construção Padrão Falha
#include <iostream>
#include <string>
class Student {
private:
std::string name;
int age;
public:
// Escreveu apenas um construtor parametrizado
Student(const std::string& n, int a) {
name = n;
age = a;
}
};
int main() {
Student s1("Alice", 20); // ✅ OK
Student s2; // ❌ Erro: sem construtor padrão
return 0;
}
Solução: Escreva explicitamente um construtor padrão, ou use = default (C++11).
// Método 1: Escrever explicitamente
Student() {
name = "Desconhecido";
age = 0;
}
// Método 2: Usar = default (C++11, recomendado)
Student() = default;
2. Lista de Inicialização
(1) 2.1 O que é uma Lista de Inicialização?
Uma lista de inicialização inicializa membros diretamente após os dois-pontos do construtor, em vez de atribuir valores no corpo da função.
Abordagem tradicional (atribuição):
Student(const std::string& n, int a) {
name = n; // Isso é atribuição, não inicialização
age = a;
}
Abordagem com lista de inicialização (recomendado):
Student(const std::string& n, int a) : name(n), age(a) {
// O corpo da função pode ficar vazio
}
💡 Vantagens:
- Mais eficiente (inicialização direta, não construção padrão seguida de atribuição)
- Casos em que a lista de inicialização é obrigatória:
- Membros
const(devem ser inicializados na criação) - Membros de referência (devem ser inicializados)
- Classes membro sem construtor padrão
- Membros
(2) 2.2 Casos em que a Lista de Inicialização é Obrigatória
Caso 1: Membros const
#include <iostream>
#include <string>
class Student {
private:
const std::string id; // membro const
std::string name;
public:
// ❌ Erro: membros const não podem ser atribuídos no corpo da função
// Student(const std::string& i, const std::string& n) {
// id = i; // ❌ membro const não pode ser atribuído
// name = n;
// }
// ✅ Correto: usar lista de inicialização
Student(const std::string& i, const std::string& n) : id(i), name(n) {
}
};
int main() {
Student s1("20240001", "Alice");
return 0;
}
3. Construtor de Cópia
(1) 3.1 O que é um Construtor de Cópia?
Um construtor de cópia é usado para inicializar um objeto a partir de outro objeto.
Quando é acionado:
- Inicializar um objeto a partir de outro
- Passar um objeto por valor para uma função (copia)
- Retornar um objeto de uma função (pode copiar)
▶ Exemplo 1: Construtor de Cópia Padrão (Dificuldade ⭐)
#include <iostream>
#include <string>
class Student {
public:
std::string name;
int age;
// O compilador gera automaticamente um construtor de cópia (cópia membro a membro)
};
int main() {
Student s1;
s1.name = "Alice";
s1.age = 20;
Student s2 = s1; // ✅ Chama o construtor de cópia
std::cout << "s2 Nome: " << s2.name << std::endl; // Alice
std::cout << "s2 Idade: " << s2.age << std::endl; // 20
return 0;
}
Saída:
s2 Nome: Alice
s2 Idade: 20
💡 Ponto Importante: Se você não escrever um construtor de cópia, o compilador gera um automaticamente (cópia membro a membro).
(2) 3.2 Construtor de Cópia Personalizado
#include <iostream>
#include <string>
class Student {
private:
std::string name;
int age;
public:
// Construtor regular
Student(const std::string& n, int a) : name(n), age(a) {
std::cout << "Construtor regular chamado:" << name << std::endl;
}
// Construtor de cópia
Student(const Student& other) : name(other.name), age(other.age) {
std::cout << "Construtor de cópia chamado: " << name << std::endl;
}
};
int main() {
Student s1("Alice", 20);
Student s2 = s1; // Chama o construtor de cópia
return 0;
}
4. Cópia Superficial vs Cópia Profunda
(1) 4.1 O Problema: A Cópia Padrão é uma "Cópia Superficial"
Se uma classe tem membros ponteiro (apontando para memória heap), o construtor de cópia padrão apenas copia o valor do ponteiro (o endereço), não o conteúdo que o ponteiro aponta.
#include <iostream>
#include <cstring>
class MyClass {
public:
char* data;
MyClass() {
data = new char[5];
std::strcpy(data, "Hello");
}
// ❌ Sem construtor de cópia personalizado (usará cópia superficial padrão)
~MyClass() {
delete data;
}
};
int main() {
MyClass obj1;
MyClass obj2 = obj1; // Cópia superficial: obj1.data e obj2.data apontam para a mesma memória
// Quando main termina, obj2 é destruído primeiro (liberando data)
// Depois obj1 é destruído (liberando data novamente, mas já foi liberado) → Crash!
return 0;
}
💡 Este é o problema da "cópia superficial" — os ponteiros de dois objetos apontam para a mesma memória, causando uma dupla liberação na destruição.
(2) 4.2 Solução: Cópia Profunda Personalizada
#include <iostream>
#include <cstring>
class MyClass {
public:
char* data;
MyClass() {
data = new char[5];
std::strcpy(data, "Hello");
}
// Construtor de cópia personalizado (cópia profunda)
MyClass(const MyClass& other) {
data = new char[std::strlen(other.data) + 1];
std::strcpy(data, other.data);
}
~MyClass() {
delete data;
}
};
int main() {
MyClass obj1;
MyClass obj2 = obj1; // Cópia profunda: obj2.data aponta para memória recém-alocada
std::cout << "obj1.data = " << obj1.data << std::endl;
std::cout << "obj2.data = " << obj2.data << std::endl;
return 0;
}
💡 Ponto Importante: Se uma classe tem membros ponteiro, você deve definir um construtor de cópia personalizado (para implementar cópia profunda).
5. Construtor de Movimentação (C++11, Opcional)
(1) 5.1 O que é um Construtor de Movimentação?
Um construtor de movimentação é um recurso introduzido no C++11 — ele "rouba" recursos de outro objeto em vez de copiá-los.
Cenário: Se você tem um objeto temporário (prestes a ser destruído), copiar seu conteúdo é desperdício — é melhor apenas "roubar" seus recursos.
#include <iostream>
#include <string>
class MyClass {
private:
std::string* data;
public:
// Construtor regular
MyClass(const std::string& s) {
data = new std::string(s);
std::cout << "Construtor regular" << std::endl;
}
// Construtor de movimentação (C++11)
MyClass(MyClass&& other) noexcept : data(other.data) {
other.data = nullptr; // Define o ponteiro do outro como nulo (previne desalocação na destruição)
std::cout << "Construtor de movimentação" << std::endl;
}
~MyClass() {
delete data;
}
};
int main() {
MyClass obj1("Hello");
MyClass obj2 = std::move(obj1); // Chama o construtor de movimentação
return 0;
}
💡 Dica: Construtores de movimentação são um tópico avançado — iniciantes podem se familiarizar com o conceito primeiro e aprofundar-se depois.
6. Prática: Classe Student Completa
▶ Exemplo 2: Classe Student com Construtor de Cópia (Dificuldade ⭐⭐)
#include <iostream>
#include <string>
class Student {
private:
std::string name;
int age;
public:
// Construtor regular
Student(const std::string& n, int a) : name(n), age(a) {
std::cout << "Construtor Regular: " << name << std::endl;
}
// Construtor de cópia
Student(const Student& other) : name(other.name), age(other.age) {
std::cout << "Construtor Regular: " << name << std::endl;
}
// Destrutor
~Student() {
std::cout << "Destrutor: " << name << std::endl;
}
void introduce() {
std::cout << "Meu nome é " << name << ",tenho " << age << " anos。" << std::endl;
}
};
int main() {
Student s1("Alice", 20);
Student s2 = s1; // Chama o construtor de cópia
s1.introduce();
s2.introduce();
return 0;
}
Saída:
Construtor Regular: Alice
Construtor Regular: Alice
Destrutor: Alice
Destrutor: Alice
Meu nome é Alice,tenho 20 anos。
Meu nome é Alice,tenho 20 anos。
❓ Perguntas Frequentes
P: Quando preciso definir um construtor de cópia personalizado? R: Quando a classe tem membros ponteiro (apontando para memória heap).
Se a classe tem apenas tipos básicos (
int,double) ou tipos STL (std::string,std::vector), não é necessário um construtor de cópia personalizado — o construtor de cópia gerado pelo compilador funciona corretamente (tipos STL implementam cópia profunda por si mesmos).
P: Qual a diferença de eficiência entre listas de inicialização e atribuição no corpo da função? R: Para tipos básicos, pouca. Mas para tipos STL (
std::string,std::vector), muita —
- Atribuição no corpo: primeiro constrói padrão um objeto vazio, depois atribui (pode alocar memória e copiar conteúdo)
- Lista de inicialização: inicializa diretamente (apenas uma alocação de memória)
Recomendação: Sempre use listas de inicialização.
P: Qual a utilidade dos construtores de movimentação? R: Melhorar a eficiência da cópia de objetos temporários.
Por exemplo, quando uma função retorna um objeto grande, o construtor de movimentação pode evitar cópia profunda (roubando diretamente a memória do objeto temporário).
▶ Exemplo 3: Construtor Delegante (Dificuldade ⭐)
#include <iostream>
#include <string>
class Student {
std::string name;
int age;
public:
Student(std::string n, int a) : name(n), age(a) {}
Student(std::string n) : Student(n, 18) {} // Construtor delegante
Student() : Student("Desconhecido") {}
void print() { std::cout << name << "," << age << "anos" << std::endl; }
};
int main() {
Student s1("Zhang San", 20);
Student s2("Li Si");
Student s3;
s1.print();
s2.print();
s3.print();
return 0;
}
Saída:
Zhang San, 20 anos
Li Si, 18 anos
Desconhecido, 18 anos
- Construtores padrão são construtores sem parâmetros
- Listas de inicialização são mais eficientes e podem lidar com membros
conste membros de referência - Construtores de cópia são usados para inicializar um objeto a partir de outro
- Se uma classe tem membros ponteiro, você deve definir uma cópia profunda personalizada
- Construtores de movimentação (C++11) podem melhorar a eficiência da cópia de objetos temporários
📖 Resumo
- Construtores delegantes: um construtor chama outro construtor
- Construtores herdados: classes derivadas herdam construtores da classe base
- Construção por movimentação: transfere posse de recursos, evita cópia profunda
- explicit: previne conversão implícita
📝 Exercícios
-
Básico (Dificuldade ⭐): Defina uma classe
Bookcom dois membros: título e autor. Escreva o construtor usando uma lista de inicialização. -
Intermediário (Dificuldade ⭐⭐): Defina um construtor de cópia personalizado para a classe
Bookque implemente cópia profunda (assuma que o título é armazenado comochar*, exigindo nova alocação de memória). -
Desafio (Dificuldade ⭐⭐⭐): Defina uma classe
DynamicArray(array dinâmico) com: -
int* data(apontando para memória heap) -
int size(tamanho do array)
Implemente: 3. Construtor regular (aloca memória) 4. Construtor de cópia (cópia profunda) 5. Destrutor (libera memória)
Teste: Crie DynamicArray obj1(10), depois use obj1 para inicializar obj2, e verifique que os data dos dois objetos apontam para endereços de memória diferentes.
- Construtor padrão: sem parâmetros ou todos com parâmetros padrão
- Listas de inicialização são mais eficientes que atribuição no corpo do construtor
- Construtores delegantes: um construtor chama outro
- explicit previne conversões implícitas de tipos
- Destrutor: chamado automaticamente quando um objeto é destruído, libera recursos
7. 🚀 Próximos Passos
Agora que você aprendeu o uso avançado de construtores, vamos avançar para Sobrecarga de Operadores (Aula 30) — fazendo tipos personalizados funcionarem com +, -, << e outros operadores!