wandres.dev
NINJA · Punteros I: fundamentos

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.

⏱ 16 min

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í.

🎯 Al terminar esta lección sabrás
  • Qué es un puntero (una dirección) y por qué existen.
  • Los operadores & (dirección de) y * (contenido de).
  • Aritmética de punteros.
  • void *, nullptr y 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
El modelo mental que lo desbloquea todo

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
🛑
Dereferenciar mal es el pecado capital de C

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);
⚔️ Interioriza los punteros
  1. Declara un int, un puntero a él con &, y modifica el int a través de *p.
  2. Imprime la dirección de una variable con %p.
  3. Recorre un array con un puntero y aritmética (p++, *(p+i)) en vez de índices.
  4. Comprueba un puntero contra nullptr antes de dereferenciar.
  5. Guarda una dirección en un void * y conviértela de vuelta para usarla.