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.
- O que é
void *e por que ele aceita o endereço de qualquer variável. - Por que não dá para fazer
*pcom umvoid *, 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-arithe-pedantic). - Por que
mallocdevolvevoid *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ósito | Em 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 etiqueta | void *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 abrir | A 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;
pguarda o endereço deidade. Não precisa de conversão: qualquer ponteiro para dado viravoid *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
pagora guarda o endereço de umdouble. Com umint *isso daria erro de tipo; comvoid *, 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
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
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
voidnão foi ignorado, como deveria ser". O resultado de*pseria um "valor de tipovoid", que é o mesmo que "nada", e você tentou guardar esse nada numint.
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").
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.
| Escrita | Compila em C? | Comentário |
|---|---|---|
int *v = malloc(n * sizeof *v); | Sim | A forma recomendada. Se um dia o tipo de v mudar, o sizeof *v acompanha. |
int *v = (int *)malloc(n * sizeof(int)); | Sim | Funciona, 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 osizeofdevolve; 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
tempestouraria o array. (Uma versão mais completa reservaria o rascunho commalloc.) - memcpy(destino, origem, n)
- Copia
nbytes deorigemparadestino. O protótipo évoid *memcpy(void *destino, const void *origem, size_t n): aceita qualquer ponteiro, porque byte é byte. Oconstna 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.
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 malloc | Um 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 tipo | O ponteiro com tipo (int *). O compilador confere tudo para você. |
| Andar byte a byte | unsigned char *, nunca void * com aritmética |
| Uma função que lê o dado sem mudá-lo | const 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.
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 *.
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
| Escrita | O 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 *)p | Lê o int para onde p aponta |
*p | Erro: void * não pode ser seguido |
p + 1 | Proibido 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:
2. Guardar o malloc
Complete, no estilo do guia, para reservar 10 char:
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:
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, ouint *pi = p;.*pdireto é erro (dereferencing ‘void *’ pointer). p + 1comvoid *é proibido pelo padrão; o gcc e o Clang aceitam como extensão GNU (1 byte) e só avisam com-Wpointer-arithou-pedantic. Para bytes, useunsigned char *.mallocdevolvevoid *efreerecebevoid *: por isso funcionam com qualquer tipo, sem cast em C.- Funções genéricas recebem
void *mais o tamanho em bytes e copiam commemcpy. Passe sempresizeofda variável. - O preço: o compilador não confere tipo nem tamanho. Use
void *só na fronteira das funções genéricas.