Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Memoria Dinámica

Universidad Nacional de Río Negro

Referencias y Lecturas Adicionales

Introducción

Desarrollo

Funciones de Gestión de Memoria (<stdlib.h>)

Las funciones de gestión de memoria dinámica están declaradas en el archivo de cabecera <stdlib.h>. Estas funciones permiten solicitar y liberar bloques de memoria del heap durante la ejecución del programa.

malloc (Memory Allocation)
Sintaxis
void *malloc(size_t size);
Propósito

Reserva un bloque contiguo de size bytes en el heap. La memoria reservada no está inicializada y contiene valores indeterminados (basura). Esto significa que los bytes asignados pueden contener cualquier valor que haya quedado de un uso previo de esa región de memoria.

La función retorna un puntero de tipo void *, que es un puntero genérico que puede convertirse implícitamente a cualquier tipo de puntero en C. Esto permite usar malloc para asignar memoria para cualquier tipo de dato.

Valor de Retorno
¿Por qué la memoria no está inicializada?

Por razones de eficiencia. Inicializar la memoria tiene un costo computacional, y en muchos casos el programador va a sobrescribir inmediatamente esos valores con datos útiles. Si necesitás memoria inicializada a cero, usá calloc en su lugar.

Uso Correcto

Según la Regla 0x3001h: Siempre verificá la asignación exitosa de memoria dinámica, siempre debés verificar que la asignación de memoria fue exitosa. Además, la Regla 0x300Bh: Usá siempre sizeof en las asignaciones de memoria dinámica, prefiriendo sizeof(*ptr) establece que debés usar sizeof para calcular el tamaño necesario en lugar de valores literales, y preferir sizeof(*puntero) sobre sizeof(tipo) para evitar errores si el tipo cambia.

La Regla 0x3003h: No mezcles operaciones de asignación y comparación en una sola línea indica que no debés mezclar operaciones de asignación y comparación en una sola línea.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h>
#include <stdlib.h>

int main()
{
    int *numeros = NULL;
    size_t cantidad = 5;

    // Asignación de memoria
    numeros = malloc(cantidad * sizeof(*numeros));

    // Verificación del resultado
    if (numeros == NULL)
    {
        fprintf(stderr, "Error: No se pudo asignar memoria.\n");
        return 1;
    }

    // Uso de la memoria
    for (size_t i = 0; i < cantidad; i++)
    {
        numeros[i] = (int)i * 10;
    }

    // Liberación de la memoria
    free(numeros);
    numeros = NULL;

    return 0;
}
calloc (Contiguous Allocation)
Sintaxis
void *calloc(size_t num_elements, size_t element_size);
Propósito

Reserva memoria para un arreglo de num_elements elementos, cada uno de tamaño element_size bytes. La diferencia fundamental con malloc es que calloc inicializa todos los bytes a cero.

El tamaño total reservado es num_elements * element_size bytes. La función realiza esta multiplicación internamente, lo que puede ser más seguro que calcularla manualmente con malloc, ya que algunas implementaciones de calloc verifican el desbordamiento (overflow) en esta multiplicación.

Ventajas
¿Cuándo usar calloc vs malloc?

Usá calloc cuando:

Usá malloc cuando:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>
#include <stdlib.h>

int main()
{
    size_t cantidad = 5;
    int *arreglo = calloc(cantidad, sizeof(*arreglo));

    if (arreglo == NULL)
    {
        fprintf(stderr, "Error: No se pudo asignar memoria.\n");
        return 1;
    }

    // Todos los elementos están inicializados en 0
    for (size_t i = 0; i < cantidad; i++)
    {
        printf("arreglo[%zu] = %d\n", i, arreglo[i]);
    }

    free(arreglo);
    arreglo = NULL;

    return 0;
}
realloc (Re-allocation)
Sintaxis
1
void *realloc(void *ptr, size_t new_size);
Propósito

Cambia el tamaño de un bloque de memoria previamente asignado.

Comportamiento
  1. Si new_size es mayor que el tamaño original:

    • Intenta expandir el bloque actual si hay espacio contiguo.

    • Si no es posible, busca un nuevo bloque de memoria lo suficientemente grande, copia el contenido del bloque antiguo al nuevo, y libera el bloque antiguo.

    • La memoria adicional no se inicializa.

  2. Si new_size es menor que el tamaño original:

    • El bloque se trunca. Los datos al final se pierden.

  3. Si new_size es 0:

    • ¡Evitar! En estándares modernos (C17/C23), llamar a realloc(ptr, 0) está formalmente declarado como comportamiento indefinido u obsoleto. No debe usarse bajo ninguna circunstancia como sustituto de free(). Para liberar memoria, utilizá siempre la función free().

Valor de Retorno
Uso Seguro

El error más común con realloc es perder la referencia al bloque original si la función falla por lo que es necesario un puntero temporal para manejar realloc de forma segura.

Incorrecto:

1
2
3
4
5
// ¡PELIGRO! Si realloc falla, se pierde el puntero original
ptr = realloc(ptr, nuevo_tamano);
if (ptr == NULL) {
    // Fuga de memoria: el bloque original se perdió
}

Correcto:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdlib.h>

int *numeros = malloc(5 * sizeof(*numeros));
if (numeros == NULL) {
    fprintf(stderr, "Error: No se pudo asignar memoria inicial.\n");
    return 1;
}
// ...

size_t nuevo_tamano = 10;
int *temp = realloc(numeros, nuevo_tamano * sizeof(*temp));

if (temp == NULL) {
    // realloc falló, pero 'numeros' sigue siendo válido
    fprintf(stderr, "Error: No se pudo redimensionar la memoria.\n");
    free(numeros); // Liberar el bloque original
    numeros = NULL;
    return 1;
}

// Éxito: ahora 'numeros' puede apuntar al nuevo bloque
numeros = temp;
free (Liberación)
Sintaxis
1
void free(void *ptr);
Propósito

Libera un bloque de memoria previamente reservado, devolviéndolo al sistema operativo para que pueda ser reutilizado.

Reglas Fundamentales

Según la Regla 0x3002h: Liberá siempre la memoria dinámica y asigná NULL al puntero para evitar punteros colgantes, debés:

  1. Liberar siempre la memoria dinámica que asignaste.

  2. Asignar NULL al puntero inmediatamente después de liberarlo para prevenir punteros colgantes.

Es seguro llamar a free(NULL), la función simplemente no hace nada.

1
2
free(ptr);
ptr = NULL;  // Previene el uso accidental del puntero colgante
El Allocator: Gestión Interna del Heap

Cuando llamás a malloc o calloc, no estás interactuando directamente con el sistema operativo en cada llamada. En cambio, estas funciones son parte de un subsistema llamado allocator (asignador de memoria) que gestiona el heap de tu proceso.

¿Cómo funciona el allocator?

El allocator mantiene su propia estructura de datos para rastrear qué bloques del heap están libres y cuáles están ocupados. Existen varias estrategias de implementación, pero todas deben resolver dos problemas fundamentales:

  1. Al asignar: ¿Qué bloque libre usar cuando hay varios disponibles?

  2. Al liberar: ¿Cómo marcar el bloque como libre y potencialmente fusionarlo con bloques adyacentes?

Estructura típica de un bloque de memoria:

Estructura de un bloque de memoria en el heap, mostrando el header con metadata,
el área de datos del usuario, y el footer opcional.

Figure 1:Estructura de un bloque de memoria en el heap, mostrando el header con metadata, el área de datos del usuario, y el footer opcional.

El header típicamente contiene:

Estrategias de asignación:

  1. First Fit (Primer ajuste):

    • Busca desde el inicio del heap hasta encontrar el primer bloque libre suficientemente grande.

    • Ventaja: Rápido (termina apenas encuentra un bloque).

    • Desventaja: Tiende a fragmentar la parte inicial del heap.

  2. Best Fit (Mejor ajuste):

    • Busca en todo el heap el bloque libre más pequeño que satisfaga la solicitud.

    • Ventaja: Minimiza el desperdicio de memoria.

    • Desventaja: Lento (debe recorrer toda la lista) y crea muchos bloques diminutos inutilizables.

  3. Next Fit (Siguiente ajuste):

    • Como First Fit, pero continúa desde donde terminó la última búsqueda.

    • Ventaja: Distribuye mejor las asignaciones por todo el heap.

    • Desventaja: Aún puede fragmentar.

  4. Segregated Free Lists (Listas libres segregadas):

    • Mantiene listas separadas para bloques de diferentes tamaños.

    • Ventaja: Muy eficiente para patrones de asignación predecibles.

    • Desventaja: Más complejo de implementar y mantener.

Interacción con el sistema operativo:

El allocator solicita memoria al sistema operativo en grandes cantidades (típicamente mediante sbrk() o mmap() en Unix/Linux) y luego la subdivide según las necesidades del programa. Esto reduce enormemente el número de llamadas al sistema, que son costosas.

Flujo de interacción entre el programa, las funciones de memoria
(malloc/calloc/free), el allocator interno que mantiene un pool de memoria, y
ocasionalmente el sistema operativo que proporciona acceso a la RAM física.

Figure 2:Flujo de interacción entre el programa, las funciones de memoria (malloc/calloc/free), el allocator interno que mantiene un pool de memoria, y ocasionalmente el sistema operativo que proporciona acceso a la RAM física.

Coalescing (Fusión de bloques):

Cuando liberás un bloque con free(), el allocator intenta fusionarlo con bloques libres adyacentes para crear bloques más grandes. Esto ayuda a combatir la fragmentación externa.

Proceso de coalescing (fusión) donde bloques libres adyacentes (LIBRE-B y
LIBRE-C) se combinan en un único bloque más grande (LIBRE-BC fusionado).

Figure 3:Proceso de coalescing (fusión) donde bloques libres adyacentes (LIBRE-B y LIBRE-C) se combinan en un único bloque más grande (LIBRE-BC fusionado).

Ejercicios de Autoevaluación (Funciones de Gestión)

Errores Comunes y Peligros

La gestión manual de memoria es una fuente frecuente de errores en C. Comprender estos errores y cómo prevenirlos es fundamental para escribir código robusto.

Memory Leak (Fuga de Memoria)

Una fuga de memoria ocurre cuando se pierde la referencia a un bloque de memoria reservado sin haberlo liberado con free. La memoria queda inutilizable para el programa hasta que este termina.

Ejemplo Problemático
1
2
3
4
5
6
7
8
9
10
11
#include <stdlib.h>

void funcion_con_fuga()
{
    int *datos = malloc(100 * sizeof(*datos));

    // Se realizan operaciones...

    // ERROR: La función termina sin liberar 'datos'
    // El bloque de memoria se pierde
}
Solución

Asegurate de que cada asignación tenga su correspondiente liberación, siguiendo la Regla 0x3002h: Liberá siempre la memoria dinámica y asigná NULL al puntero para evitar punteros colgantes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void funcion_sin_fuga()
{
    int *datos = malloc(100 * sizeof(*datos));

    if (datos == NULL)
    {
        return;
    }

    // Operaciones...

    free(datos);
    datos = NULL;
}
Dangling Pointer (Puntero Colgante)

Un puntero colgante es un puntero que apunta a una dirección de memoria que ya ha sido liberada con free. Intentar acceder a través de él produce comportamiento indefinido.

Ejemplo Problemático
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdlib.h>
#include <stdio.h>

int main()
{
    int *ptr = malloc(sizeof(*ptr));
    if (ptr == NULL)
    {
        return 1;
    }
    *ptr = 42;

    free(ptr);
    ptr = NULL;  // Previene el uso del puntero colgante

    printf("%d\n", *ptr);  // Comportamiento indefinido

    return 0;
}
Solución

Asigná NULL al puntero inmediatamente después de llamar a free, como exige la Regla 0x3002h: Liberá siempre la memoria dinámica y asigná NULL al puntero para evitar punteros colgantes.

1
2
3
4
5
6
7
8
9
10
11
12
13
int main()
{
    int *ptr = malloc(sizeof(*ptr));
    *ptr = 42;

    free(ptr);
    ptr = NULL;  // Previene el uso del puntero colgante

    // Intentar desreferenciar ptr ahora causará un error inmediato
    // en lugar de comportamiento indefinido silencioso

    return 0;
}
Double Free (Doble Liberación)

Intentar liberar el mismo bloque de memoria dos veces causa comportamiento indefinido y puede corromper la gestión de memoria del heap.

Ejemplo Problemático
1
2
3
4
5
6
7
8
9
10
11
#include <stdlib.h>

int main()
{
    int *ptr = malloc(sizeof(*ptr));

    free(ptr);
    free(ptr);  // ERROR: Doble liberación

    return 0;
}
Solución

Asignar NULL después de cada free previene este problema, ya que free(NULL) es una operación segura que no hace nada.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int main()
{
    int *ptr = malloc(sizeof(*ptr));
    if (ptr == NULL)
    {
        return 1;
    }

    free(ptr);
    ptr = NULL;

    free(ptr);  // Seguro: free(NULL) no hace nada

    return 0;
}
Acceso Fuera de Límites

Leer o escribir fuera de los límites del bloque de memoria reservado corrompe datos adyacentes y causa comportamiento impredecible.

Ejemplo Problemático
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdlib.h>

int main()
{
    int *arreglo = malloc(5 * sizeof(*arreglo));

    if (arreglo == NULL)
    {
        return 1;
    }

    // ERROR: Acceso fuera de límites
    for (size_t i = 0; i <= 5; i++)  // Debería ser i < 5
    {
        arreglo[i] = (int)i;
    }

    free(arreglo);
    arreglo = NULL;

    return 0;
}
Solución

La Regla 0x300Ch: Verificá siempre los límites de los arreglos antes de acceder a sus elementos exige verificar siempre los límites de los arreglos antes de acceder a sus elementos. La Regla 0x3010h: Las variables que representan tamaños o índices de arreglos deben ser de tipo size_t establece que las variables que representan tamaños o índices de arreglos deben ser de tipo size_t.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int main()
{
    size_t tamano = 5;
    int *arreglo = malloc(tamano * sizeof(*arreglo));

    if (arreglo == NULL)
    {
        return 1;
    }

    // Correcto: i < tamano previene el acceso fuera de límites
    for (size_t i = 0; i < tamano; i++)
    {
        arreglo[i] = (int)i;
    }

    free(arreglo);
    arreglo = NULL;

    return 0;
}
Uso de Memoria Después de free

Acceder a memoria después de liberarla es un error similar al puntero colgante.

Ejemplo Problemático
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdlib.h>
#include <stdio.h>

int main()
{
    int *ptr = malloc(sizeof(*ptr));
    *ptr = 100;

    free(ptr);

    // ERROR: Uso de memoria liberada
    printf("%d\n", *ptr);

    return 0;
}
Solución

Asegurate de no usar el puntero después de liberarlo, y asigná NULL para detectar errores fácilmente.

Ejercicios de Autoevaluación (Errores Comunes y Peligros)

Seguridad de Memoria: Una Perspectiva Profunda

La seguridad de memoria (memory safety) es uno de los desafíos más importantes en programación de sistemas. Comprender por qué los errores de memoria son tan peligrosos requiere entender qué significa “comportamiento indefinido” y cómo puede ser explotado.

Comportamiento Indefinido (Undefined Behavior)

Cuando el estándar de C dice que una operación tiene “comportamiento indefinido” (UB), significa que absolutamente cualquier cosa puede pasar. El compilador no está obligado a hacer nada razonable.

¿Por qué existe el UB?

El comportamiento indefinido existe por dos razones principales:

  1. Rendimiento: Verificar todos los accesos a memoria en tiempo de ejecución sería prohibitivamente lento. C delega la responsabilidad al programador para mantener la máxima velocidad.

  2. Flexibilidad del compilador: El compilador puede hacer optimizaciones agresivas asumiendo que tu código no tiene UB. Si tenés UB, esas optimizaciones pueden hacer que tu programa haga cosas completamente inesperadas.

Ejemplos de UB en gestión de memoria:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// UB #1: Desreferenciar puntero NULL
int *ptr = NULL;
*ptr = 42;  // Crash probable, pero no garantizado

// UB #2: Uso después de free
int *ptr = malloc(sizeof(int));
free(ptr);
*ptr = 42;  // Puede parecer funcionar, pero es UB

// UB #3: Doble free
free(ptr);
free(ptr);  // Puede corromper el heap

// UB #4: Acceso fuera de límites
int arr[10];
arr[15] = 42;  // Puede sobrescribir otras variables

// UB #5: Retornar dirección de variable local
int *funcion() {
    int x = 42;
    return &x;  // x desaparece al retornar
}

Consecuencias del UB:

El comportamiento indefinido no solo causa crashes. Puede:

  1. Parecer funcionar: El programa parece correr bien en tu máquina, pero falla en producción.

  2. Funcionar hasta que cambies algo no relacionado: Agregar una línea de código en otro lado hace que el programa crashee, porque cambió el layout de memoria.

  3. Ser explotado por atacantes: Los buffer overflows son la base de muchas vulnerabilidades de seguridad.

  4. Ser “optimizado” por el compilador de forma sorprendente:

:linenos: // El programador escribe: if (ptr != NULL) { *ptr = 42; }

// Pero si el compilador ve *ptr antes del if, // puede asumir que ptr nunca es NULL (porque desreferenciarlo // cuando es NULL sería UB), y eliminar la verificación.


(vulnerabilidades-comunes)=
#### Vulnerabilidades Comunes

Los errores de memoria no son solo bugs: son vulnerabilidades de seguridad.
Comprender los ataques comunes te ayuda a escribir código más defensivo.

**Buffer Overflow:**

Un buffer overflow ocurre cuando escribís más datos de los que un buffer puede
contener, sobrescribiendo memoria adyacente.

:::{code-block}c
:linenos:
void vulnerable()
{
 char buffer[10];
 char *datos_importantes = "SECRETO";

 // Un atacante puede escribir más de 10 bytes:
 strcpy(buffer, datos_maliciosos_largos);
 // Ahora datos_importantes puede haber sido sobrescrito
}

:::
<!-- {code-block}c -->

En el stack, un atacante puede sobrescribir la dirección de retorno para
ejecutar código arbitrario:

:::{figure} 5/buffer_overflow.svg
:name: fig-buffer-overflow
:width: 100%

Visualización de buffer overflow en el stack: antes del overflow el buffer tiene
su espacio asignado y la dirección de retorno está protegida; después del
overflow, datos excesivos (representados como 'A') sobrescriben el buffer, los
datos intermedios, y finalmente corrompen la dirección de retorno, permitiendo
potencialmente la ejecución de código malicioso.

:::
<!-- {figure} 1/buffer_overflow.svg -->

**Use-After-Free (UAF):**

Usar memoria después de liberarla puede permitir que un atacante controle datos
críticos:

:::{code-block}c
:linenos:
struct usuario {
 char nombre[50];
 int es_admin;
};

struct usuario *usr = malloc(sizeof(*usr));
usr->es_admin = 0;  // Usuario normal
free(usr);

// ... código intermedio ...

// Otro código asigna memoria que reutiliza el mismo espacio:
char *buffer = malloc(100);
strcpy(buffer, datos_del_atacante);

// Ahora usr apunta a memoria controlada por el atacante:
if (usr->es_admin) {  // ⚠️ UAF: usa memoria liberada
 // El atacante pudo sobrescribir es_admin a 1
 dar_privilegios_admin();
}

:::
<!-- {code-block}c -->

**Double Free:**

Liberar memoria dos veces puede corromper las estructuras internas del
allocator, permitiendo ataques sofisticados:

:::{code-block}c
:linenos:
free(ptr);
// ... código ...
free(ptr);  // Corrompe la lista de bloques libres

// Asignaciones posteriores pueden retornar direcciones sobrepuestas:
int *a = malloc(100);
int *b = malloc(100);
// Ahora 'a' y 'b' podrían apuntar a la misma memoria!

:::
<!-- {code-block}c -->

(estrategias-defensivas)=
#### Estrategias Defensivas

**1. Principio de mínimo privilegio:** No uses más memoria de la que necesitás,
y no la mantengas asignada más tiempo del necesario.

**2. Verificación exhaustiva:**

:::{code-block}c
:linenos:
// No solo verificar malloc:
if (ptr == NULL) { /* error */ }

// También verificar límites:
if (indice >= tamano) { /* error */ }

// Y validar punteros recibidos:
if (ptr_entrada == NULL) { /* error */ }

:::
<!-- {code-block}c -->

**3. Inicialización defensiva:**

:::{code-block}c
:linenos:
// Inicializar punteros:
int *ptr = NULL;

// Después de free, anular:
free(ptr);
ptr = NULL;

// Inicializar estructuras completamente:
struct datos d = {0};  // Todos los campos en cero

:::
<!-- {code-block}c -->

**4. Encapsulación:** Ocultá la gestión de memoria detrás de funciones:

:::{code-block}c
:linenos:
// En lugar de exponer punteros directamente:
recurso_t *crear_recurso(void);
void usar_recurso(recurso_t *r);
void destruir_recurso(recurso_t *r);

// Los usuarios nunca ven malloc/free directamente

:::
<!-- {code-block}c -->

**5. Usar funciones seguras:**

:::{code-block}c
:linenos:
// En lugar de:
strcpy(dest, src);  // No verifica límites

// Usar:
strncpy(dest, src, sizeof(dest) - 1);
dest[sizeof(dest) - 1] = '\0';

// O mejor aún, alocar dinámicamente con el tamaño correcto

:::
<!-- {code-block}c -->

:::{important} La Seguridad No Es Opcional

En el desarrollo profesional, los errores de memoria no son solo bugs molestos:
son **vulnerabilidades de seguridad** que pueden ser explotadas. Cada uno de los
errores discutidos (buffer overflow, UAF, double free) ha sido la base de
ataques reales en sistemas de producción.

Escribir código memory-safe no es solo seguir buenas prácticas: es una
responsabilidad ética. Tu código podría procesar datos sensibles, ejecutarse en
infraestructura crítica, o estar expuesto a atacantes motivados. La seguridad
debe ser parte del diseño desde el principio, no un añadido posterior.

:::
<!-- {important} La Seguridad No Es Opcional -->

(ejercicios-de-autoevaluacion-seguridad-de-memoria)=
#### Ejercicios de Autoevaluación (Seguridad de Memoria)








(ejemplo-integrador-arreglo-dinamico-de-tamano-fijo)=
### Ejemplo Integrador: Arreglo Dinámico de Tamaño Fijo


Este ejemplo demuestra cómo aplicar las buenas prácticas de gestión de memoria
en un caso realista: una estructura que encapsula un arreglo dinámico de enteros
de tamaño fijo.

:::{code-block}c
:linenos:
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

#define ERROR_MEMORIA -1
#define ERROR_INDICE -2
#define EXITO 0

typedef struct
{
 int *datos;
 size_t tamano;
} arreglo_t;

/**
* Crea un nuevo arreglo dinámico de tamaño fijo.
* @param tamano El tamaño del arreglo (debe ser mayor que 0).
* @returns Un puntero al arreglo creado. El llamador es responsable
*          de liberar esta memoria con destruir_arreglo().
*          Retorna NULL si no hay memoria disponible o si tamano es 0.
* @post El arreglo está inicializado con todos sus elementos en 0.
*/
arreglo_t *crear_arreglo(size_t tamano)
{
 if (tamano == 0)
 {
     return NULL;
 }

 arreglo_t *arreglo = malloc(sizeof(*arreglo));
 if (arreglo == NULL)
 {
     return NULL;
 }

 arreglo->datos = calloc(tamano, sizeof(*(arreglo->datos)));
 if (arreglo->datos == NULL)
 {
     free(arreglo);
     return NULL;
 }

 arreglo->tamano = tamano;

 return arreglo;
}

/**
* Establece el valor de un elemento en el arreglo.
* @param arreglo Puntero al arreglo (no debe ser NULL).
* @param indice Índice del elemento a modificar.
* @param valor Nuevo valor para el elemento.
* @pre arreglo no debe ser NULL.
* @pre indice debe ser menor que el tamaño del arreglo.
* @returns EXITO si el valor se estableció correctamente,
*          ERROR_INDICE si el índice es inválido.
*/
int establecer_elemento(arreglo_t *arreglo, size_t indice, int valor)
{
 if (arreglo == NULL)
 {
     return ERROR_MEMORIA;
 }

 if (indice >= arreglo->tamano)
 {
     return ERROR_INDICE;
 }

 arreglo->datos[indice] = valor;
 return EXITO;
}

/**
* Obtiene el valor de un elemento del arreglo.
* @param arreglo Puntero al arreglo (no debe ser NULL).
* @param indice Índice del elemento a obtener.
* @param valor_out Puntero donde se almacenará el valor (no debe ser NULL).
* @pre arreglo y valor_out no deben ser NULL.
* @pre indice debe ser menor que el tamaño del arreglo.
* @returns true si se obtuvo el elemento, false si algún parámetro es inválido.
*/
bool obtener_elemento(const arreglo_t *arreglo, size_t indice, int *valor_out)
{
 if (arreglo == NULL || valor_out == NULL)
 {
     return false;
 }

 if (indice >= arreglo->tamano)
 {
     return false;
 }

 *valor_out = arreglo->datos[indice];
 return true;
}

/**
* Calcula la suma de todos los elementos del arreglo.
* @param arreglo Puntero al arreglo (no debe ser NULL).
* @pre arreglo no debe ser NULL.
* @returns La suma de todos los elementos, o 0 si el arreglo es NULL.
*/
int sumar_elementos(const arreglo_t *arreglo)
{
 if (arreglo == NULL)
 {
     return 0;
 }

 int suma = 0;
 for (size_t i = 0; i < arreglo->tamano; i++)
 {
     suma = suma + arreglo->datos[i];
 }

 return suma;
}

/**
* Imprime todos los elementos del arreglo.
* @param arreglo Puntero al arreglo (no debe ser NULL).
* @pre arreglo no debe ser NULL.
*/
void imprimir_arreglo(const arreglo_t *arreglo)
{
 if (arreglo == NULL)
 {
     return;
 }

 printf("Arreglo [tamaño: %zu]: [", arreglo->tamano);

 for (size_t i = 0; i < arreglo->tamano; i++)
 {
     printf("%d", arreglo->datos[i]);
     if (i < arreglo->tamano - 1)
     {
         printf(", ");
     }
 }

 printf("]\n");
}

/**
* Destruye un arreglo y libera toda la memoria asociada.
* @param arreglo Doble puntero al arreglo a destruir.
* @post Toda la memoria del arreglo es liberada y el puntero se anula.
*/
void destruir_arreglo(arreglo_t **arreglo)
{
 if (arreglo != NULL && *arreglo != NULL)
 {
     if ((*arreglo)->datos != NULL)
     {
         free((*arreglo)->datos);
         (*arreglo)->datos = NULL;
     }
     free(*arreglo);
     *arreglo = NULL;
 }
}

int main()
{
 size_t tamano = 8;
 arreglo_t *mi_arreglo = crear_arreglo(tamano);

 if (mi_arreglo == NULL)
 {
     fprintf(stderr, "Error: No se pudo crear el arreglo.\n");
     return 1;
 }

 // Establecer algunos valores
 for (size_t i = 0; i < tamano; i++)
 {
     if (establecer_elemento(mi_arreglo, i, (int)(i * i)) != EXITO)
     {
         fprintf(stderr, "Error: No se pudo establecer elemento.\n");
         destruir_arreglo(mi_arreglo);
         return 1;
     }
 }

 imprimir_arreglo(mi_arreglo);

 // Obtener y mostrar un elemento específico
 int valor = 0;
 if (obtener_elemento(mi_arreglo, 3, &valor))
 {
     printf("Elemento en índice 3: %d\n", valor);
 }

 // Calcular la suma
 int suma = sumar_elementos(mi_arreglo);
 printf("Suma de todos los elementos: %d\n", suma);

 // Liberar recursos
 destruir_arreglo(&mi_arreglo);

 return 0;
}

:::
<!-- {code-block}c -->

:::{note} Análisis del Ejemplo

Este ejemplo integra múltiples buenas prácticas:

- Inicialización de punteros a `NULL` ({ref}`0x0003h`)
- Verificación de todas las asignaciones de memoria ({ref}`0x3001h`)
- Uso de `calloc` para inicialización automática a cero
- Uso de `sizeof(*puntero)` ({ref}`0x300Bh`)
- Simetría con `crear_arreglo()` y `destruir_arreglo()` ({ref}`0x3002h`)
- Documentación completa de funciones ({ref}`0x000Ah`)
- Uso de `const` para parámetros no modificados ({ref}`0x3007h`)
- Uso de `size_t` para tamaños e índices ({ref}`0x3010h`)
- Verificación de límites antes de acceder a elementos ({ref}`0x300Ch`) 

:::
<!-- {note} Análisis del Ejemplo -->


(conceptos-avanzados-y-rendimiento-de-bajo-nivel)=
### Conceptos Avanzados y Rendimiento de Bajo Nivel


En esta sección se presentan detalles técnicos complementarios sobre la
ejecución y la jerarquía de hardware, orientados a comprender el rendimiento
real de los programas.

(funcionamiento-de-la-pila-en-ensamblador-x86-64)=
#### Funcionamiento de la Pila en Ensamblador (x86-64)

A nivel de arquitectura de hardware, la pila se gestiona a través de registros
del procesador. En la arquitectura x86-64:
- El registro `rsp` (*Stack Pointer*) apunta al tope actual de la pila.
- El registro `rbp` (*Base Pointer* o *Frame Pointer*) apunta al inicio del
marco de pila de la función en ejecución.

A continuación se muestra cómo se ve un prólogo y epílogo típico de una función
en código ensamblador simplificado:

```{code-block} asm
:linenos:
funcion:
 push rbp              ; Guardar frame pointer anterior
 mov rbp, rsp          ; Establecer nuevo frame pointer
 sub rsp, 16           ; Reservar espacio para variables locales

 ; ... cuerpo de la función ...

 mov rsp, rbp          ; Restaurar stack pointer
 pop rbp               ; Restaurar frame pointer anterior
 ret                   ; Retornar
Ejercicios de Autoevaluación (Conceptos Avanzados y Bajo Nivel)

Conexión con el Siguiente Tema

Con memoria dinámica dominada, tenemos las herramientas para implementar cualquier estructura de datos. Pero antes de construir estructuras complejas, necesitamos entender cómo medir su eficiencia: ¿cuánto tiempo toma buscar un elemento? ¿Cómo crece el tiempo de ejecución al duplicar el tamaño de entrada?

El apunte Referencias y Lecturas Complementarias introduce el análisis asintótico de algoritmos:

El análisis de complejidad es fundamental para tomar decisiones informadas: ¿vale la pena usar una lista enlazada (memoria dinámica, O(n)O(n) búsqueda) o un arreglo redimensionable (overhead de copia, O(1)O(1) acceso)? Sin complejidad, solo podemos intuir; con ella, podemos demostrar matemáticamente qué solución es mejor.

Después, el apunte Resumen de la Unidad muestra cómo encapsular estructuras con memoria dinámica en Tipos Abstractos de Datos, ocultando detalles de implementación y exponiendo interfaces limpias.

Pregunta puente: Una búsqueda lineal en lista enlazada toma O(n)O(n) tiempo. ¿Podemos hacer mejor? ¿Cómo cuantificamos “mejor”? La respuesta requiere análisis formal de complejidad algorítmica.

Ejercicios de Autoevaluación

Solution to Exercise 1

La solución requiere reservar primero un arreglo de punteros (int **) y luego cada fila de enteros (int *) de forma individual. En caso de error en cualquier paso intermedio, se debe realizar una liberación ordenada en sentido inverso (lazo de liberación) para evitar fugas de memoria:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdlib.h>

int **crear_matriz(size_t filas, size_t columnas) {
    if (filas == 0 || columnas == 0) {
        return NULL;
    }

    // Reservar el arreglo de punteros a filas
    int **matriz = (int **)malloc(filas * sizeof(*matriz));
    if (matriz == NULL) {
        return NULL;
    }

    // Reservar e inicializar en cero cada fila
    for (size_t i = 0; i < filas; i++) {
        matriz[i] = (int *)calloc(columnas, sizeof(*(matriz[i])));
        if (matriz[i] == NULL) {
            // Lazo de liberación en caso de fallo intermedio
            for (size_t j = 0; j < i; j++) {
                free(matriz[j]);
            }
            free(matriz);
            return NULL;
        }
    }

    return matriz;
}
Solution to Exercise 2

El error más común con realloc es asignar el retorno directamente sobre la misma variable. Si falla, realloc devuelve NULL y se pierde el puntero original, provocando una fuga de memoria. Se debe emplear un puntero temporal:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdlib.h>
#include <stdbool.h>

bool redimensionar_arreglo(int **arreglo, size_t capacidad_actual, size_t nueva_capacidad) {
    if (arreglo == NULL || nueva_capacidad == 0) {
        return false;
    }

    // Uso de un puntero temporal para resguardar la dirección original
    int *temp = (int *)realloc(*arreglo, nueva_capacidad * sizeof(*temp));
    if (temp == NULL) {
        // La memoria original en *arreglo sigue siendo válida
        return false;
    }

    // Asignación exitosa
    *arreglo = temp;
    return true;
}
Solution to Exercise 3
  • malloc: Solo reserva el espacio virtual de direcciones en el proceso, retornando un puntero. El sistema operativo utiliza paginación bajo demanda, lo que significa que no asigna páginas de memoria física reales hasta que el programa intente leer o escribir efectivamente en cada dirección del bloque reservado. Por ende, la llamada a malloc es prácticamente instantánea.

  • calloc: Además de reservar el espacio virtual, garantiza que todos los bytes estén inicializados en cero. Para hacer esto, la biblioteca estándar escribe ceros en toda la memoria solicitada, lo que fuerza al sistema operativo a asignar físicamente todas las páginas de memoria de forma inmediata. Esto produce una penalización medible de tiempo de CPU y acceso a disco/RAM durante la llamada a calloc.

Solution to Exercise 4

El fragmento de código presenta cuatro infracciones graves de las pautas de gestión de memoria:

  1. Falta de validación de asignación: No se comprueba si datos es NULL antes de escribir en él, violando la regla {ref}0x3001h``. Si el sistema se queda sin memoria, ocurrirá una falla de segmentación al intentar escribir.

  2. Acceso fuera de límites (Buffer Overflow): Si n > 10, se ejecuta datos[n] = 100. Para un arreglo de tamaño n, los índices válidos van desde 0 hasta n - 1. Acceder al índice n sobrescribe datos del heap fuera del espacio asignado (violación de la regla {ref}0x300Ch``).

  3. Fuga de memoria (Memory Leak): Dentro del bloque if (n > 10), la función retorna (return;) sin invocar free(datos), dejando el bloque huérfano en el heap y violando la regla {ref}0x3002h``.

  4. Uso de memoria después de liberar (Use-After-Free): Tras ejecutar free(datos);, se intenta imprimir datos[0]. Acceder a memoria liberada (puntero colgante) produce comportamiento indefinido.

Solution to Exercise 5

El arreglo saludo es una variable automática alocada localmente en el marco de pila (Stack Frame) de la función obtener_saludo. Cuando la función retorna, su marco de pila se destruye y la memoria asignada a saludo queda disponible para ser sobrescrita por cualquier otra llamada a función. Por lo tanto, el puntero retornado apunta a una zona inválida del stack.

Para solucionarlo de modo que la memoria persista en el tiempo, debés alocar la cadena en el heap mediante memoria dinámica:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdlib.h>
#include <string.h>

char *obtener_saludo_dinamico(void) {
    const char *texto = "Hola mundo";
    // Sumamos 1 para el caracter terminador nulo '\0'
    char *saludo = (char *)malloc((strlen(texto) + 1) * sizeof(*saludo));
    if (saludo == NULL) {
        return NULL;
    }
    strcpy(saludo, texto);
    return saludo;
}
Solution to Exercise 6

El allocator de memoria dinámica gestiona las zonas libres a través de estructuras internas (listas enlazadas llamadas bins). Al ejecutar free(ptr), el allocator toma la dirección de memoria y la reinserta en la lista de bloques disponibles. Si realizás un doble free sobre el mismo puntero, el allocator intentará reinsertar el mismo bloque por segunda vez en su lista de libres, corrompiendo los punteros internos de la lista (generando ciclos infinitos o nodos duplicados). Esto puede provocar que llamadas futuras a malloc entregar en el mismo bloque de memoria a dos variables distintas, lo que genera corrupción de datos cruzada y graves fallos de seguridad.

La regla {ref}0x3002h`` exige establecer ptr = NULL inmediatamente después de liberarlo. Como el estándar de C define que llamar a free(NULL) no realiza ninguna operación ni produce efectos secundarios, cualquier llamada posterior a free sobre ese puntero será inocua.

Solution to Exercise 7

En la pila (stack), cada llamada a función apila un marco de pila que contiene las variables locales de la función y, justo por encima de ellas, los datos de control del flujo de ejecución, principalmente la dirección de retorno de la función (el puntero a la instrucción que se debe ejecutar cuando la función termine).

Dado que las variables se llenan desde direcciones de memoria más bajas hacia direcciones más altas, escribir más caracteres de los permitidos en un buffer local sobrescribirá la memoria adyacente en el stack. Si el desbordamiento es lo suficientemente grande, sobrescribirá la dirección de retorno guardada. Un atacante puede diseñar la entrada para inyectar código binario ejecutable (shellcode) en la pila y reemplazar la dirección de retorno original por la dirección de inicio del shellcode. Al ejecutarse la instrucción de retorno de la función (ret en x86-64), el registro de instrucción del procesador (rip) saltará a la dirección modificada por el atacante, ejecutando el código inyectado con los privilegios del programa actual.

Solution to Exercise 8

El programa presenta un error crítico de Use-After-Free (UAF).

  1. Se reserva memoria para la estructura p y luego se libera mediante free(p), pero el puntero p no se anula.

  2. Inmediatamente después, se solicita memoria para mensaje con el mismo tamaño exacto de la estructura perfil_t. Para optimizar recursos y evitar la fragmentación, el allocator del heap suele reutilizar el último bloque liberado. Por ende, el puntero mensaje recibirá casi con seguridad la misma dirección de memoria que tenía p.

  3. Al leer de stdin con fgets(mensaje, ...), el usuario escribe directamente sobre el bloque de memoria que p todavía apunta.

  4. Un atacante puede ingresar 16 caracteres cualquiera seguidos por el valor entero 1 en formato binario (escribiendo en la posición que correspondía a es_admin).

  5. Cuando el código evalúa if (p->es_admin == 1), accede al bloque a través del puntero colgante p y lee el valor modificado por el usuario, activando la llamada a ejecutar_privilegios().

Solution to Exercise 9

Cuando se calcula el tamaño de memoria con cantidad * sizeof(*datos), si cantidad es un valor extremadamente grande proporcionado por un usuario malicioso, la multiplicación puede superar el valor máximo representable por el tipo size_t. Esto produce un desbordamiento de enteros (integer overflow), provocando que el resultado se envuelva modularmente a un número muy chico.

Por ejemplo, si el cálculo da como resultado virtual 4 bytes debido al desbordamiento, malloc(4) se ejecutará con éxito y asignará un bloque de tamaño mínimo en el heap. Sin embargo, el programa principal asumirá que la asignación fue exitosa para la cantidad original solicitada. Al ejecutar un lazo para escribir datos en el arreglo, se sobrepasará la capacidad real de 4 bytes del heap, corrompiendo la memoria del allocator y provocando un desbordamiento de buffer en el heap (heap overflow).

calloc(cantidad, sizeof(*datos)) evita esto porque realiza la multiplicación de forma interna en su implementación y verifica explícitamente si se produce un desbordamiento antes de solicitar memoria. Si se detecta un desbordamiento de enteros, calloc aborta la operación de forma segura retornando NULL.

Solution to Exercise 10

Para mantener la simetría de asignación y liberación, la función constructora debe inicializar todos los recursos dinámicos y la destructora debe liberarlos en orden inverso:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <stdlib.h>
#include <string.h>

typedef struct {
    char *titulo;
    int anio_publicacion;
} libro_t;

libro_t *libro_crear(const char *titulo, int anio) {
    if (titulo == NULL) {
        return NULL;
    }

    libro_t *l = (libro_t *)malloc(sizeof(*l));
    if (l == NULL) {
        return NULL;
    }

    // Alocar memoria para copiar la cadena del título
    l->titulo = (char *)malloc((strlen(titulo) + 1) * sizeof(*(l->titulo)));
    if (l->titulo == NULL) {
        free(l);
        return NULL;
    }

    strcpy(l->titulo, titulo);
    l->anio_publicacion = anio;
    return l;
}

void libro_destruir(libro_t **l) {
    if (l != NULL && *l != NULL) {
        if ((*l)->titulo != NULL) {
            free((*l)->titulo);
            (*l)->titulo = NULL;
        }
        free(*l);
        *l = NULL;
    }
}
Solution to Exercise 11

El puntero de origen debe ser de solo lectura (const int *), mientras que el destino debe ser modificable. Además, se deben validar y documentar las precondiciones contra punteros nulos:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdlib.h>
#include <stdbool.h>

/**
 * Copia los elementos de un arreglo de enteros a otro.
 * @param destino Arreglo donde se copiarán los datos. No debe ser NULL.
 * @param origen Arreglo fuente de datos. No debe ser NULL.
 * @param n Cantidad de elementos a copiar.
 * @pre destino != NULL
 * @pre origen != NULL
 * @returns true si la copia fue exitosa, false si algún puntero es NULL.
 */
bool copiar_enteros(int *destino, const int *origen, size_t n) {
    if (destino == NULL || origen == NULL) {
        return false;
    }

    for (size_t i = 0; i < n; i++) {
        destino[i] = origen[i];
    }
    return true;
}
Solution to Exercise 12

El uso de sizeof(*ptr) asegura que el compilador resuelva el tamaño basándose directamente en la declaración del puntero. Si se utiliza sizeof(tipo) y posteriormente se refactoriza el tipo de datos de la variable, el operando del sizeof podría quedar desactualizado si el programador olvida modificarlo manualmente, lo que causa una asignación de tamaño erróneo.

Ejemplo de bug de refactorización:

// Inicialmente el puntero era float
float *valores = (float *)malloc(100 * sizeof(float));

Si el programa se refactoriza para usar mayor precisión (double) y solo se edita el tipo de la variable:

double *valores = (double *)malloc(100 * sizeof(float)); // ¡BUG SILENCIOSO!

En un sistema de 64 bits, sizeof(double) == 8 and sizeof(float) == 4. Se asignarán 400 bytes en lugar de los 800 bytes necesarios para almacenar 100 doubles. Al iterar y escribir sobre el arreglo, se producirá corrupción de memoria en el heap.

Si se hubiera usado la regla recomendada, la asignación se adapta automáticamente sin inducir errores:

double *valores = (double *)malloc(100 * sizeof(*valores));
Solution to Exercise 13
  • rsp (Stack Pointer): Apunta a la dirección de memoria que representa la posición actual del tope del stack. Se modifica dinámicamente mediante instrucciones de apilado/desapilado (push/pop) o al restar espacio para variables locales (sub rsp, X).

  • rbp (Base Pointer): Apunta a una dirección fija en el marco de pila de la función en ejecución. Permite que la CPU acceda a los parámetros y variables locales de forma constante usando desplazamientos relativos fijos (como rbp - 8 o rbp + 16).

En el prólogo de una función, se ejecuta:

push rbp      ; Guarda el Base Pointer de la función llamadora
mov rbp, rsp  ; Establece el inicio del nuevo marco de pila

En el epílogo, antes de retornar, se ejecuta:

mov rsp, rbp  ; Libera el espacio de variables locales del stack
pop rbp       ; Restaura el Base Pointer de la función llamadora
ret           ; Retorna control

Si rbp no se restaura de forma correcta al finalizar la ejecución, la función llamadora reanudará sus instrucciones con un marco de pila corrompido, haciendo que intente leer sus propias variables locales y parámetros desde direcciones erróneas, provocando comportamientos indefinidos y típicamente una violación de segmento (segmentation fault).

Solution to Exercise 14
  1. Antes de ejecutar la instrucción call, la dirección contenida en rsp es múltiplo de 16 bytes (alineación de 16 bytes).

  2. Cuando el procesador ejecuta la instrucción call, empuja automáticamente en la pila la dirección de retorno de la instrucción siguiente (que ocupa 8 bytes en arquitecturas de 64 bits). Esto desalinea el registro rsp en 8 bytes (dejándolo como múltiplo_de_16 + 8).

  3. Al ingresar a la función invocada, lo primero que hace el prólogo en ensamblador es realizar push rbp. Esta operación empuja otros 8 bytes en la pila, con lo cual el desplazamiento total acumulado respecto a la alineación original es de 16 bytes (8+88 + 8). Esto hace que rsp vuelva a quedar perfectamente alineado a un múltiplo de 16 bytes.

  4. Si la función requiere variables locales, el compilador debe ajustar el desplazamiento del stack (por ejemplo, con sub rsp, X) de manera que X sea un múltiplo de 16 para conservar este alineamiento a lo largo de las siguientes llamadas a funciones. La desalineación del stack en llamadas externas a funciones de biblioteca estándar (como printf) que utilicen instrucciones vectoriales SSE/AVX provocará una caída inmediata del programa.

Solution to Exercise 15
  • Asignación en la pila (Stack): Tiene un costo temporal prácticamente despreciable (generalmente se ejecuta en una sola instrucción y tarda menos de un ciclo de reloj de CPU). El compilador simplemente resta un valor constante al registro rsp (por ejemplo, sub rsp, 16) al iniciar la función. No hay algoritmos de búsqueda ni llamadas al sistema involucradas.

  • Asignación en el heap (Heap): Tiene un costo temporal considerable y variable (puede tomar de decenas a cientos de ciclos de CPU). El allocator de la biblioteca estándar de C debe:

    1. Ejecutar algoritmos de búsqueda (como listas segregadas o búsqueda de primer ajuste) para localizar un bloque libre que cumpla con el tamaño requerido en sus estructuras de datos.

    2. Modificar la metadata de los bloques y manejar la fragmentación.

    3. En caso de no contar con bloques suficientes en su pool, ejecutar una llamada al sistema (syscall como sbrk o mmap) para pedir memoria al sistema operativo, lo que requiere un costoso cambio de contexto del procesador entre modo usuario y modo kernel.

Glosario

Stack (Pila)
Región de memoria que se gestiona automáticamente bajo el orden LIFO para variables locales y llamadas a funciones.
Heap (Montículo)
Región de memoria para asignación dinámica que el programador gestiona manualmente en tiempo de ejecución.
Memory Leak (Fuga de memoria)
Pérdida de la referencia a memoria dinámica sin liberarla.
Dangling Pointer (Puntero colgante)
Puntero que apunta a una dirección ya liberada.
Double Free (Doble liberación)
Intento de liberar la misma dirección de memoria más de una vez.

Síntesis y Resumen

Resumen de Buenas Prácticas

La gestión segura de memoria dinámica requiere disciplina y adherencia a un conjunto de prácticas probadas. Este resumen consolida las reglas fundamentales.

Inicializar Punteros

Siempre inicializá los punteros a NULL al declararlos si no tenés una dirección válida para asignarles inmediatamente. Esto está codificado en la Regla 0x0003h: Siempre debés inicializar las variables a un valor conocido y la Regla 0x3008h: Los punteros nulos deben ser inicializados y comparados con NULL, no con 0.

1
int *ptr = NULL;
Verificar Asignaciones

Siempre comprobá si el valor devuelto por malloc o calloc es NULL antes de usar el puntero. La Regla 0x3001h: Siempre verificá la asignación exitosa de memoria dinámica lo exige explícitamente.

1
2
3
4
5
6
7
ptr = malloc(tamano);
if (ptr == NULL)
{
    // Manejo de error
    fprintf(stderr, "Error: No se pudo asignar memoria.\n");
    return ERROR_MEMORIA;
}
Liberar Memoria

Por cada asignación exitosa con malloc o calloc, debe haber una llamada correspondiente a free. La Regla 0x3002h: Liberá siempre la memoria dinámica y asigná NULL al puntero para evitar punteros colgantes establece esta simetría como obligatoria.

1
free(ptr);
Anular Punteros Después de Liberar

Después de llamar a free(puntero), asigná puntero = NULL para evitar punteros colgantes. La Regla 0x3002h: Liberá siempre la memoria dinámica y asigná NULL al puntero para evitar punteros colgantes lo exige.

1
2
free(ptr);
ptr = NULL;
Mantener Simetría

Intentá que la función que reserva la memoria sea también responsable de liberarla, o que haya una correspondencia clara, como crear_estructura() y destruir_estructura(). Esta práctica está documentada en la Regla 0x3002h: Liberá siempre la memoria dinámica y asigná NULL al puntero para evitar punteros colgantes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
recurso_t *crear_recurso()
{
    recurso_t *r = malloc(sizeof(*r));
    if (r == NULL)
    {
        return NULL;
    }
    // Inicialización...
    return r;
}

void destruir_recurso(recurso_t *r)
{
    if (r != NULL)
    {
        // Liberación de recursos internos...
        free(r);
    }
}
Documentar Propiedad

La Regla 0x3006h: Documentá la propiedad de los recursos al utilizar punteros exige que documentes claramente quién es el responsable de liberar la memoria cuando una función recibe o devuelve un puntero a memoria dinámica.

1
2
3
4
5
6
7
8
/**
 * Crea un nuevo nodo de lista.
 * @param valor El valor a almacenar en el nodo.
 * @returns Un puntero al nuevo nodo. El llamador es responsable
 *          de liberar esta memoria con destruir_nodo().
 *          Retorna NULL si no hay memoria disponible.
 */
nodo_t *crear_nodo(int valor);
Usar const Apropiadamente

Según la Regla 0x3007h: Los argumentos de tipo puntero deben ser const siempre que la función no los modifique, los argumentos de tipo puntero deben ser const siempre que la función no los modifique. Esto establece un contrato claro y permite al compilador detectar modificaciones no intencionales.

1
2
3
4
5
6
7
8
void imprimir_arreglo(const int *arreglo, size_t tamano)
{
    for (size_t i = 0; i < tamano; i++)
    {
        printf("%d ", arreglo[i]);
    }
    printf("\n");
}
Usar sizeof Correctamente

La Regla 0x300Bh: Usá siempre sizeof en las asignaciones de memoria dinámica, prefiriendo sizeof(*ptr) establece que debés usar siempre sizeof en las asignaciones de memoria dinámica, y preferir sizeof(*puntero) sobre sizeof(tipo).

1
2
3
4
5
// Preferido
int *ptr = malloc(n * sizeof(*ptr));

// Evitar
int *ptr = malloc(n * sizeof(int));  // Si el tipo de ptr cambia, esto falla
Usar size_t para Tamaños e Índices

La Regla 0x3010h: Las variables que representan tamaños o índices de arreglos deben ser de tipo size_t exige que las variables que representan tamaños o índices de arreglos sean de tipo size_t.

1
2
3
4
5
6
7
size_t tamano = 10;
int *arreglo = malloc(tamano * sizeof(*arreglo));

for (size_t i = 0; i < tamano; i++)
{
    arreglo[i] = 0;
}
Verificar Límites

La Regla 0x300Ch: Verificá siempre los límites de los arreglos antes de acceder a sus elementos exige verificar siempre los límites de los arreglos antes de acceder a sus elementos.

1
2
3
4
5
6
7
void establecer_elemento(int *arreglo, size_t tamano, size_t indice, int valor)
{
    if (indice < tamano)
    {
        arreglo[indice] = valor;
    }
}

Conceptos Clave

Este apunte explora la gestión de memoria dinámica, el mecanismo que permite a los programas solicitar y liberar memoria durante la ejecución, habilitando estructuras de datos flexibles y adaptables.

Conclusiones

La gestión de memoria dinámica es una de las características más poderosas y peligrosas de C. Su dominio requiere comprender no solo las funciones y sintaxis, sino también los principios fundamentales de cómo funciona la memoria en un programa.

Las buenas prácticas presentadas en este apunte no son sugerencias opcionales: son requisitos para escribir código C profesional y confiable. Cada regla existe porque previene errores reales que han causado innumerables problemas en sistemas de producción.

A medida que adquirás experiencia, estas prácticas se vuelven segunda naturaleza. Inicialmente pueden parecer restrictivas, pero con el tiempo reconocerás que son liberadoras: te permiten escribir código complejo con confianza, sabiendo que has evitado las trampas más comunes.

Referencias y Lecturas Complementarias

Referencias y Lecturas Adicionales

Para profundizar en la gestión de memoria, consultá:

Para las reglas de estilo, consultá el documento Regla 0x0000h: La claridad y prolijidad son de máxima importancia donde se detallan todas las convenciones utilizadas en este curso.