Dominando Listas de Inicialização em C++

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.

Tags: C++ OOP Constructor MemoryManagement

Publicado em 7-29 14:24