Ponteiros void* são uma ferramenta poderosa em C para escrever código genérico e reutilizável. Diferentemente de ponteiros tipados, um ponteiro void* pode apontar para qualquer tipo de dado sem necessidade de conversão explícita na atribuição. No entanto, para acessar ou manipular os dados apontados, é obrigatório realizar um casting para o tipo apropriado, pois o compilador não conhece o tamanho ou a estrutura do dado. Nesta aula, exploraremos a sintaxe, os usos comuns e os riscos associados a ponteiros void*.

O uso de void* é essencial em bibliotecas padrão como qsort, malloc e funções de callback. Dominar esse recurso permite criar funções que operam sobre diferentes tipos de dados, aumentando a flexibilidade do código. No entanto, a falta de verificação de tipos em tempo de compilação exige disciplina do programador para evitar erros de acesso à memória.

void*

Um ponteiro void* é declarado como void *ptr;. Ele pode receber o endereço de qualquer variável, independentemente do tipo, sem necessidade de casting explícito na atribuição. Por exemplo:

int a = 10;
float b = 3.14;
char c = 'A';

void *p;
p = &a;   // ok
p = &b;   // ok
p = &c;   // ok

No entanto, não é possível dereferenciar um ponteiro void* diretamente, pois o compilador não sabe quantos bytes ler. A expressão *p é inválida. É necessário primeiro converter o ponteiro para um tipo específico. Além disso, aritmética de ponteiros com void* não é padrão em C (embora alguns compiladores a tratem como aritmética de char*), e é considerada comportamento indefinido na maioria dos casos.

Casting

Para acessar os dados apontados por um void*, é preciso fazer um casting explícito para um tipo de ponteiro específico. O casting tem a forma (tipo_desejado*) expressão. Exemplo:

int a = 42;
void *p = &a;
int *pi = (int*) p;   // casting para int*
printf("%d\n", *pi); // imprime 42

O casting não altera o valor do ponteiro, apenas informa ao compilador como interpretar o dado apontado. É responsabilidade do programador garantir que o tipo para o qual se faz o casting corresponde ao tipo real do dado. Um casting incorreto pode levar a violações de alinhamento, interpretação errada dos bytes e comportamento indefinido.

Casos de uso

Um dos principais usos de void* é em funções que precisam operar sobre diferentes tipos de dados, como a função qsort da biblioteca padrão. Ela recebe um ponteiro para a base do array, o número de elementos, o tamanho de cada elemento e uma função de comparação que recebe dois ponteiros void*:

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

int main() {
    int arr[] = {5, 2, 9, 1, 5, 6};
    size_t n = sizeof(arr)/sizeof(arr[0]);
    qsort(arr, n, sizeof(int), compara_int);
    // arr agora está ordenado
    return 0;
}

Outro exemplo é a função malloc, que retorna um void* para o bloco de memória alocado. O programador deve fazer o casting para o tipo desejado:

int *p = (int*) malloc(10 * sizeof(int));

Ponteiros void* também são usados em estruturas de dados genéricas, como listas encadeadas que armazenam qualquer tipo de dado, e em callbacks de bibliotecas gráficas ou de eventos.

Cuidados

O uso de void* elimina a verificação de tipos em tempo de compilação, aumentando o risco de erros. Os principais cuidados incluem:

  • Casting correto: Sempre certifique-se de que o tipo no casting corresponde ao tipo real do dado. Erros podem causar corrupção de memória.
  • Alinhamento: Alguns tipos exigem alinhamento específico. Ao usar void*, o ponteiro pode não estar alinhado para o tipo desejado, resultando em falha de acesso (ex: em arquiteturas ARM).
  • Aritmética de ponteiros: Evite fazer aritmética com void* diretamente, pois o comportamento não é definido pelo padrão. Prefira converter para char* ou outro tipo antes de realizar incrementos.
  • Const e volatile: Ao fazer casting, preserve os qualificadores const e volatile para evitar descartar acidentalmente a constância.

Exemplo de erro comum:

void imprime(void *p) {
    printf("%d\n", *(int*)p); // assume que p aponta para int
}

int main() {
    float f = 3.14;
    imprime(&f); // comportamento indefinido: interpreta float como int
    return 0;
}

Boas práticas

Ao projetar funções genéricas com void*, documente claramente o tipo esperado para os dados. Prefira usar macros ou funções inline para evitar repetição de casting. Em código crítico, considere usar uniões ou genéricos (como em C11 com _Generic) para maior segurança.

Referências

Exercícios

  1. Escreva uma função void troca(void *a, void *b, size_t tamanho) que troque o conteúdo de duas variáveis de qualquer tipo, usando void*.
  2. ✓ Resposta:
    #include <string.h>
    
    void troca(void *a, void *b, size_t tamanho) {
        char temp[tamanho];
        memcpy(temp, a, tamanho);
        memcpy(a, b, tamanho);
        memcpy(b, temp, tamanho);
    }
    
  3. Crie uma função int maior(void *a, void *b, int (*cmp)(void*, void*)) que retorna 1 se a for maior que b, 0 caso contrário, usando uma função de comparação genérica.
  4. ✓ Resposta:
    int maior(void *a, void *b, int (*cmp)(void*, void*)) {
        return cmp(a, b) > 0 ? 1 : 0;
    }
    
    // Exemplo de uso:
    int cmp_int(void *a, void *b) {
        int ia = *(int*)a;
        int ib = *(int*)b;
        return ia - ib;
    }
    
  5. Explique por que não se pode fazer aritmética de ponteiros com void* no padrão C.
  6. ✓ Resposta:

    Porque o compilador não conhece o tamanho do tipo apontado. A aritmética de ponteiros depende do tamanho do tipo para calcular o deslocamento em bytes. Como void é um tipo incompleto, não há como determinar quantos bytes avançar. Alguns compiladores tratam como char*, mas isso não é portável.

  7. Escreva um programa que aloque dinamicamente um array de inteiros usando malloc (que retorna void*) e preencha com valores de 1 a 10.
  8. ✓ Resposta:
    #include <stdlib.h>
    
    int main() {
        int *arr = (int*) malloc(10 * sizeof(int));
        if (arr == NULL) return 1;
        for (int i = 0; i < 10; i++) {
            arr[i] = i + 1;
        }
        free(arr);
        return 0;
    }
    
  9. Dado o código abaixo, identifique o problema e corrija-o:
    void imprime(void *p) {
        printf("%f\n", *(float*)p);
    }
    int main() {
        double d = 3.1415;
        imprime(&d);
        return 0;
    }
    
  10. ✓ Resposta:

    O problema é que a função imprime espera um ponteiro para float, mas recebe um ponteiro para double. O tamanho e a representação são diferentes, causando interpretação incorreta. A correção é passar um float ou alterar a função para double. Exemplo corrigido:

    void imprime(void *p) {
        printf("%f\n", *(double*)p);
    }
    int main() {
        double d = 3.1415;
        imprime(&d);
        return 0;
    }