Introducción¶
Este apunte explora conceptos avanzados de memoria dinámica en C, construyendo sobre las bases presentadas en Memoria y Punteros. Aquí profundizamos en el manejo de Estructuras que contienen punteros, problemas comunes de gestión de memoria, y técnicas para trabajar con matrices dinámicas.
Punteros a Estructuras¶
Cuando una estructura (struct) contiene punteros a otros datos, debemos gestionar la memoria en múltiples niveles. Como vimos en El Montón (Heap), cada llamada a malloc reserva memoria en el heap que debe ser liberada explícitamente. Con estructuras anidadas, este principio se aplica recursivamente.
Creación de Estructuras Dinámicas¶
Para crear una instancia de una estructura que contiene punteros (como char* nombre), se requieren múltiples asignaciones de memoria. Consideremos una estructura persona_t:
typedef struct {
char *nombre;
int edad;
} persona_t;El proceso de creación involucra tres pasos fundamentales:
Paso 1: Asignar la Estructura Contenedora¶
Primero, reservamos memoria para la estructura en sí:
persona_t *nuevo = malloc(sizeof(persona_t));
if (nuevo == NULL) {
// Manejar error de asignación
return NULL;
}Paso 2: Asignar Miembros Internos¶
Luego, reservamos memoria para cada puntero dentro de la estructura:
// +1 para el carácter nulo '\0'
nuevo->nombre = malloc(sizeof(char) * (strlen(nombre) + 1));
if (nuevo->nombre == NULL) {
free(nuevo); // Liberar lo ya asignado
return NULL;
}Paso 3: Copiar Datos¶
Finalmente, copiamos los datos a la memoria recién asignada:
strcpy(nuevo->nombre, nombre);
nuevo->edad = edad;Operador Flecha (->)¶
El operador -> es un atajo sintáctico para acceder a miembros de una estructura a través de un puntero. Como se explica en Punteros, este operador combina la desreferencia y el acceso a miembro en una sola operación.
Equivalencia:
puntero->miembro ≡ (*puntero).miembroEjemplo comparativo:
persona_t *p = /* ... */;
// Usando ->
p->edad = 30;
p->nombre[0] = 'J';
// Equivalente sin ->
(*p).edad = 30;
(*p).nombre[0] = 'J';La notación con -> es más legible y es la forma idiomática en C para trabajar con punteros a estructuras.
Destrucción de Estructuras Dinámicas¶
La liberación de memoria debe seguir el orden inverso al de la creación. Este patrón se conoce como “de adentro hacia afuera” o LIFO (Last In, First Out).
Orden Correcto de Liberación¶
void persona_destruir(persona_t *persona) {
if (persona == NULL) {
return; // Nada que hacer
}
// 1. Liberar miembros internos primero
free(persona->nombre);
// 2. Liberar la estructura contenedora
free(persona);
}¿Por Qué Este Orden?¶
Si liberás persona primero, perdés el puntero a persona->nombre. Una vez que free(persona) se ejecuta, acceder a persona->nombre es comportamiento indefinido (ver Dangling Pointer (Puntero Colgante)). Esto resulta en un memory leak porque la memoria de nombre queda asignada pero inaccesible.
Figure 1:Orden correcto vs incorrecto de liberación de memoria en estructuras anidadas.
Generalización: Estructuras con Múltiples Punteros¶
Para estructuras con varios niveles de punteros, aplicá el mismo principio recursivamente:
typedef struct {
char *nombre;
char *apellido;
int *calificaciones; // Array dinámico
} estudiante_t;
void estudiante_destruir(estudiante_t *est) {
if (est == NULL) return;
free(est->calificaciones); // Nivel más profundo primero
free(est->apellido);
free(est->nombre);
free(est); // Contenedor al final
}Problemas Comunes de Memoria Dinámica¶
Esta sección detalla errores frecuentes en la gestión de memoria dinámica y sus soluciones. Estos problemas se amplían en Errores Comunes y Peligros.
Fragmentación del Heap¶
La fragmentación externa ocurre cuando la memoria libre se divide en bloques pequeños y no contiguos, aunque la suma total de memoria libre sea suficiente para una solicitud.
Escenario Ilustrativo¶
Figure 2:Proceso de fragmentación del heap: bloques libres no contiguos impiden asignaciones grandes.
Problema: Aunque hay 150 KB libres (100 + 50), no podés asignar un bloque contiguo de 120 KB.
Soluciones¶
Table 1:Estrategias contra Fragmentación
Estrategia | Descripción | Cuándo Usar |
|---|---|---|
Asignación en bloque | Pedir memoria en bloques grandes, subdividir internamente | Arrays redimensionables, pools de objetos |
Memory pools | Pre-asignar conjunto de objetos del mismo tamaño | Asignaciones/liberaciones frecuentes del mismo tipo |
Compactación | Reorganizar bloques para unir espacios libres | Raramente posible en C (requiere actualizar punteros) |
Punteros Colgantes (Dangling Pointers)¶
Un puntero colgante (dangling pointer) es un puntero que apunta a memoria que ya ha sido liberada con free. Este es uno de los errores más peligrosos en C (ver Dangling Pointer (Puntero Colgante) para más detalles).
Causa¶
Cuando llamás free(puntero), la memoria se libera pero la variable puntero no cambia. Sigue conteniendo la dirección antigua, que ahora es inválida.
int *datos = malloc(sizeof(int) * 10);
// ... usar datos ...
free(datos);
// En este punto, 'datos' sigue apuntando a la dirección antigua
// pero esa memoria puede estar siendo usada por otra parte del programaRiesgo: Comportamiento Indefinido¶
Usar un puntero colgante (leer o escribir) invoca undefined behavior. El programa puede:
Aparentar funcionar correctamente (el peor caso, porque oculta el error)
Crashear inmediatamente
Corromper otros datos silenciosamente
Comportarse de forma impredecible
free(datos);
datos[0] = 42; // UNDEFINED BEHAVIORSolución: Poner en NULL Después de free¶
free(puntero);
puntero = NULL; // Ahora es seguro verificar con if (puntero != NULL)Ejemplo de Función Defensiva¶
void datos_liberar(int **ptr) {
if (ptr == NULL || *ptr == NULL) {
return; // Nada que hacer
}
free(*ptr);
*ptr = NULL; // El llamador ve el puntero actualizado
}
// Uso:
int *datos = malloc(sizeof(int) * 10);
datos_liberar(&datos); // Pasa la dirección del puntero
// Ahora datos == NULLLiberar Memoria No Dinámica¶
Intentar liberar memoria que no fue asignada dinámicamente es un error grave que resulta en undefined behavior.
Regla Fundamental¶
Errores Comunes¶
1. Liberar variables del stack:
int main() {
char automatica[] = "hola mundo"; // En el stack
free(automatica); // ERROR: undefined behavior
}Como se explica en Segmentación de la Memoria, las variables automáticas se gestionan automáticamente en el stack. No necesitan (ni deben) ser liberadas manualmente.
2. Liberar literales de cadena:
char *mensaje = "Hola"; // Literal en .rodata (read-only data)
free(mensaje); // ERROR: undefined behaviorLos literales de cadena residen en el segmento .rodata (ver Segmentación de la Memoria) y son de solo lectura.
3. Liberar variables globales:
int global_arr[100]; // Segmento .bss o .data
void funcion() {
free(global_arr); // ERROR: undefined behavior
}Funciones Adicionales de Gestión de Memoria¶
Más allá de malloc y free, C proporciona funciones adicionales para manipular memoria dinámica. Estas se detallan completamente en Funciones de Gestión de Memoria (<stdlib.h>).
calloc: Asignación con Inicialización¶
void *calloc(size_t cantidad, size_t tamaño);Asigna memoria para un arreglo de cantidad elementos, cada uno de tamaño bytes. Crucialmente, inicializa toda la memoria a cero.
Comparación con malloc:
// Usando malloc
int *arr1 = malloc(10 * sizeof(int));
// arr1[i] contiene basura
// Usando calloc
int *arr2 = calloc(10, sizeof(int));
// arr2[i] == 0 para todo irealloc: Redimensionar Bloques¶
void *realloc(void *bloque, size_t nuevo_tamaño);Cambia el tamaño de un bloque de memoria existente. Esta función es fundamental para implementar arrays redimensionables.
Comportamiento de realloc¶
Table 2:Casos de realloc
Condición | Comportamiento | Notas |
|---|---|---|
| Equivalente a | Útil para simplificar código |
| Equivalente a | Devuelve |
| Expande el bloque | Memoria adicional no inicializada |
| Reduce el bloque | Datos más allá de |
Uso Correcto de realloc¶
Patrón correcto:
int *temp = realloc(arr, nuevo_tamaño * sizeof(int));
if (temp == NULL) {
// realloc falló, arr sigue válido
// Manejar error (liberar arr si es necesario)
return ERROR;
}
arr = temp; // Éxito: actualizar puntero¿Por Qué realloc Puede Mover el Bloque?¶
Si no hay espacio contiguo para expandir el bloque en su ubicación actual, realloc:
Asigna un nuevo bloque más grande en otra ubicación
Copia los datos del bloque original al nuevo
Libera el bloque original
Retorna la dirección del nuevo bloque
Figure 3:Proceso de realloc cuando debe mover el bloque a una nueva ubicación.
memset: Relleno de Memoria¶
void *memset(void *destino, int valor, size_t count);Rellena los primeros count bytes de destino con valor (convertido a unsigned char).
Usos comunes:
// Inicializar array a cero
int arr[100];
memset(arr, 0, sizeof(arr));
// Limpiar buffer sensible
char password[64];
// ... usar password ...
memset(password, 0, sizeof(password)); // Borrar rastrosmemcpy: Copia de Memoria¶
void *memcpy(void *destino, const void *origen, size_t count);Copia count bytes desde origen a destino. Las regiones no deben solaparse.
Ejemplo:
int src[5] = {1, 2, 3, 4, 5};
int dst[5];
memcpy(dst, src, sizeof(src));
// dst == {1, 2, 3, 4, 5}Arreglos de Largo Variable (VLA)¶
Los VLA (Variable Length Arrays) son arreglos cuyo tamaño se determina en tiempo de ejecución, no en compilación.
void funcion(int cantidad) {
int arreglo[cantidad]; // <-- VLA: tamaño determinado en runtime
}¿Por Qué Prohibimos VLAs?¶
1. Asignación en el Stack¶
Los VLAs se crean en el stack, no en el heap (ver Comparación Stack vs Heap). El stack tiene tamaño limitado (típicamente 1-8 MB).
void procesar(int n) {
int datos[n]; // VLA en el stack
// Si n es grande (por ejemplo, 1,000,000), esto causa stack overflow
}2. No Hay Mecanismo de Error¶
A diferencia de malloc, que retorna NULL si falla, un VLA que excede el stack simplemente crashea el programa:
int *heap_arr = malloc(1000000 * sizeof(int));
if (heap_arr == NULL) {
// Podemos manejar el error
fprintf(stderr, "Memoria insuficiente\n");
return ERROR;
}
// vs
int stack_arr[1000000]; // VLA: ¡CRASH sin posibilidad de recuperación!3. Problemas de Portabilidad¶
El límite del stack varía entre plataformas y configuraciones. Código que funciona en una máquina puede crashear en otra.
Alternativa Correcta: Memoria Dinámica¶
void funcion(int cantidad) {
int *arreglo = malloc(cantidad * sizeof(int));
if (arreglo == NULL) {
// Manejar error
return;
}
// Usar arreglo...
free(arreglo);
}Matrices Dinámicas¶
Una matriz (arreglo bidimensional) puede implementarse de varias formas en memoria dinámica. Cada enfoque tiene trade-offs en complejidad, eficiencia de memoria y acceso.
Como se explica en El Montón (Heap), la memoria dinámica nos permite crear estructuras de tamaño arbitrario. Las matrices dinámicas extienden este concepto a dos dimensiones.
Enfoque 1: Matriz “Dentada” (Array de Punteros)¶
Este enfoque crea un arreglo de punteros, donde cada puntero apunta a una fila (otro arreglo). Se llama “dentada” (jagged array) porque cada fila puede tener largo diferente (aunque típicamente usamos filas del mismo tamaño).
Figure 4:Representación de una matriz dentada: array de punteros a arrays.
Asignación¶
int **matriz;
int filas = 3, columnas = 4;
// Paso 1: Array de punteros a filas
matriz = (int **)malloc(filas * sizeof(int *));
if (matriz == NULL) {
return NULL;
}
// Paso 2: Cada fila
for (int i = 0; i < filas; i++) {
matriz[i] = (int *)malloc(columnas * sizeof(int));
if (matriz[i] == NULL) {
// Error: liberar lo ya asignado
for (int j = 0; j < i; j++) {
free(matriz[j]);
}
free(matriz);
return NULL;
}
}Acceso¶
El acceso es natural con la sintaxis estándar de C:
matriz[i][j] = 42;
int valor = matriz[i][j];Liberación¶
Siguiendo el principio “de adentro hacia afuera” (Destrucción de Estructuras Dinámicas):
// 1. Liberar cada fila
for (int i = 0; i < filas; i++) {
free(matriz[i]);
}
// 2. Liberar el array de punteros
free(matriz);Ventajas y Desventajas¶
Ventajas:
Sintaxis natural
matriz[i][j]Filas pueden tener tamaños diferentes
Fácil de entender conceptualmente
Desventajas:
Fragmentación: Cada fila es un bloque separado en el heap
Overhead de memoria: Punteros adicionales para cada fila
Cache-unfriendly: Filas no están contiguas en memoria (ver Jerarquía de Memoria y Caché)
Enfoque 2: Bloque Único (Simulación Manual)¶
Este enfoque asigna toda la matriz como un único bloque contiguo en memoria. Es más eficiente pero requiere calcular índices manualmente.
Figure 5:Matriz almacenada como bloque contiguo: todas las filas consecutivas en memoria.
Asignación¶
int *matriz;
int filas = 3, columnas = 4;
matriz = (int *)malloc(filas * columnas * sizeof(int));
if (matriz == NULL) {
return NULL;
}Acceso Manual¶
No podés usar matriz[i][j] directamente porque matriz es int *, no int **. Debés calcular el índice lineal:
// Acceso: fila i, columna j
int valor = matriz[i * columnas + j];
// Asignación
matriz[i * columnas + j] = 42;Explicación del cálculo:
Cada fila tiene
columnaselementosPara llegar a la fila
i, saltamosi * columnaselementosLuego avanzamos
jcolumnas dentro de esa fila
Figure 6:Mapeo entre la representación lógica 2D y la memoria lineal contigua.
Liberación¶
Solo una llamada a free:
free(matriz);Función de Acceso Helper¶
Para mejorar la legibilidad, podés crear una función:
static inline int matriz_get(int *matriz, int fila, int col, int num_cols) {
return matriz[fila * num_cols + col];
}
static inline void matriz_set(int *matriz, int fila, int col, int num_cols, int valor) {
matriz[fila * num_cols + col] = valor;
}
// Uso:
matriz_set(matriz, i, j, columnas, 42);
int val = matriz_get(matriz, i, j, columnas);Ventajas y Desventajas¶
Ventajas:
Memoria contigua: Excelente localidad de cache (ver Jerarquía de Memoria y Caché)
Solo una asignación: Más eficiente, menos fragmentación
Menos overhead: No hay punteros intermedios
Desventajas:
Sintaxis de acceso menos intuitiva
Todas las filas deben tener el mismo tamaño
Fácil cometer errores en el cálculo de índices
Enfoque 3: Bloque Único con Cast Avanzado¶
Este enfoque combina lo mejor de ambos mundos: memoria contigua del Enfoque 2 con la sintaxis natural del Enfoque 1, mediante un cast especial del puntero.
Asignación con Puntero a Array¶
int filas = 3, columnas = 4;
// Puntero a un array de 'columnas' enteros
int (*matriz)[columnas] = (int (*)[columnas])malloc(
sizeof(int) * columnas * filas
);
if (matriz == NULL) {
return NULL;
}Acceso Natural¶
Ahora podés usar la sintaxis estándar:
matriz[i][j] = 42;
int valor = matriz[i][j];¿Cómo funciona?
El compilador sabe que
matrizapunta a arrays decolumnaselementosmatriz[i]avanzai * columnas * sizeof(int)bytesmatriz[i][j]accede al elementojdentro de ese array
Liberación¶
Solo un free:
free(matriz);Comparación de Declaraciones¶
// Enfoque 1: Array de punteros
int **matriz1; // Puntero a puntero a int
// Enfoque 2: Puntero simple
int *matriz2; // Puntero a int
// Enfoque 3: Puntero a array
int (*matriz3)[columnas]; // Puntero a array de columnas intsLimitación: Tamaño de Columnas en Compile-Time (C99+)¶
Para C89, usarías:
#define COLUMNAS 4
int (*matriz)[COLUMNAS] = malloc(sizeof(int) * COLUMNAS * filas);Ventajas y Desventajas¶
Ventajas:
Sintaxis natural
matriz[i][j]Memoria contigua (buena localidad de cache)
Solo una asignación/liberación
Desventajas:
Sintaxis de declaración compleja
En C89, requiere tamaño de columnas constante
Menos portable que los otros enfoques
Comparación de Enfoques¶
Table 3:Comparación de Implementaciones de Matrices
Aspecto | Enfoque 1 (Dentada) | Enfoque 2 (Bloque Manual) | Enfoque 3 (Bloque + Cast) |
|---|---|---|---|
Sintaxis de acceso |
|
|
|
Asignaciones |
|
|
|
Overhead de memoria |
|
|
|
Localidad de cache | Baja ⚠️ | Alta ✅ | Alta ✅ |
Fragmentación | Alta ⚠️ | Ninguna ✅ | Ninguna ✅ |
Filas de tamaño variable | Sí ✅ | No ⚠️ | No ⚠️ |
Complejidad código | Media | Media | Alta ⚠️ |
Conceptos Clave¶
Este apunte explora patrones avanzados de memoria dinámica en C, construyendo sobre los fundamentos de Introducción: El Mapa de Memoria de un Programa y Punteros.
Conexión con el Siguiente Tema¶
Dominando la gestión avanzada de memoria dinámica, tenés las herramientas para implementar estructuras de datos complejas: listas enlazadas, árboles, grafos, hash tables. Pero construir estas estructuras correctamente requiere algo más que conocimiento técnico de punteros.
El apunte TAD, Pilas y Colas introduce el concepto de Tipos Abstractos de Datos (TADs):
Encapsulación: Ocultar detalles de implementación
Interfaces limpias: Separar “qué hace” de “cómo lo hace”
Invariantes: Propiedades que siempre deben cumplirse
Patrones de diseño: crear/destruir, getters/setters, iteradores
Un TAD bien diseñado permite cambiar completamente la implementación interna (por ejemplo, de matriz dentada a bloque único) sin afectar al código cliente. Esta separación de concerns es fundamental para escribir software mantenible y escalable.
Los punteros y la memoria dinámica son las herramientas de bajo nivel; los TADs son los principios arquitecturales que guían su uso profesional.
Pregunta puente: Una lista enlazada y un array dinámico implementan la misma interfaz abstracta (secuencia de elementos). ¿Cómo decidir cuál usar? ¿Cómo diseñar la interfaz para que sea independiente de la implementación? El análisis de TADs responde estas preguntas.
Referencias y Lecturas Complementarias¶
Textos Fundamentales sobre Memoria Dinámica¶
Kernighan & Ritchie (2014). Capítulo 8: The UNIX System Interface. Gestión de memoria y estructuras complejas.
King (2008). Capítulo 17: Advanced Uses of Pointers. Matrices multidimensionales y estructuras auto-referenciadas.
Gustedt (2019). Level 2, Takeaway 2.6.5: Dynamic allocation. Patrones modernos de gestión de memoria.
Gestión de Memoria y Debugging¶
Seacord (2013). Capítulo 4: Dynamic Memory Management. Errores comunes y cómo evitarlos.
Valgrind Memcheck Manual - https://
valgrind .org /docs /manual /mc -manual .html Herramienta esencial para detectar leaks y dangling pointers.
Matrices y Estructuras Multidimensionales¶
Linden (1994). Capítulo 4: The Shocking Truth: C Arrays and Pointers Are NOT the Same!
Multi-dimensional Arrays in C - https://www.cs.cmu.edu/~ab/15-123S11/lectures/Lecture 09 - Multidimensional Arrays.pdf
Optimización y Performance¶
Bryant & O'Hallaron (2015). Capítulo 6: Memory Hierarchy. Localidad de cache y performance de acceso.
Warren (2012). Capítulo 9: Memory. Técnicas de optimización de memoria.
Patrones de Diseño con Memoria Dinámica¶
Hanson (1996). Interfaces y implementaciones con gestión de memoria robusta.
Plauger (1992). Implementaciones de la biblioteca estándar que usan memoria dinámica.
Herramientas de Análisis¶
AddressSanitizer - https://
github .com /google /sanitizers /wiki /AddressSanitizer Detector de errores de memoria integrado en GCC/Clang.
Dr. Memory - https://
drmemory .org/ Alternativa a Valgrind para Windows.
- Kernighan, B. W., & Ritchie, D. M. (2014). C Programming Language, 2nd Edition.
- King, K. N. (2008). C Programming: A Modern Approach (2nd ed.). W. W. Norton & Company.
- Gustedt, J. (2019). Modern C. Manning Publications. https://modernc.gforge.inria.fr/
- Seacord, R. C. (2013). Secure Coding in C and C++ (2nd ed.). Addison-Wesley.
- van der Linden, P. (1994). Expert C Programming: Deep C Secrets. Prentice Hall.
- Bryant, R. E., & O’Hallaron, D. R. (2015). Computer Systems: A Programmer’s Perspective (3rd ed.). Pearson.
- Warren, H. S. (2012). Hacker’s Delight (2nd ed.). Addison-Wesley.
- Hanson, D. R. (1996). C Interfaces and Implementations: Techniques for Creating Reusable Software. Addison-Wesley.
- Plauger, P. J. (1992). The Standard C Library. Prentice Hall.