Punteros I: el concepto que define a C
Direcciones, dereferencia y aritmética de punteros. El tema que más asusta y el que más poder da: una vez lo interiorizas, C entero cobra sentido.
Si hay un tema que separa a quien “sabe algo de C” de quien lo entiende, son los punteros. No son magia ni una dificultad artificial: son, sencillamente, direcciones de memoria. Y como en C todo vive en memoria, los punteros están en todas partes. Domínalos aquí.
- Qué es un puntero (una dirección) y por qué existen.
- Los operadores
&(dirección de) y*(contenido de). - Aritmética de punteros.
void *,nullptry la seguridad al dereferenciar.
Un puntero es una dirección
La memoria es una secuencia enorme de bytes, cada uno con una dirección (un número). Una variable normal guarda un valor; un puntero guarda la dirección de otra variable.
int x = 42; // x vive en alguna dirección, digamos 0x7ffe...
int *p = &x; // p guarda la dirección de x
Dos operadores hacen todo el trabajo:
&x— “la dirección de x”.*p— “el contenido en la dirección que guarda p” (dereferenciar).
printf("%d\n", x); // 42 (el valor)
printf("%p\n", (void*)&x); // la dirección de x
printf("%d\n", *p); // 42 (dereferenciando p llegamos a x)
*p = 100; // escribir a través del puntero
printf("%d\n", x); // 100 — cambiamos x a través de p
Imagina la memoria como una calle de casas numeradas. Una variable int x = 42 es una casa (dirección 0x100) con un 42 dentro. Un puntero int *p = &x es un papelito donde escribiste “0x100” — no contiene el 42, contiene dónde está el 42. *p es ir a esa dirección y mirar dentro. Con este modelo, todo encaja: pasar un puntero a una función es darle la dirección para que edite tu casa; un array es una fila de casas contiguas; nullptr es un papelito con “dirección cero”, que no apunta a ninguna casa válida. Cuando “ves” la memoria así, los punteros dejan de dar miedo y se vuelven tu herramienta más poderosa.
Tipos de puntero
Un puntero tiene tipo: int * apunta a un int, char * a un char. El tipo importa porque le dice a C cuántos bytes leer al dereferenciar y cómo avanzar en la aritmética.
int *pi; // apunta a un int (lee/escribe 4 bytes al dereferenciar)
char *pc; // apunta a un char (1 byte)
double *pd; // apunta a un double (8 bytes)
nullptr y punteros no inicializados
Un puntero que no apunta a nada válido debe ser nullptr (C23) — nunca lo dejes sin inicializar:
int *p = nullptr; // no apunta a nada, explícito y seguro
if (p != nullptr) *p = 5; // comprueba antes de dereferenciar
int *malo; // ❌ puntero "salvaje": apunta a basura
// *malo = 5; // 💥 comportamiento indefinido: crash o corrupción
Los tres errores que causan la mayoría de los crashes y vulnerabilidades en C: dereferenciar un puntero nulo, uno no inicializado (salvaje), o uno colgante (que apunta a memoria ya liberada). Ninguno da un error amable: dan corrupción de memoria o crash, a veces mucho después del fallo real. La disciplina que te salva: inicializa siempre a nullptr, comprueba antes de dereferenciar, y pon a nullptr tras liberar. Los sanitizers (nivel 24) cazan estos errores; pero la mejor defensa es el hábito.
Aritmética de punteros
Sumar a un puntero no avanza bytes: avanza elementos del tipo apuntado. Esto conecta punteros y arrays:
int nums[3] = {10, 20, 30};
int *p = nums; // p apunta al primer elemento
printf("%d\n", *p); // 10
p++; // avanza UN int (4 bytes), no un byte
printf("%d\n", *p); // 20
printf("%d\n", *(p + 1)); // 30 — un elemento más allá
*(p + i) es exactamente lo que hace nums[i] por debajo. Los índices de array son azúcar sintáctico sobre aritmética de punteros (lo verás en el nivel 9).
void *: el puntero genérico
void * apunta a “algo” sin tipo. Es el puntero genérico de C — lo usa malloc, y lo verás en APIs genéricas. No puedes dereferenciarlo directamente (no sabe el tamaño): primero conviertes a un tipo concreto.
void *generico = &x;
int *como_int = generico; // convertir antes de usar
printf("%d\n", *como_int);
- Declara un
int, un puntero a él con&, y modifica el int a través de*p. - Imprime la dirección de una variable con
%p. - Recorre un array con un puntero y aritmética (
p++,*(p+i)) en vez de índices. - Comprueba un puntero contra
nullptrantes de dereferenciar. - Guarda una dirección en un
void *y conviértela de vuelta para usarla.