Memoria dinámica en C — teoría con malloc, calloc, realloc y free, un vector de notas que crece (y el Segmentation fault que me enseñó dónde vive cada cosa)
Primer mes de IC2, laboratorio de los jueves. Tenía que guardar las notas de una clase y pensé: «hago una función crear_notas() que monte el vector y me lo devuelva, como en Python». La escribí en dos minutos, compilé, ejecuté… y esto:
#include <stdio.h>
/* Quería una función que "fabricara" el vector de notas y me lo devolviera */
double *crear_notas(void) {
double notas[3] = {7.5, 4.0, 9.0}; /* vector LOCAL: vive en la pila de crear_notas */
return notas; /* devuelvo su dirección... pero al salir, deja de existir */
}
int main(void) {
double *v = crear_notas();
printf("Vector creado. Primera nota:\n");
printf("%.1f\n", v[0]); /* intento leer un vector que ya no existe */
return 0;
}
Al compilar con gcc -Wall -std=c11, el compilador ya me estaba avisando (y yo, claro, no lo leí):
gancho.c: In function 'crear_notas': gancho.c:6:12: warning: function returns address of local variable [-Wreturn-local-addr]
Y al ejecutar:
Vector creado. Primera nota: Segmentation fault (core dumped)
El programa imprime la primera línea y muere. ¿Por qué? Porque notas es un vector local: vive dentro de crear_notas y desaparece en cuanto la función hace return. Le estaba devolviendo a main la dirección de algo que ya no existe. GCC, al detectarlo, incluso cambia esa dirección por NULL, y leer v[0] es leer la dirección 0: Segmentation fault.
Hoy aprendes a pedir memoria que sobrevive a las funciones y cuyo tamaño decides mientras el programa se ejecuta: la memoria dinámica. Usaremos malloc, calloc, realloc y free con un ejemplo que nos acompañará en los tres artículos: las notas de una clase, cuando no sabes cuántos alumnos hay hasta el último momento. Si no tienes fresco lo de las direcciones y el *, repasa antes punteros en C y funciones en C. Cuando acabes, la práctica de memoria dinámica te espera con tres programas completos.
Tabla de Contenidos
Dos sitios para guardar datos: la pila y el heap
Cuando ejecutas un programa en C, sus variables pueden vivir en dos zonas de memoria muy distintas.
La pila (stack) es donde viven las variables locales y los parámetros. Cada vez que llamas a una función se apila un «marco» con sus variables, y cuando la función termina, ese marco se quita entero. Es rápida y automática: no tienes que hacer nada para liberarla. A cambio, tiene dos limitaciones:
- El tamaño de lo que guardas se fija al compilar:
double notas[30]son 30 casillas y punto. - Lo que hay en el marco de una función muere con ella.
El heap (montón, o almacenamiento libre) es una zona grande que el programa puede pedir en tiempo de ejecución. Pides los bytes que necesites, cuando los necesites, y siguen ahí hasta que tú digas que ya no los quieres. Eso es la memoria dinámica.
Fíjate en el dibujo: a la izquierda, el marco de crear_notas con su vector local, que se borra al hacer return (eso es lo que rompió el gancho). Abajo, el marco de main con un puntero. A la derecha, el bloque del heap: el puntero de main apunta a él, y ese bloque no depende de ninguna función.
En C no hay recolector de basura como en Python o Java. Si pides memoria del heap, tú eres responsable de devolverla. Es el precio de tener el control.
¿Cuándo necesito memoria dinámica?
| Situación | ¿Pila o heap? | Por qué |
|---|---|---|
Sé el tamaño al escribir el código (int dias[7]) | Pila | Es fijo, la pila es más rápida y se libera sola |
| El tamaño lo dice el usuario o un archivo al ejecutar | Heap | La pila necesita el tamaño al compilar |
| Una función tiene que devolver un vector | Heap | Lo local muere con la función |
| El vector tiene que crecer o encoger | Heap | Solo realloc cambia el tamaño de un bloque |
| Muchos datos (millones de casillas) | Heap | La pila es pequeña (unos pocos MB) |
sizeof: malloc cuenta bytes, no casillas
Antes de pedir memoria hay que saber cuánta pedir, y aquí está la trampa número uno del tema: las funciones de memoria dinámica trabajan en bytes. Si quieres 6 notas double, no pides 6: pides 6 × lo que ocupa un double. Para eso está sizeof.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
double fijas[5] = {7.5, 4.0, 9.0, 6.5, 8.0}; /* estático: 5 casillas para siempre */
double *din = malloc(5 * sizeof(double)); /* dinámico: 5 casillas pedidas al heap */
if (din == NULL) return 1; /* sin memoria: no seguimos */
printf("sizeof(double) = %zu bytes\n", sizeof(double));
printf("sizeof(fijas) = %zu bytes (5 x 8)\n", sizeof(fijas));
printf("sizeof(din) = %zu bytes (es un puntero)\n", sizeof(din));
printf("bytes pedidos = %zu\n", 5 * sizeof(double));
printf("casillas fijas = %zu\n", sizeof(fijas) / sizeof(fijas[0]));
free(din); /* lo que se pide con malloc se devuelve con free */
return 0;
}
Salida:
sizeof(double) = 8 bytes sizeof(fijas) = 40 bytes (5 x 8) sizeof(din) = 8 bytes (es un puntero) bytes pedidos = 40 casillas fijas = 5
Vamos línea a línea:
sizeof(double)vale 8: cadadoubleocupa 8 bytes en un ordenador de hoy.sizeof(fijas)vale 40: es un vector de verdad, así quesizeofmide el vector entero, 5 × 8 = 40.sizeof(din)vale 8, no 40.dinno es un vector, es un puntero, y un puntero ocupa 8 bytes apunte a lo que apunte.sizeofno tiene ni idea de cuántos bytes hay al otro lado de la flecha.5 * sizeof(double)= 5 × 8 = 40: los bytes que hemos pedido de verdad.sizeof(fijas) / sizeof(fijas[0])= 40 / 8 = 5: el truco clásico para contar casillas… que solo funciona con vectores, nunca con punteros.
El dibujo lo deja claro: malloc(6) te da 6 bytes, que no llegan ni a una nota. malloc(6 * sizeof(double)) te da 48 bytes, justo 6 casillas de 8.
Escribir malloc(n) pensando en «n notas». El compilador no se queja, el programa a veces parece funcionar… y estás escribiendo en memoria que no es tuya. Lo veremos en directo en los ejercicios.
En lugar de repetir el tipo, escribe malloc(n * sizeof *notas). sizeof *notas es «lo que ocupa aquello a lo que apunta notas«. Si un día cambias double por float, la línea sigue siendo correcta sin tocarla.
malloc: pedir memoria en tiempo de ejecución
malloc (de memory allocation) está en <stdlib.h>. Recibe un número de bytes y devuelve un puntero al principio del bloque reservado, o NULL si no hay memoria suficiente:
double *notas = malloc(n * sizeof(double));
Lo que devuelve es un void *, un «puntero a lo que sea». En C se convierte solo al tipo de la variable, por eso no hace falta el cast. En tus apuntes verás (int*) malloc(...): es lo mismo, solo que más largo (en C++ sí es obligatorio).
Este programa reserva sitio para un número de notas que «llega» al ejecutar:
#include <stdio.h>
#include <stdlib.h> /* aquí viven malloc, calloc, realloc y free */
int main(void) {
int n = 6; /* imagina que lo ha tecleado el profe: no lo sabíamos al compilar */
const double datos[] = {7.5, 4.0, 9.0, 6.5, 3.5, 8.0};
/* n casillas de double = n * sizeof(double) bytes (no n bytes) */
double *notas = malloc(n * sizeof(double));
if (notas == NULL) { /* malloc devuelve NULL si no hay memoria */
printf("No hay memoria para %d notas\n", n);
return 1;
}
double suma = 0;
for (int i = 0; i < n; i++) {
notas[i] = datos[i]; /* se usa como un vector normal: notas[i] */
suma += notas[i];
}
printf("Reservados %zu bytes para %d notas\n", n * sizeof(double), n);
printf("Media: %.2f\n", suma / n);
free(notas); /* devolvemos la memoria */
notas = NULL; /* y dejamos claro que ya no apunta a nada válido */
return 0;
}
Salida:
Reservados 48 bytes para 6 notas Media: 6.42
Entre líneas:
- **
int n = 6;** hace de «lo que teclea el profe». Conscanffuncionaría igual: el tamaño se decide al ejecutar, que es lo importante. - **
if (notas == NULL)** no es paranoia: simallocno consigue memoria devuelveNULL, y usarnotas[i]sobreNULLes unSegmentation faultasegurado. Siempre se comprueba. - **
notas[i]**: una vez reservado, el bloque se usa exactamente igual que un vector. Recuerda de punteros en C quenotas[i]es lo mismo que*(notas + i). - Los bytes: 6 × 8 = 48, como dice la salida.
- La media: 7.5 + 4.0 + 9.0 + 6.5 + 3.5 + 8.0 = 38.5, y 38.5 / 6 = 6.4166…, que con
%.2fse imprime como 6.42. - **
free(notas); notas = NULL;** devuelve el bloque y deja el puntero «desactivado». Lo explicamos bien más abajo.
malloc no pone a cero lo que reserva. Las casillas recién pedidas contienen lo que hubiera antes en esa memoria (basura). Si las lees antes de escribirlas, el resultado es imprevisible.
calloc: memoria que empieza a cero
calloc (de contiguous allocation) hace lo mismo que malloc con dos diferencias:
- Recibe dos argumentos: cuántas casillas y cuánto ocupa cada una:
calloc(11, sizeof(int)). - Pone todos los bytes a cero.
¿Cuándo importa eso? Cuando el bloque va a ser una colección de contadores. Este programa hace un histograma de notas: un contador por cada nota de 0 a 10.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
const int notas[] = {5, 7, 7, 4, 9, 5, 6, 7, 3, 5, 10, 7};
int n = sizeof(notas) / sizeof(notas[0]); /* 12 notas */
/* 11 contadores (notas 0..10) que TIENEN que empezar a cero: trabajo para calloc */
int *cuantos = calloc(11, sizeof(int));
if (cuantos == NULL) return 1;
for (int i = 0; i < n; i++)
cuantos[notas[i]]++; /* la nota hace de índice de su contador */
for (int nota = 0; nota <= 10; nota++) {
printf("%2d | ", nota);
for (int k = 0; k < cuantos[nota]; k++)
printf("*"); /* una estrella por alumno con esa nota */
printf(" (%d)\n", cuantos[nota]);
}
free(cuantos);
return 0;
}
Salida:
0 | (0) 1 | (0) 2 | (0) 3 | * (1) 4 | * (1) 5 | *** (3) 6 | * (1) 7 | **** (4) 8 | (0) 9 | * (1) 10 | * (1)
La línea clave es cuantos[notas[i]]++: la nota hace de índice. Cuando llega un 7, se suma uno a cuantos[7]. Si los contadores no empezaran en cero, cada ++ sumaría sobre basura. Comprobemos dos filas a mano: en la lista hay tres 5 (posiciones 0, 5 y 9) y cuatro 7 (posiciones 1, 2, 7 y 11). Total: 1 + 1 + 3 + 1 + 4 + 1 + 1 = 12 notas, las mismas que tiene el vector.
malloc(n * tam) | calloc(n, tam) | |
|---|---|---|
| Argumentos | Bytes totales | Casillas y tamaño de cada una |
| Contenido inicial | Basura | Todo a cero |
| Velocidad | Algo más rápido | Algo más lento (tiene que poner ceros) |
| Úsalo para… | Datos que vas a rellenar enseguida | Contadores, acumuladores, tablas que empiezan vacías |
Con malloc + un bucle que ponga ceros conseguirías lo mismo, pero calloc lo hace en una línea y sin olvidos.
realloc: cambiar el tamaño sin perder los datos
Aquí está la gran ventaja de la memoria dinámica: un bloque puede crecer o encoger. realloc(puntero, nuevos_bytes) intenta ampliar el bloque donde está; si no puede, reserva uno nuevo, copia los datos y libera el viejo. Te devuelve la dirección del bloque bueno (que puede ser distinta) o NULL si no hay memoria.
En nuestra clase han llegado dos alumnos tarde:
#include <stdio.h>
#include <stdlib.h>
void mostrar(const double *v, int n) {
printf("[");
for (int i = 0; i < n; i++)
printf(i == 0 ? "%.1f" : ", %.1f", v[i]);
printf("] (%d notas)\n", n);
}
int main(void) {
int n = 4;
double *notas = malloc(n * sizeof(double));
if (notas == NULL) return 1;
const double primeras[] = {7.5, 4.0, 9.0, 6.5};
for (int i = 0; i < n; i++) notas[i] = primeras[i];
mostrar(notas, n);
/* Llegan 2 alumnos tarde: necesitamos 6 casillas */
int nuevo_n = n + 2;
double *tmp = realloc(notas, nuevo_n * sizeof(double)); /* resultado en un puntero TEMPORAL */
if (tmp == NULL) { /* si falla, notas sigue intacto: no perdemos nada */
printf("No se pudo ampliar; sigo con %d notas\n", n);
free(notas);
return 1;
}
notas = tmp; /* solo con éxito actualizamos notas */
notas[4] = 5.5; /* las casillas nuevas llegan sin inicializar: */
notas[5] = 8.0; /* hay que rellenarlas nosotros */
n = nuevo_n;
mostrar(notas, n); /* las 4 primeras notas siguen ahí */
free(notas);
return 0;
}
Salida:
[7.5, 4.0, 9.0, 6.5] (4 notas) [7.5, 4.0, 9.0, 6.5, 5.5, 8.0] (6 notas)
Las 4 primeras notas siguen intactas después de crecer: realloc las ha conservado. Hagamos zoom a la parte delicada:
/* Llegan 2 alumnos tarde: necesitamos 6 casillas */
int nuevo_n = n + 2;
double *tmp = realloc(notas, nuevo_n * sizeof(double)); /* resultado en un puntero TEMPORAL */
if (tmp == NULL) { /* si falla, notas sigue intacto: no perdemos nada */
printf("No se pudo ampliar; sigo con %d notas\n", n);
free(notas);
return 1;
}
notas = tmp; /* solo con éxito actualizamos notas */
notas[4] = 5.5; /* las casillas nuevas llegan sin inicializar: */
notas[5] = 8.0; /* hay que rellenarlas nosotros */
n = nuevo_n;
mostrar(notas, n); /* las 4 primeras notas siguen ahí */
¿Por qué ese tmp? Porque realloc puede fallar. Si escribes directamente notas = realloc(notas, ...) y devuelve NULL, acabas de sobrescribir tu único puntero al bloque viejo con NULL: has perdido las notas y además ese bloque se queda reservado sin que nadie pueda liberarlo (una fuga de memoria). Con el temporal, si falla, notas sigue apuntando a los datos de siempre.
Mira los tres casos del dibujo. En los dos de la izquierda, pase lo que pase, nunca pierdes el vector. En el de la derecha, un realloc fallido te deja con NULL y un bloque huérfano.
v = realloc(v, ...). Funciona el 99,9 % de las veces, y justo por eso cuesta tanto encontrarlo cuando falla. Usa siempre un puntero temporal.
Las casillas nuevas que añade realloc no se inicializan (como con malloc). Por eso el programa rellena notas[4] y notas[5] antes de mostrarlas.
Dos casos especiales que conviene conocer: realloc(NULL, bytes) se comporta como malloc(bytes), y encoger un bloque (pasar de 8 a 7 casillas, por ejemplo) también se hace con realloc. Lo usaremos en la práctica.
free: devolver lo que pediste
Todo bloque que pides con malloc, calloc o realloc se devuelve una vez con free(puntero). Ahora podemos arreglar el gancho: la función reserva en el heap, y main, que es quien usa el vector, lo libera.
#include <stdio.h>
#include <stdlib.h>
/* Ahora el vector vive en el heap: sobrevive al return. Quien llama debe liberarlo. */
double *crear_notas(int n, const double *datos) {
double *v = malloc(n * sizeof *v); /* sizeof *v = tamaño de lo que apunta v */
if (v == NULL) return NULL; /* el fallo se le pasa a quien llama */
for (int i = 0; i < n; i++)
v[i] = datos[i];
return v; /* devolvemos una dirección que sigue siendo válida */
}
int main(void) {
const double datos[] = {7.5, 4.0, 9.0};
double *v = crear_notas(3, datos);
if (v == NULL) return 1;
printf("Vector creado. Primera nota:\n");
printf("%.1f\n", v[0]);
free(v); /* main lo pidió (a través de crear_notas): main lo libera */
v = NULL;
free(v); /* free(NULL) no hace nada: por eso conviene poner NULL tras liberar */
printf("Liberado; el segundo free(NULL) no hizo nada\n");
return 0;
}
Salida:
Vector creado. Primera nota: 7.5 Liberado; el segundo free(NULL) no hizo nada
Esta vez la primera nota sale bien, porque el bloque está en el heap y no muere con crear_notas. Tres ideas importantes:
- Quien recibe el bloque es su dueño.
crear_notaslo reserva, pero se lo entrega amain, ymaintiene que liberarlo. Es buena costumbre decirlo en un comentario encima de la función. - **
freeno cambia el puntero.** Después defree(v),vsigue guardando la misma dirección, pero esa memoria ya no es tuya: es un puntero colgante (dangling pointer). Usarlo es un error. - **Por eso se pone
v = NULL.**free(NULL)no hace nada, así que si alguien vuelve a liberar ese puntero por despiste, no pasa nada. Y leerv[0]convaNULLfalla enseguida y de forma clara, en lugar de devolver basura en silencio.
Las fugas de memoria y cómo verlas
¿Y si te olvidas del free? El programa no falla: simplemente esa memoria se queda reservada hasta que el programa termina. Si ocurre dentro de un bucle o en un programa que está horas encendido, la memoria se va acumulando. Es una fuga (memory leak). Este programa calcula la media sobre una copia y nunca la libera:
#include <stdio.h>
#include <stdlib.h>
/* Calcula la media de una copia de las notas... y se olvida de liberar la copia */
double media_copia(const double *notas, int n) {
double *copia = malloc(n * sizeof *copia);
if (copia == NULL) return -1;
double suma = 0;
for (int i = 0; i < n; i++) {
copia[i] = notas[i];
suma += copia[i];
}
return suma / n; /* FALTA free(copia): estos bytes se quedan reservados */
}
int main(void) {
const double notas[] = {7.5, 4.0, 9.0, 6.5, 3.5, 8.0};
for (int dia = 1; dia <= 3; dia++) /* tres consultas = tres copias perdidas */
printf("Consulta %d: media %.2f\n", dia, media_copia(notas, 6));
return 0;
}
Normalmente no verías nada raro. Pero si compilas con el detector de errores de memoria de GCC, AddressSanitizer (gcc -Wall -std=c11 -g -fsanitize=address fuga.c -o fuga), al terminar te enseña la fuga. Esta es la salida (recortada a las líneas importantes):
Consulta 1: media 6.42
Consulta 2: media 6.42
Consulta 3: media 6.42
=================================================================
==831==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 144 byte(s) in 3 object(s) allocated from:
#1 in media_copia fuga.c:6
#2 in main fuga.c:19
SUMMARY: AddressSanitizer: 144 byte(s) leaked in 3 allocation(s).
Léelo de abajo arriba: se han perdido 144 bytes en 3 bloques. 3 consultas × 6 notas × 8 bytes = 144. Y te dice dónde se reservaron: en media_copia, línea 6 de fuga.c, llamada desde main en la línea 19. La solución es un free(copia); antes del return.
Acostúmbrate a compilar tus prácticas de memoria dinámica con -g -fsanitize=address mientras las pruebas. Detecta fugas, escrituras fuera del bloque y dobles free en el momento. En Fedora también tienes valgrind --leak-check=full ./programa, que hace algo parecido sin recompilar.
Errores típicos con memoria dinámica
Devolver la dirección de un vector local (el gancho). Si una función tiene que devolver un vector, resérvalo con malloc.
Olvidar sizeof: malloc(n) reserva n bytes, no n casillas.
No comprobar NULL después de malloc, calloc o realloc.
v = realloc(v, ...) sin puntero temporal: si falla, pierdes el vector y lo dejas como fuga.
Usar un puntero después de free o liberarlo dos veces. Después de free(v), escribe v = NULL.
Usar sizeof(puntero) para saber cuántas casillas tiene un vector dinámico. Siempre vale 8. El tamaño lo tienes que guardar tú en una variable (n).
Míralo paso a paso en Python Tutor
Python Tutor no es solo para Python: elige C (gcc) en el desplegable de lenguaje, pega este programa y pulsa Visualize Execution. Ejecuta el código línea a línea y dibuja la pila (Stack) a la izquierda y el heap (Heap) a la derecha, con flechas para los punteros. Es justo lo que necesitamos para ver la memoria dinámica.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int n = 3;
double *notas = malloc(n * sizeof(double));
if (notas == NULL) return 1;
notas[0] = 7.5;
notas[1] = 4.0;
notas[2] = 9.0;
double *tmp = realloc(notas, 5 * sizeof(double));
if (tmp == NULL) { free(notas); return 1; }
notas = tmp;
notas[3] = 6.5;
notas[4] = 8.0;
n = 5;
printf("Ultima nota: %.1f\n", notas[n - 1]);
free(notas);
notas = NULL;
return 0;
}
Qué observar paso a paso:
- Tras el
malloc, en la zona Heap aparece un bloque de 3 casillas sin valores conocidos, ynotas(en la pila, dentro demain) es una flecha hacia él. - Al ejecutar las tres asignaciones, el bloque se rellena con 7.5, 4.0 y 9.0. Fíjate en que
notasno cambia: lo que cambia es lo que hay al otro lado de la flecha. - Tras el
realloc, el heap muestra un bloque de 5 casillas con las 3 notas copiadas. Según la herramienta, puede que el bloque haya cambiado de sitio:tmpapunta al bloque nuevo y, trasnotas = tmp, tambiénnotas. - Después de
free(notas), el bloque desaparece del heap ynotaspasa a ser una flecha a nada, hasta quenotas = NULLlo deja explícitamente vacío.
Resumen
| Función | Qué hace | Devuelve | Ojo con… |
|---|---|---|---|
malloc(bytes) | Reserva un bloque | Puntero al bloque o NULL | No inicializa; multiplica por sizeof |
calloc(n, tam) | Reserva n casillas de tam bytes, a cero | Puntero o NULL | Ideal para contadores |
realloc(p, bytes) | Cambia el tamaño conservando los datos | Puntero (quizá nuevo) o NULL | Usa un temporal; lo nuevo no se inicializa |
free(p) | Devuelve el bloque | Nada | Una vez por bloque; después, p = NULL |
Tres ideas para quedarte
- Pila para lo fijo y local, heap para lo que decides al ejecutar o tiene que sobrevivir a una función.
- Bytes, no casillas:
n * sizeof *v, y comprueba siempreNULL. - **Cada bloque tiene un dueño y un único
free.**reallocsiempre con temporal y, después de liberar,NULL.
Ahora toca practicarlo: en la práctica de memoria dinámica en C montamos un acta de notas que se ajusta a las notas válidas, un vector que va creciendo según llegan alumnos y una tabla dinámica de notas por asignatura. Y en el siguiente tema del temario de IC2, los structs y las cadenas, verás que la memoria dinámica es lo que permite guardar tantos registros como haga falta.
Siguiente paso: prácticaMemoria dinámica en C — práctica con un acta de notas que se ajusta, un vector que crece al llegar alumnos y una tabla de notas en el heap (y el realloc que casi me deja sin datos)¿Te ha servido este artículo? Así puedes apoyar SergioLearns →

Un comentario