Ponteiros Avançados

void*, ponteiros para funções, const correctness e ponteiros duplos.

Depois de dominar ponteiros básicos, você encontra casos que exigem técnicas mais sofisticadas: funções genéricas, callbacks, APIs que modificam seus ponteiros e garantias de imutabilidade.

Void Pointers (void*)

Um void* é um ponteiro genérico — pode apontar para qualquer tipo de dado.

int x = 42;
float f = 3.14;
char c = 'A';

void *ptr;

ptr = &x;  // OK
ptr = &f;  // OK
ptr = &c;  // OK

Limitações

O compilador não sabe o tamanho do tipo apontado, então duas operações são proibidas:

void *ptr = &x;

*ptr = 10;   // ERRO: dereferência impossível
ptr++;       // ERRO: aritmética impossível

Para usar o valor, você precisa fazer cast:

void *ptr = &x;
int valor = *(int*)ptr;  // Cast para int* e dereferencia

Caso de Uso: Funções Genéricas

A biblioteca padrão usa void* para funções que operam em qualquer tipo:

// qsort: ordena qualquer tipo de array
void qsort(void *base, size_t nmemb, size_t size,
           int (*compar)(const void *, const void *));

Função de comparação para ordenar inteiros:

int comparar_ints(const void *a, const void *b) {
    int ia = *(const int*)a;
    int ib = *(const int*)b;
    return ia - ib;
}

int arr[] = {5, 2, 8, 1, 9};
qsort(arr, 5, sizeof(int), comparar_ints);

O void* permite que a mesma função qsort ordene ints, floats, structs — qualquer coisa.

Ponteiros para Funções

Funções têm endereços na memória. Você pode armazenar esse endereço e chamar a função através dele.

Sintaxe

// Declaração: ponteiro para função que recebe (int, int) e retorna int
int (*operacao)(int, int);

Parênteses em (*operacao) são essenciais. Sem eles:

  • int *operacao(int, int) — função que retorna ponteiro para int

Exemplo Básico

int soma(int a, int b) { return a + b; }
int multiplica(int a, int b) { return a * b; }

int main() {
    int (*op)(int, int);

    op = soma;
    printf("%d\n", op(3, 4));  // 7

    op = multiplica;
    printf("%d\n", op(3, 4));  // 12

    return 0;
}

Caso de Uso: Callbacks

Callbacks permitem que uma função “chame de volta” código fornecido pelo usuário:

void processar_lista(int *arr, int n, void (*callback)(int)) {
    for (int i = 0; i < n; i++) {
        callback(arr[i]);
    }
}

void imprimir(int x) {
    printf("%d ", x);
}

void dobrar_e_imprimir(int x) {
    printf("%d ", x * 2);
}

int main() {
    int arr[] = {1, 2, 3, 4, 5};

    processar_lista(arr, 5, imprimir);        // 1 2 3 4 5
    printf("\n");
    processar_lista(arr, 5, dobrar_e_imprimir);  // 2 4 6 8 10

    return 0;
}

Typedef para Legibilidade

typedef int (*Comparador)(const void*, const void*);

void ordenar(void *dados, size_t n, size_t tamanho, Comparador cmp);

Muito mais legível que repetir a sintaxe de ponteiro para função.

Const Correctness

A palavra-chave const em ponteiros pode aplicar-se ao dado apontado, ao próprio ponteiro, ou ambos.

Leia da Direita para Esquerda

DeclaraçãoLeituraSignificado
int *ptr”ptr é ponteiro para int”Tudo mutável
const int *ptr”ptr é ponteiro para int constante”Dado imutável, ponteiro mutável
int * const ptr”ptr é ponteiro constante para int”Dado mutável, ponteiro imutável
const int * const ptr”ptr é ponteiro constante para int constante”Tudo imutável

Exemplos

int x = 10, y = 20;

// Ponteiro para const: não pode modificar o dado
const int *p1 = &x;
*p1 = 30;    // ERRO: dado é const
p1 = &y;     // OK: ponteiro pode mudar

// Const ponteiro: não pode mudar para onde aponta
int * const p2 = &x;
*p2 = 30;    // OK: dado é mutável
p2 = &y;     // ERRO: ponteiro é const

// Ambos const
const int * const p3 = &x;
*p3 = 30;    // ERRO
p3 = &y;     // ERRO

Por que Isso Importa

  1. Documentação: const comunica intenção
  2. Segurança: O compilador impede modificações acidentais
  3. Otimização: O compilador pode fazer otimizações sabendo que dados não mudam

Funções devem usar const quando não modificam seus parâmetros:

// Indica que a função não modifica a string
size_t strlen(const char *s);

// Indica que src não é modificado
void *memcpy(void *dest, const void *src, size_t n);

Ponteiros Duplos (**)

Um ponteiro duplo armazena o endereço de um ponteiro.

int x = 10;
int *ptr = &x;
int **pptr = &ptr;

printf("%d\n", x);       // 10
printf("%d\n", *ptr);    // 10
printf("%d\n", **pptr);  // 10

Caso de Uso 1: Modificar um Ponteiro em Função

Se uma função precisa fazer um ponteiro apontar para outro lugar:

void alocar(int **ptr) {
    *ptr = malloc(sizeof(int));
    **ptr = 42;
}

int main() {
    int *p = NULL;
    alocar(&p);  // Passa endereço do ponteiro
    printf("%d\n", *p);  // 42
    free(p);
    return 0;
}

Sem o ponteiro duplo, a função só modificaria uma cópia local.

Caso de Uso 2: Arrays Bidimensionais Dinâmicos

int **criar_matriz(int linhas, int colunas) {
    int **matriz = malloc(linhas * sizeof(int*));

    for (int i = 0; i < linhas; i++) {
        matriz[i] = malloc(colunas * sizeof(int));
    }

    return matriz;
}

void liberar_matriz(int **matriz, int linhas) {
    for (int i = 0; i < linhas; i++) {
        free(matriz[i]);
    }
    free(matriz);
}

Caso de Uso 3: Array de Strings

argv em main é um array de strings, ou seja, array de ponteiros para char:

int main(int argc, char **argv) {
    // argv[0] é o nome do programa
    // argv[1] é o primeiro argumento
    for (int i = 0; i < argc; i++) {
        printf("arg[%d] = %s\n", i, argv[i]);
    }
    return 0;
}

char **argv é equivalente a char *argv[].

Resumo Visual

Ponteiro Simples:
    int *ptr ──────► [ valor int ]

Ponteiro Duplo:
    int **pptr ──────► int *ptr ──────► [ valor int ]

Ponteiro para Função:
    int (*fn)(int) ──────► [ código da função ]

void*:
    void *vp ──────► [ qualquer coisa ] (tamanho desconhecido)

Referências:

  • Kernighan, B. W., & Ritchie, D. M. (1988). The C Programming Language, Chapter 5
  • Van der Linden, P. (1994). Expert C Programming: Deep C Secrets
Progresso do Tópico