Ponteiros void e genéricos
Esta aula aborda os ponteiros void* em C, que permitem criar código genérico ao apontar para qualquer tipo de dado. São explicados os conceitos de casting, casos de uso como funções de comparação genéricas e os cuidados necessários para evitar erros de tipo e acessos inválidos.
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
- cppreference: Ponteiros em C
- GNU C Library: Void Pointers
- Wikipedia: Pointer - C pointers
- TutorialsPoint: Void Pointer in C
- Stack Overflow: Concept of void pointer in C
Exercícios
- 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*. - 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. - Explique por que não se pode fazer aritmética de ponteiros com void* no padrão C.
- Escreva um programa que aloque dinamicamente um array de inteiros usando malloc (que retorna void*) e preencha com valores de 1 a 10.
- 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; }
#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);
}
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;
}
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.
#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;
}
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;
}