🧠 Memoria y punteros en C
El modelo de memoria de una computadora: RAM, direcciones, stack vs heap, punteros en C, arrays, paso de argumentos, malloc/free, leaks y buffer overflow.
Memoria y punteros en C
Entender la memoria es lo que separa a quien «escribe código» de quien «entiende qué hace el código». C no te protege: tú eres el que pide memoria, la usa y la devuelve. Esta es la base sobre la que se apoya todo lo demás (kernel, drivers, runtimes como V8 o libuv).
El modelo de memoria
Una computadora tiene RAM, que es un array gigante de bytes. Cada byte tiene una dirección (un número). El hardware lee/escribe un byte (o varios) en una dirección:
#include <stdio.h>
int main(void) {
int x = 42;
printf("x vale %d y vive en la dirección %p\n", x, (void*)&x);
return 0;
}
xes el valor.&xes la dirección donde vive ese valor.%pimprime una dirección (puntero).
💡 Direcciones: en una máquina de 64 bits, las direcciones son números de 64 bits.
0x7ffc...son típicas del stack,0x55.../0x560...del heap y del código,0x1valores especiales.
Qué vive en cada zona
| Zona | Qué se guarda | Crecimiento | Quién la gestiona |
|---|---|---|---|
| Text/código | Instrucciones del programa | fijo | compilador |
| Data/BSS | Variables globales/estáticas | fijo | compilador |
| Heap | Memoria dinámica (malloc) |
hacia arriba | tú (free) |
| Stack | Variables locales, llamadas a funciones | hacia abajo | el compilador (automático) |
Direcciones altas
+----------------+
| Stack | <- crece hacia abajo (abajo = direcciones bajas)
| (locales) |
+----------------+
| |
| Heap | <- crece hacia arriba
| (malloc) |
+----------------+
| Data / BSS | <- globales y estáticas
+----------------+
| Text | <- código
Direcciones bajas
Stack vs heap
- Stack: memoria automática. Cada llamada a una función reserva un stack frame con sus locales y lo libera al volver. Rápida, predecible, pero limitada (unos 8 MB por hilo típicamente) y de vida corta.
- Heap: memoria manual. La pides con
mallocy la devuelves confree. Puede ser mucha, pero eres responsable de su ciclo de vida.
int foo(void) { // 'local' vive en el STACK de foo
int local = 10;
return local; // se copia el VALOR al volver
}
int* bar(void) {
int local = 10;
return &local; // ⚠️ PELIGRO: devuelves dirección del stack
} // que ya se liberó al salir de bar
Punteros en C
Un puntero es una variable que guarda una dirección. Se declara con *:
int x = 10; // variable normal
int *p; // puntero a int (guarda una dirección)
p = &x; // p = dirección de x
Dereferencia
Con * accedes al valor apuntado:
#include <stdio.h>
int main(void) {
int x = 10;
int *p = &x;
*p = 25; // escribe 25 en la memoria de x
printf("%d\n", x); // 25
return 0;
}
💡 Regla mental:
int *p= «p es una variable que, cuando hago*p, me da unint».*pes el int,pes la dirección.
Pointer arithmetic
Sumar/restar a un puntero avanza en unidades del tipo apuntado:
int arr[] = {10, 20, 30, 40};
int *p = arr; // p -> arr[0]
p++; // p -> arr[1]
printf("%d\n", *p); // 20
printf("%d\n", *(p + 2)); // 40 (arr[3])
p + 1 suma 1 * sizeof(int) bytes, no 1 byte. Eso es lo que hace arr[i] en realidad: *(arr + i).
NULL y punteros colgantes
int *p = NULL; // puntero nulo: no apunta a nada válido
if (p != NULL) {
*p = 5; // seguro: solo entramos si p es válido
}
// *p = 5; // ¡crash (segfault)! dereferenciar NULL es indefinido
Un puntero colgante (dangling) es uno que apunta a memoria ya liberada:
int *p = malloc(sizeof(int));
free(p);
*p = 10; // ⚠️ undefined behavior: usas memoria liberada
p = NULL; // buena práctica: anular después de free
Leaks
Un memory leak ocurre cuando reservas heap y nunca lo liberas:
void leak(void) {
char *buf = malloc(1024);
// olvidas free(buf); -> se pierden 1024 bytes por llamada
}
Con el tiempo, el programa consume toda la RAM disponible y muere. Herramientas: valgrind (Linux), AddressSanitizer (-fsanitize=address), LeakSanitizer.
Arrays y punteros
En C, el nombre de un array decae a puntero a su primer elemento en casi todos los contextos:
int arr[4] = {1, 2, 3, 4};
arr // puntero a arr[0] (equivalente a &arr[0])
*arr // 1
arr[2] // 3
*(arr + 2) // 3 (idéntico a arr[2])
sizeof(arr) // 16 = 4 * sizeof(int) (solo dentro del scope del array)
sizeof(arr + 0) // 8 (ya es puntero)
⚠️ Cuando pasas un array a una función, no pasas el array, pasas un puntero a su primer elemento. Por eso las funciones que reciben arrays casi siempre reciben también el tamaño.
Paso de argumentos: por valor y por referencia
En C todo se pasa por valor (se copia). «Pasar por referencia» es un truco: pasas un puntero para poder modificar la variable original.
void no_cambia(int n) { n = 99; } // copia local, no afecta al caller
void si_cambia(int *n) { *n = 99; } // dereferencia y modifica el original
int main(void) {
int x = 1;
no_cambia(x); // x sigue siendo 1
si_cambia(&x); // x ahora es 99
return 0;
}
Para structs grandes conviene pasar punteros en vez de copiar el struct entero:
typedef struct { int a; int b; } Par;
void imprime(const Par *p) { // 'const': no modificamos
printf("%d %d\n", p->a, p->b);
}
Structs
Un struct agrupa variables. El operador -> accede a un campo a través de un puntero; . accede directamente:
#include <stdio.h>
#include <string.h>
typedef struct {
char nombre[32];
int edad;
} Persona;
int main(void) {
Persona p;
strcpy(p.nombre, "Ana");
p.edad = 30;
Persona *pp = &p;
printf("%s, %d\n", pp->nombre, pp->edad); // p->x == (*p).x
return 0;
}
💡 Alineamiento: un struct puede ocupar más bytes que la suma de sus campos por el padding (el compilador alinea cada campo a su tamaño).
sizeofsiempre te dice el real.
malloc y free
malloc pide memoria en el heap. Devuelve void* (puntero genérico) o NULL si no hay memoria. Siempre hay que comprobar el retorno y liberar con free:
#include <stdlib.h>
#include <string.h>
int main(void) {
int n = 100;
int *arr = malloc(n * sizeof(int)); // pide espacio para n ints
if (arr == NULL) {
return 1; // sin memoria: abortar con elegancia
}
memset(arr, 0, n * sizeof(int)); // llena con ceros
arr[0] = 42; // usa el array
free(arr); // devuelve la memoria
arr = NULL; // evita el dangling pointer
return 0;
}
| Función | Qué hace |
|---|---|
malloc(bytes) |
Reserva bytes sin inicializar, devuelve puntero |
calloc(n, size) |
Reserva n * size y lo pone a cero |
realloc(p, bytes) |
Redimensiona un bloque (puede moverlo) |
free(p) |
Libera un bloque reservado |
⚠️ Regla de oro: todo
malloc/calloc/reallocdebe tener sufree. Asigna y libera en el mismo nivel de abstracción.freedos veces el mismo puntero o liberar algo que no vino demalloces undefined behavior.
Buffer overflow: por qué es peligroso
Un buffer overflow ocurre cuando escribes más allá de la memoria reservada. C no comprueba los límites; strcpy, gets, sprintf escriben sin freno:
#include <string.h>
int main(void) {
char buf[8];
strcpy(buf, "esta cadena es mucho mas larga que ocho"); // ⚠️ desborda buf
return 0;
}
¿Qué pasa? Escribes sobre memoria adyacente: otros locals, el stack frame de la función llamadora, el return address (que indica a dónde volver). Si un atacante controla el contenido, puede sobrescribir el return address y redirigir la ejecución a su código (exploit clásico, ROP, shellcode).
| Escritura insegura | Alternativa segura |
|---|---|
strcpy(dst, src) |
strncpy / strlcpy (con límite) |
gets(buf) |
fgets(buf, n, stdin) |
sprintf(buf, fmt, x) |
snprintf(buf, n, fmt, x) |
💡 Los compiladores modernos y los sistemas operativos mitigan esto: stack canaries (valor centinela), ASLR (aleatoriza direcciones), DEP/NX (memoria no ejecutable). Pero la responsabilidad de no desbordar sigue siendo del programador.
Cheatsheet
| Necesito… | Código |
|---|---|
| Dirección de una variable | &x |
| Valor apuntado | *p |
| Campo de struct por puntero | p->campo |
| Reservar en heap | malloc(n * sizeof(T)) |
| Liberar | free(p); p = NULL; |
| Tamaño real | sizeof(x) |
| Puntero nulo | p = NULL; |
| Pasar y modificar | f(&x) + void f(int *p) |
Para profundizar
- The C Programming Language (K&R): el libro canónico, punteros y memoria incluidos.
- Learning the C Language (cplusplus): tutorial de punteros.
- Stanford CS Library: Pointers and Memory: la guía clásica de Nick Parlante.
- Valgrind: detección de leaks y errores de memoria.
- OWASP Buffer Overflow: por qué es una vulnerabilidad de seguridad.
- Ruta completa: Sistemas y bajo nivel.