No desenvolvimento em C++, é comum confundir a atribuição de valores dentro do corpo de um construtor com a inicialização real dos membros de uma classe. Embora o resultado final pareça idêntico, o processo subjacente é fundamentalmente diferante.
Diferença entre Inicialização e Atribuição
Considere o exemplo abaixo, onde os valores são definidos dentro do corpo do construtor:
class Cronometro {
public:
Cronometro(int h, int m, int s) {
_hora = h; // Atribuição, não inicialização
_minuto = m;
_segundo = s;
}
private:
int _hora;
int _minuto;
int _segundo;
};
Neste caso, os membros são primeiro criados com valores padrão (ou lixo de memória) e depois recebem um novo valor via operador de atribuição. A verdaderia inicialização ocorre antes da execução da primeira linha de código dentro das chaves {}.
Sintaxe da Lista de Inicialização
A lista de inicialização começa com um caractere de dois pontos (:), seguido por uma lista de membros separados por vírgula, onde cada membro é seguido pelo seu valor inicial entre parênteses ou chaves.
class Cronometro {
public:
Cronometro(int h, int m, int s)
: _hora(h)
, _minuto(m)
, _segundo(s)
{} // Corpo vazio
private:
int _hora;
int _minuto;
int _segundo;
};
Regras e Casos Obrigatórios
Existem situações onde o uso da lista de inicialização não é opcional, mas obrigatório:
- Membros Constantes (const): Como constantes não podem ser alteradas após a criação, elas devem ser inicializadas na lista.
- Referências (&): Referências devem ser vinculadas a um objeto no momento da criação.
- Objetos sem Construtor Padrão: Se um membro de classe for um objeto de outra classe que não possui um construtor sem argumentos, você deve especificar como construí-lo na lista.
Ordem de Execução
Um detalhe crítico em C++ é que a ordem de inicialização dos membros segue estritamente a ordem de declaração na classe, e não a ordem em que aparecem na lista de inicialização do construtor.
class Exemplo {
public:
Exemplo(int val) : _b(val), _a(_b) {}
// Erro potencial: _a será inicializado antes de _b porque aparece primeiro na declaração.
private:
int _a;
int _b;
};
Delegação de Construtores
A partir do C++11, é possível que um construtor chame outro construtor da mesma classe utilizando a lista de inicialização. Tentar fazer isso dentro do corpo do construtor resulta na criação de um objeto temporário anônimo que não afeta o objeto atual.
class Localizacao {
public:
// Forma correta: Delegação via lista de inicialização
Localizacao() : Localizacao(0, 0) {}
Localizacao(double lat, double lon)
: _latitude(lat)
, _longitude(lon)
{}
void Exibir() {
std::cout << _latitude << ", " << _longitude << std::endl;
}
private:
double _latitude;
double _longitude;
};
Se tentássemos chamar Localizacao(0, 0) dentro do corpo de Localizacao(), o compilador criaria um novo objeto temporário e o destruiria imediatamente, deixando os membros do objeto original com valores indefinidos.
Separação entre Declaração e Definição
Quando a implementação do construtor é separada do arquivo de cabeçalho (Header), os parâmetros padrão devem residir na declaração, enquanto a lista de inicialização deve obrigatoriamente estar na definição (arquivo .cpp).
// No arquivo .h
class Sensor {
public:
Sensor(int id = 0);
private:
int _id;
};
// No arquivo .cpp
Sensor::Sensor(int id) : _id(id) {
// Implementação
}
Para tipos de dados complexos ou classes definidas pelo usuário, o uso da lista de inicialização evita chamadas desnecessárias ao construtor padrão seguido pelo operador de atribuição, otimizando o desempenho do software.