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

Exercícios

  1. 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.
  2. ✓ Resposta:
    #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;
    }
    
  3. 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.
  4. ✓ Resposta:
    #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);
    }
    
  5. Explique por que o código abaixo está errado e corrija-o:
  6. void *ptr;
    int x = 10;
    ptr = &x;
    *ptr = 20;  // erro
    

    ✓ Resposta: O erro é que não se pode dereferenciar um void* diretamente. Correção: fazer cast para int* antes de atribuir: *(int*)ptr = 20;
  7. Implemente uma função que recebe um array de void* e um inteiro n, e imprime os endereços (ponteiros) armazenados no array.
  8. ✓ Resposta:
    #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;
    }
    
  9. Qual a diferença entre void* e char*? Em que situações usar um ou outro?
  10. ✓ Resposta: void* é um ponteiro genérico sem tipo, usado para armazenar endereços de qualquer tipo, mas não permite aritmética ou dereferenciamento sem cast. char* é um ponteiro para char, usado para strings ou manipulação de bytes; permite aritmética (incremento/decremento) e dereferenciamento. Use void* para funções genéricas (ex: qsort) e char* para manipulação de bytes ou quando o tipo base é char.