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.pcontiene basura y cualquier acceso es UB. - Desreferenciar NULL: acceder a
*pcuandop == 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
