Ponteiros void
Ponteiros genéricos em C. Conversões, aritmética, funções e estruturas genéricas, qsort e os cuidados necessários.
Ponteiros void, também chamados de ponteiros genéricos, são um dos
recursos mais flexíveis da linguagem C. Eles guardam o endereço de qualquer
tipo de dado, o que permite escrever funções e estruturas de dados que
funcionam para qualquer tipo.
O que é um ponteiro void?
Um ponteiro void * pode apontar para qualquer tipo de dado. Diferente dos
ponteiros tipados (int *, char *, float *), ele não tem um tipo associado:
guarda só o endereço.
void *ponteiro;O mesmo ponteiro pode receber endereços de tipos diferentes:
int x = 10;
float y = 3.14;
char c = 'A';
void *ponteiro;
ponteiro = &x; /* aponta para um int */
ponteiro = &y; /* aponta para um float */
ponteiro = &c; /* aponta para um char */Não pode ser desreferenciado diretamente
Como o compilador não sabe o tipo do dado apontado, ele não sabe quantos bytes
ler nem como interpretá-los. Por isso, um void * não pode ser
desreferenciado sem antes ser convertido (cast) para um ponteiro tipado.
Experimente: o programa abaixo não compila. Corrija a linha 7 trocando
*ponteiro por *(int *)ponteiro.
Um ponteiro void precisa ser convertido para um tipo concreto antes de ser
desreferenciado.
O mesmo ponteiro pode ser lido como tipos diferentes, desde que cada conversão corresponda ao tipo do dado que está naquele endereço:
Não permite aritmética de ponteiros
A aritmética de ponteiros depende do tamanho do tipo apontado: p + 1 avança
sizeof(*p) bytes. Como void não tem tamanho, void * não admite
aritmética:
int vetor[] = {1, 2, 3};
void *ponteiro = vetor;
/* ponteiro++; ERRO: void não tem tamanho */
ponteiro = (int *)ponteiro + 1; /* CORRETO: avança sizeof(int) bytes */Pelo mesmo motivo, não é possível indexar um void *: por definição,
ponteiro[i] é o mesmo que *(ponteiro + i), o que exige saber o tamanho
do tipo.
void *ponteiro = vetor;
/* int x = ponteiro[1]; ERRO no C padrão */
int x = ((int *)ponteiro)[1]; /* CORRETO */O GCC aceita aritmética com void * como uma extensão, tratando-o como
char * (avança 1 byte por unidade). Esse código não é C padrão, pode não
compilar em outros compiladores e quase nunca faz o que se espera. Evite.
Quando precisar andar byte a byte por um bloco genérico, converta para
char * (um char ocupa exatamente 1 byte): o elemento i de um vetor cujos
elementos têm tamanho bytes começa em (char *)v + i * tamanho.
Casos de uso
Funções genéricas
A função abaixo troca dois valores de qualquer tipo, copiando os bytes com
memcpy. Quem chama informa o tamanho de cada valor:
Alocação dinâmica
As funções malloc, calloc e realloc devolvem void *, justamente para
servir a qualquer tipo. Em C, a conversão de void * para outro ponteiro é
automática, então o cast é desnecessário:
int *v = malloc(5 * sizeof(int)); /* não precisa de (int *) */Veja Alocação de Memória.
Estruturas de dados genéricas
Uma lista encadeada cujos nós guardam um void * pode armazenar dados de
qualquer tipo. Abaixo, a mesma estrutura guarda inteiros numa lista e strings
em outra:
Os nós guardam apenas endereços: a lista não copia os dados. Por isso
liberarLista libera só os nós; os dados pertencem a quem os criou. Se os
dados também fossem alocados com malloc, alguém precisaria liberá-los.
Ordenação genérica com qsort
A função qsort, da biblioteca padrão, ordena vetores de qualquer tipo. Ela
recebe o vetor como void *, o número de elementos, o tamanho de cada um e
uma função de comparação, que recebe dois const void * e devolve um
valor negativo, zero ou positivo:
void qsort(void *base, size_t n, size_t tamanho,
int (*comparar)(const void *, const void *));Complete a função comparar para ordenar os alunos por nota, da maior para
a menor e, em caso de empate, por nome em ordem alfabética. Dentro dela,
converta os parâmetros para const Aluno *.
Uma solução
int comparar(const void *a, const void *b) {
const Aluno *x = a;
const Aluno *y = b;
if (x->nota > y->nota) return -1; /* maior nota vem antes */
if (x->nota < y->nota) return 1;
return strcmp(x->nome, y->nome);
}Cuidados
Converta para o tipo certo
O compilador não verifica se o cast corresponde ao tipo do dado. Uma conversão errada não gera erro nem aviso: o programa apenas interpreta os bytes de forma equivocada.
Você precisa lembrar o tipo original do dado. O compilador não vai avisar se você errar o cast.
Saiba qual é o tipo
Uma função que recebe só um void * não tem como descobrir o tipo do dado:
void funcaoGenerica(void *dado) {
/* int, float ou struct? Não há como saber. */
}Por isso, funções genéricas recebem informação extra: o tamanho do elemento
(como trocar e qsort), uma função que sabe lidar com o tipo (como a
comparação do qsort) ou um parâmetro que indica o tipo (por exemplo, um
enum). Documente o que a função espera:
/**
* Processa um vetor de elementos.
* @param dados vetor de int (int *)
* @param tamanho número de elementos
*/
void processarVetor(void *dados, size_t tamanho);Alinhamento
Cada tipo exige que seus valores fiquem em endereços múltiplos de um certo
número (o alinhamento): um int, em geral, em múltiplos de 4. A memória
devolvida por malloc é alinhada para qualquer tipo, mas um endereço obtido
avançando byte a byte pode não ser:
Não devolva endereços de variáveis locais
void *obterDado(void) {
int x = 42; /* variável local */
return &x; /* ERRO: x deixa de existir quando a função termina */
}Para dados que precisam existir depois que a função termina, use malloc.
Nunca devolva o endereço de uma variável local.
Resumo das regras
- Atribuir qualquer ponteiro a um
void *:void *p = &x; - Atribuir um
void *a qualquer ponteiro:int *v = malloc(...); - Comparar ponteiros
void *:if (p1 == p2) - Passar
void *como parâmetro:void funcao(void *dado) - Atribuir
NULL:void *p = NULL;
- Desreferenciar diretamente:
*p(converta antes:*(int *)p) - Fazer aritmética ou indexar:
p++,p[1] - Acessar campos:
p->nome(converta antes:((Pessoa *)p)->nome)
| Característica | Ponteiro tipado | Ponteiro void |
|---|---|---|
| Tipo associado | Sim | Não |
| Desreferência direta | Sim | Não |
| Aritmética de ponteiros | Sim | Não |
| Conversão implícita | Só de/para void * | De/para qualquer ponteiro |
| Segurança de tipo | Alta | Baixa |
| Flexibilidade | Baixa | Alta |
Ponteiros void permitem escrever código genérico e reutilizável, como
qsort, memcpy e estruturas de dados que guardam qualquer tipo. Em troca,
o compilador deixa de verificar os tipos: cabe a você converter corretamente
e saber o que está em cada endereço.
Exercícios
1. O que o trecho imprime?
int v[] = {10, 20, 30, 40};
void *p = v;
printf("%d\n", *((int *)p + 2));
printf("%d\n", *(int *)((char *)p + 4));Resposta
30 e 20. Em (int *)p + 2, o deslocamento é de 2 inteiros (8 bytes). Em
(char *)p + 4, é de 4 bytes, isto é, um inteiro: o elemento v[1].
2. Por que memcpy recebe void * e um tamanho em bytes?
Resposta
Porque copia bytes sem se importar com o tipo: o void * permite receber
endereços de qualquer tipo, e o tamanho diz quantos bytes copiar, já que a
função não tem como descobrir isso sozinha.
3. Implemente. Escreva inverte, que inverte a ordem dos elementos de um
vetor de qualquer tipo. Os testes usam vetores de int, double, char
e de struct.