C Ponteiro void*

Todo ponteiro que você viu até agora tinha um tipo: int * aponta para um int, char * para um char. O C tem também um ponteiro sem tipo, o void *, que aceita o endereço de qualquer coisa. Ele é a peça que permite escrever funções genéricas, que trabalham com dados de qualquer tipo, e é o que o malloc devolve. Mas tem um preço: o compilador para de vigiar os tipos, e você precisa dizer o que está do outro lado antes de usar.

Você vai aprender
  • O que é void * e por que ele aceita o endereço de qualquer variável.
  • Por que não dá para fazer *p com um void *, e como converter para o tipo certo.
  • Por que p + 1 é proibido pelo padrão, mas o gcc aceita (e como fazê-lo avisar: -Wpointer-arith e -pedantic).
  • Por que malloc devolve void * e por que, em C, não precisa de conversão.
  • Como escrever uma função de troca que serve para qualquer tipo, com memcpy.
  • O perigo: com void * o compilador não percebe quando você erra o tipo ou o tamanho.

Esta aula supõe que você já leu Ponteiros e Ponteiros e Arrays. Todos os programas foram compilados com o gcc 15.2 na máquina fictícia da Ana (usuária ana, computador notebook), na pasta ~/c, com o comando padrão do guia: gcc -std=c23 -Wall -Wextra arquivo.c -o programa.


A ideia: o endereço sem a etiqueta do que tem lá dentro

A Ana trabalha num depósito. Cada prateleira tem um número (o endereço), e cada caixa tem uma etiqueta: "6 ovos", "1 garrafa de 2 litros". Com a etiqueta, ela sabe o que vai encontrar e quanto espaço aquilo ocupa.

Um dia chega um bilhete que diz só "prateleira 120". Sem etiqueta. A Ana sabe onde ir, mas não sabe o que vai achar: uma dúzia de ovos? um saco de arroz? Antes de abrir a caixa, alguém precisa dizer o que tem dentro.

No depósitoEm C
Bilhete "prateleira 120, caixa de ovos"int *p: endereço e tipo. O C sabe ler 4 bytes e tratá-los como inteiro.
Bilhete "prateleira 120", sem etiquetavoid *p: só o endereço. O C não sabe quantos bytes ler nem o que eles significam.
Alguém diz "lá tem ovos" antes de abrirA conversão (int *)p: agora dá para usar.

A palavra void quer dizer "vazio". Você já a viu em int main(void) ("sem parâmetros") e em void mostrar(…) ("não devolve nada"). Em void * ela tem outro sentido: "ponteiro para algo de tipo desconhecido". Não confunda: void sozinho é "nada"; void * é "um endereço de qualquer coisa".


1. Um ponteiro que aceita qualquer endereço

Um void * pode receber o endereço de um int, de um double, de um char, de um array, de uma struct… sem reclamação. Para ler o que está lá, você converte para o tipo certo:

#include <stdio.h>

int main(void) {
    int idade = 29;
    double altura = 1.65;
    char inicial = 'A';

    void *p = &idade;
    printf("p aponta para um int:    %d\n", *(int *)p);

    p = &altura;
    printf("p aponta para um double: %.2f\n", *(double *)p);

    p = &inicial;
    printf("p aponta para um char:   %c\n", *(char *)p);
    return 0;
}
void *p = &idade;
p guarda o endereço de idade. Não precisa de conversão: qualquer ponteiro para dado vira void * sozinho.
(int *)p
Uma conversão (cast, veja Conversão de Tipos): "trate este endereço como um int *". É você colando a etiqueta na caixa.
*(int *)p
Agora sim, com o tipo conhecido, o * sabe ler: 4 bytes, como inteiro. Leia de dentro para fora: primeiro converte, depois segue o endereço.
p = &altura;
O mesmo p agora guarda o endereço de um double. Com um int * isso daria erro de tipo; com void *, passa.
*(double *)p
Lê 8 bytes e os interpreta como número com vírgula.
$ gcc -std=c23 -Wall -Wextra vp1.c -o vp1
$ ./vp1
p aponta para um int:    29
p aponta para um double: 1.65
p aponta para um char:   A
p void * 0x…120 0x…120 ? ? ? ? ? ? ? ? (char *)p → lê 1 byte (int *)p → lê 4 bytes (double *)p → lê 8 bytes
Um void * é só o endereço do primeiro byte. Quantos bytes ler, e como entendê-los, quem decide é a conversão que você escreve. O compilador não tem como conferir se você escolheu certo.

Em C, a conversão na atribuição é automática

Na direção contrária, de void * para um ponteiro com tipo, o C também converte sozinho, sem cast. Então, em vez de converter a cada uso, é comum copiar para um ponteiro com tipo uma vez só:

void *p = &idade;
int *pi = p;          // void * → int *: automático em C
printf("%d\n", *pi);  // 29
C e C++ são diferentes aqui

Em C++, int *pi = p; é erro: lá a conversão de void * para outro ponteiro exige cast. É por isso que você vai ver (int *)malloc(…) em código antigo ou escrito por quem vem do C++. Em C, a conversão é automática e o guia não usa esse cast (seção 4).


2. O que não dá para fazer com void *

Seguir o endereço: *p

Sem saber o tipo, o C não sabe quantos bytes ler. Então *p num void * não tem sentido:

#include <stdio.h>

int main(void) {
    int idade = 29;
    void *p = &idade;
    int x = *p;
    printf("%d\n", x);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra vp5.c -o vp5
vp5.c: In function ‘main’:
vp5.c:6:13: warning: dereferencing ‘void *’ pointer
    6 |     int x = *p;
      |             ^~
vp5.c:6:13: error: void value not ignored as it ought to be
    6 |     int x = *p;
      |             ^
warning: dereferencing ‘void *’ pointer
"Seguindo (desreferenciando) um ponteiro void *". Dereference é o nome técnico do *p.
error: void value not ignored as it ought to be
"Valor void não foi ignorado, como deveria ser". O resultado de *p seria um "valor de tipo void", que é o mesmo que "nada", e você tentou guardar esse nada num int.

O Clang do editor diz com outras palavras: ISO C does not allow indirection on operand of type 'void *' ("o C padrão não permite seguir um operando do tipo void *") e initializing 'int' with an expression of incompatible type 'void'. O conserto, nos dois: int x = *(int *)p;.

Fazer conta: p + 1

Em Ponteiros e Arrays você viu que p + 1 pula um item inteiro: 4 bytes num int *, 8 num double *. Num void *, qual é o tamanho de um item? Não existe resposta, e por isso o padrão do C proíbe aritmética com void *. Mas veja o que o gcc faz:

#include <stdio.h>

int main(void) {
    int notas[3] = {7, 8, 6};
    void *p = notas;
    void *q = p + 1;
    printf("p = %p\nq = %p\n", p, q);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra vp3.c -o vp3        « nenhum aviso!
$ ./vp3
p = 0x7ffea22caf9c
q = 0x7ffea22caf9d          « andou 1 byte, não 4

Compilou calado, e p + 1 andou um byte. É uma extensão GNU: o gcc (e o Clang, que copia o gcc nisso) trata o void * como se apontasse para bytes. Muito código do Linux usa isso, mas é código que não compila em outros compiladores, como o da Microsoft. Para o gcc avisar, peça -Wpointer-arith ou o modo "padrão estrito", -pedantic (que liga esse aviso e outros):

$ gcc -std=c23 -Wall -Wextra -Wpointer-arith vp3.c -o vp3
vp3.c: In function ‘main’:
vp3.c:6:17: warning: pointer of type ‘void *’ used in arithmetic [-Wpointer-arith]
    6 |     void *q = p + 1;
      |                 ^
$ gcc -std=c23 -Wall -Wextra -pedantic vp3.c -o vp3
vp3.c: In function ‘main’:
vp3.c:6:17: warning: pointer of type ‘void *’ used in arithmetic [-Wpointer-arith]
    6 |     void *q = p + 1;
      |                 ^
$ gcc -std=c23 -Wall -Wextra -pedantic-errors vp3.c -o vp3
vp3.c: In function ‘main’:
vp3.c:6:17: error: pointer of type ‘void *’ used in arithmetic [-Wpointer-arith]
    6 |     void *q = p + 1;
      |                 ^

Pointer of type ‘void *’ used in arithmetic = "ponteiro do tipo void * usado em aritmética". Com -pedantic-errors, o aviso vira erro e o programa não é gerado. No editor do guia, o Clang também fica calado com as opções padrão; com -Wpointer-arith ou -pedantic ele diz arithmetic on a pointer to void is a GNU extension [-Wgnu-pointer-arith] ("aritmética num ponteiro para void é uma extensão GNU").

O jeito portátil de andar byte a byte

Se você quer mesmo andar de byte em byte, diga isso com o tipo certo: unsigned char *b = p; e então b + 1. O unsigned char tem exatamente 1 byte (sizeof(unsigned char) é sempre 1) e é o tipo que o C reserva para "olhar os bytes crus" de qualquer coisa. É o que a função de troca da seção 5 faria por dentro.

Missão: a função mostrar recebe um const void * (o endereço de qualquer coisa, que ela promete não mudar) e uma letra que diz o tipo: 'i' para int, 'c' para char. Falta o caso 'd' (double). Acrescente um else if que converta p para const double * e mostre double: 93.5 (use %.1f).

#include <stdio.h>

void mostrar(const void *p, char tipo) {
    if (tipo == 'i') {
        printf("int: %d\n", *(const int *)p);
    } else if (tipo == 'c') {
        printf("char: %c\n", *(const char *)p);
    }
    // falta o 'd' (double)
}

int main(void) {
    int pontos = 120;
    char nivel = 'B';
    double tempo = 93.5;

    mostrar(&pontos, 'i');
    mostrar(&nivel, 'c');
    mostrar(&tempo, 'd');
    return 0;
}
#include <stdio.h>

void mostrar(const void *p, char tipo) {
    if (tipo == 'i') {
        printf("int: %d\n", *(const int *)p);
    } else if (tipo == 'c') {
        printf("char: %c\n", *(const char *)p);
    } else if (tipo == 'd') {
        printf("double: %.1f\n", *(const double *)p);
    }
}

int main(void) {
    int pontos = 120;
    char nivel = 'B';
    double tempo = 93.5;

    mostrar(&pontos, 'i');
    mostrar(&nivel, 'c');
    mostrar(&tempo, 'd');
    return 0;
}

Repare que a função não tem como descobrir sozinha o tipo: quem chama precisa contar (aqui, com a letra). Se você chamar mostrar(&tempo, 'i'), ela vai ler os bytes de um double como se fossem um int, e nenhum compilador vai reclamar. Toda função com void * funciona assim: o tipo viaja "por fora", num parâmetro, numa letra, num tamanho.


3. Por que o %p pede (void *)

Nas aulas de ponteiros, todo endereço mostrado com %p veio com um (void *) na frente. O motivo: o %p do printf espera exatamente um void *. Na prática, no Linux, passar um int * funciona igual, e o -Wall nem reclama; mas o modo estrito reclama:

$ cat vp4.c
#include <stdio.h>

int main(void) {
    int idade = 29;
    int *p = &idade;
    printf("%p\n", p);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra vp4.c -o vp4
$ gcc -std=c23 -Wall -Wextra -pedantic vp4.c -o vp4
vp4.c: In function ‘main’:
vp4.c:6:14: warning: format ‘%p’ expects argument of type ‘void *’, but argument 2 has type ‘int *’ [-Wformat=]
    6 |     printf("%p\n", p);
      |             ~^     ~
      |              |     |
      |              |     int *
      |              void *
      |             %ls

O printf recebe seus argumentos "sem tipo declarado" (ele é uma função variádica), então a conversão automática para void * não acontece sozinha. O (void *)p deixa o código correto em qualquer máquina, e é por isso que o guia sempre o escreve. (O %ls sugerido no fim é engano do gcc; ignore.)


4. malloc devolve void *

O void * mais famoso do C é o que o malloc devolve. O malloc reserva um pedaço de memória do tamanho pedido (aula malloc e free), mas não sabe para que você vai usá-lo: notas de alunos? letras de um nome? Então ele devolve o endereço sem etiqueta, e você guarda num ponteiro do tipo que quiser:

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int *notas = malloc(3 * sizeof *notas);   // void * → int *, sem cast
    if (notas == NULL) {
        printf("Faltou memória!\n");
        return 1;
    }
    notas[0] = 7;
    notas[1] = 8;
    notas[2] = 6;
    printf("Notas: %d %d %d\n", notas[0], notas[1], notas[2]);
    free(notas);                              // free recebe void *: aceita qualquer ponteiro
    return 0;
}
$ gcc -std=c23 -Wall -Wextra vp6.c -o vp6
$ ./vp6
Notas: 7 8 6

Os protótipos explicam tudo: void *malloc(size_t tamanho) e void free(void *p). Como void * se converte sozinho, int *notas = malloc(…) não precisa de (int *), e free(notas) aceita o int * sem reclamar.

EscritaCompila em C?Comentário
int *v = malloc(n * sizeof *v);SimA forma recomendada. Se um dia o tipo de v mudar, o sizeof *v acompanha.
int *v = (int *)malloc(n * sizeof(int));SimFunciona, mas o cast é desnecessário em C e repete o tipo em três lugares. Obrigatório só se o arquivo também for compilado como C++.

Várias funções da biblioteca padrão trabalham com void * pelo mesmo motivo, servir para qualquer tipo: memcpy e memset (de string.h), qsort e bsearch (de stdlib.h), fread e fwrite (de stdio.h).


5. Uma função de troca genérica com memcpy

Em Passar por Ponteiro você escreveu void trocar(int *a, int *b). Ela só serve para int. Para trocar dois double, seria outra função; para dois nomes, outra… Com void * dá para escrever uma só. Como ela não sabe o tipo, quem chama informa o tamanho em bytes, e ela copia os bytes crus com memcpy ("memory copy", copiar memória):

#include <stdio.h>
#include <string.h>

void trocar(void *a, void *b, size_t tamanho) {
    unsigned char temp[64];
    if (tamanho > sizeof temp) {
        return;                 // grande demais para esta versão
    }
    memcpy(temp, a, tamanho);   // temp <- a
    memcpy(a, b, tamanho);      // a    <- b
    memcpy(b, temp, tamanho);   // b    <- temp
}

int main(void) {
    int x = 1, y = 2;
    trocar(&x, &y, sizeof x);
    printf("x = %d, y = %d\n", x, y);

    double preco1 = 4.50, preco2 = 12.90;
    trocar(&preco1, &preco2, sizeof preco1);
    printf("preco1 = %.2f, preco2 = %.2f\n", preco1, preco2);

    char nome1[10] = "Ana", nome2[10] = "Bia";
    trocar(nome1, nome2, sizeof nome1);
    printf("nome1 = %s, nome2 = %s\n", nome1, nome2);
    return 0;
}
void *a, void *b
Endereços de qualquer coisa. A função nem tenta saber o que são.
size_t tamanho
Quantos bytes trocar. size_t é o tipo que o C usa para tamanhos (o que o sizeof devolve; veja sizeof).
unsigned char temp[64];
Um espaço de rascunho de 64 bytes. unsigned char = "byte cru".
if (tamanho > sizeof temp) return;
Proteção: se pedirem mais de 64 bytes, copiar para temp estouraria o array. (Uma versão mais completa reservaria o rascunho com malloc.)
memcpy(destino, origem, n)
Copia n bytes de origem para destino. O protótipo é void *memcpy(void *destino, const void *origem, size_t n): aceita qualquer ponteiro, porque byte é byte. O const na origem promete que ela não será mudada.
trocar(&x, &y, sizeof x)
Quem chama passa os endereços (que viram void * sozinhos) e o tamanho de um item.
trocar(nome1, nome2, sizeof nome1)
Arrays já viram endereços; sizeof nome1 é 10, o array inteiro.
$ gcc -std=c23 -Wall -Wextra troca.c -o troca
$ ./troca
x = 2, y = 1
preco1 = 12.90, preco2 = 4.50
nome1 = Bia, nome2 = Ana

Uma função, três tipos. É exatamente assim que o qsort da biblioteca padrão consegue ordenar arrays de qualquer tipo: ele recebe o array como void *, o tamanho de cada item e uma função que sabe comparar dois itens (aulas Ponteiros para Funções e Ordenação).

O preço: o compilador não confere mais nada

Com int *, se você passar o endereço de um double, o compilador dá erro. Com void *, tudo passa. Veja o que acontece se a Ana errar o tamanho e passar sizeof(int) para trocar dois double:

    double preco1 = 4.50, preco2 = 12.90;
    trocar(&preco1, &preco2, sizeof(int));     // errado: um double tem 8 bytes
    printf("preco1 = %.15f\npreco2 = %.15f\n", preco1, preco2);
$ gcc -std=c23 -Wall -Wextra troca_errada.c -o troca_errada     « nenhum aviso
$ ./troca_errada
preco1 = 4.500003051757813
preco2 = 12.899993896484375

Nenhum erro, nenhum aviso, e o resultado é um monstro: só os 4 primeiros dos 8 bytes de cada número foram trocados, e cada preço ficou com metade de si mesmo e metade do outro. (Com %.2f o defeito nem apareceria: sairia 4.50 e 12.90, como se nada tivesse acontecido.) Por isso a regra: use sizeof da própria variável (sizeof preco1), nunca um tipo escrito à mão. Se a variável mudar de tipo, o tamanho acompanha.

Converter para o tipo errado é comportamento indefinido

Ler um double como int (*(int *)p com p apontando para um double) não é só "ler um número estranho": o padrão diz que o resultado é comportamento indefinido, e o otimizador pode fazer coisas inesperadas. A única exceção é olhar os bytes com unsigned char * (ou char *), que é sempre permitido, e é o que o memcpy faz.


Formas diferentes e qual usar

Você quer…Use
Guardar o resultado do mallocUm ponteiro com tipo, sem cast: int *v = malloc(n * sizeof *v);
Uma função que trabalha com qualquer tipo (copiar, trocar, ordenar, guardar numa lista genérica)void * + o tamanho em bytes (e, se precisar comparar, uma função: veja Ponteiros para Funções)
Uma função que só serve para um tipoO ponteiro com tipo (int *). O compilador confere tudo para você.
Andar byte a byteunsigned char *, nunca void * com aritmética
Uma função que lê o dado sem mudá-loconst void *, como a origem do memcpy

Regra de bolso: void * só na fronteira. Ele entra e sai das funções genéricas, mas, assim que você sabe o tipo, guarde num ponteiro com tipo e trabalhe com ele.

Ponteiro de função não cabe em void *

O void * serve para endereços de dados. O padrão do C não garante que o endereço de uma função caiba nele (em algumas máquinas, código e dados ficam em memórias diferentes). Para guardar funções existe outro tipo de ponteiro, assunto de Ponteiros para Funções.


Quando dá errado

warning: dereferencing ‘void *’ pointer + error: void value not ignored as it ought to be

O que significa: você escreveu *p com p do tipo void *. O C não sabe que tipo está lá.

Como consertar: converta antes: *(int *)p, ou copie para um ponteiro com tipo (int *pi = p;) e use *pi. Dentro do printf, a mesma falta aparece no gcc como error: invalid use of void expression ("uso inválido de expressão void") e no Clang como argument type 'void' is incomplete.

warning: pointer of type ‘void *’ used in arithmetic [-Wpointer-arith]

O que significa: você fez p + n, p++ ou p[i] com um void *. O padrão proíbe; o gcc aceita como extensão e anda de 1 em 1 byte. O aviso só aparece com -Wpointer-arith ou -pedantic.

Como consertar: converta para o tipo dos itens (int *v = p; v + 1) ou, se quer bytes, para unsigned char *.

Valores "quase certos" ou absurdos depois de uma função genérica

O que significa: o tamanho passado não bate com o tipo (como o sizeof(int) para double acima), ou o void * foi convertido para o tipo errado. O compilador não tem como perceber.

Como consertar: passe sempre sizeof da variável (sizeof x, sizeof *v) e confira que a conversão usa o mesmo tipo do dado original.

error: invalid conversion from ‘void*’ to ‘int*’

O que significa: o arquivo está sendo compilado como C++ (com g++, ou com a extensão .cpp). Em C++, int *v = malloc(…) é erro.

Como consertar: compile C com gcc e arquivos .c. Se o código precisa mesmo compilar nas duas linguagens, aí sim escreva o cast: (int *)malloc(…).


Referência rápida

EscritaO que faz
void *p = &x;Guarda o endereço de qualquer dado, sem conversão
int *pi = p;Converte void * para int * (automático em C)
*(int *)pLê o int para onde p aponta
*pErro: void * não pode ser seguido
p + 1Proibido pelo padrão; extensão GNU (anda 1 byte). Avisos: -Wpointer-arith, -pedantic
unsigned char *b = p;O jeito certo de olhar os bytes, um por um
const void *Endereço de qualquer dado que não será mudado
printf("%p", (void *)ptr)Mostrar um endereço do jeito correto
void *malloc(size_t n)Devolve memória nova como void *
void *memcpy(void *dest, const void *orig, size_t n)Copia n bytes de qualquer coisa para qualquer coisa (string.h)

Exercícios

1. Converter para ler

p é um void * que aponta para um double. Complete para guardar o valor em v:

double v = *()p;

2. Guardar o malloc

Complete, no estilo do guia, para reservar 10 char:

char *nome = (10 * sizeof *nome);

3. O tamanho certo

Complete a chamada da troca genérica para trocar os double a e b, usando o tamanho da própria variável:

trocar(&a, &b, );

4. Com void *p = &idade;, o que acontece com int x = *p;?

5. No gcc, sem opções extras, void *q = p + 1;…

6. Por que, em C, int *v = malloc(5 * sizeof *v); não precisa de (int *)?

Missão final: a troca genérica

Missão: escreva o corpo de trocar usando memcpy três vezes (rascunho ← a, a ← b, b ← rascunho), com a proteção do tamanho. A mesma função tem de trocar os preços e os nomes. A saída deve ser, sem avisos:

preco1 = 12.90, preco2 = 4.50
nome1 = Bia, nome2 = Ana
#include <stdio.h>
#include <string.h>

void trocar(void *a, void *b, size_t tamanho) {
    unsigned char temp[64];
    // 1. se tamanho for maior que sizeof temp, saia com return
    // 2. troque os bytes de a e b com memcpy
}

int main(void) {
    double preco1 = 4.50, preco2 = 12.90;
    trocar(&preco1, &preco2, sizeof preco1);
    printf("preco1 = %.2f, preco2 = %.2f\n", preco1, preco2);

    char nome1[10] = "Ana", nome2[10] = "Bia";
    trocar(nome1, nome2, sizeof nome1);
    printf("nome1 = %s, nome2 = %s\n", nome1, nome2);
    return 0;
}
#include <stdio.h>
#include <string.h>

void trocar(void *a, void *b, size_t tamanho) {
    unsigned char temp[64];
    if (tamanho > sizeof temp) {
        return;
    }
    memcpy(temp, a, tamanho);
    memcpy(a, b, tamanho);
    memcpy(b, temp, tamanho);
}

int main(void) {
    double preco1 = 4.50, preco2 = 12.90;
    trocar(&preco1, &preco2, sizeof preco1);
    printf("preco1 = %.2f, preco2 = %.2f\n", preco1, preco2);

    char nome1[10] = "Ana", nome2[10] = "Bia";
    trocar(nome1, nome2, sizeof nome1);
    printf("nome1 = %s, nome2 = %s\n", nome1, nome2);
    return 0;
}

Quer ir além? Acrescente dois int no main e troque-os com a mesma função. E tente trocar dois arrays de 100 int: a proteção vai barrar (400 bytes não cabem em 64), e nada muda. Em malloc e free você aprende a reservar um rascunho do tamanho que precisar.


Resumo

  • void * é um ponteiro sem tipo: guarda o endereço de qualquer dado. A conversão de e para outros ponteiros de dado é automática em C (em C++, não).
  • Para usar o que está lá, converta para o tipo certo: *(int *)p, ou int *pi = p;. *p direto é erro (dereferencing ‘void *’ pointer).
  • p + 1 com void * é proibido pelo padrão; o gcc e o Clang aceitam como extensão GNU (1 byte) e só avisam com -Wpointer-arith ou -pedantic. Para bytes, use unsigned char *.
  • malloc devolve void * e free recebe void *: por isso funcionam com qualquer tipo, sem cast em C.
  • Funções genéricas recebem void * mais o tamanho em bytes e copiam com memcpy. Passe sempre sizeof da variável.
  • O preço: o compilador não confere tipo nem tamanho. Use void * só na fronteira das funções genéricas.