Estrutura de Dados I
Memória dinâmica

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.

desreferencia.cC
Regra

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:

conversao.cC

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 */
E o operador []?

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:

trocar.cC

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:

lista_generica.cC
Quem é dono dos dados?

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 *));
qsort.cC
Desafio

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 *.

ordena_alunos.cC
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.

cast_errado.cC
Lição

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:

alinhamento.cC

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 */
}
Solução

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

O que pode
  • 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;
O que não pode
  • 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ísticaPonteiro tipadoPonteiro void
Tipo associadoSimNão
Desreferência diretaSimNão
Aritmética de ponteirosSimNão
Conversão implícitaSó de/para void *De/para qualquer ponteiro
Segurança de tipoAltaBaixa
FlexibilidadeBaixaAlta
Para lembrar

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.

Invertendo um vetor de qualquer tipoC
Executa o seu código uma vez, com os dados de entrada abaixo. Serve para experimentar; não conta como resposta.Executa o seu código em todos os casos de teste (os de exemplo e os ocultos) e mostra quais passaram.

Nesta página