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.

CPP
#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

CPP
#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).

TEXT 📖 Somente leitura
// 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):

CPP
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):

TEXT 📖 Somente leitura
Student(const std::string& n, int a) : name(n), age(a) {
 // O corpo da função pode ficar vazio
}

💡 Vantagens:

  1. Mais eficiente (inicialização direta, não construção padrão seguida de atribuição)
  2. 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

(2) 2.2 Casos em que a Lista de Inicialização é Obrigatória

Caso 1: Membros const

CPP
#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:

  1. Inicializar um objeto a partir de outro
  2. Passar um objeto por valor para uma função (copia)
  3. Retornar um objeto de uma função (pode copiar)

▶ Exemplo 1: Construtor de Cópia Padrão (Dificuldade ⭐)

CPP
#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;
}
▶ Experimente

Saída:

TEXT 📖 Somente leitura
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

CPP
#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.

TEXT 📖 Somente leitura
#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

CPP
#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.

CPP
#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 ⭐⭐)

CPP
#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;
}
▶ Experimente

Saída:

TEXT 📖 Somente leitura
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 ⭐)

CPP
#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;
}
▶ Experimente

Saída:

TEXT 📖 Somente leitura
Zhang San, 20 anos
Li Si, 18 anos
Desconhecido, 18 anos
💡 Dica: Construtores delegantes permitem que um construtor chame outro, evitando duplicação de código.



📖 Resumo

📝 Exercícios

  1. Básico (Dificuldade ⭐): Defina uma classe Book com dois membros: título e autor. Escreva o construtor usando uma lista de inicialização.

  2. Intermediário (Dificuldade ⭐⭐): Defina um construtor de cópia personalizado para a classe Book que implemente cópia profunda (assuma que o título é armazenado como char*, exigindo nova alocação de memória).

  3. Desafio (Dificuldade ⭐⭐⭐): Defina uma classe DynamicArray (array dinâmico) com:

  4. int* data (apontando para memória heap)

  5. 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.


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!

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%