C const e Ponteiros
Na aula Constantes você usou const para lacrar uma variável: const int dias = 7; não muda mais. Com ponteiros, o const pode lacrar duas coisas diferentes: o valor para onde o ponteiro aponta, ou o próprio ponteiro. Parece um detalhe, mas é uma das ferramentas mais úteis do C: é assim que uma função promete "eu só vou ler o seu array" e o compilador cobra a promessa. Nesta aula você vai aprender as três combinações, uma regra para ler qualquer declaração e os erros que o const pega antes de o programa rodar.
- A diferença entre
const int *p,int *const peconst int *const p. - A regra para ler essas declarações: da direita para a esquerda.
- Como escrever funções que só leem um array ou uma string (
const int *v,const char *s). - Por que o compilador reclama quando um ponteiro "perde" o
const. - Por que literais de texto pedem
const char *, o que a opção-Wwrite-stringsfaz e o que o C23 mudou nostrchr.
Os programas foram compilados na máquina da Ana, na pasta ~/c, com gcc -std=c23 -Wall -Wextra arquivo.c -o programa.
A ideia: o visitante, o zelador e o segurança
No museu da cidade há três tipos de gente andando pelas salas:
| Quem | Pode olhar e mudar o quadro? | Pode ir para outra sala? | Em C |
|---|---|---|---|
| O visitante | Só olhar: tocar no quadro é proibido | Sim, anda à vontade | const int *p |
| O zelador de uma sala | Sim: limpa, troca a moldura | Não: ele é daquela sala | int *const p |
| O segurança de um quadro | Só olhar | Não: fica parado ali | const int *const p |
O ponteiro é a pessoa; a variável apontada é o quadro. O const antes do * proíbe tocar no quadro; o const depois do * proíbe mudar de sala.
1. const int *p: ponteiro para valor constante
É o mais comum dos três. Através de p você pode ler o valor, mas não escrever:
#include <stdio.h>
int main(void) {
int nota = 7;
const int *p = ¬a;
printf("*p = %d\n", *p);
*p = 10;
printf("*p = %d\n", *p);
return 0;
}
- const int *p
- "
pé um ponteiro para umintconstante". Oconstestá grudado noint: é o valor apontado que fica lacrado, quando visto porp. - printf("*p = %d\n", *p);
- Ler pelo ponteiro: permitido.
- *p = 10;
- Escrever pelo ponteiro: proibido.
$ gcc -std=c23 -Wall -Wextra leitura.c -o leitura
leitura.c: In function ‘main’:
leitura.c:8:8: error: assignment of read-only location ‘*p’
8 | *p = 10;
| ^
Assignment of read-only location ‘*p’ = "atribuição a um local só de leitura, *p". O erro aparece na compilação: o programa nem chega a ser gerado. No editor, o Clang diz read-only variable is not assignable ("variável só de leitura não aceita atribuição").
O const vale para o ponteiro, não para a variável
Repare que nota não é const. O lacre é só do "visitante" p: quem olha por ele não pode mexer, mas a própria variável continua mudando normalmente. E o ponteiro pode andar para outro lugar:
#include <stdio.h>
int main(void) {
int notas[3] = {7, 9, 6};
const int *p = notas;
printf("%d\n", *p);
p++;
printf("%d\n", *p);
notas[1] = 10;
printf("%d\n", *p);
return 0;
}
$ gcc -std=c23 -Wall -Wextra anda.c -o anda $ ./anda 7 9 « o p++ funcionou: o ponteiro andou 10 « a nota mudou pelo nome do array, e o p vê a mudança
- p++
- Mudar o endereço guardado em
pé permitido: o visitante foi para a sala ao lado. - notas[1] = 10;
- O array não é
const: por ele, a escrita é livre. Op, que aponta paranotas[1], mostra o valor novo.
Por isso const int *p se lê melhor como "ponteiro que só lê" do que "ponteiro para algo que nunca muda".
Missão: provoque o erro. A função mostrar recebe as notas como const int *. Rode e veja que funciona. Depois, dentro da função, antes do for, tente zerar a primeira nota com notas[0] = 0; e rode de novo: o compilador tem de recusar.
#include <stdio.h>
void mostrar(const int *notas, int n) {
for (int i = 0; i < n; i++) {
printf("%d ", notas[i]);
}
printf("\n");
}
int main(void) {
int notas[4] = {7, 9, 6, 8};
mostrar(notas, 4);
return 0;
}
#include <stdio.h>
void mostrar(const int *notas, int n) {
notas[0] = 0;
for (int i = 0; i < n; i++) {
printf("%d ", notas[i]);
}
printf("\n");
}
int main(void) {
int notas[4] = {7, 9, 6, 8};
mostrar(notas, 4);
return 0;
}
2. int *const p: ponteiro constante
Agora o const vem depois do *. O lacre é no próprio ponteiro: ele aponta para uma variável só, para sempre, mas pode mudar o valor dela à vontade. É o zelador da sala:
#include <stdio.h>
int main(void) {
int pontos = 0;
int vidas = 3;
int *const placar = &pontos;
*placar = 100;
printf("pontos = %d\n", pontos);
placar = &vidas;
return 0;
}
$ gcc -std=c23 -Wall -Wextra fixo.c -o fixo
fixo.c: In function ‘main’:
fixo.c:10:12: error: assignment of read-only variable ‘placar’
10 | placar = &vidas;
| ^
- int *const placar = &pontos;
- "
placaré um ponteiro constante paraint". Como ele não pode mudar depois, tem de receber o endereço na declaração. - *placar = 100;
- Mudar o valor apontado: permitido (a linha 8 não deu erro).
- placar = &vidas;
- Fazer o ponteiro apontar para outra variável: proibido. Assignment of read-only variable ‘placar’ = "atribuição à variável só de leitura
placar". Repare na diferença: aqui é variable (o ponteiro); na seção 1 era location ‘*p’ (o lugar apontado).
No editor, o Clang é bem explícito: cannot assign to variable 'placar' with const-qualified type 'int *const' ("não dá para atribuir à variável placar, de tipo int *const").
Esta forma aparece bem menos que a primeira. Ela serve para deixar claro que um ponteiro nunca vai trocar de alvo, por exemplo um ponteiro que sempre aponta para o placar do jogo.
3. const int *const p: tudo lacrado
Com dois const, um de cada lado do *, o ponteiro não muda de alvo e não muda o valor. Só lê, sempre o mesmo lugar: o segurança parado na frente do quadro.
#include <stdio.h>
int main(void) {
int nota = 7;
int outra = 9;
const int *const p = ¬a;
printf("%d\n", *p);
*p = 8;
p = &outra;
return 0;
}
$ gcc -std=c23 -Wall -Wextra tudo.c -o tudo
tudo.c: In function ‘main’:
tudo.c:9:8: error: assignment of read-only location ‘*(const int *)p’
9 | *p = 8;
| ^
tudo.c:10:7: error: assignment of read-only variable ‘p’
10 | p = &outra;
| ^
Dois erros, um para cada lacre: read-only location (o valor) e read-only variable (o ponteiro). A leitura da linha 8 continua permitida.
As três formas lado a lado
| Declaração | Ler *p | Mudar *p | Mudar p (p++, p = &y) | No museu |
|---|---|---|---|---|
int *p | sim | sim | sim | funcionário com a chave de tudo |
const int *p | sim | não | sim | visitante |
int *const p | sim | sim | não | zelador de uma sala |
const int *const p | sim | não | não | segurança de um quadro |
4. A regra: leia da direita para a esquerda
Para entender qualquer declaração, comece pelo nome e leia para a esquerda, trocando * por "ponteiro para":
| Declaração | Lida da direita para a esquerda |
|---|---|
int *p | p é um ponteiro para int |
int const *p | p é um ponteiro para const int |
int *const p | p é um ponteiro constante para int |
int const *const p | p é um ponteiro constante para const int |
O único caso que foge da leitura é o const no começo: const int *p e int const *p são a mesma coisa. O C deixa o const ficar antes ou depois do tipo; o que importa é se ele está à esquerda ou à direita do *.
#include <stdio.h>
int main(void) {
int x = 1;
int const *a = &x;
const int *b = &x;
printf("%d %d\n", *a, *b);
return 0;
}
$ gcc -std=c23 -Wall -Wextra direita.c -o direita $ ./direita 1 1
a e b são do mesmo tipo. Quase todo código em C escreve const int *, com o const na frente, e este guia também. Se ficar em dúvida, procure o *: const antes do * lacra o valor; depois do *, lacra o ponteiro.
5. Parâmetros const: a função promete só ler
É aqui que o const com ponteiros brilha. Quando uma função recebe um array, ela recebe um ponteiro para os itens originais (Ponteiros e Arrays) e poderia mudá-los. Um const no parâmetro é uma promessa escrita no cabeçalho: "esta função só lê". O carrinho da lojinha da Ana:
#include <stdio.h>
double total(const double *precos, int n) {
double soma = 0.0;
for (int i = 0; i < n; i++) {
soma += precos[i];
}
return soma;
}
void desconto(double *precos, int n, double porcento) {
for (int i = 0; i < n; i++) {
precos[i] = precos[i] * (1 - porcento / 100);
}
}
int main(void) {
double carrinho[3] = {12.50, 30.00, 7.50};
printf("Total: R$ %.2f\n", total(carrinho, 3));
desconto(carrinho, 3, 10);
printf("Com 10%% de desconto: R$ %.2f\n", total(carrinho, 3));
return 0;
}
- double total(const double *precos, int n)
- Só soma: o
constavisa a quem chama que os preços voltam intactos. - void desconto(double *precos, int n, double porcento)
- Muda os preços de propósito: sem
const. Só de olhar os dois cabeçalhos, você já sabe qual função mexe no carrinho. - total(carrinho, 3)
- O
carrinhonão éconst, mas pode ser passado para um parâmetroconst: ganhar um lacre é sempre permitido (o funcionário pode visitar o museu como visitante).
$ gcc -std=c23 -Wall -Wextra carrinho.c -o carrinho $ ./carrinho Total: R$ 50.00 Com 10% de desconto: R$ 45.00
E se alguém, um dia, escrever dentro de total uma linha que muda um preço (um engano, ou uma "melhoria" apressada)? O compilador recusa:
$ gcc -std=c23 -Wall -Wextra constdentro.c -o constdentro
constdentro.c: In function ‘total’:
constdentro.c:5:19: error: assignment of read-only location ‘*(precos + (sizetype)((long unsigned int)i * 8))’
5 | precos[i] = 0;
| ^
A mensagem parece assustadora, mas é a mesma da seção 1: read-only location. O trecho entre aspas é o jeito do gcc escrever precos[i] por dentro (a conta do endereço: precos mais i vezes 8 bytes, o tamanho de um double). Leia só o começo e a linha marcada.
Experimente: rode o carrinho. Depois acrescente, dentro do for de total, a linha precos[i] = 0; e veja o compilador recusar. Por último, tire o const do cabeçalho de total: o programa compila, e o segundo total sai R$ 0.00, porque a função "só de leitura" zerou o carrinho.
#include <stdio.h>
double total(const double *precos, int n) {
double soma = 0.0;
for (int i = 0; i < n; i++) {
soma += precos[i];
}
return soma;
}
void desconto(double *precos, int n, double porcento) {
for (int i = 0; i < n; i++) {
precos[i] = precos[i] * (1 - porcento / 100);
}
}
int main(void) {
double carrinho[3] = {12.50, 30.00, 7.50};
printf("Total: R$ %.2f\n", total(carrinho, 3));
desconto(carrinho, 3, 10);
printf("Com 10%% de desconto: R$ %.2f\n", total(carrinho, 3));
return 0;
}
Todo parâmetro ponteiro que a função não usa para escrever deve ser const: const int *v, const double *precos, const char *texto. A própria biblioteca do C faz assim: o cabeçalho do strlen é size_t strlen(const char *s);, e o do strcpy é char *strcpy(char *destino, const char *origem); (o destino muda; a origem, não).
6. Tirar o const: o compilador reclama
Ganhar um lacre é livre; perder um lacre, não. Passar um array const para uma função que pode escrever nele quebraria a promessa:
#include <stdio.h>
void desconto(double *precos, int n, double porcento) {
for (int i = 0; i < n; i++) {
precos[i] = precos[i] * (1 - porcento / 100);
}
}
int main(void) {
const double tabela[3] = {12.50, 30.00, 7.50};
desconto(tabela, 3, 10);
printf("%.2f\n", tabela[0]);
return 0;
}
$ gcc -std=c23 -Wall -Wextra passarconst.c -o passarconst
passarconst.c: In function ‘main’:
passarconst.c:11:14: warning: passing argument 1 of ‘desconto’ discards ‘const’ qualifier from pointer target type [-Wdiscarded-qualifiers]
11 | desconto(tabela, 3, 10);
| ^~~~~~
passarconst.c:3:23: note: expected ‘double *’ but argument is of type ‘const double *’
3 | void desconto(double *precos, int n, double porcento) {
| ~~~~~~~~^~~~~~
$ ./passarconst
11.25 « a tabela "constante" mudou
- discards ‘const’ qualifier from pointer target type
- "Descarta o qualificador
constdo tipo apontado". Qualificador é o nome técnico de palavras comoconst, que "qualificam" um tipo. - note: expected ‘double *’ but argument is of type ‘const double *’
- A função espera um ponteiro que pode escrever, e recebeu um que só pode ler.
- 11.25
- É só um aviso, então o programa foi gerado, e a tabela que era para ser constante foi alterada (12,50 menos 10%). Mudar algo declarado
consté comportamento indefinido: aqui "funcionou", mas poderia dar outro resultado ou derrubar o programa.
No editor, o aviso do Clang é passing 'const double[3]' to parameter of type 'double *' discards qualifiers. Trate esse aviso como erro: ou a função deveria ter const no parâmetro, ou você está mesmo tentando mudar algo que prometeu não mudar.
O resultado depende do compilador
O mesmo acontece ao guardar o endereço de uma variável const num ponteiro comum:
#include <stdio.h>
int main(void) {
const int limite = 10;
int *p = &limite;
*p = 20;
printf("limite = %d\n", limite);
printf("*p = %d\n", *p);
return 0;
}
$ gcc -std=c23 -Wall -Wextra descarta.c -o descarta
descarta.c: In function ‘main’:
descarta.c:5:14: warning: initialization discards ‘const’ qualifier from pointer target type [-Wdiscarded-qualifiers]
5 | int *p = &limite;
| ^
$ ./descarta
limite = 20
*p = 20
$ gcc -std=c23 -Wall -Wextra -O2 descarta.c -o descarta 2>/dev/null « o mesmo, com otimização
$ ./descarta
limite = 10 « o gcc confiou no const e usou o 10 direto
*p = 20
O mesmo programa mostrou duas respostas diferentes. Com -O2 (otimização, explicada em Compilar), o gcc confiou na promessa do const e escreveu o 10 direto no printf, sem ler a variável. No editor, também sai limite = 10. Esse é o preço do comportamento indefinido: o resultado muda conforme o compilador e as opções. Um const só protege se ninguém o descartar.
7. Strings: const char *
Você viu em Ponteiros e Strings que char *aviso = "a prova foi adiada"; aponta para um literal que fica numa área só de leitura, e que passar esse ponteiro para uma função que escreve derruba o programa, sem aviso do compilador. Com const, o aviso aparece:
#include <stdio.h>
#include <ctype.h>
void gritar(char *s) {
for (char *p = s; *p != '\0'; p++) {
*p = toupper(*p);
}
}
int main(void) {
const char *aviso = "a prova foi adiada";
gritar(aviso);
printf("%s\n", aviso);
return 0;
}
$ gcc -std=c23 -Wall -Wextra literal.c -o literal
literal.c: In function ‘main’:
literal.c:12:12: warning: passing argument 1 of ‘gritar’ discards ‘const’ qualifier from pointer target type [-Wdiscarded-qualifiers]
12 | gritar(aviso);
| ^~~~~
literal.c:4:19: note: expected ‘char *’ but argument is of type ‘const char *’
4 | void gritar(char *s) {
| ~~~~~~^
$ ./literal
Falha de segmentação (imagem do núcleo gravada) ./literal
O programa ainda morre (era só um aviso), mas agora o compilador apontou a linha exata do problema antes de você rodar. Sem o const, a mesma falha passava calada. No editor, o programa não cai (mostra A PROVA FOI ADIADA), mas o aviso do Clang aparece igual: passing 'const char *' to parameter of type 'char *' discards qualifiers.
O conserto depende da intenção: se o texto deve mudar, use um array (char aviso[] = "…";); se a função só lê, ela deve receber const char *s.
A opção -Wwrite-strings
Por uma razão histórica, o C deixa escrever char *nome = "Ana"; sem const e sem aviso, mesmo com -Wall -Wextra. A opção -Wwrite-strings faz o gcc tratar os literais como const char, e aí a falta do const aparece:
$ cat wstrings.c
#include <stdio.h>
int main(void) {
char *nome = "Ana";
printf("%s\n", nome);
return 0;
}
$ gcc -std=c23 -Wall -Wextra wstrings.c -o wstrings « nenhum aviso
$ gcc -std=c23 -Wall -Wextra -Wwrite-strings wstrings.c -o wstrings
wstrings.c: In function ‘main’:
wstrings.c:4:18: warning: initialization discards ‘const’ qualifier from pointer target type [-Wdiscarded-qualifiers]
4 | char *nome = "Ana";
| ^~~~~
É uma opção que vale a pena ligar nos seus projetos. O conserto é escrever const char *nome = "Ana";.
O strchr do C23 guarda o const
Até o C17, o strchr (que procura uma letra, Funções de String) recebia um const char * e devolvia um char * comum: um jeito escondido de perder o const. No C23, ele passou a devolver um ponteiro com o mesmo const do que recebeu. A glibc do laboratório (versão 2.43) já faz isso:
$ cat strchr23.c
#include <stdio.h>
#include <string.h>
int main(void) {
const char *email = "ana@escola.br";
char *arroba = strchr(email, '@');
printf("%s\n", arroba + 1);
return 0;
}
$ gcc -std=c23 -Wall -Wextra strchr23.c -o strchr23
strchr23.c: In function ‘main’:
strchr23.c:6:20: warning: initialization discards ‘const’ qualifier from pointer target type [-Wdiscarded-qualifiers]
6 | char *arroba = strchr(email, '@');
| ^~~~~~
$ ./strchr23
escola.br
$ gcc -std=c17 -Wall -Wextra strchr23.c -o strchr23 « no C17, nenhum aviso
O conserto é const char *arroba = strchr(email, '@');. O mesmo vale para strrchr, strstr, strpbrk e memchr. O editor do guia ainda não tem essa novidade: lá o strchr23.c compila sem aviso, mesmo com -std=c23.
Qual usar
| Situação | Declaração |
|---|---|
| Função que só lê um array | int soma(const int *v, int n) |
| Função que só lê uma string | int contar(const char *s, char c) |
| Função que muda o array ou a string | void zerar(int *v, int n), void gritar(char *s) |
| Ponteiro para um literal de texto | const char *msg = "Olá"; |
| Lista de textos fixos | const char *dias[] = {…}; |
| Ponteiro que nunca deve trocar de alvo | int *const p = &x; (raro) |
Parâmetro simples por valor (int n) | Sem const: a função já recebe uma cópia. (const int n é permitido, mas não protege nada de quem chama.) |
Quando dá errado
error: assignment of read-only location ‘*p’
O que significa: você escreveu no valor apontado por um ponteiro para const (const int *p ou um parâmetro const). No editor: read-only variable is not assignable.
Como consertar: se a função não deveria mudar o valor, o erro achou um engano: tire a linha. Se ela deve mesmo mudar, tire o const do parâmetro, e quem chama passa a saber que os dados podem mudar.
error: assignment of read-only variable ‘p’
O que significa: o ponteiro é constante (int *const p) e você tentou fazê-lo apontar para outro lugar, com p = … ou p++. (Também aparece com uma variável comum const, como em Constantes.)
Como consertar: se o ponteiro precisa andar, o const está do lado errado: você provavelmente queria const int *p.
warning: … discards ‘const’ qualifier from pointer target type
O que significa: um ponteiro para const foi passado (ou guardado) como ponteiro comum, que pode escrever. É só aviso, mas qualquer escrita por ele é comportamento indefinido: pode "funcionar", mudar de resultado com -O2 ou derrubar o programa (literais). No editor: discards qualifiers [-Wincompatible-pointer-types-discards-qualifiers].
Como consertar: ponha const no parâmetro ou na variável que recebe. Se a função precisa escrever, passe uma cópia que seja sua (um array comum).
(int *) &limite
O que significa: um cast (Conversão de Tipos) faz o aviso sumir, mas não muda nada na memória: escrever num objeto const continua sendo comportamento indefinido.
Como consertar: não use cast para tirar const. Se aparecer a vontade, o desenho das funções está pedindo outro const em algum parâmetro.
Referência rápida
| Código | O que quer dizer |
|---|---|
const int *p (= int const *p) | Ponteiro para int constante: lê *p, não escreve; p pode andar |
int *const p = &x; | Ponteiro constante: escreve *p; p não muda de alvo |
const int *const p = &x; | Nada muda: nem *p, nem p |
| Regra | const à esquerda do * lacra o valor; à direita, o ponteiro |
f(const int *v, int n) | A função promete só ler o array |
const char *s | Texto só para ler (literais, parâmetros de leitura) |
int * → const int * | Sempre permitido (ganhar lacre) |
const int * → int * | Aviso discards ‘const’ qualifier (perder lacre) |
-Wwrite-strings | Opção do gcc: literais viram const char e char *p = "…" passa a avisar |
strchr no C23 | Recebe const char *, devolve const char * |
Exercícios
1. Só olhar
Complete o parâmetro de uma função que só lê as notas:
2. Endereço fixo
Complete para criar um ponteiro que sempre aponta para pontos, mas pode mudar o valor dele:
3. O literal
Complete a declaração certa de um ponteiro para um literal de texto:
4. Com const int *p = &x;, qual linha não compila?
5. Uma variável int nota (sem const) pode ser passada para um parâmetro const int *?
6. int const *p e const int *p são…
Missão final: o total do cardápio, sem mexer nos preços
Missão: a tabela de preços da lanchonete é const: ninguém pode mudá-la. A função total ainda está incompleta e o compilador avisa que ela "descarta o const". Faça as duas coisas:
- escreva o corpo de
total, somando osnpreços; - prometa no cabeçalho que a função só lê os preços, para o aviso de
constsumir.
A saída tem de ser esta, sem nenhum aviso:
Total do pedido: R$ 54.90
#include <stdio.h>
double total(double *precos, int n) {
double soma = 0.0;
// some os n preços
return soma;
}
int main(void) {
const double cardapio[4] = {12.50, 30.00, 7.50, 4.90};
printf("Total do pedido: R$ %.2f\n", total(cardapio, 4));
return 0;
}
#include <stdio.h>
double total(const double *precos, int n) {
double soma = 0.0;
for (int i = 0; i < n; i++) {
soma += precos[i];
}
return soma;
}
int main(void) {
const double cardapio[4] = {12.50, 30.00, 7.50, 4.90};
printf("Total do pedido: R$ %.2f\n", total(cardapio, 4));
return 0;
}
Com o const no parâmetro, o cabeçalho conta toda a história: total recebe preços e devolve um número, sem mudar nada. Se, um dia, alguém escrever precos[i] = 0; dentro dela, o compilador recusa com read-only location.
Resumo
constà esquerda do*(const int *p): o valor não muda porp, masppode andar. É o mais usado.constà direita do*(int *const p):pnão troca de alvo, mas o valor pode mudar. Com os dois (const int *const p), nada muda.- Leia da direita para a esquerda: "
pé um ponteiro (constante?) paraint(constante?)".const int=int const. - Parâmetro ponteiro que só lê leva
const:const int *v,const char *s. A biblioteca do C faz assim (strlen,strcpy). - Ganhar
consté livre; perder dá o avisodiscards ‘const’ qualifier. Escrever num objetoconsté comportamento indefinido. - Literais de texto:
const char *. A opção-Wwrite-stringsajuda a pegar os esquecidos. No C23,strchre companhia devolvemconst char *quando recebem um. - Próxima aula: Ponteiro para Ponteiro.