Ponteiros void e genéricos
Esta aula aborda ponteiros void (void*) em C, que permitem criar funções e estruturas genéricas. Serão explicados o conceito de void*, casting, principais casos de uso (como funções de comparação genérica) e cuidados importantes para evitar erros de tipo.
Ponteiros void (void*) são um recurso poderoso da linguagem C que permite escrever código genérico, ou seja, que funciona com diferentes tipos de dados. Diferentemente de ponteiros tipados (como int*, char*), um ponteiro void não tem um tipo associado, servindo como um 'coringa' que pode apontar para qualquer tipo de dado. Isso é fundamental para implementar funções como qsort, malloc e estruturas de dados genéricas.
No entanto, essa flexibilidade vem com responsabilidades: o programador deve gerenciar manualmente o casting e garantir que o tipo real seja compatível, pois o compilador não verifica tipos com void*. Nesta aula, exploraremos o uso correto de void*, casting, exemplos práticos e os cuidados necessários para evitar erros.
void*
O tipo void* é um ponteiro genérico, capaz de armazenar o endereço de qualquer variável, independentemente do tipo. Ele é definido na linguagem C como um ponteiro para 'void', ou seja, um ponteiro sem tipo. Isso significa que você pode atribuir a ele o endereço de uma variável int, double, struct, etc., sem necessidade de casting explícito (embora em C++ seja necessário).
Por exemplo:
int a = 10;
char c = 'x';
void *ptr;
ptr = &a; // ok
ptr = &c; // ok
Note que não é possível dereferenciar diretamente um void*, pois o compilador não sabe quantos bytes ler. Para acessar o valor, é necessário fazer um casting para o tipo apropriado.
Casting
Casting é a operação de converter explicitamente um ponteiro void para um ponteiro de um tipo específico, ou vice-versa. A sintaxe é: (tipo_desejado*) ptr_void. Por exemplo:
void *ptr;
int a = 42;
ptr = &a;
int *p_int = (int*) ptr;
printf("%d\n", *p_int); // imprime 42
O casting é necessário para dereferenciar ou realizar aritmética de ponteiros com void*. Em C, o casting de void* para outro tipo de ponteiro é implícito (não precisa de cast explícito), mas muitos programadores preferem usar o cast para deixar a intenção clara. Em C++, o cast explícito é obrigatório. Além disso, ao usar funções como malloc, que retorna void*, o cast é opcional em C, mas comum.
Exemplo com malloc:
int *arr = (int*) malloc(10 * sizeof(int));
Casos de uso
Os principais casos de uso de void* incluem:
- Funções genéricas: como qsort, que recebe um ponteiro para um array e uma função de comparação que opera sobre void*.
- Estruturas de dados genéricas: como listas encadeadas, pilhas e filas que armazenam dados de qualquer tipo.
- Callbacks genéricos: funções que recebem ponteiros para funções com parâmetros void*.
- Manipulação de memória: funções como memcpy e memset que trabalham com bytes.
Exemplo prático: uma função que imprime qualquer valor, recebendo um void* e um tipo:
#include <stdio.h>
void print_value(void *ptr, char type) {
switch (type) {
case 'i':
printf("%d\n", *((int*)ptr));
break;
case 'f':
printf("%f\n", *((float*)ptr));
break;
case 'c':
printf("%c\n", *((char*)ptr));
break;
default:
printf("Unknown type\n");
}
}
int main() {
int a = 10;
float b = 3.14;
char c = 'Z';
print_value(&a, 'i');
print_value(&b, 'f');
print_value(&c, 'c');
return 0;
}
Cuidados
Apesar da flexibilidade, o uso de void* requer atenção para evitar erros:
- Segurança de tipo: o compilador não verifica se o tipo real corresponde ao tipo usado no casting. Erros de casting podem levar a comportamento indefinido.
- Aritmética de ponteiros: não é possível fazer aritmética diretamente com void*, pois o compilador não conhece o tamanho do tipo. É necessário cast para um tipo completo antes.
- Dereferenciamento: nunca dereferencie um void* sem antes fazer o cast para o tipo correto.
- Aliasing: cuidado com regras de aliasing estrito; void* pode contornar essas regras, mas o uso inadequado pode gerar otimizações incorretas do compilador.
Exemplo de erro comum:
void *ptr;
double d = 3.14;
ptr = &d;
int *p = (int*) ptr;
printf("%d\n", *p); // comportamento indefinido: interpreta bytes de double como int
Referências
- cppreference: Pointer declaration
- cppreference: void type
- GNU C Manual: Void Pointers
- Wikipedia: Generic programming in C
- POSIX: malloc
Exercícios
- Escreva uma função que recebe um void* e um inteiro indicando o tipo (1 para int, 2 para float, 3 para char) e imprime o valor correspondente sem usar switch. Use um array de ponteiros para funções.
- Crie uma função genérica de troca (swap) que recebe dois ponteiros void* e um tamanho (size_t) e troca os conteúdos byte a byte.
- Explique por que o código abaixo está errado e corrija-o:
- Implemente uma função que recebe um array de void* e um inteiro n, e imprime os endereços (ponteiros) armazenados no array.
- Qual a diferença entre void* e char*? Em que situações usar um ou outro?
#include <stdio.h>
void print_int(void *p) { printf("%d\n", *(int*)p); }
void print_float(void *p) { printf("%f\n", *(float*)p); }
void print_char(void *p) { printf("%c\n", *(char*)p); }
void print(void *ptr, int type) {
void (*funcs[])(void*) = {print_int, print_float, print_char};
funcs[type-1](ptr);
}
int main() {
int a = 42;
float b = 2.71;
char c = 'A';
print(&a, 1);
print(&b, 2);
print(&c, 3);
return 0;
}
#include <string.h>
void swap(void *a, void *b, size_t size) {
unsigned char temp[size];
memcpy(temp, a, size);
memcpy(a, b, size);
memcpy(b, temp, size);
}
void *ptr;
int x = 10;
ptr = &x;
*ptr = 20; // erro
*(int*)ptr = 20;#include <stdio.h>
void print_addresses(void *arr[], int n) {
for (int i = 0; i < n; i++) {
printf("%p\n", arr[i]);
}
}
int main() {
int a = 1, b = 2, c = 3;
void *ptrs[] = {&a, &b, &c};
print_addresses(ptrs, 3);
return 0;
}