Como inicializar variáveis em C++

Inicialização de Variáveis em C++

Em C++, inicializar uma variável significa fornecer a ela um valor inicial no momento de sua criação (declaração). Isso é diferente de atribuição, que substitui o valor de um objeto já existente.

Formas de Inicialização

FormaNomeO que acontece
int a;Inicialização padrão (default-initialization)Para variáveis locais de tipos fundamentais (int, double, ponteiros), não define um valor inicial (contém valor indeterminado). Ler a antes de atribuir algo causa comportamento indefinido (undefined behavior). (Nota: variáveis globais ou static são inicializadas com zero).
int b = 5;Inicialização por cópia (copy-initialization)Inicializa b com 5. Apesar do símbolo =, não é uma atribuição posterior, mas parte da declaração.
int c(6);Inicialização direta (direct-initialization)Inicializa c com 6 usando parênteses. Em classes, invoca diretamente o construtor correspondente.
int d{7};Inicialização direta por lista (direct-list-initialization)Inicializa d com 7 (introduzida no C++11 como inicialização uniforme). Impede conversões de estreitamento (narrowing conversions).
int d = {7};Inicialização por cópia de lista (copy-list-initialization)Semelhante a int d{7}; (também proíbe narrowing), mas não considera construtores marcados como explicit.
int e{};Inicialização por valor (value-initialization)Para tipos fundamentais como int, realiza zero-initialization (inicializa com 0). Para classes, chama o construtor padrão.

Comparação e Quando Utilizar Cada Forma

Embora várias sintaxes pareçam fazer a mesma coisa para tipos primitivos, cada uma tem um propósito e um contexto ideal no C++ moderno:

1. Inicialização por Lista / Uniforme (int d{7}; e int e{};) — A escolha padrão

  • Comparação: É a forma mais moderna (introduzida no C++11), segura e consistente. Funciona para qualquer tipo (tipos primitivos, agregados, structs, classes e contêineres) e proíbe conversões com perda de dados (narrowing conversions).
  • Quando usar:
    • Quase sempre como padrão em código C++ moderno.
    • Use Tipo var{valor}; quando já souber o valor inicial e quiser garantia contra truncamento acidental de dados.
    • Use Tipo var{}; quando quiser inicializar a variável com um valor padrão seguro (0, nullptr, false ou construtor padrão), evitando lixo de memória e a ambiguidade com declaração de funções (Most Vexing Parse).

2. Inicialização por Cópia (int b = 5;) — Tradicional e legível

  • Comparação: Herdada do C, é a forma mais familiar. Porém, é menos restritiva (permite narrowing silencioso em tipos primitivos) e não funciona com construtores marcados como explicit.
  • Quando usar:
    • Em tipos escalares simples (int, bool, double) quando a equipe ou base de código prefere a legibilidade tradicional do =.
    • Ao usar dedução de tipo com auto (ex.: auto x = 5; ou AAA style — Almost Always Auto), onde o tipo já é determinado diretamente pela expressão à direita.

3. Inicialização Direta com Parênteses (int c(6);) — Casos específicos de construtores

  • Comparação: Chama construtores diretamente (inclusive explicit), mas permite narrowing conversions e sofre com o Most Vexing Parse se usada sem argumentos (Tipo c();).
  • Quando usar:
    • Quando uma classe possui sobrecarga para std::initializer_list e você quer chamar outro construtor específico que não seja o da lista. O exemplo clássico é std::vector<int> v(5, 10); (cria 5 elementos com valor 10), em vez de std::vector<int> v{5, 10}; (cria 2 elementos: 5 e 10).
    • Dentro de funções de encaminhamento ou factories genéricas em código legado antes do C++20.

4. Inicialização por Cópia de Lista (int d = {7};) — Agregados e retorno de funções

  • Comparação: Combina o visual do = com a segurança das chaves {} contra narrowing, mas não permite chamar construtores explicit.
  • Quando usar:
    • Ao inicializar structs agregadas ou arrays de forma clara: Ponto p = {10, 20};.
    • Quando quiser deixar explícito para o leitor (e para o compilador) que nenhuma conversão explicit deve ocorrer.

5. Inicialização Padrão (int a;) — Evitar, exceto por performance ou preenchimento imediato

  • Comparação: É a única que deixa tipos primitivos locais com lixo de memória (valor indeterminado). Para classes com construtor padrão (como std::string s;), ela chama o construtor normalmente, mas para tipos fundamentais é uma fonte comum de bugs.
  • Quando usar:
    • Evite por padrão.
    • Use apenas quando a variável será preenchida imediatamente na linha seguinte por uma função de entrada ou leitura (ex.: int x; std::cin >> x;).
    • Em cenários críticos de alta performance (ex.: grandes buffers ou arrays numéricos) onde o custo de zerar a memória antes de sobrescrevê-la logo em seguida é comprovadamente desnecessário.

Resumo Rápido de Decisão

SituaçãoForma RecomendadaExemplo
Valor inicial conhecido (regra geral)Lista direta {valor}int idade{25};
Precisa começar zerado / vazioValor {}int contador{}; / int* ptr{};
Construtor conflita com initializer_listDireta (args)std::vector<int> v(100, 0);
Uso com autoCópia = ou {}auto total = calculaTotal();
Preenchida por std::cin ou out-paramValor {} (ou padrão se imediato)int entrada{}; std::cin >> entrada;

Principais Diferenças e Armadilhas

Para tipos simples como int, as formas b, c e d resultam no mesmo valor final. Porém, as diferenças tornam-se críticas em três cenários:

1. Conversões de estreitamento (Narrowing Conversions)

A inicialização com chaves {} impede conversões implícitas que possam perder dados em tempo de compilação:

int x1 = 3.5;  // Compila (com aviso): trunca o valor para 3
int x2(3.5);   // Compila (com aviso): trunca o valor para 3
int x3{3.5};   // ERRO de compilação: narrowing conversion de double para int

2. Construtores explicit

Em tipos definidos pelo usuário (class / struct), a inicialização por cópia (=) não aceita construtores marcados como explicit, enquanto a inicialização direta (() ou {}) os aceita:

struct Caixa {
    explicit Caixa(int tamanho) {}
};

Caixa c1(10);      // OK: inicialização direta
Caixa c2{10};      // OK: inicialização direta por lista
// Caixa c3 = 10;  // ERRO: construtor é explicit
// Caixa c4 = {10};// ERRO: copy-list-initialization não aceita explicit

3. Most Vexing Parse (O problema dos parênteses vazios)

Tentar inicializar um objeto com parênteses vazios é interpretado pelo compilador como a declaração de uma função, e não como uma variável:

int f();   // Declara uma função chamada 'f' que retorna int (NÃO cria uma variável!)
int g{};   // Cria uma variável 'g' inicializada com 0

4. Cuidado com std::initializer_list

Ao usar chaves {} com tipos da biblioteca padrão que possuem um construtor recebendo std::initializer_list (como std::vector), o compilador dará preferência a esse construtor:

#include <vector>

std::vector<int> v1(5, 10); // Cria um vetor com 5 elementos, todos com valor 10
std::vector<int> v2{5, 10}; // Cria um vetor com 2 elementos: {5, 10}

Boas Práticas em C++ Moderno

Regra geral (C++ Core Guidelines — ES.23): Prefira a sintaxe com chaves {} (list-initialization).

  • Quando tiver um valor inicial: use int x{valor}; — evita conversões acidentais com perda de precisão.
  • Quando quiser um valor padrão/zero: use int x{}; — garante que a variável comece zerada sem risco de lixo de memória ou Most Vexing Parse.
  • Quando usar auto: prefira auto x = valor; ou auto x{valor}; (com um único elemento a partir do C++17, auto x{5}; deduz int, mas auto x = {5}; deduz std::initializer_list<int>).

Referência

https://www.learncpp.com/cpp-tutorial/variable-assignment-and-initialization/ https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#es20-always-initialize-an-object