Em C, o qualificador const pode ser aplicado a ponteiros de maneiras diferentes, controlando se o valor apontado pode ser modificado ou se o próprio ponteiro pode ser alterado. Entender essas variações é essencial para escrever código robusto, evitar efeitos colaterais indesejados e comunicar intenções claramente. Nesta aula, veremos três combinações principais: ponteiro para const, const ponteiro, e a combinação de ambos.

Esses conceitos são amplamente utilizados em APIs de bibliotecas, onde parâmetros de função são declarados como ponteiros para const para garantir que a função não modifique os dados passados. Além disso, o uso de const ajuda o compilador a otimizar o código e previne erros comuns de programação.

Ponteiro para const

Um ponteiro para const é um ponteiro que aponta para um valor constante. Isso significa que o valor apontado não pode ser modificado através do ponteiro, mas o ponteiro em si pode ser alterado para apontar para outro local.

A sintaxe é: const tipo *ptr; ou tipo const *ptr; (ambas equivalentes). Por exemplo:

int valor = 10;
const int *ptr = &valor;  // ponteiro para const int
// *ptr = 20;  // ERRO: não é permitido modificar o valor apontado
ptr = &outraVariavel; // OK: o ponteiro pode ser alterado

Note que a variável valor em si não é constante; apenas o acesso via ptr é restrito. Isso é útil para funções que desejam ler dados sem modificá-los.

const ponteiro

Um const ponteiro é um ponteiro que não pode ser alterado após a inicialização, ou seja, ele sempre apontará para o mesmo endereço. No entanto, o valor apontado pode ser modificado (a menos que também seja const).

A sintaxe é: tipo * const ptr;. Exemplo:

int valor = 10;
int * const ptr = &valor;  // ponteiro constante
*ptr = 20; // OK: modifica o valor apontado
// ptr = &outro; // ERRO: não é possível alterar o ponteiro

Esse tipo é frequentemente usado em hardwares embarcados para apontar para registradores de endereço fixo, ou em funções que não devem realocar o buffer.

Combinações

Podemos combinar as duas formas: const tipo * const ptr; — um ponteiro constante para um valor constante. Nesse caso, nem o ponteiro nem o valor apontado podem ser modificados.

Exemplo:

int valor = 10;
const int * const ptr = &valor;
// *ptr = 20; // ERRO
// ptr = &outro; // ERRO

Há também a possibilidade de usar const em ponteiros para ponteiros, como int ** const, int * const *, const int **, etc. A regra geral: o const se aplica ao que está imediatamente à sua esquerda, exceto se for o primeiro token, quando se aplica ao que está à direita. Por exemplo:

  • const int *p → ponteiro para int const
  • int const *p → idem
  • int * const p → ponteiro const para int
  • const int * const p → ponteiro const para int const

Boas práticas

  • Use const sempre que possível: Isso torna o código mais seguro e auto-documentado. Funções que não modificam parâmetros devem declarar esses parâmetros como ponteiros para const.
  • Prefira a ordem const tipo para evitar ambiguidade: Embora tipo const seja equivalente, a forma const tipo é mais comum e legível.
  • Cuidado com conversões: Você pode passar um ponteiro não-const para um parâmetro const (promoção segura), mas o contrário requer um cast explícito e é perigoso.
  • Em APIs, use const para indicar que a função não altera os dados: Isso permite que o compilador otimize e que o usuário confie na função.
  • Evite remover const com casts: A menos que absolutamente necessário, pois pode levar a comportamentos indefinidos se o objeto original for realmente constante.

Referências

Exercícios

  1. Declare um ponteiro para const char que aponte para uma string literal "Hello". Tente modificar o caractere apontado e veja o erro do compilador. Escreva o código.

    ✓ Resposta:
    const char *str = "Hello";
    // str[0] = 'h'; // ERRO: assignment of read-only location '*str'
  2. Declare um ponteiro constante para um inteiro, inicializado com o endereço de uma variável. Modifique o valor da variável através do ponteiro e tente alterar o ponteiro. Escreva o código.

    ✓ Resposta:
    int x = 5;
    int * const ptr = &x;
    *ptr = 10; // OK
    // ptr = &y; // ERRO: assignment of read-only variable 'ptr'
  3. Declare um ponteiro constante para um valor constante do tipo double. Inicialize com o endereço de uma variável double. Tente modificar o valor e o ponteiro. Escreva o código.

    ✓ Resposta:
    double valor = 3.14;
    const double * const ptr = &valor;
    // *ptr = 2.71; // ERRO
    // ptr = &outro; // ERRO
  4. Escreva uma função que recebe um ponteiro para const int e imprime o valor. Em seguida, chame-a com um int não-const. Explique por que isso é permitido.

    ✓ Resposta:
    void print(const int *p) {
        printf("%d\n", *p);
    }
    
    int main() {
        int a = 10;
        print(&a); // OK: promove int* para const int*
        return 0;
    }

    É permitido porque a função promete não modificar o valor, e passar um ponteiro não-const é seguro (não quebra a promessa).

  5. Identifique o tipo de cada declaração: const int *p1, int * const p2, const int * const p3. Explique o que pode e o que não pode ser modificado.

    ✓ Resposta:
    • const int *p1: ponteiro para const int → pode modificar o ponteiro, não pode modificar o valor apontado.
    • int * const p2: ponteiro const para int → pode modificar o valor apontado, não pode modificar o ponteiro.
    • const int * const p3: ponteiro const para const int → não pode modificar nem o ponteiro nem o valor apontado.