Memoria Dinámica. En este capítulo exploraremos una de las características más potentes del lenguaje C: la gestión dinámica de memoria. A diferencia de la memoria estática, que se reserva en tiempo de compilación, la memoria dinámica permite al programa reservar y liberar espacio en tiempo de ejecución según la necesitemos. Esto es especialmente útil para trabajar con estructuras de datos como listas, árboles o arreglos cuyo tamaño no se conoce de antemano y van creciendo y disminuyendo mientras el programa está en ejecución.
Aprenderemos a utilizar las funciones de la biblioteca estándar (malloc
, calloc
, realloc
y free
) para gestionar manualmente bloques de memoria en el heap. Veremos cómo estas funciones se integran con punteros, qué riesgos existen (como fugas de memoria o errores de segmentación) y cómo evitarlos mediante buenas prácticas. También estudiaremos la diferencia entre memoria estática, automática y dinámica, y cómo depurar errores relacionados con su uso incorrecto.
La gestión de la memoria es uno de los aspectos que conviene dominar, por lo que además de esta introducción que te vamos a enseñar te podemos aconsejar la lectura del libro: «Memory Management Algorithms and Implementation in C/C++ (Windows Programming/Development)» de Bill Blunden, Editorial Wordware Publishing Inc. para ampliar detalladamente su uso y asentar conocimientos.
Cómo organiza la memoria el Sistema Operativo en un programa en C
Cuando ejecutamos un programa en C, el sistema operativo reserva diferentes regiones de memoria para organizar los distintos tipos de datos y funciones que el programa necesita. Aunque estos detalles pueden variar ligeramente entre sistemas, en general la memoria de un proceso se organiza en cinco grandes segmentos:
1. Segmento de código (Text Segment)
- Contiene el código ejecutable del programa (las instrucciones compiladas).
- Es de solo lectura, para evitar que el programa modifique accidentalmente su propio código.
- También contiene las funciones y etiquetas como
main()
,printf()
, etc.
2. Segmento de datos (Data Segment)
- Se divide a su vez en dos subsegmentos:
- Datos inicializados: variables globales o estáticas que tienen un valor inicial (por ejemplo,
int x = 5;
). - Datos no inicializados (BSS): variables globales o estáticas sin valor inicial asignado (por ejemplo,
int x;
fuera de cualquier función).
- Datos inicializados: variables globales o estáticas que tienen un valor inicial (por ejemplo,
- Este segmento es reservado cuando se carga el programa en memoria.
3. Segmento de pila (Stack)
- Aquí se almacenan:
- Variables locales dentro de funciones.
- Parámetros pasados a las funciones.
- Dirección de retorno tras una llamada de función.
- La memoria en la pila se gestiona automáticamente: se reserva al entrar en una función y se libera al salir de ella.
- Tiene un tamaño limitado; un uso excesivo (como recursividad profunda) puede causar un desbordamiento de pila (stack overflow).
4. Segmento de montículo (Heap)
- Esta es la zona de memoria dinámica.
- Permite al programa reservar memoria en tiempo de ejecución mediante funciones como
malloc()
,calloc()
yrealloc()
. - A diferencia de la pila, el programador es responsable de liberar esta memoria con
free()
. - Su tamaño puede crecer o disminuir durante la ejecución del programa (dentro de los límites que impone el sistema).
5. Registros (Registers)
- Aunque no forman parte directamente de la RAM gestionada por el sistema operativo, los registros son pequeñas zonas de memoria ultrarrápida gestionadas por la CPU, donde se almacenan temporalmente variables y direcciones de memoria para operaciones inmediatas.
Esquema visual.
Podemos ver visualmente como el Sistema Operativo distribuye la memoria de un proceso:
+-----------------------+ ← Direcciones más bajas
| Segmento de código | → Código ejecutable
+-----------------------+
| Segmento de datos | → Variables globales / estáticas
+-----------------------+
| BSS (no inicializado) |
+-----------------------+
| Heap | ← Crece hacia arriba (direcciones mayores)
+-----------------------+
| Stack | ← Crece hacia abajo (direcciones menores)
+-----------------------+ ← Direcciones más altas
8.1 Funciones de asignación de memoria (malloc
, calloc
, realloc
, free
)
¿Qué es la memoria dinámica?
Como ya hemos adelantado, en C la memoria dinámica se refiere a la porción de memoria que se reserva en tiempo de ejecución, normalmente en el segmento heap de la memoria. Esto es útil cuando no sabemos de antemano cuánta memoria necesitaremos o cuando queremos que los datos persistan más allá del alcance de una función.
Para ello, el lenguaje C proporciona varias funciones definidas en la librería <stdlib.h>
. Una de las más utilizadas es:
malloc
– Memory Allocation
malloc
(Memory Allocation) reserva un bloque de memoria contiguo de un tamaño específico en bytes y devuelve un puntero al inicio de esa memoria. La sintaxis es:
void* malloc(size_t size);
size
: número de bytes que se desean reservar.- Devuelve: un puntero genérico (
void *
), que normalmente se convierte (cast) al tipo deseado. - Si la reserva falla (por falta de memoria), devuelve
NULL
.
Ejemplo 1: Reservar memoria para un solo int
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr = (int *) malloc(sizeof(int)); // reserva 4 bytes (en la mayoría de sistemas)
if (ptr == NULL) {
printf("Error al reservar memoria.\n");
return 1;
}
*ptr = 42;
printf("Valor almacenado: %d\n", *ptr);
free(ptr); // liberamos la memoria
return 0;
}
Explicación:
malloc(sizeof(int))
: reserva 4 bytes (suponiendo queint
ocupa 4 bytes).ptr
apunta a ese bloque de memoria.- Se guarda el número 42 en esa memoria usando
*ptr
. - Al final, se libera la memoria con
free(ptr)
para evitar fugas de memoria (memory leaks).
Ejemplo 2: Reservar memoria para un array dinámico
#include <stdio.h>
#include <stdlib.h>
int main() {
int n = 5;
int *arr = (int *) malloc(n * sizeof(int)); // reserva espacio para 5 enteros
if (arr == NULL) {
printf("Fallo en la reserva de memoria.\n");
return 1;
}
// Inicializamos el array
for (int i = 0; i < n; i++) {
arr[i] = i + 1;
}
// Mostramos el array
for (int i = 0; i < n; i++) {
printf("Elemento %d: %d\n", i, arr[i]);
}
free(arr); // Liberar memoria
return 0;
}
Explicación:
- Se reserva un bloque de memoria para un array de 5 enteros.
- Se accede a los elementos como si fuera un array común (
arr[i]
). - Es imprescindible liberar la memoria con
free()
.
¡Cuidado con los errores comunes!
- Olvidar liberar la memoria: puede provocar pérdida de memoria (leak).
- Usar memoria sin inicializar:
malloc
no inicializa la memoria (puede contener basura). Si quieres memoria con valores en cero, usacalloc()
en lugar demalloc()
. - Acceder fuera de los límites reservados: provoca comportamiento indefinido o errores de segmentación.
Alternativa: calloc
(reserva e inicializa el contenido con ceros)
int *arr = (int *) calloc(n, sizeof(int)); // reserva n elementos, todos a 0
A diferencia de malloc
, calloc
inicializa toda la memoria a cero.
Liberar memoria: free()
Toda memoria reservada con malloc
, calloc
o realloc
debe ser liberada manualmente con free()
cuando ya no sea necesaria:
free(ptr);
No hacerlo provoca fugas de memoria, especialmente en programas grandes o que corren por mucho tiempo.
¿Cuándo usar malloc
?
Usamos malloc
(y en general memoria dinámica):
- Cuando no sabemos el tamaño que necesitaremos hasta que el programa esté en ejecución.
- Cuando necesitamos estructuras grandes que no caben en la pila.
- Cuando queremos devolver memoria reservada desde funciones (pues las variables locales desaparecen al salir de la función).
realloc
– Resize Memory Block
realloc
cambia el tamaño de un bloque de memoria previamente reservado con malloc
o calloc
.
Sintaxis:
void* realloc(void *ptr, size_t new_size);
ptr
: puntero al bloque previamente reservado.
new_size
: nuevo tamaño en bytes.- Si no puede redimensionar en el mismo lugar,
realloc
asigna un nuevo bloque, copia los datos y libera el anterior.
Ejemplo:
#include <stdio.h>
#include <stdlib.h>
int main() {
int *arr = (int *) malloc(3 * sizeof(int));
if (arr == NULL) return 1;
arr[0] = 1; arr[1] = 2; arr[2] = 3;
// Redimensionamos a 5 elementos
arr = (int *) realloc(arr, 5 * sizeof(int));
if (arr == NULL) return 1;
arr[3] = 4;
arr[4] = 5;
for (int i = 0; i < 5; i++) {
printf("arr[%d] = %d\n", i, arr[i]);
}
free(arr);
return 0;
}
realloc
nos permite ampliar o reducir el tamaño sin perder los datos anteriores.
Si realloc
falla, devuelve NULL
y no libera el bloque original, por eso conviene usar una variable temporal:
int *temp = realloc(arr, new_size);
if (temp != NULL) {
arr = temp;
} else {
// manejar error
}
Tabla resumen.
Comparativa entre funciones
Función | Inicializa memoria | Parámetros | Cuándo usar |
---|---|---|---|
malloc | ❌ No | Tamaño en bytes | Si necesitas velocidad y te ocupas tú de la inicialización |
calloc | ✅ Sí (a 0) | Nº elementos y tamaño | Si necesitas memoria ya inicializada a cero |
realloc | ❌/✅ | Puntero y nuevo tamaño | Si necesitas cambiar el tamaño de memoria |
free | N/A | Puntero a memoria reservada | Para liberar y evitar leaks |
8.2 Gestión eficiente de memoria
¿Qué significa gestionar la memoria eficientemente?
La gestión eficiente de memoria consiste en usar solo la memoria necesaria, liberarla cuando ya no se use, y evitar errores como:
- Fugas de memoria (memory leaks): cuando se reserva memoria con
malloc
,calloc
orealloc
pero no se libera. - Accesos inválidos: usar memoria ya liberada o no inicializada.
- Fragmentación: reservar y liberar bloques de forma desorganizada puede llevar a una fragmentación que dificulta nuevas reservas.
Reglas básicas para gestionar memoria eficientemente
1. Reserva solo lo que necesites
No reserves bloques grandes «por si acaso». Usa estructuras dinámicas (listas, realloc, etc.) si el tamaño varía:
char *cadena = (char *) malloc(1000); // ❌ innecesario si vas a usar solo 20
Mejor:
char *cadena = (char *) malloc(21); // ✔️ 20 caracteres + '\0'
2. Libera memoria cuando ya no la uses
Usa free()
siempre que termines de utilizar un bloque dinámico:
int *arr = malloc(10 * sizeof(int));
/* ... usar arr ... */
free(arr); // ✔️
No hacerlo genera memory leaks que aumentan con el tiempo, especialmente en programas largos o que corren constantemente (como servicios).
3. Usa herramientas de análisis
Existen herramientas que detectan errores de gestión de memoria:
- Valgrind (Linux): detecta fugas de memoria, accesos erróneos, etc.
- ASan (Address Sanitizer): opción de compilación con GCC/Clang.
valgrind ./programa
Usar estas herramientas debería ser parte habitual del desarrollo con C.
4. No uses punteros colgantes
Después de liberar memoria, asigna el puntero a NULL
:
free(arr);
arr = NULL; // evita accesos erróneos
Así, si intentas acceder, el programa fallará inmediatamente en lugar de corromper datos.
5. Evita llamadas innecesarias a malloc
/free
No reserves y liberes dentro de bucles innecesariamente:
// Mala práctica
for (int i = 0; i < 1000; i++) {
int *x = malloc(sizeof(int));
// usar x
free(x);
}
Esto es costoso. En su lugar, reserva una vez:
int *x = malloc(sizeof(int));
for (int i = 0; i < 1000; i++) {
// usar x
}
free(x);
6. Organiza la memoria con estructuras
Para programas complejos, organiza los datos con estructuras (struct
) y considera liberar por partes si los recursos están divididos:
typedef struct {
char *nombre;
int *calificaciones;
} Estudiante;
void liberarEstudiante(Estudiante *e) {
free(e->nombre);
free(e->calificaciones);
}
Tabla resumen de errores comunes
Error | Descripción |
---|---|
double free() | Liberar el mismo bloque dos veces. |
use after free | Usar un puntero después de free . |
memory leak | No liberar memoria dinámica. |
realloc sin comprobación de retorno | Puede perder la dirección original si falla. |
Buenas prácticas de gestión de memoria
Buen hábito | Beneficio |
---|---|
Inicializar punteros a NULL | Seguridad |
Comprobar siempre el retorno de malloc /calloc /realloc | Robustez |
Usar free en el orden inverso al malloc | Mantenimiento lógico |
Agrupar malloc y free por módulo o función | Legibilidad |
Usar valgrind o asan en desarrollo | Prevención de errores |
Veamos un pequeo ejemplo con todo integrado.
#include <stdio.h>
#include <stdlib.h>
int main() {
int *numeros = malloc(5 * sizeof(int));
if (numeros == NULL) {
perror("No se pudo asignar memoria");
return 1;
}
for (int i = 0; i < 5; i++) {
numeros[i] = i * 10;
}
int *masNumeros = realloc(numeros, 10 * sizeof(int));
if (masNumeros == NULL) {
free(numeros); // liberamos memoria original si realloc falla
return 1;
}
numeros = masNumeros;
for (int i = 5; i < 10; i++) {
numeros[i] = i * 10;
}
for (int i = 0; i < 10; i++) {
printf("numeros[%d] = %d\n", i, numeros[i]);
}
free(numeros); // ✔️ gestión correcta
numeros = NULL; // ✔️ seguridad
return 0;
}
8.3 Ejemplos prácticos (listas, vectores)
Para entender mejor cómo se construyen y manipulan estructuras de datos dinámicas en C utilizando punteros y funciones de gestión de memoria dinámica (malloc
, free
, etc.) vamos a ver con unos ejemplos dos tipos de estructuras de datos: Las listas y los Vectores.
Ejemplo de Vector dinámico
Un vector (array) cuyo tamaño se determina en tiempo de ejecución (no sabemos su tamaño en tiempo de ejecución) se denomina vector dinámico, lo que implica que podemos añadir o quitar elemenos del vector según necesitemos..
En este ejemplo, vamos a reservar memoria para un vector de enteros, llenarlo con datos, mostrar su contenido y luego ampliar su tamaño con realloc
.
Código:
#include <stdio.h>
#include <stdlib.h>
int main() {
int n;
printf("¿Cuántos elementos quieres en el vector? ");
scanf("%d", &n);
int *vec = (int *) malloc(n * sizeof(int));
if (vec == NULL) {
printf("Error de memoria.\n");
return 1;
}
for (int i = 0; i < n; i++) {
vec[i] = i + 1;
}
printf("Vector original:\n");
for (int i = 0; i < n; i++) {
printf("%d ", vec[i]);
}
// Ampliar el vector
int nuevoTamanio = n + 3;
vec = realloc(vec, nuevoTamanio * sizeof(int));
if (vec == NULL) {
printf("Error al ampliar el vector.\n");
return 1;
}
for (int i = n; i < nuevoTamanio; i++) {
vec[i] = (i + 1) * 10;
}
printf("\nVector ampliado:\n");
for (int i = 0; i < nuevoTamanio; i++) {
printf("%d ", vec[i]);
}
free(vec);
return 0;
}
Explicación:
- Usamos
malloc
para reservar memoria paran
enteros. - Llenamos el vector con valores simples.
- Luego usamos
realloc
para ampliar el vector. - Los nuevos elementos se llenan con múltiplos de 10.
- Finalmente, liberamos la memoria.
Ejemplo de lista enlazada simple
Las listas enlazadas permiten almacenar una cantidad variable de elementos, donde cada nodo apunta al siguiente, digamos que es como una cadena donde cada elemento es un eslabón, contiene el dato y además se «engarza/apunta» al eslabón siguiente. Al ser lista simple, cada elemento apunta solo al siguiente, si fuese doblemente enlazada apuntaría al elemento anterior y al siguiente, pero para este primer ejemplo haremos la lista simple.
Código:
#include <stdio.h>
#include <stdlib.h>
typedef struct Nodo {
int dato;
struct Nodo *siguiente;
} Nodo;
void agregarAlFinal(Nodo **inicio, int valor) {
Nodo *nuevo = (Nodo *) malloc(sizeof(Nodo));
nuevo->dato = valor;
nuevo->siguiente = NULL;
if (*inicio == NULL) {
*inicio = nuevo;
} else {
Nodo *actual = *inicio;
while (actual->siguiente != NULL) {
actual = actual->siguiente;
}
actual->siguiente = nuevo;
}
}
void mostrarLista(Nodo *inicio) {
Nodo *actual = inicio;
while (actual != NULL) {
printf("%d -> ", actual->dato);
actual = actual->siguiente;
}
printf("NULL\n");
}
void liberarLista(Nodo *inicio) {
Nodo *temp;
while (inicio != NULL) {
temp = inicio;
inicio = inicio->siguiente;
free(temp);
}
}
int main() {
Nodo *lista = NULL;
agregarAlFinal(&lista, 10);
agregarAlFinal(&lista, 20);
agregarAlFinal(&lista, 30);
printf("Lista enlazada:\n");
mostrarLista(lista);
liberarLista(lista);
return 0;
}
Explicación:
- Se define una estructura
Nodo
con undato
y un puntero al siguiente nodo. agregarAlFinal()
permite insertar un nodo al final de la lista.mostrarLista()
imprime cada nodo hasta llegar aNULL
.liberarLista()
recorre la lista y libera cada nodo para evitar pérdidas de memoria.
Resumen
Aquí concluye el capítulo de la gestión dinámica de memoria. En este capítulo hemos explorado cómo gestionar de forma eficiente la memoria dinámica en C/C++. Comenzamos con una introducción a cómo el sistema operativo organiza la memoria (stack, heap, segmento de datos y código), lo cual es esencial para comprender el uso de punteros. Aprendimos a reservar y liberar memoria con funciones como malloc
, calloc
, realloc
y free
, y vimos cómo evitar errores comunes como las pérdidas de memoria y los punteros colgantes. Vayamos a la parte práctica.
Ejercicio 1: Reserva de memoria para un array dinámico
Enunciado:
Escribe un programa en C que pida al usuario cuántos enteros quiere almacenar, reserve dinámicamente un array de ese tamaño, permita al usuario introducir los valores y luego los muestre en pantalla. Finalmente, libera la memoria reservada.
Enunciado:
Escribe un programa en C que pida al usuario cuántos enteros quiere almacenar, reserve dinámicamente un array de ese tamaño, permita al usuario introducir los valores y luego los muestre en pantalla. Finalmente, libera la memoria reservada.
Código:
#include <stdio.h>
#include <stdlib.h>
int main() {
int n, i;
int *arr;
printf("¿Cuántos enteros quieres guardar? ");
scanf("%d", &n);
arr = (int *)malloc(n * sizeof(int));
if (arr == NULL) {
printf("Error al reservar memoria.\n");
return 1;
}
for (i = 0; i < n; i++) {
printf("Introduce el número %d: ", i + 1);
scanf("%d", &arr[i]);
}
printf("Los valores introducidos son:\n");
for (i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
free(arr); // ¡No olvides liberar la memoria!
return 0;
}
Explicación:
Este ejercicio refuerza el uso de malloc
para reservar memoria según el número de elementos requerido por el usuario. Se accede al array con notación de subíndice y se libera la memoria con free
.
Ejercicio 2: Redimensionamiento de un array dinámico con realloc
Enunciado:
Modifica el programa anterior para permitir que el usuario incremente el tamaño del array y añada más elementos sin perder los anteriores.
realloc
Enunciado:
Modifica el programa anterior para permitir que el usuario incremente el tamaño del array y añada más elementos sin perder los anteriores.
Código:
#include <stdio.h>
#include <stdlib.h>
int main() {
int *arr;
int n, nuevo_tam, i;
printf("Introduce el tamaño inicial del array: ");
scanf("%d", &n);
arr = (int *)malloc(n * sizeof(int));
if (arr == NULL) {
printf("Error de memoria.\n");
return 1;
}
for (i = 0; i < n; i++) {
printf("Elemento %d: ", i);
scanf("%d", &arr[i]);
}
printf("¿Cuántos elementos más deseas añadir?: ");
scanf("%d", &nuevo_tam);
arr = (int *)realloc(arr, (n + nuevo_tam) * sizeof(int));
if (arr == NULL) {
printf("Error al redimensionar memoria.\n");
return 1;
}
for (i = n; i < n + nuevo_tam; i++) {
printf("Nuevo elemento %d: ", i);
scanf("%d", &arr[i]);
}
printf("Array completo:\n");
for (i = 0; i < n + nuevo_tam; i++) {
printf("%d ", arr[i]);
}
free(arr);
return 0;
}
🔍 Explicación:
Este ejercicio introduce realloc
, que permite redimensionar un bloque de memoria ya existente. Se mantiene el contenido anterior y se añade más espacio para nuevos valores.
Ejercicio 3: Lista enlazada simple
Enunciado:
Crea un programa que implemente una lista enlazada simple. El programa debe permitir al usuario insertar tres valores y luego mostrarlos recorriendo la lista.
Enunciado:
Crea un programa que implemente una lista enlazada simple. El programa debe permitir al usuario insertar tres valores y luego mostrarlos recorriendo la lista.
Código:
#include <stdio.h>
#include <stdlib.h>
typedef struct Nodo {
int dato;
struct Nodo *siguiente;
} Nodo;
int main() {
Nodo *inicio = NULL, *nuevo = NULL, *actual = NULL;
int valor, i;
for (i = 0; i < 3; i++) {
printf("Introduce un valor: ");
scanf("%d", &valor);
nuevo = (Nodo *)malloc(sizeof(Nodo));
if (nuevo == NULL) {
printf("Fallo al asignar memoria.\n");
return 1;
}
nuevo->dato = valor;
nuevo->siguiente = NULL;
if (inicio == NULL) {
inicio = nuevo;
} else {
actual->siguiente = nuevo;
}
actual = nuevo;
}
printf("Elementos de la lista:\n");
actual = inicio;
while (actual != NULL) {
printf("%d -> ", actual->dato);
Nodo *temp = actual;
actual = actual->siguiente;
free(temp); // liberar cada nodo mientras lo recorremos
}
printf("NULL\n");
return 0;
}
Explicación:
Aquí se aplica el concepto de memoria dinámica para construir nodos enlazados. Cada nodo se crea con malloc
y se conecta al siguiente. Al final, liberamos cada nodo uno por uno.
Bibliografía del tutorial de C/C++.
- C/C++. Curso de programación. Autor: Miguel Angel Acera (Editorial: Anaya Multimedia)
- C/C++. Curso de programación. Autor: Francisco José Ceballos (Editorial: RA-MA)
- Un recorrido por C++. Autor Bjarne Stroustrup (Editorial: Anaya Multimedia)
- 115 Ejercicios resueltos de programación C++. Autor Jorge Fernando Betancourt e Inma Yolanda Polanco (Editorial: RA-MA)