Punteros en C: aritmética, doble puntero y los errores más comunes

Los punteros son el mecanismo central de C: sin entenderlos bien, es imposible escribir código eficiente o trabajar con estructuras de datos dinámicas. También son la fuente de la mayoría de los bugs más difíciles de diagnosticar. Este artículo cubre la aritmética de punteros, el doble puntero y los errores más habituales.

Qué es un puntero

Un puntero es una variable que almacena una dirección de memoria. El tipo del puntero determina cómo se interpreta esa dirección:

int x = 42;
int* p = &x;   /* p contiene la dirección de x */

printf("Valor: %dn", *p);    /* desreferencia: imprime 42 */
printf("Dirección: %pn", (void*)p); /* dirección de memoria */

*p = 100;  /* modifica x a través del puntero */
printf("x ahora vale: %dn", x);  /* imprime 100 */

Aritmética de punteros

La aritmética de punteros tiene en cuenta el tamaño del tipo apuntado. Al sumar 1 a un int*, el puntero avanza sizeof(int) bytes (típicamente 4), no 1 byte:

int arr[5] = {10, 20, 30, 40, 50};
int* p = arr;  /* equivale a &arr[0] */

printf("%dn", *p);       /* 10 */
printf("%dn", *(p + 1)); /* 20 — avanza 4 bytes */
printf("%dn", *(p + 4)); /* 50 */

/* Recorrer array con aritmética de punteros */
for (int* q = arr; q < arr + 5; q++) {
    printf("%d ", *q);
}

La diferencia entre dos punteros del mismo tipo devuelve el número de elementos entre ellos (tipo ptrdiff_t):

int* inicio = &arr[0];
int* fin    = &arr[4];
ptrdiff_t n = fin - inicio; /* resultado: 4 */

void* — el puntero genérico

void* es un puntero sin tipo. Puede apuntar a cualquier dirección pero no puede desreferenciarse directamente ni aplicársele aritmética. Es el tipo que devuelven malloc, calloc y realloc:

void* bloque = malloc(100);

/* En C, la conversión implícita de void* es automática */
int* nums  = bloque;     /* OK en C */
char* text = bloque;     /* también OK */

/* Para usar void* hay que hacer cast explícito primero */
/* ((char*)bloque)[0] = 'A'; */

En C++, a diferencia de C, el cast explícito de void* es obligatorio. Esto es una diferencia importante entre los dos lenguajes.

nullptr en C23 (y NULL antes)

Hasta C17, el puntero nulo se representaba con NULL, definido como (void*)0 o simplemente 0 según la implementación. C23 introduce nullptr con tipo propio nullptr_t:

/* C99/C11/C17 */
int* p = NULL;

/* C23 */
int* q = nullptr;  /* más preciso, sin ambigüedad de tipo */

Ambas formas son válidas. nullptr no puede usarse en contextos aritméticos, lo que evita errores sutiles como NULL + 1.

Doble puntero

Un puntero a puntero es un tipo completamente legítimo y muy usado. El ejemplo más conocido es char** argv en la firma de main:

int main(int argc, char** argv) {
    /* argv[0] es el nombre del programa */
    /* argv[1] es el primer argumento, etc. */
    for (int i = 0; i < argc; i++) {
        printf("arg[%d] = %sn", i, argv[i]);
    }
    return 0;
}

El doble puntero también permite modificar un puntero desde una función (pasar por referencia):

void reservar(int** ptr, int n) {
    *ptr = malloc(n * sizeof(int));
}

int* buf = NULL;
reservar(&buf, 50);
/* buf ahora apunta a un bloque de 50 ints */
free(buf);

Sin el doble puntero, la función solo modificaría su copia local del puntero, dejando buf a NULL.

Punteros a funciones

En C, las funciones también tienen dirección de memoria. Un puntero a función almacena esa dirección y permite llamar a la función indirectamente:

int sumar(int a, int b)  { return a + b; }
int restar(int a, int b) { return a - b; }

/* Declaración: int (*nombre)(int, int) */
int (*operacion)(int, int) = sumar;
printf("%dn", operacion(10, 3)); /* 13 */

operacion = restar;
printf("%dn", operacion(10, 3)); /* 7 */

Los punteros a funciones son la base de los callbacks y de estructuras polimórficas en C.

Errores comunes con punteros

  • Puntero sin inicializar: declarar int* p; sin asignar nada. p contiene basura y cualquier acceso es UB.
  • Desreferenciar NULL: acceder a *p cuando p == NULL. Provoca segmentation fault.
  • Puntero colgante (dangling): usar un puntero después de free(). La memoria puede haber sido reasignada.
  • Buffer overflow por aritmética: avanzar el puntero más allá del array reservado.
  • Confundir p++ con (*p)++: el primero mueve el puntero, el segundo incrementa el valor apuntado.
/* Error clásico: confusión de precedencia */
int x = 5;
int* p = &x;

(*p)++;  /* incrementa x: x == 6 */
*p++;    /* ojo: incrementa p (el puntero), no x */
         /* equivale a *(p++) por precedencia de operadores */

Para detectar estos errores automáticamente, el artículo sobre gestión de memoria y Valgrind y el de debugging con gdb y AddressSanitizer ofrecen las herramientas necesarias. Si te interesa un lenguaje donde el compilador verifica la validez de los punteros en tiempo de compilación, Rust 2024 Edition implementa ese modelo.

Imagen: Pexels / Myburgh Roux

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP