Por que constantes existem
Uma constante é um valor que você promete nunca alterar depois de defini-lo. Marcar algo como const faz duas coisas ao mesmo tempo: documenta sua intenção para qualquer um que leia o código e permite que o compilador imponha essa intenção — qualquer linha que tente modificar o valor vira um erro de compilação em vez de um bug silencioso em tempo de execução.
Enquanto a palavra-chave auto deixa o compilador inferir o tipo de uma variável, const restringe o que você pode fazer com essa variável. As duas se combinam livremente: const auto limit = 100; é um int somente leitura.
Declarando um valor const
Coloque const antes do tipo. Uma variável const deve ser inicializada na mesma linha, porque não existe um momento posterior em que você tenha permissão para atribuir um valor a ela.
Descomente a atribuição e o programa não vai compilar — o compilador reporta "assignment of read-only variable". É exatamente esse o propósito: o erro é detectado antes de o programa chegar a rodar.
Um hábito comum de iniciante herdado do C é #define MAX_USERS 100. Evite. Uma macro é uma substituição de texto cega, sem tipo e sem respeitar o escopo, então não pode ser inspecionada em um depurador e produz mensagens de erro confusas. Uma variável const (ou constexpr) tem verificação de tipo e escopo como qualquer outra.
const vs constexpr
Ambas as palavras-chave dão a você um valor que não pode mudar, mas elas respondem a perguntas diferentes. const diz "isto nunca muda depois de definido". constexpr diz algo mais forte: "isto pode ser calculado em tempo de compilação" — e tudo que é constexpr é automaticamente const também.
A regra prática: recorra a constexpr sempre que o valor for um literal fixo ou um cálculo que o compilador consegue fazer (tamanhos de arrays, comprimentos de buffers, rótulos de switch, argumentos de templates). Use const simples quando o valor for decidido em tempo de execução mas não deva mudar depois — como uma cópia const de um argumento de função.
Desde o C++20 existe também o consteval, usado em funções que devem rodar em tempo de compilação:
consteval int square(int x) { return x * x; }
constexpr int area = square(8); // calculado durante a compilação
Uma função constexpr pode rodar em tempo de compilação; uma função consteval sempre tem que rodar, caso contrário é um erro.
Ponteiros e const: leia da direita para a esquerda
É aqui que const confunde as pessoas, porque a palavra-chave pode ficar de qualquer lado do * e os dois significados são opostos. O truque é ler a declaração da direita para a esquerda.
Leia int* const p2 da direita para a esquerda: "p2 é um ponteiro const para int". Leia const int* p1 como "p1 é um ponteiro para const int". Erre nisso e você vai perder um tempo real confuso com um erro que diz que você não pode modificar algo que achava ser mutável.
Uma pegadinha prática: nunca pegue o endereço de um const e remova o const com um cast para modificar o objeto subjacente. Fazer isso é comportamento indefinido se o objeto original for de fato const, e o compilador tem liberdade para assumir que o valor nunca muda — sua "escrita" pode simplesmente ser ignorada.
Referências const como parâmetros de função
O uso cotidiano mais comum de const é passar objetos grandes por referência sem copiá-los. Um parâmetro const& evita a cópia e promete que a função não vai modificar o argumento de quem a chamou.
Passar por const& é a escolha padrão para qualquer parâmetro maior que alguns poucos bytes (strings, vetores, suas próprias classes). Observe que isso também permite que a função aceite um temporário como "Grace" — uma referência não const simples não pode se ligar a um temporário, então remover o const aqui rejeitaria essa segunda chamada.
Funções-membro const
Quando você escreve uma classe, marque qualquer método que não modifique o objeto com um const ao final. É isso que torna o método chamável em instâncias const e em parâmetros const& — sem isso, você não consegue ler seu próprio objeto através de um handle const.
A disciplina de marcar métodos somente leitura como const é chamada de const correctness. Acerte nisso cedo — um método que deveria ter sido const é fácil de adicionar, mas adaptar const em uma base de código grande depois é doloroso, porque cada chamador através de uma referência const depende disso.
Próximo: operadores
Agora que seus valores podem ser travados com const, o próximo passo é fazer coisas com eles. A página de operadores cobre os operadores aritméticos, de comparação, lógicos e de atribuição — incluindo as pegadinhas em torno da divisão inteira, da precedência de operadores e de como const interage com os operadores de atribuição que você não tem permissão para usar.
Perguntas frequentes
Qual é a diferença entre const e constexpr em C++?
const significa que o valor não pode mudar após a inicialização, mas ele pode ser calculado em tempo de execução. constexpr é mais forte: garante que o valor pode ser calculado em tempo de compilação, de modo que pode ser usado onde uma constante de tempo de compilação é exigida (tamanhos de arrays, argumentos de templates, rótulos de switch). Todo objeto constexpr também é const, mas nem todo objeto const é constexpr.
Como declaro uma constante em C++?
Coloque const antes do tipo e atribua um valor: const int maxUsers = 100;. Uma variável const deve ser inicializada na declaração, porque você nunca poderá atribuir um valor a ela depois. Para constantes de tempo de compilação, prefira constexpr int maxUsers = 100;. Evite a antiga macro #define no estilo C: ela não tem tipo e ignora o escopo.
O que significa um ponteiro const em C++?
Depende de onde o const fica. const int* p é um ponteiro para const: você pode reapontar p, mas não pode alterar *p. int* const p é um ponteiro const: você pode alterar *p, mas não pode reapontar p. Leia a declaração da direita para a esquerda: int* const é "ponteiro const para int".