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.
Tabla de Contenidos
Cómo usar este artículo
- 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. - Compara tu salida con la salida esperada, carácter a carácter.
- Si no te sale, abre la pista. Si sigue sin salir, mira la solución, ciérrala y vuelve a intentarlo desde cero.
- 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): insertaxen su sitio para que el vector esté siempre ordenado de menor a mayor. Si no cabe, duplica la capacidad conrealloc. 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.
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 escribierasv[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)enmain** y nuncafree(notas):notases un vector normal de la pila, y liberar algo que no viene demalloces 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):
| i | v[i] | ¿≥ 5? | Se copia en | j después |
|---|---|---|---|---|
| 0 | 6.5 | Sí | r[0] | 1 |
| 1 | 4.0 | No | — | 1 |
| 2 | 9.5 | Sí | r[1] | 2 |
| 3 | 5.0 | Sí | r[2] | 3 |
| 4 | 7.0 | Sí | r[3] | 4 |
| 5 | 3.0 | No | — | 4 |
| 6 | 8.5 | Sí | r[4] | 5 |
Por qué está hecho así:
- **Dos pasadas en lugar de
reallocen cada aprobado**: la primera solo cuenta, así que elmalloces exacto y no hay que crecer ni encoger. - **
*k == 0→NULL**:malloc(0)puede devolverNULLo un puntero que no se puede usar, según el sistema. DevolverNULLa propósito deja claro que el vector está vacío. - **Si
mallocfalla,*k = 0**: así quien llama nunca recorrekcasillas de un vector que no existe. - **
free(ap)sinif**: en el grupo B,apesNULLyfree(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(¬as, &n, &cap, llegan[i])) break;
printf("+%.1f -> ", llegan[i]);
mostrar(notas, n, cap);
}
quitar_suspensos(¬as, &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):
| Paso | i | (*v)[i] | ¿> 5.0? | Qué hace | Vector |
|---|---|---|---|---|---|
| Inicio | 2 | 9.5 | Sí | v[3] = v[2] | 4.0 6.5 9.5 9.5 |
| 2 | 1 | 6.5 | Sí | v[2] = v[1] | 4.0 6.5 6.5 9.5 |
| 3 | 0 | 4.0 | No | sale del bucle | 4.0 6.5 6.5 9.5 |
| Final | 0 | — | — | v[i + 1] = 5.0 | 4.0 5.0 6.5 9.5 |
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]coni = n - 1escribiría una casilla fuera del bloque. Por eso elreallocva lo primero. - **
i >= 0antes que(*v)[i] > x**: el&&se evalúa de izquierda a derecha y se corta en cuanto algo es falso. Cuandoxes la nota más pequeña (el 3.0),illega a -1 y la segunda condición ni se mira: sin ese orden, leeríasv[-1]. - Las capacidades: 1 → 2 (al llegar el 4.0) → 4 (el 9.5) → 8 (el 7.0). Tres
reallocpara 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** evitarealloc(*v, 0)si todos suspendieran: en ese caso el vector se queda con su capacidad yn = 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:
- Durante la primera pasada no aparece nada en el heap: solo cambia
ken la pila (termina valiendo 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. - En la segunda pasada, fíjate en
j: solo avanza cuando la nota es un aprobado, mientrasiavanza siempre. - Después de
free(ap), el bloque desaparece, y conap = NULLla 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. mallocno inicializa;callocpone a cero;realloccambia el tamaño conservando los datos.- Después de cada reserva, comprueba
NULL. reallocsiempre 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ódigo | Resultado |
|---|---|---|
| n doubles sin inicializar | double *v = malloc(n * sizeof *v); | Bloque de n × 8 bytes o NULL |
| n contadores a cero | int *c = calloc(n, sizeof *c); | Bloque de n × 4 bytes a 0 o NULL |
| Pasar a m casillas | double *tmp = realloc(v, m * sizeof *v); if (tmp) v = tmp; | Datos conservados; si falla, v intacto |
| Devolver la memoria | free(v); v = NULL; | Bloque liberado, puntero desactivado |
| Saber cuántas casillas tengo | Guardarlo en una variable n | sizeof(v) vale 8: no sirve |
Plantillas de los ejercicios
| Patrón | Código clave | Resultado con los datos de los ejercicios |
|---|---|---|
| Función que devuelve un vector nuevo | double *r = malloc(n * sizeof *r); … return r; | subir_notas → 5.0 6.5 10.0 3.5 8.0 |
| Reservar lo justo | Contar → malloc(k * sizeof *r) → copiar | 5 aprobados en 40 bytes |
| Vector que crece | if (n == cap) { tmp = realloc(v, 2 * cap * sizeof *v); … } | cap 1 → 2 → 4 → 8 con 7 notas |
| Filtrar en el sitio | if (ok) v[j++] = v[i]; y luego realloc a j | 7 notas → 5 aprobados, cap 5 |
| Tabla dinámica | double **t = malloc(f * sizeof *t); + t[i] = calloc(c, sizeof **t); | Libera filas y después t |
Síntoma y causa probable
| Síntoma | Causa probable |
|---|---|
Segmentation fault al usar el vector | malloc/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 detected | Dos free del mismo bloque (dos dueños) |
detected memory leaks | Falta un free, o v = realloc(v, …) falló y perdió el bloque |
| Números raros en casillas nuevas | malloc o realloc no inicializan: usa calloc o rellénalas |
warning: function returns address of local variable | Devuelves un vector de la pila: resérvalo en el heap |
Antes de entregar, pregúntate
- ¿Cada
malloc,callocyreallocmultiplica porsizeofy compruebaNULL? - ¿Todos los
reallocusan un puntero temporal? - ¿Sé quién es el dueño de cada bloque y hay exactamente un
freepor bloque en todos los caminos del programa? - ¿He compilado con
-g -fsanitize=addressy 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 →
