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.

Você vai aprender
  • A diferença entre const int *p, int *const p e const 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-strings faz e o que o C23 mudou no strchr.

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:

QuemPode olhar e mudar o quadro?Pode ir para outra sala?Em C
O visitanteSó olhar: tocar no quadro é proibidoSim, anda à vontadeconst int *p
O zelador de uma salaSim: limpa, troca a molduraNão: ele é daquela salaint *const p
O segurança de um quadroSó olharNão: fica parado aliconst 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 = &nota;

    printf("*p = %d\n", *p);
    *p = 10;
    printf("*p = %d\n", *p);
    return 0;
}
const int *p
"p é um ponteiro para um int constante". O const está grudado no int: é o valor apontado que fica lacrado, quando visto por p.
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. O p, que aponta para notas[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 para int". 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 = &nota;

    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çãoLer *pMudar *pMudar p (p++, p = &y)No museu
int *psimsimsimfuncionário com a chave de tudo
const int *psimnãosimvisitante
int *const psimsimnãozelador de uma sala
const int *const psimnãonãoseguranç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çãoLida da direita para a esquerda
int *pp é um ponteiro para int
int const *pp é um ponteiro para const int
int *const pp é um ponteiro constante para int
int const *const pp é 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 const avisa 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 carrinho não é const, mas pode ser passado para um parâmetro const: 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;
}
A regra das funções

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 const do tipo apontado". Qualificador é o nome técnico de palavras como const, 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çãoDeclaração
Função que só lê um arrayint soma(const int *v, int n)
Função que só lê uma stringint contar(const char *s, char c)
Função que muda o array ou a stringvoid zerar(int *v, int n), void gritar(char *s)
Ponteiro para um literal de textoconst char *msg = "Olá";
Lista de textos fixosconst char *dias[] = {…};
Ponteiro que nunca deve trocar de alvoint *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).

Forçar com um cast: (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ódigoO 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
Regraconst à esquerda do * lacra o valor; à direita, o ponteiro
f(const int *v, int n)A função promete só ler o array
const char *sTexto 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-stringsOpção do gcc: literais viram const char e char *p = "…" passa a avisar
strchr no C23Recebe const char *, devolve const char *

Exercícios

1. Só olhar

Complete o parâmetro de uma função que só lê as notas:

double media( *notas, int n)

2. Endereço fixo

Complete para criar um ponteiro que sempre aponta para pontos, mas pode mudar o valor dele:

int * placar = &pontos;

3. O literal

Complete a declaração certa de um ponteiro para um literal de texto:

*msg = "Bom dia";

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 os n preços;
  • prometa no cabeçalho que a função só lê os preços, para o aviso de const sumir.

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 por p, mas p pode andar. É o mais usado.
  • const à direita do * (int *const p): p nã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?) para int (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 aviso discards ‘const’ qualifier. Escrever num objeto const é comportamento indefinido.
  • Literais de texto: const char *. A opção -Wwrite-strings ajuda a pegar os esquecidos. No C23, strchr e companhia devolvem const char * quando recebem um.
  • Próxima aula: Ponteiro para Ponteiro.