void*: El puntero genérico y sus reglas de uso

Un void* es un tipo de puntero que almacena una dirección de memoria sin asociarle un tipo de dato específico. Es, esencialmente, una dirección pura sin metadatos de tipo. Mientras que un int* le dice al compilador “aquí hay un entero de 4 bytes”, un void* solo dice “aquí hay una dirección”. Esto significa que el compilador sabe que existe un valor en esa posición, pero no tiene ni idea de cuánto mide el objeto que reside allí ni cómo debe interpretarse su contenido.

Esta falta de información es, paradójicamente, su mayor virtud. Gracias a esto, un void* puede almacenar la dirección de cualquier objeto y, en C, puede convertirse implícitamente a (y desde) cualquier otro tipo de puntero sin necesidad de un cast explícito (una diferencia fundamental con C++). Es por esto que funciones de la biblioteca estándar como malloc devuelven void*: la función simplemente reserva un bloque de memoria de $n$ bytes y te entrega la dirección; no tiene la responsabilidad de saber si vas a usar esos bytes para un struct complejo o para un float. Sin embargo, esta flexibilidad te exige un cuidado extremo: no puedes desreferenciar un void* directamente (*ptr es ilegal) ni realizar aritmética sobre él (ptr + 1 no tiene sentido si no sabemos el tamaño del tipo), porque el compilador no sabría cuántos bytes debe saltar. Si intentas esto, el compilador fallará. Úsalo cuando necesites implementar contenedores genéricos (como listas o árboles) o funciones de ordenación como qsort, pero ten presente que estás sacrificando la verificación de tipos en tiempo de compilación, delegando la seguridad a tu propia disciplina.

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

/* Estructura de un nodo para una lista genérica */
typedef struct Node {
    void *data;             /* Puntero genérico para cualquier tipo de dato */
    struct Node *next;
} Node;

typedef struct {
    Node *head;
} GenericList;

/* Inserta un elemento en la cabeza de la lista */
void list_push(GenericList *list, void *new_data) {
    Node *new_node = malloc(sizeof(Node));
    if (!new_node) {
        perror("Fallo al asignar nodo");
        exit(EXIT_FAILURE);
    }
    new_node->data = new_data;
    new_node->next = list->head;
    list->head = new_node;
}

/* Libera la lista y los datos si se proporciona un destructor */
void list_destroy(GenericList *list, void (*free_func)(void *)) {
    Node *current = list->head;
    while (current != NULL) {
        Node *next = current->next;
        if (free_func && current->data) {
            free_func(current->data);
        }
        free(current);
        current = next;
    }
    list->head = NULL;
}

/* Función de comparación para qsort (estándar C) */
int compare_ints(const void *a, const void *b) {
    /* qsort pasa punteros a los elementos. 
       Como estamos ordenando un array de int, a y b son int* */
    int arg1 = *(const int *)a;
    int arg2 = *(const int *)b;
    return (arg1 > arg2) - (arg1 < arg2);
}

/* Función para imprimir bytes mediante char* (aliasing legal) */
void debug_inspect_bytes(void *ptr, size_t size) {
    unsigned char *p = (unsigned char *)ptr;
    printf("Bytes: ");
    for (size_t i = 0; i < size; i++) {
        printf("%02x ", p[i]);
    }
    printf("\n");
}

int main(void) {
    GenericList my_list = { .head = NULL };

    /* Creamos datos de diferentes tipos */
    int *val1 = malloc(sizeof(int));
    *val1 = 42;
    int *val2 = malloc(sizeof(int));
    *val2 = 7;

    list_push(&my_list, val1);
    list_push(&my_list, val2);

    /* Inspección de memoria: un void* puede verse como char* sin problemas */
    printf("Inspeccionando val1 (42):\n");
    debug_inspect_bytes(val1, sizeof(int));

    /* Ordenación de un array de enteros usando qsort */
    int arr[] = {10, 5, 20, 15, 30};
    size_t n = sizeof(arr) / sizeof(arr[0]);
    qsort(arr, n, sizeof(int), compare_ints);

    printf("Array ordenado: ");
    for (size_t i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    list_destroy(&my_list, free);
    return 0;
}

Desglose del código

En la estructura Node, el miembro void *data es el corazón de la abstracción. Nos permite que la GenericList sea agnóstica al tipo de contenido: puede guardar int*, double* o incluso otros punteros a structs.

Fíjate en la función compare_ints. qsort es una función genérica que requiere un comparador con la firma int (*)(const void *, const void *). Como qsort no sabe qué está comparando, nos entrega const void *. Para poder realizar la comparación, tenemos que hacer un cast explícito a const int * antes de desreferenciar. Si intentáramos hacer *a directamente, el compilador daría error porque no sabe cuánto mide lo que hay en a.

En debug_inspect_bytes, aplicamos una regla fundamental de la implementación del modelo de memoria de C: el aliasing. Aunque val1 es un int*, podemos tratar su dirección como un unsigned char* para inspeccionar la memoria byte a byte. Esto es seguro y está estipulado por el estándar, permitiendo la manipulación de objetos a nivel de nivel más bajo.

El uso de malloc en list_push es el ejemplo clásico de la semántica de void*. malloc nos devuelve un void* que luego asignamos a new_node->data. No hace falta un cast en C (aunque sea necesario en C++), lo cual simplifica el código pero traslada la responsabilidad de la coherencia de tipos al programador.

El error frecuente
Un error clásico es intentar realizar aritmética de punteros sobre un void* o intentar acceder a su contenido sin una conversión previa.

void *ptr = malloc(100);
ptr = ptr + 1;    // ERROR: Operación aritmética sobre void* no estándar
int x = *ptr;     // ERROR: Desreferencia de tipo incompleto

Aunque algunos compiladores como GCC permiten la aritmética en void* tratándolo internamente como un puntero a char (donde el tamaño es 1), esto no es parte del estándar ISO C y romperá tu código si intentas compilarlo con un compilador estrictamente conforme o en arquitecturas con alineaciones de memoria distintas. Además, la desreferencia directa es un error de sintaxis inmediato. Para usar la memoria, siempre debes pasar por un tipo con tamaño definido.

UBSan (Undefined Behavior Sanitizer) detectará desreferencias de punteros con tipos incompletos, y Valgrind te avisará si la lógica de tus casts ha provocado accesos a memoria no alineada.

49

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio