🔎 Buscar

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

Wiki / Apuntes📖 Contenido

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;
}
  • x es el valor.
  • &x es la dirección donde vive ese valor.
  • %p imprime 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, 0x1 valores 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 malloc y la devuelves con free. 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 un int». *p es el int, p es 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). sizeof siempre 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/realloc debe tener su free. Asigna y libera en el mismo nivel de abstracción. free dos veces el mismo puntero o liberar algo que no vino de malloc es 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

Estudio · Recursos de todo el mundo (inglés, chino, japonés, español, francés, ruso…) curados y traducidos al español.