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.
- A diferença entre
const int* p,int* const peconst 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
constsó cabe numconst 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ção | Em 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 dois | Papel 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ção | Ler *p | Mudar o valor (*p = 5) | Mudar o alvo (p = &y) | Erro ao tentar o proibido |
|---|---|---|---|---|
int* p | Sim | Sim | Sim | (nenhum) |
const int* p | Sim | Não | Sim | assignment of read-only location ‘* p’ |
int* const p | Sim | Sim | Não | assignment of read-only variable ‘p’ |
const int* const p | Sim | Não | Não | Os 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ção | Lida da direita para a esquerda |
|---|---|
int* p | p é um ponteiro para int. |
const int* p | p é um ponteiro para int constante. (O int é que é constante.) |
int const* p | p é um ponteiro para constante int. (A mesma coisa.) |
int* const p | p é constante: um ponteiro para int. (O p é que é constante.) |
const int* const p | p é constante: um ponteiro para int constante. |
const int& r | r é 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:
- 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. - Não muda: o
constpromete 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;
| ~~~~~^~~~~~~~~~
- Aceita textos e valores diretos, como
saudar("Bruno"). Sem oconst, 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ó ler | Por valor: int n | const std::string& s |
| Mudar a variável de quem chamou | int& n | std::string& s |
| Ler algo que pode não existir | const 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
$ 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.
$ 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 = ….
$ 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.
$ 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ódigo | Leitura | Pode mudar |
|---|---|---|
const int* p ou int const* p | ponteiro para int constante | o alvo (p = &y) |
int* const p = &x | ponteiro constante para int | o valor (*p = 5) |
const int* const p = &x | ponteiro constante para int constante | nada |
const int& r = x | referência para int constante | nada (pelo r) |
void f(const std::string& s) | parâmetro: lê sem copiar | nada; aceita f("texto") |
void f(const int* v, std::size_t n) | parâmetro: lê uma sequência | nada |
Exercícios
1. Só olhar
Complete para que p possa trocar de alvo, mas não possa mudar o valor apontado:
2. Endereço fixo
Complete para que p aponte sempre para x, mas possa mudar o valor dele:
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 (
constantes do*) ou o endereço (constdepois 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 paraintconstante". - O endereço de uma variável
constsó cabe numconst 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).