Memoria dinámica en C — ejercicios para subir notas, filtrar aprobados y mantener un vector ordenado que crece (y el doble free que me tumbó el acta)

La semana que cerré mi primer «acta de notas» en C me sentía invencible: malloc, realloc con temporal, free… todo en orden. Hice una función cerrar_acta que calculaba la media y, por limpieza, liberaba el vector. Y en main, por limpieza también, volví a liberarlo. Resultado: free(): double free detected in tcache 2 y el programa abortado justo después de imprimir la media. Dos free «por si acaso» son uno de más. Ese fallo es el tercer fragmento de «¿Qué falla aquí?», y la idea que deja (cada bloque tiene un dueño) atraviesa todos los ejercicios.

Aquí ya no te doy el código hecho. Tienes tres ejercicios de dificultad creciente con las notas de una clase, tres fragmentos con errores para que predigas qué pasa y, al final, las soluciones comentadas y el chuletario. Si te atascas con algo de base, vuelve a la teoría de memoria dinámica en C o a la práctica.

Cómo usar este artículo

  1. Lee el enunciado de un nivel y escribe tu solución sin mirar las mías. Compila con gcc -Wall -std=c11 -g -fsanitize=address.
  2. Compara tu salida con la salida esperada, carácter a carácter.
  3. Si no te sale, abre la pista. Si sigue sin salir, mira la solución, ciérrala y vuelve a intentarlo desde cero.
  4. Haz «¿Qué falla aquí?» antes de leer las explicaciones: predecir el fallo es el mejor entrenamiento.

Nivel 1 (básico): subir las notas sin tocar las originales

El profe quiere ver cómo quedarían las notas si subiera medio punto a todo el mundo, pero sin modificar las notas reales. Escribe una función

double *subir_notas(const double *v, int n, double extra)

que devuelva un vector nuevo (reservado con malloc) con cada nota subida extra puntos, sin pasar de 10. En main, con las notas {4.5, 6.0, 9.8, 3.0, 7.5} y extra = 0.5, muestra las originales y las subidas, y libera lo que haga falta.

Salida esperada:

Originales: 4.5 6.0 9.8 3.0 7.5
Subidas   : 5.0 6.5 10.0 3.5 8.0

La función reserva, pero quien usa el resultado es main: ¿dónde va el free? Y para el tope, basta con un if después de sumar.

Nivel 2 (intermedio): filtrar los aprobados con la memoria justa

Escribe

double *filtrar_aprobados(const double *v, int n, int *k)

que devuelva un vector nuevo solo con los aprobados (nota ≥ 5) y deje en *k cuántos hay. Tiene que reservar exactamente la memoria necesaria (ni una casilla de más) y devolver NULL si no hay ningún aprobado. Pruébala con dos grupos: {6.5, 4.0, 9.5, 5.0, 7.0, 3.0, 8.5} y {2.0, 4.5}.

Salida esperada:

Grupo A: 5 de 7 aprobados -> 6.5 9.5 5.0 7.0 8.5
Grupo B: 0 de 2 aprobados -> nadie

Para reservar lo justo necesitas saber cuántos aprobados hay antes del malloc. Recorre el vector dos veces.

Nivel 3 (desafío): un vector ordenado que crece y se limpia

Las notas llegan desordenadas: {6.5, 4.0, 9.5, 5.0, 7.0, 3.0, 8.5}. Escribe:

  • int insertar_ordenado(double **v, int *n, int *cap, double x): inserta x en su sitio para que el vector esté siempre ordenado de menor a mayor. Si no cabe, duplica la capacidad con realloc. Devuelve 1 si va bien y 0 si no hay memoria.
  • void quitar_suspensos(double **v, int *n, int *cap): elimina las notas menores que 5 y encoge el vector a su tamaño justo.

Empieza con capacidad 1 y muestra el vector, n y cap después de cada inserción y al final.

Salida esperada:

+6.5 -> [6.5] n=1 cap=1
+4.0 -> [4.0 6.5] n=2 cap=2
+9.5 -> [4.0 6.5 9.5] n=3 cap=4
+5.0 -> [4.0 5.0 6.5 9.5] n=4 cap=4
+7.0 -> [4.0 5.0 6.5 7.0 9.5] n=5 cap=8
+3.0 -> [3.0 4.0 5.0 6.5 7.0 9.5] n=6 cap=8
+8.5 -> [3.0 4.0 5.0 6.5 7.0 8.5 9.5] n=7 cap=8
Sin suspensos -> [5.0 6.5 7.0 8.5 9.5] n=5 cap=5

Para insertar ordenado, empieza por el final y corre una casilla a la derecha cada nota mayor que x; el hueco que queda es su sitio. Para quitar suspensos, usa un índice de lectura y otro de escritura, como en el acta de la práctica.

¿Qué falla aquí?

Tres fragmentos con un error cada uno. Lee el código, predice qué pasa y después abre la explicación.

Fragmento 1: las faltas de asistencia

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int n = 8;                           /* 8 alumnos */
    int *faltas = malloc(n);             /* faltas de asistencia de cada alumno */
    if (faltas == NULL) return 1;
    for (int i = 0; i < n; i++)
        faltas[i] = i % 3;
    printf("Faltas del ultimo: %d\n", faltas[n - 1]);
    free(faltas);
    return 0;
}

¿Qué crees que pasa? Sin herramientas, lo más probable es que imprima un número y parezca que todo va bien. Compilado con gcc -Wall -std=c11 -g -fsanitize=address, esta es la salida (el informe de AddressSanitizer, recortado a las líneas importantes):

=================================================================
==723==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x502000000018
WRITE of size 4 at 0x502000000018 thread T0
    #0 in main fragmento.c:9
0x502000000018 is located 0 bytes after 8-byte region [0x502000000010,0x502000000018)
allocated by thread T0 here:
    #1 in main fragmento.c:6
SUMMARY: AddressSanitizer: heap-buffer-overflow fragmento.c:9 in main

malloc(n) reserva n bytes: 8 bytes. Un int ocupa 4, así que solo caben 2 faltas. En la vuelta i = 2 el programa escribe 4 bytes justo después del bloque, y AddressSanitizer lo caza: WRITE of size 4, 0 bytes after 8-byte region, línea 9 (faltas[i] = i % 3;), sobre un bloque reservado en la línea 6.

Que un programa con este fallo «funcione» no significa que esté bien. Estás pisando memoria que puede ser de otro dato, y el fallo aparecerá cuando menos te lo esperes.

El arreglo es multiplicar por el tamaño de cada casilla:

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int n = 8;                                 /* 8 alumnos */
    int *faltas = malloc(n * sizeof *faltas);  /* 8 casillas de int = 32 bytes */
    if (faltas == NULL) return 1;
    for (int i = 0; i < n; i++)
        faltas[i] = i % 3;
    printf("Faltas del ultimo: %d\n", faltas[n - 1]);
    free(faltas);
    return 0;
}

Salida:

Faltas del ultimo: 1

La última falta es la del alumno 7: 7 % 3 = 1.

Con sizeof *faltas no tienes que acordarte del tipo. Si mañana las faltas pasan a ser short, la línea sigue siendo correcta.

Fragmento 2: las bajas mal contadas

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int n = 3;
    double *notas = malloc(n * sizeof(double));
    if (notas == NULL) return 1;
    notas[0] = 7.5; notas[1] = 4.0; notas[2] = 9.0;

    int bajas = 4;                     /* se dan de baja 4... pero solo había 3 */
    int nuevo_n = n - bajas;
    printf("Ajustando a %d notas...\n", nuevo_n);
    notas = realloc(notas, nuevo_n * sizeof(double));
    printf("Primera nota: %.1f\n", notas[0]);
    free(notas);
    return 0;
}

¿Qué crees que pasa? Salida real:

Ajustando a -1 notas...
Segmentation fault (core dumped)

Hay dos errores encadenados. El primero, de cuentas: había 3 notas y se restan 4 bajas, así que nuevo_n vale -1. Al multiplicar -1 * sizeof(double), el -1 se convierte a size_t (un entero sin signo) y se transforma en un número gigantesco: se le están pidiendo a realloc casi 18 trillones de bytes. Lógicamente, falla y devuelve NULL.

El segundo error es el que convierte un fallo de cuentas en un desastre: notas = realloc(notas, ...) sin temporal. Ahora notas vale NULL, el bloque de 3 notas se ha quedado reservado sin que nadie pueda llegar a él, y notas[0] lee la dirección 0: Segmentation fault.

Pasar a malloc o realloc un tamaño calculado sin comprobarlo. Un número negativo se convierte en un tamaño enorme sin ningún aviso.

El arreglo comprueba las cuentas y usa un temporal:

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int n = 3;
    double *notas = malloc(n * sizeof(double));
    if (notas == NULL) return 1;
    notas[0] = 7.5; notas[1] = 4.0; notas[2] = 9.0;

    int bajas = 4;
    int nuevo_n = n - bajas;
    if (nuevo_n < 1) {                 /* comprobamos las cuentas antes de pedir memoria */
        printf("Tamaño imposible (%d): me quedo con %d notas\n", nuevo_n, n);
    } else {
        double *tmp = realloc(notas, nuevo_n * sizeof(double));
        if (tmp != NULL) { notas = tmp; n = nuevo_n; }  /* si falla, notas sigue intacto */
    }
    printf("Primera nota: %.1f\n", notas[0]);
    free(notas);
    return 0;
}

Salida:

Tamaño imposible (-1): me quedo con 3 notas
Primera nota: 7.5

Ahora el programa detecta el tamaño imposible, conserva las 3 notas y sigue funcionando.

En un caso real, el error de cuentas también habría que arreglarlo (¿por qué se dan de baja 4 si hay 3?). El temporal no corrige las cuentas, pero evita que un fallo se convierta en pérdida de datos.

Fragmento 3: el acta que se cerró dos veces

#include <stdio.h>
#include <stdlib.h>

void cerrar_acta(double *notas, int n) {
    double suma = 0;
    for (int i = 0; i < n; i++) suma += notas[i];
    printf("Acta cerrada: media %.2f\n", suma / n);
    free(notas);                       /* la función libera... */
}

int main(void) {
    double *notas = malloc(4 * sizeof(double));
    if (notas == NULL) return 1;
    notas[0] = 7.5; notas[1] = 4.0; notas[2] = 9.0; notas[3] = 6.5;
    cerrar_acta(notas, 4);
    free(notas);                       /* ...y main vuelve a liberar */
    printf("Fin del programa\n");
    return 0;
}

¿Qué crees que pasa? Salida real:

Acta cerrada: media 6.75
free(): double free detected in tcache 2
Aborted (core dumped)

cerrar_acta recibe una copia del puntero, no una copia del vector: los dos notas (el de main y el parámetro) apuntan al mismo bloque. La función lo libera y main lo vuelve a liberar. La biblioteca de C detecta que ese bloque ya estaba libre (double free detected) y aborta el programa: «Fin del programa» no llega a imprimirse.

Dos punteros, un solo bloqueEl puntero notas de main y el parámetro notas de cerrar_acta apuntan al mismo bloque de 4 notas. cerrar_acta lo libera y después main lo vuelve a liberar, lo que provoca el aborto del programa.main()double *notascerrar_acta()double *notas7.54.09.06.5ya liberado1. free(notas) en cerrar_acta: el bloque vuelve al sistema2. free(notas) en main: el MISMO bloque otra vez → abort
Copiar un puntero no copia el bloque: dos punteros al mismo bloque y dos free son un doble free.

El dibujo muestra el problema: dos punteros, un solo bloque y dos free.

Liberar «por si acaso» en dos sitios. Decide quién es el dueño del bloque y libéralo solo allí.

El arreglo: cerrar_acta solo lee (lo marcamos con const) y el único free está en main, que es quien reservó.

#include <stdio.h>
#include <stdlib.h>

void cerrar_acta(const double *notas, int n) {   /* const: solo lee, no es la dueña */
    double suma = 0;
    for (int i = 0; i < n; i++) suma += notas[i];
    printf("Acta cerrada: media %.2f\n", suma / n);
}

int main(void) {
    double *notas = malloc(4 * sizeof(double));
    if (notas == NULL) return 1;
    notas[0] = 7.5; notas[1] = 4.0; notas[2] = 9.0; notas[3] = 6.5;
    cerrar_acta(notas, 4);
    free(notas);                       /* un único dueño, un único free */
    notas = NULL;
    printf("Fin del programa\n");
    return 0;
}

Salida:

Acta cerrada: media 6.75
Fin del programa

Media comprobada: (7.5 + 4.0 + 9.0 + 6.5) / 4 = 27 / 4 = 6.75.

Si una función libera un bloque que le pasan, dilo en su nombre o en un comentario (destruir_acta, «libera v»). Y quien la llama, que ponga su puntero a NULL justo después.

Soluciones

Solución del nivel 1

#include <stdio.h>
#include <stdlib.h>

/* Devuelve un vector NUEVO con cada nota subida 'extra' puntos (sin pasar de 10).
   El original no se toca: por eso es const. Quien llama libera el resultado. */
double *subir_notas(const double *v, int n, double extra) {
    double *r = malloc(n * sizeof *r);
    if (r == NULL) return NULL;
    for (int i = 0; i < n; i++) {
        r[i] = v[i] + extra;
        if (r[i] > 10) r[i] = 10;           /* tope: nadie saca un 10.3 */
    }
    return r;
}

void mostrar(const char *titulo, const double *v, int n) {
    printf("%s:", titulo);
    for (int i = 0; i < n; i++) printf(" %.1f", v[i]);
    printf("\n");
}

int main(void) {
    double notas[] = {4.5, 6.0, 9.8, 3.0, 7.5};
    int n = sizeof notas / sizeof notas[0];

    double *subidas = subir_notas(notas, n, 0.5);
    if (subidas == NULL) return 1;

    mostrar("Originales", notas, n);
    mostrar("Subidas   ", subidas, n);

    free(subidas);                          /* lo reservó subir_notas, lo libera main */
    return 0;
}

Salida:

Originales: 4.5 6.0 9.8 3.0 7.5
Subidas   : 5.0 6.5 10.0 3.5 8.0

Por qué está hecho así:

  • **const double *v**: la función promete no modificar las notas originales. Si por error escribieras v[i] += extra, el compilador daría un error.
  • **Un vector nuevo con malloc**: devolver un vector local sería repetir el error del Segmentation fault de la teoría. El resultado tiene que vivir en el heap.
  • El tope: 9.8 + 0.5 = 10.3, que se recorta a 10.0. El resto: 4.5 + 0.5 = 5.0, 6.0 + 0.5 = 6.5, 3.0 + 0.5 = 3.5 y 7.5 + 0.5 = 8.0.
  • **free(subidas) en main** y nunca free(notas): notas es un vector normal de la pila, y liberar algo que no viene de malloc es un error grave.
  • **"Subidas " con espacios** es solo para que las dos líneas queden alineadas.

Solución del nivel 2

#include <stdio.h>
#include <stdlib.h>

/* Devuelve un vector nuevo SOLO con los aprobados y deja su tamaño en *k.
   Dos pasadas: contar y luego copiar, para pedir exactamente la memoria justa. */
double *filtrar_aprobados(const double *v, int n, int *k) {
    *k = 0;
    for (int i = 0; i < n; i++)             /* pasada 1: ¿cuántos aprobados hay? */
        if (v[i] >= 5) (*k)++;

    if (*k == 0) return NULL;               /* malloc(0) no sirve para nada: NULL = vacío */

    double *r = malloc(*k * sizeof *r);
    if (r == NULL) { *k = 0; return NULL; }

    int j = 0;                              /* j = siguiente casilla libre de r */
    for (int i = 0; i < n; i++)             /* pasada 2: copiar */
        if (v[i] >= 5) r[j++] = v[i];
    return r;
}

void informe(const char *grupo, const double *v, int n) {
    int k;
    double *ap = filtrar_aprobados(v, n, &k);
    printf("%s: %d de %d aprobados ->", grupo, k, n);
    if (ap == NULL) printf(" nadie");
    for (int i = 0; i < k; i++) printf(" %.1f", ap[i]);
    printf("\n");
    free(ap);                               /* free(NULL) es seguro: no hace falta un if */
}

int main(void) {
    double grupo_a[] = {6.5, 4.0, 9.5, 5.0, 7.0, 3.0, 8.5};
    double grupo_b[] = {2.0, 4.5};
    informe("Grupo A", grupo_a, 7);
    informe("Grupo B", grupo_b, 2);
    return 0;
}

Salida:

Grupo A: 5 de 7 aprobados -> 6.5 9.5 5.0 7.0 8.5
Grupo B: 0 de 2 aprobados -> nadie

Zoom a la función, que es donde están las decisiones:

/* Devuelve un vector nuevo SOLO con los aprobados y deja su tamaño en *k.
   Dos pasadas: contar y luego copiar, para pedir exactamente la memoria justa. */
double *filtrar_aprobados(const double *v, int n, int *k) {
    *k = 0;
    for (int i = 0; i < n; i++)             /* pasada 1: ¿cuántos aprobados hay? */
        if (v[i] >= 5) (*k)++;

    if (*k == 0) return NULL;               /* malloc(0) no sirve para nada: NULL = vacío */

    double *r = malloc(*k * sizeof *r);
    if (r == NULL) { *k = 0; return NULL; }

    int j = 0;                              /* j = siguiente casilla libre de r */
    for (int i = 0; i < n; i++)             /* pasada 2: copiar */
        if (v[i] >= 5) r[j++] = v[i];
    return r;
}

Traza del grupo A en la segunda pasada (*k ya vale 5, así que se reservan 5 × 8 = 40 bytes):

iv[i]¿≥ 5?Se copia enj después
06.5Sír[0]1
14.0No—1
29.5Sír[1]2
35.0Sír[2]3
47.0Sír[3]4
53.0No—4
68.5Sír[4]5

Por qué está hecho así:

  • **Dos pasadas en lugar de realloc en cada aprobado**: la primera solo cuenta, así que el malloc es exacto y no hay que crecer ni encoger.
  • ***k == 0 → NULL**: malloc(0) puede devolver NULL o un puntero que no se puede usar, según el sistema. Devolver NULL a propósito deja claro que el vector está vacío.
  • **Si malloc falla, *k = 0**: así quien llama nunca recorre k casillas de un vector que no existe.
  • **free(ap) sin if**: en el grupo B, ap es NULL y free(NULL) no hace nada. La línea vale para los dos casos.

Solución del nivel 3

#include <stdio.h>
#include <stdlib.h>

void mostrar(const double *v, int n, int cap) {
    printf("[");
    for (int i = 0; i < n; i++) printf(i ? " %.1f" : "%.1f", v[i]);
    printf("] n=%d cap=%d\n", n, cap);
}

/* Inserta x manteniendo el vector ORDENADO de menor a mayor. Duplica la capacidad si hace falta. */
int insertar_ordenado(double **v, int *n, int *cap, double x) {
    if (*n == *cap) {
        double *tmp = realloc(*v, 2 * *cap * sizeof **v);
        if (tmp == NULL) return 0;
        *v = tmp;
        *cap *= 2;
    }
    int i = *n - 1;
    while (i >= 0 && (*v)[i] > x) {         /* desde el final, corremos a la derecha los mayores */
        (*v)[i + 1] = (*v)[i];
        i--;
    }
    (*v)[i + 1] = x;                        /* el hueco que queda es el sitio de x */
    (*n)++;
    return 1;
}

/* Quita los suspensos compactando el vector y lo encoge a su tamaño justo */
void quitar_suspensos(double **v, int *n, int *cap) {
    int j = 0;
    for (int i = 0; i < *n; i++)
        if ((*v)[i] >= 5) (*v)[j++] = (*v)[i];   /* los aprobados se corren a la izquierda */
    *n = j;
    if (j > 0 && j < *cap) {
        double *tmp = realloc(*v, j * sizeof **v);
        if (tmp != NULL) { *v = tmp; *cap = j; }  /* si falla, el vector grande sigue valiendo */
    }
}

int main(void) {
    const double llegan[] = {6.5, 4.0, 9.5, 5.0, 7.0, 3.0, 8.5};
    int n = 0, cap = 1;
    double *notas = malloc(cap * sizeof *notas);
    if (notas == NULL) return 1;

    for (int i = 0; i < 7; i++) {
        if (!insertar_ordenado(&notas, &n, &cap, llegan[i])) break;
        printf("+%.1f -> ", llegan[i]);
        mostrar(notas, n, cap);
    }
    quitar_suspensos(&notas, &n, &cap);
    printf("Sin suspensos -> ");
    mostrar(notas, n, cap);

    free(notas);
    return 0;
}

Salida:

+6.5 -> [6.5] n=1 cap=1
+4.0 -> [4.0 6.5] n=2 cap=2
+9.5 -> [4.0 6.5 9.5] n=3 cap=4
+5.0 -> [4.0 5.0 6.5 9.5] n=4 cap=4
+7.0 -> [4.0 5.0 6.5 7.0 9.5] n=5 cap=8
+3.0 -> [3.0 4.0 5.0 6.5 7.0 9.5] n=6 cap=8
+8.5 -> [3.0 4.0 5.0 6.5 7.0 8.5 9.5] n=7 cap=8
Sin suspensos -> [5.0 6.5 7.0 8.5 9.5] n=5 cap=5

La función más delicada es la inserción:

/* Inserta x manteniendo el vector ORDENADO de menor a mayor. Duplica la capacidad si hace falta. */
int insertar_ordenado(double **v, int *n, int *cap, double x) {
    if (*n == *cap) {
        double *tmp = realloc(*v, 2 * *cap * sizeof **v);
        if (tmp == NULL) return 0;
        *v = tmp;
        *cap *= 2;
    }
    int i = *n - 1;
    while (i >= 0 && (*v)[i] > x) {         /* desde el final, corremos a la derecha los mayores */
        (*v)[i + 1] = (*v)[i];
        i--;
    }
    (*v)[i + 1] = x;                        /* el hueco que queda es el sitio de x */
    (*n)++;
    return 1;
}

Veamos qué pasa al insertar el 5.0, cuando el vector es [4.0 6.5 9.5] con n = 3 y cap = 4 (cabe sin crecer):

Pasoi(*v)[i]¿> 5.0?Qué haceVector
Inicio29.5Sív[3] = v[2]4.0 6.5 9.5 9.5
216.5Sív[2] = v[1]4.0 6.5 6.5 9.5
304.0Nosale del bucle4.0 6.5 6.5 9.5
Final0——v[i + 1] = 5.04.0 5.0 6.5 9.5
Insertar 5.0 manteniendo el ordenCuatro filas. Antes: 4.0, 6.5, 9.5 y un hueco. El 9.5 se copia a la casilla 3, el 6.5 a la casilla 2, el bucle para en 4.0 y el 5.0 se coloca en la casilla 1.antes: n=3, entra 5.04.06.59.5i=2: 9.5 > 5.0 → corre4.06.59.59.5i=1: 6.5 > 5.0 → corre4.06.56.59.5i=0: 4.0 no es mayor → para4.05.06.59.5v[i + 1] = 5.0[0][1][2][3]
Los mayores que x se corren una casilla a la derecha; el hueco que queda es el sitio de x.

En el dibujo se ve el «corrimiento»: cada nota mayor que 5.0 se copia una casilla a la derecha (por eso, durante un momento, una nota aparece repetida), y el hueco que queda es exactamente el sitio del 5.0.

Y la limpieza final:

/* Quita los suspensos compactando el vector y lo encoge a su tamaño justo */
void quitar_suspensos(double **v, int *n, int *cap) {
    int j = 0;
    for (int i = 0; i < *n; i++)
        if ((*v)[i] >= 5) (*v)[j++] = (*v)[i];   /* los aprobados se corren a la izquierda */
    *n = j;
    if (j > 0 && j < *cap) {
        double *tmp = realloc(*v, j * sizeof **v);
        if (tmp != NULL) { *v = tmp; *cap = j; }  /* si falla, el vector grande sigue valiendo */
    }
}

Por qué está hecho así:

  • Crecer antes de correr: si el vector está lleno, (*v)[i + 1] con i = n - 1 escribiría una casilla fuera del bloque. Por eso el realloc va lo primero.
  • **i >= 0 antes que (*v)[i] > x**: el && se evalúa de izquierda a derecha y se corta en cuanto algo es falso. Cuando x es la nota más pequeña (el 3.0), i llega a -1 y la segunda condición ni se mira: sin ese orden, leerías v[-1].
  • Las capacidades: 1 → 2 (al llegar el 4.0) → 4 (el 9.5) → 8 (el 7.0). Tres realloc para siete notas.
  • **quitar_suspensos** usa lectura (i) y escritura (j) como el acta de la práctica: 3.0 y 4.0 no se copian y quedan 5 notas. Después encoge de 8 a 5 con temporal.
  • **j > 0** evita realloc(*v, 0) si todos suspendieran: en ese caso el vector se queda con su capacidad y n = 0.

Míralo paso a paso en Python Tutor

Abre Python Tutor, elige C (gcc) y pega esta versión corta del nivel 2, con las dos pasadas en main:

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    double v[] = {6.5, 4.0, 9.5, 3.0};
    int n = 4, k = 0;

    for (int i = 0; i < n; i++)
        if (v[i] >= 5) k++;

    double *ap = malloc(k * sizeof(double));
    if (ap == NULL) return 1;

    int j = 0;
    for (int i = 0; i < n; i++)
        if (v[i] >= 5) ap[j++] = v[i];

    printf("%d aprobados, el primero %.1f\n", k, ap[0]);
    free(ap);
    ap = NULL;
    return 0;
}

Qué observar paso a paso:

  1. Durante la primera pasada no aparece nada en el heap: solo cambia k en la pila (termina valiendo 2).
  2. Tras el malloc, aparece en el heap un bloque de exactamente 2 casillas, ni una más: la primera pasada ha servido para eso.
  3. En la segunda pasada, fíjate en j: solo avanza cuando la nota es un aprobado, mientras i avanza siempre.
  4. Después de free(ap), el bloque desaparece, y con ap = NULL la flecha deja de apuntar a ningún sitio.

Chuletario: memoria dinámica en C

Lo esencial en 30 segundos

  • Todo está en <stdlib.h>. Los tamaños son en bytes: n * sizeof *v.
  • malloc no inicializa; calloc pone a cero; realloc cambia el tamaño conservando los datos.
  • Después de cada reserva, comprueba NULL.
  • realloc siempre con puntero temporal.
  • Cada bloque, un dueño y un único free. Después, p = NULL.
  • Una función que devuelve un vector lo reserva con malloc; quien la llama lo libera.

Reservar y liberar

Quiero…CódigoResultado
n doubles sin inicializardouble *v = malloc(n * sizeof *v);Bloque de n × 8 bytes o NULL
n contadores a ceroint *c = calloc(n, sizeof *c);Bloque de n × 4 bytes a 0 o NULL
Pasar a m casillasdouble *tmp = realloc(v, m * sizeof *v); if (tmp) v = tmp;Datos conservados; si falla, v intacto
Devolver la memoriafree(v); v = NULL;Bloque liberado, puntero desactivado
Saber cuántas casillas tengoGuardarlo en una variable nsizeof(v) vale 8: no sirve

Plantillas de los ejercicios

PatrónCódigo claveResultado con los datos de los ejercicios
Función que devuelve un vector nuevodouble *r = malloc(n * sizeof *r); … return r;subir_notas → 5.0 6.5 10.0 3.5 8.0
Reservar lo justoContar → malloc(k * sizeof *r) → copiar5 aprobados en 40 bytes
Vector que creceif (n == cap) { tmp = realloc(v, 2 * cap * sizeof *v); … }cap 1 → 2 → 4 → 8 con 7 notas
Filtrar en el sitioif (ok) v[j++] = v[i]; y luego realloc a j7 notas → 5 aprobados, cap 5
Tabla dinámicadouble **t = malloc(f * sizeof *t); + t[i] = calloc(c, sizeof **t);Libera filas y después t

Síntoma y causa probable

SíntomaCausa probable
Segmentation fault al usar el vectormalloc/realloc devolvió NULL y no se comprobó, o devolviste un vector local
heap-buffer-overflow (AddressSanitizer)Reservaste bytes en lugar de casillas o te sales del índice n - 1
free(): double free detectedDos free del mismo bloque (dos dueños)
detected memory leaksFalta un free, o v = realloc(v, …) falló y perdió el bloque
Números raros en casillas nuevasmalloc o realloc no inicializan: usa calloc o rellénalas
warning: function returns address of local variableDevuelves un vector de la pila: resérvalo en el heap

Antes de entregar, pregúntate

  1. ¿Cada malloc, calloc y realloc multiplica por sizeof y comprueba NULL?
  2. ¿Todos los realloc usan un puntero temporal?
  3. ¿Sé quién es el dueño de cada bloque y hay exactamente un free por bloque en todos los caminos del programa?
  4. ¿He compilado con -g -fsanitize=address y el programa termina sin ningún informe?

Con esto cierras la memoria dinámica. El siguiente paso en IC2 son los structs y las cadenas en C, donde todo lo de hoy se usa para guardar registros completos.

Sigue aprendiendoTodos los artículos de C

¿Te ha servido este artículo? Así puedes apoyar SergioLearns →

Sigue aprendiendo

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *