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