Menu

Construtores em C++: inicialize objetos do jeito certo

Um construtor é a função-membro especial que roda quando um objeto é criado. Aprenda os construtores padrão, parametrizados e de cópia, as listas de inicialização de membros e como evitar deixar objetos pela metade.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

O que é um construtor

Na página anterior você definiu classes e deu a elas variáveis-membro. Mas os membros de um objeto recém-criado contêm o lixo que estava naquela memória, a menos que você os defina. Um construtor resolve isso: é uma função-membro especial que roda automaticamente no instante em que um objeto é criado, e seu único trabalho é deixar o objeto em um estado válido e totalmente inicializado.

Um construtor tem o mesmo nome da classe e nenhum tipo de retorno, nem mesmo void. Você nunca o chama diretamente; o compilador o chama por você sempre que um objeto passa a existir.

O Counter() sem parâmetros é chamado de construtor padrão: é o usado quando você cria um objeto sem passar nenhum argumento.

Construtores parametrizados

Um construtor que não recebe argumentos é válido, mas normalmente você quer criar um objeto com valores específicos. Um construtor parametrizado aceita argumentos e os usa para inicializar os membros.

Uma classe pode ter mais de um construtor, desde que suas listas de parâmetros sejam diferentes; isso é a sobrecarga de funções comum aplicada aos construtores. Aqui Point pode ser criado com ou sem coordenadas:

Uma armadilha comum: Point p(); não cria um objeto; o compilador lê isso como a declaração de uma função chamada p que retorna um Point. Para chamar o construtor padrão, escreva Point p; (sem parênteses) ou Point p{}; com chaves.

Listas de inicialização de membros

Até aqui os exemplos atribuíam aos membros dentro do corpo do construtor. Isso funciona para tipos simples, mas é a ferramenta errada. Quando o corpo roda, cada membro já foi construído por padrão; o corpo então joga isso fora e atribui por cima. Uma lista de inicialização de membros inicializa cada membro diretamente, antes do corpo, em uma única etapa.

A sintaxe é dois-pontos depois da lista de parâmetros, seguidos de pares member(value):

Para um membro string isso também evita construir uma string vazia e depois atribuir: a lista de inicialização a constrói corretamente na primeira tentativa.

A lista de inicialização não é só uma otimização; ela é obrigatória em três casos, porque o corpo só pode atribuir, não inicializar:

  • Membros const: você não pode atribuir a um const depois que ele existe.
  • Membros de referência: uma referência precisa ser vinculada no momento em que nasce.
  • Membros cujo tipo não tem construtor padrão.
class Sensor {
    const int id;        // membro const
    int& slot;           // membro de referência

public:
    Sensor(int sensorId, int& s) : id(sensorId), slot(s) {}
    // Tentar definir id ou slot no corpo não compilaria.
};

Uma sutileza importante: os membros são inicializados na ordem em que são declarados na classe, não na ordem em que você os lista na lista de inicialização. Se o inicializador de um membro lê outro, o que importa é a ordem de declaração; confundir as duas é uma fonte clássica de usar um valor ainda não inicializado.

Argumentos padrão e construtores delegantes

Você nem sempre precisa de sobrecargas separadas. Argumentos padrão permitem que um único construtor cubra vários casos: omita um argumento e o valor padrão entra no lugar.

Tome cuidado ao combinar um construtor parametrizado com valores padrão com um construtor padrão Point() separado: o compilador não consegue saber qual chamar para Point p; e vai reportar uma ambiguidade. Escolha uma abordagem.

Quando você tem vários construtores que compartilham uma configuração, um construtor delegante (C++11) permite que um construtor chame outro em vez de repetir a lógica. Você "delega" colocando o outro construtor na lista de inicialização:

O construtor de cópia

Quando você cria um objeto como cópia de outro (ao passá-lo por valor, retorná-lo ou escrever Foo b = a;), o construtor de cópia roda. Sua assinatura recebe uma referência const ao mesmo tipo:

ClassName(const ClassName& other);

Se você não escrever um, o compilador gera um construtor de cópia padrão que copia cada membro. Para classes que contêm apenas valores (ints, strings, vectors), isso é exatamente o correto e você não deve escrever o seu próprio.

A grande armadilha vive no próximo capítulo sobre memória: se sua classe possui um ponteiro bruto para memória do heap, o construtor de cópia padrão copia o ponteiro, não os dados, então dois objetos acabam apontando para a mesma memória, e os dois vão tentar liberá-la. Esse é o bug do double-free. A regra prática é a Regra de Três/Cinco: se você escreve um destrutor personalizado, é quase certo que também precisa de um construtor de cópia personalizado (e da atribuição por cópia). No C++ moderno a correção mais limpa é manter um std::vector ou um ponteiro inteligente, para que a cópia gerada pelo compilador simplesmente funcione.

Note também que receber o parâmetro por referência é obrigatório, não opcional: um construtor de cópia que recebesse seu argumento por valor precisaria copiar o argumento para se chamar, uma recursão infinita que nem sequer compilaria.

A seguir: destrutores

Um construtor monta um objeto; um destrutor o desmonta. Quando um objeto sai de escopo ou é deletado, seu destrutor roda automaticamente: o lugar perfeito para liberar arquivos, conexões de rede ou a memória do heap que o objeto guardava. A próxima página explica como os destrutores funcionam, quando exatamente eles disparam e como se combinam com os construtores para dar ao C++ seu poderoso padrão RAII.

Perguntas frequentes

O que é um construtor em C++?

Um construtor é uma função-membro especial que tem o mesmo nome da classe e nenhum tipo de retorno. Ele roda automaticamente quando um objeto é criado, e seu trabalho é colocar o objeto em um estado válido e totalmente inicializado antes que qualquer outro código o use.

Qual a diferença entre um construtor padrão e um parametrizado?

Um construtor padrão não recebe argumentos e é usado quando você cria um objeto sem fornecer valores (Point p;). Um construtor parametrizado recebe argumentos para que quem o chama possa inicializar o objeto com valores específicos (Point p(3, 4);). Uma classe pode ter os dois, porque os construtores são sobrecarregados pelas suas listas de parâmetros.

Por que eu deveria usar uma lista de inicialização de membros em C++?

Uma lista de inicialização de membros (: name(n), age(a)) inicializa os membros diretamente, antes de o corpo do construtor rodar. Ela é obrigatória para membros const, referências e membros sem construtor padrão, e evita o desperdício de construir-por-padrão-e-depois-atribuir que acontece quando você atribui dentro do corpo.

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR