C++ const com Ponteiros e Referências

Você já conhece o const de Constantes: ele trava uma variável para que ninguém a mude. Com um ponteiro, existem duas coisas que podem ser travadas: o valor lá na casa apontada e o endereço anotado no próprio ponteiro. Dependendo de onde você escreve o const, trava uma, a outra ou as duas. Parece um quebra-cabeça, mas existe uma regra simples para ler: da direita para a esquerda.

Você vai aprender
  • A diferença entre const int* p, int* const p e const int* const p.
  • Os erros reais do g++ ao tentar mudar o que está travado.
  • A regra "leia da direita para a esquerda".
  • Por que o endereço de uma variável const só cabe num const int*.
  • Por que const& é o jeito padrão de passar coisas grandes para funções.

Antes, leia Ponteiros e Referências. Os programas foram compilados na máquina fictícia da Ana (usuária ana, computador notebook), na pasta ~/cpp, com g++ -std=c++23 -Wall -Wextra arquivo.cpp -o programa. Nesta aula, a maior parte das saídas são erros de compilação, e é isso mesmo que queremos: o const serve para o compilador barrar mudanças que você não queria.


A ideia: dois cadeados diferentes

Volte ao ponteiro como um papel com um endereço anotado. Dá para colocar um cadeado em dois lugares:

Cadeado em…ComparaçãoEm C++
A casa (o valor)Uma vitrine: você vê o bolo, mas não pode mexer nele. Pode, porém, ir olhar outra vitrine.const int* p
O papel (o endereço)Um papel plastificado: o endereço anotado nunca muda, mas lá dentro da casa você mexe à vontade.int* const p
Os doisPapel plastificado apontando para uma vitrine: só olhar, sempre a mesma.const int* const p

O segredo é a posição do const em relação ao *: antes do *, ele trava o valor; depois do *, trava o ponteiro.


1. const int* p: ponteiro para valor constante

O const está antes do *, junto do int: o que é constante é o int lá na casa. Pelo ponteiro, você pode ler e pode trocar de alvo, mas não pode mudar o valor:

#include <iostream>

int main() {
    int ana = 7;
    int bruno = 3;
    const int* p = &ana;     // ponteiro para const int

    std::cout << "*p = " << *p << "\n";   // ler pode
    p = &bruno;                           // trocar o alvo pode
    std::cout << "*p = " << *p << "\n";
    *p = 100;                             // mudar o valor NÃO pode
}
$ g++ -std=c++23 -Wall -Wextra ptr-const.cpp -o ptr-const
ptr-const.cpp: In function ‘int main()’:
ptr-const.cpp:11:8: error: assignment of read-only location ‘* p’
   11 |     *p = 100;                             // mudar o valor NÃO pode
      |     ~~~^~~~~

Tradução: "atribuição numa posição somente-leitura *p". O g++ diz location (posição, lugar) porque o problema está no lugar para onde p aponta, o *p. As linhas 8, 9 e 10 (ler e trocar o alvo) passaram sem reclamação.

O const vale para o ponteiro, não para a variável

Assim como na referência para const, quem fica travado é o caminho pelo ponteiro. A variável original continua livre:

#include <iostream>

int main() {
    int saldo = 500;
    const int* so_ver = &saldo;

    saldo = 800;                 // o dono continua livre
    std::cout << "*so_ver = " << *so_ver << "\n";
}
$ g++ -std=c++23 -Wall -Wextra dono.cpp -o dono
$ ./dono
*so_ver = 800

Pense no extrato do aplicativo do banco: a Ana não pode mudar o saldo por ali, mas o saldo muda quando cai o salário, e o extrato mostra o valor novo.

int const* é a mesma coisa

Você vai encontrar int const* p em alguns códigos. É idêntico a const int* p: o que importa é o const estar antes do *. A ordem entre const e int não muda nada.


2. int* const p: ponteiro constante

Agora o const está depois do *, colado no nome: o que é constante é o próprio p, o papel. O endereço não muda nunca, mas o valor da casa pode mudar:

#include <iostream>

int main() {
    int ana = 7;
    int bruno = 3;
    int* const p = &ana;     // ponteiro const para int

    *p = 100;                             // mudar o valor pode
    std::cout << "ana = " << ana << "\n";
    p = &bruno;                           // trocar o alvo NÃO pode
}
$ g++ -std=c++23 -Wall -Wextra const-ptr.cpp -o const-ptr
const-ptr.cpp: In function ‘int main()’:
const-ptr.cpp:10:7: error: assignment of read-only variable ‘p’
   10 |     p = &bruno;                           // trocar o alvo NÃO pode
      |     ~~^~~~~~~~

Compare as mensagens: antes era read-only location ‘* p’ (a casa); agora é read-only variable ‘p’ (a variável p, o papel). A mensagem diz exatamente qual dos dois cadeados você tentou abrir.

Um int* const se comporta como uma referência: sempre o mesmo alvo, e o valor pode mudar. E, como toda constante, ele precisa nascer com valor:

int main() {
    int* const p;
}
$ g++ -std=c++23 -Wall -Wextra sem-init.cpp -o sem-init
sem-init.cpp: In function ‘int main()’:
sem-init.cpp:2:16: error: uninitialized ‘const p’ [-fpermissive]
    2 |     int* const p;
      |                ^
sem-init.cpp:2:16: warning: unused variable ‘p’ [-Wunused-variable]

"p é const e não foi inicializada": se o endereço nunca pode mudar, ele precisa ser dado na criação.


3. const int* const p: tudo travado

Com const nos dois lados do *, nada muda por p: nem o valor, nem o alvo. As duas tentativas dão erro:

#include <iostream>

int main() {
    int ana = 7;
    int bruno = 3;
    const int* const p = &ana;   // nada pode mudar por p

    std::cout << "*p = " << *p << "\n";
    *p = 100;
    p = &bruno;
}
$ g++ -std=c++23 -Wall -Wextra const-const.cpp -o const-const
const-const.cpp: In function ‘int main()’:
const-const.cpp:9:8: error: assignment of read-only location ‘*(const int*)p’
    9 |     *p = 100;
      |     ~~~^~~~~
const-const.cpp:10:7: error: assignment of read-only variable ‘p’
   10 |     p = &bruno;
      |     ~~^~~~~~~~

Dois erros, um para cada cadeado. Na primeira mensagem, o g++ escreve *(const int*)p, mostrando que olhou o p como "ponteiro para const int" e achou a casa travada.

As três formas lado a lado

DeclaraçãoLer *pMudar o valor (*p = 5)Mudar o alvo (p = &y)Erro ao tentar o proibido
int* pSimSimSim(nenhum)
const int* pSimNãoSimassignment of read-only location ‘* p’
int* const pSimSimNãoassignment of read-only variable ‘p’
const int* const pSimNãoNãoOs dois acima

E o que é permitido, funcionando:

#include <iostream>

int main() {
    int ana = 7;
    int bruno = 3;

    const int* p1 = &ana;        // valor travado, alvo livre
    p1 = &bruno;

    int* const p2 = &ana;        // alvo travado, valor livre
    *p2 = 100;

    std::cout << "*p1 = " << *p1 << ", ana = " << ana << "\n";
}
$ g++ -std=c++23 -Wall -Wextra ok.cpp -o ok
$ ./ok
*p1 = 3, ana = 100

4. A regra: leia da direita para a esquerda

Para não precisar decorar a tabela, leia a declaração de trás para a frente, começando pelo nome. Leia o * como "ponteiro para":

DeclaraçãoLida da direita para a esquerda
int* pp é um ponteiro para int.
const int* pp é um ponteiro para int constante. (O int é que é constante.)
int const* pp é um ponteiro para constante int. (A mesma coisa.)
int* const pp é constante: um ponteiro para int. (O p é que é constante.)
const int* const pp é constante: um ponteiro para int constante.
const int& rr é uma referência para int constante.

Repare que não existe int& const r: uma referência nunca troca de alvo, então ela já é "constante" por natureza. Só o const int& (valor travado) faz sentido.

Missão (provoque o erro): o ponteiro consulta só deixa olhar o estoque. Acrescente, antes do } final, a linha *consulta = 0; e clique em Rodar. Leia o erro: ele diz que a casa apontada é somente-leitura. Depois, tente trocar o const int* por int* const e veja qual linha passa a ser aceita.

#include <iostream>

int main() {
    int estoque = 12;
    const int* consulta = &estoque;

    std::cout << "Estoque: " << *consulta << "\n";
}
#include <iostream>

int main() {
    int estoque = 12;
    const int* consulta = &estoque;

    std::cout << "Estoque: " << *consulta << "\n";
    *consulta = 0;
}

No editor do guia (Clang), as mensagens são outras, mas a ideia é a mesma: read-only variable is not assignable ("variável somente-leitura não pode receber valor") para o *p = … e cannot assign to variable 'p' with const-qualified type 'int *const' para o p = ….


5. Variável const pede ponteiro para const

Se a própria variável é constante, um int* comum seria uma porta dos fundos para mudá-la. O compilador fecha essa porta:

#include <iostream>

int main() {
    const int limite = 10;
    int* p = &limite;
    *p = 99;
    std::cout << limite << "\n";
}
$ g++ -std=c++23 -Wall -Wextra limite.cpp -o limite
limite.cpp: In function ‘int main()’:
limite.cpp:5:14: error: invalid conversion from ‘const int*’ to ‘int*’ [-fpermissive]
    5 |     int* p = &limite;
      |              ^~~~~~~
      |              |
      |              const int*

"Conversão inválida de const int* para int*". O endereço de um const int é um const int*, e ele não pode virar um ponteiro que permite escrever. O erro aparece na linha 5, antes mesmo de alguém tentar mudar o valor. Conserto:

#include <iostream>

int main() {
    const int limite = 10;
    const int* p = &limite;
    std::cout << *p << "\n";
}
$ g++ -std=c++23 -Wall -Wextra limite-ok.cpp -o limite-ok
$ ./limite-ok
10

O caminho contrário é sempre permitido: um int* pode virar const int*, porque tirar uma permissão nunca é perigoso. Foi o que aconteceu com const int* p = &ana; nos exemplos acima.


6. const& em parâmetros

Este é o uso de const que você vai escrever todos os dias. Quando uma função recebe algo grande (uma std::string, um std::vector) só para ler, o melhor tipo de parâmetro é a referência para const:

#include <iostream>
#include <string>

void saudar(const std::string& nome) {    // não copia e não muda
    std::cout << "Olá, " << nome << "! (" << nome.size() << " letras)\n";
}

int main() {
    std::string cliente = "Ana Beatriz";
    saudar(cliente);           // uma variável
    saudar("Bruno");           // um texto direto: const& aceita
}
$ g++ -std=c++23 -Wall -Wextra param.cpp -o param
$ ./param
Olá, Ana Beatriz! (11 letras)
Olá, Bruno! (5 letras)

O const std::string& dá três garantias de uma vez:

  1. Não copia: é um apelido (o &). Para um texto de mil letras ou um vetor de um milhão de itens, isso economiza tempo e memória.
  2. Não muda: o const promete a quem chama que a variável dele volta intacta. Se a função tentar mudar, o compilador barra:
#include <iostream>

void dar_bonus(const int& nota) {
    nota = nota + 1;
}

int main() {
    int nota_ana = 8;
    dar_bonus(nota_ana);
    std::cout << nota_ana << "\n";
}
$ g++ -std=c++23 -Wall -Wextra param-muda.cpp -o param-muda
param-muda.cpp: In function ‘void dar_bonus(const int&)’:
param-muda.cpp:4:10: error: assignment of read-only reference ‘nota’
    4 |     nota = nota + 1;
      |     ~~~~~^~~~~~~~~~
  1. Aceita textos e valores diretos, como saudar("Bruno"). Sem o const, isso não compila:
#include <iostream>
#include <string>

void saudar(std::string& nome) {
    std::cout << "Olá, " << nome << "!\n";
}

int main() {
    saudar("Bruno");
}
$ g++ -std=c++23 -Wall -Wextra param-sem-const.cpp -o param-sem-const
param-sem-const.cpp: In function ‘int main()’:
param-sem-const.cpp:9:12: error: cannot bind non-const lvalue reference of type ‘std::string&’ {aka ‘std::__cxx11::basic_string<char>&’} to an rvalue of type ‘std::string’ {aka ‘std::__cxx11::basic_string<char>’}
    9 |     saudar("Bruno");
      |            ^~~~~~~
In file included from /usr/include/c++/15/string:56,
                 from /usr/include/c++/15/bits/locale_classes.h:42,
                 from /usr/include/c++/15/bits/ios_base.h:43,
                 from /usr/include/c++/15/ios:46,
                 from /usr/include/c++/15/bits/ostream.h:43,
                 from /usr/include/c++/15/ostream:42,
                 from /usr/include/c++/15/iostream:43,
                 from param-sem-const.cpp:1:
/usr/include/c++/15/bits/basic_string.h:706:7: note:   after user-defined conversion: ‘constexpr std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::basic_string(const _CharT*, const _Alloc&) [with <template-parameter-2-1> = std::allocator<char>; _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>]’
  706 |       basic_string(const _CharT* __s, const _Alloc& __a = _Alloc())
      |       ^~~~~~~~~~~~
param-sem-const.cpp:4:26: note: initializing argument 1 of ‘void saudar(std::string&)’
    4 | void saudar(std::string& nome) {
      |             ~~~~~~~~~~~~~^~~~

Assusta, mas só a primeira linha de erro importa. É a mesma mensagem que você viu em Referências com int& r = 10;: para chamar a função, o C++ precisou criar uma std::string temporária a partir de "Bruno" (um rvalue), e uma referência comum não pode se ligar a um temporário. O resto são notas: o {aka …} mostra o nome "verdadeiro" de std::string dentro da biblioteca, e o bloco In file included from … mostra onde está a conversão de texto para std::string que ele tentou usar. A última nota aponta o parâmetro culpado, na linha 4. Conserto: const std::string& nome.

Como escolher o tipo do parâmetro

A função vai…Tipo pequeno (int, double, char, bool)Tipo grande (std::string, std::vector, structs)
Só lerPor valor: int nconst std::string& s
Mudar a variável de quem chamouint& nstd::string& s
Ler algo que pode não existirconst int* p (e conferir nullptr)const std::string* p (e conferir nullptr)

Copiar um int custa o mesmo que copiar um endereço, então const int& não traz vantagem para tipos pequenos. A aula Por Valor × Por Referência discute essa escolha com mais exemplos.


Quando dá errado

"assignment of read-only location ‘* p’"
$ g++ -std=c++23 -Wall -Wextra ptr-const.cpp -o ptr-const
ptr-const.cpp: In function ‘int main()’:
ptr-const.cpp:11:8: error: assignment of read-only location ‘* p’
   11 |     *p = 100;                             // mudar o valor NÃO pode
      |     ~~~^~~~~

O que aconteceu: você tentou mudar o valor por um const int*.

Como resolver: se a mudança é intencional, mude pela própria variável ou use um ponteiro sem const antes do *. Se a função recebeu const int*, é porque prometeu não mudar: talvez a mudança deva acontecer em outro lugar.

"assignment of read-only variable ‘p’"
$ g++ -std=c++23 -Wall -Wextra const-ptr.cpp -o const-ptr
const-ptr.cpp: In function ‘int main()’:
const-ptr.cpp:10:7: error: assignment of read-only variable ‘p’
   10 |     p = &bruno;                           // trocar o alvo NÃO pode
      |     ~~^~~~~~~~

O que aconteceu: você tentou trocar o alvo de um ponteiro const (int* const).

Como resolver: se o ponteiro precisa mudar de alvo, tire o const de depois do *. Talvez você quisesse escrever *p = ….

"invalid conversion from ‘const int*’ to ‘int*’"
$ g++ -std=c++23 -Wall -Wextra limite.cpp -o limite
limite.cpp: In function ‘int main()’:
limite.cpp:5:14: error: invalid conversion from ‘const int*’ to ‘int*’ [-fpermissive]
    5 |     int* p = &limite;
      |              ^~~~~~~
      |              |
      |              const int*

O que aconteceu: um ponteiro comum tentou apontar para algo constante. O mesmo erro aparece ao passar um const int* para uma função que pede int*.

Como resolver: declare o ponteiro (ou o parâmetro da função) como const int*. Nunca "resolva" com conversões forçadas: mudar algo que nasceu const é comportamento indefinido.

"uninitialized ‘const p’"
$ g++ -std=c++23 -Wall -Wextra sem-init.cpp -o sem-init
sem-init.cpp: In function ‘int main()’:
sem-init.cpp:2:16: error: uninitialized ‘const p’ [-fpermissive]
    2 |     int* const p;
      |                ^
sem-init.cpp:2:16: warning: unused variable ‘p’ [-Wunused-variable]

O que aconteceu: um ponteiro const nasceu sem endereço e nunca poderia receber um.

Como resolver: dê o endereço na criação: int* const p = &x;.


Referência rápida

CódigoLeituraPode mudar
const int* p ou int const* pponteiro para int constanteo alvo (p = &y)
int* const p = &xponteiro constante para into valor (*p = 5)
const int* const p = &xponteiro constante para int constantenada
const int& r = xreferência para int constantenada (pelo r)
void f(const std::string& s)parâmetro: lê sem copiarnada; aceita f("texto")
void f(const int* v, std::size_t n)parâmetro: lê uma sequêncianada

Exercícios

1. Só olhar

Complete para que p possa trocar de alvo, mas não possa mudar o valor apontado:

int* p = &x;

2. Endereço fixo

Complete para que p aponte sempre para x, mas possa mudar o valor dele:

int* p = &x;

3. Com const int* p = &a;, qual linha compila?

4. O g++ mostrou error: assignment of read-only variable ‘p’. O que foi travado?

5. Qual é o melhor parâmetro para uma função que só mostra um std::vector<int> enorme?

Missão final: o total do carrinho, sem copiar

Missão: a função total recebe o carrinho por cópia, o que é desperdício, e ainda não soma nada. (1) Troque o parâmetro para uma referência para const (const std::vector<double>& precos). (2) Some os preços. A saída deve ser Total: 20.5.

#include <iostream>
#include <vector>

double total(std::vector<double> precos) {
    double soma = 0;
    // some os preços
    return soma;
}

int main() {
    std::vector<double> carrinho = {4.5, 3.0, 12.0, 1.0};
    std::cout << "Total: " << total(carrinho) << "\n";
}
#include <iostream>
#include <vector>

double total(const std::vector<double>& precos) {
    double soma = 0;
    for (double p : precos) {
        soma = soma + p;
    }
    return soma;
}

int main() {
    std::vector<double> carrinho = {4.5, 3.0, 12.0, 1.0};
    std::cout << "Total: " << total(carrinho) << "\n";
}

Resumo

  • Num ponteiro, dá para travar o valor (const antes do *) ou o endereço (const depois do *), ou os dois.
  • const int* p: lê e troca de alvo, não muda o valor (read-only location).
  • int* const p: muda o valor, não troca de alvo (read-only variable); precisa nascer com endereço.
  • Leia da direita para a esquerda: "p é um ponteiro para int constante".
  • O endereço de uma variável const só cabe num const int*.
  • Para ler coisas grandes numa função, use const&: não copia, não muda e aceita valores diretos.
  • Próxima aula: Memória Dinâmica (new e delete).