- Aprender
- Informática
- Programación
- Programación en C
- Gestión dinámica de memoria en C
Gestión dinámica de memoria en C
PrincipianteUnidad 5 de 8 del curso Curso de Programación en C
Lo que aprenderás
- Describe la pila, el montículo y la memoria estática, cuánto tiempo existen los datos en cada uno y cuándo necesita un programa memoria del montículo.
- Comparar la pila, el montículo y el almacenamiento estáticoDescribe en qué se diferencian el almacenamiento automático, el estático y el reservado en cuanto a su tiempo de vida y quién los gestiona, y qué ocurre con cada uno durante las llamadas a funciones.
- Decidir cuándo almacenar los datos en el montículoElige entre variables locales y reserva de memoria en el montículo según el tamaño, el tiempo de vida y el crecimiento de los datos, y evita devolver la dirección de una variable local.
- Comparar la pila, el montículo y el almacenamiento estático
- Reserva bloques del tamaño adecuado con malloc y calloc, comprueba que cada solicitud se haya realizado correctamente y libera cada bloque exactamente una vez bajo la responsabilidad de un propietario claramente definido.
- Solicitar memoria con malloc y sizeofCalcula el número de bytes que debes solicitar, llama a malloc, comprueba si falla y usa el bloque como un array.
- Obtener memoria rellenada con ceros mediante callocUsa calloc para reservar arrays cuyos elementos comiencen en cero y conoce cuándo es mejor opción que malloc.
- Liberar memoria con free y llevar el control de su propiedadLibera cada bloque exactamente una vez y en el momento adecuado, decide qué parte del programa es propietaria de cada bloque y libera los recursos correctamente cuando falla una reserva.
- Solicitar memoria con malloc y sizeof
- Amplía y reduce bloques del montículo sin perder datos ni memoria, y construye arrays que se expanden a medida que llegan datos.
- Ampliar y reducir un bloque de forma segura con reallocLlama a realloc mediante un puntero temporal, mantén seguros los datos y el bloque original si falla y actualiza todos los punteros después de que el bloque se desplace.
- Construir un array que pueda crecerMantén una longitud y una capacidad junto al puntero, aumenta la capacidad geométricamente y lee una entrada de longitud desconocida en un búfer que se expande.
- Ampliar y reducir un bloque de forma segura con realloc
- Identifica fugas de memoria, punteros colgantes, dobles liberaciones, accesos fuera de los límites y lecturas de memoria sin inicializar en código C, explica por qué son peligrosos y rastrea un fallo de segmentación hasta su causa.
- Detectar y prevenir fugas de memoriaExplica qué es una fuga, identifica los patrones de código que las causan y aplica hábitos que las previenen.
- Evitar punteros colgantes, usos después de la liberación y dobles liberacionesReconoce los punteros a memoria cuyo tiempo de vida ha terminado, explica por qué usarla o liberarla de nuevo es comportamiento indefinido y aplica los hábitos que lo previenen.
- Respetar los límites del bloque: desbordamientos y lecturas de memoria sin inicializarMantén todos los accesos dentro de los límites de su bloque, dimensiona correctamente los búferes de cadenas y establece la memoria antes de leerla.
- Explicar los fallos de segmentación y rastrear su origenExplica qué es un fallo de segmentación, enumera sus causas habituales y rastrea el fallo hasta la línea que contiene el error.
- Detectar y prevenir fugas de memoria
- Ejecuta programas con Memcheck de Valgrind y AddressSanitizer, lee sus informes y úsalos para corregir fugas y accesos inválidos hasta que el programa se ejecute sin errores.
- Comprobar un programa con Valgrind MemcheckCompila un programa para Valgrind, ejecútalo con la comprobación de fugas y lee sus mensajes de error, el resumen del montículo y las categorías de fugas.
- Utilizar AddressSanitizer y LeakSanitizerCompila un programa con AddressSanitizer, lee sus informes y elige entre los sanitizadores y Valgrind.
- Corregir errores de memoria a partir del informe de una herramientaSigue un proceso repetible desde el informe de una herramienta hasta la línea que contiene el error, corrige la causa y confirma la corrección con una ejecución sin errores.
- Comprobar un programa con Valgrind Memcheck
Guía de estudio de Gestión dinámica de memoria en C
1Explicar dónde almacena sus datos un programa en C
Todo objeto tiene un tiempo de vida y un puntero solo es útil mientras exista aquello a lo que apunta. Las variables locales, los datos estáticos y los bloques reservados siguen reglas diferentes de tiempo de vida. Relacionar esas reglas con la pila y el montículo explica por qué una función puede devolver un array reservado, pero no puede devolver una dirección utilizable de su propio array local. El tamaño, el tiempo de vida y el crecimiento posterior orientan la elección del almacenamiento para cada tarea.
1.1 Almacenamiento y tiempo de vida
La dirección de una variable indica dónde están sus datos. Su duración del almacenamiento indica cuánto tiempo existen esos datos. Conservar una dirección no mantiene vivo el objeto.
Tres tiempos de vida
| Duración del almacenamiento | Objetos típicos | El tiempo de vida termina |
|---|---|---|
| Automática | Variables locales ordinarias y parámetros | Cuando termina su bloque |
| Estática | Variables globales y variables locales declaradas static | Cuando termina el programa |
| Asignada | Bloques obtenidos de funciones de reserva | Cuando se libera el bloque |
En los sistemas más comunes, los objetos automáticos usan la pila de llamadas y los bloques reservados usan el montículo. C define los tiempos de vida; no exige estas regiones físicas. El código máquina y los datos estáticos ocupan otras regiones, cuya disposición depende del sistema.
Una llamada a una función normalmente crea un marco de pila que contiene sus parámetros, variables locales e información de retorno. Las llamadas se anidan: el marco más reciente se elimina primero. Al retornar se libera el almacenamiento automático de esa llamada, de modo que las llamadas posteriores pueden reutilizar el espacio. El asignador del montículo gestiona un conjunto de memoria independiente. Reserva un bloque contiguo cuando se le solicita y proporciona su dirección inicial. El retorno de una función no libera ese bloque.
El puntero y su bloque son objetos separados
Considera esta función, con <stdlib.h> disponible:
int *make_array(size_t n) {
if (n == 0 || n > SIZE_MAX / sizeof(int)) return NULL;
int *p = malloc(n * sizeof *p);
if (p == NULL) return NULL;
return p;
}
SIZE_MAX procede de <stdint.h>. Cuando main llama a int *nums = make_array(5);, el parámetro n y la variable de tipo puntero p pertenecen a la llamada. El array reservado no. Cuando un int ocupa 4 bytes, el bloque contiene 20 bytes. El retorno copia su dirección en nums; no copia el array. La función llamadora comprueba nums, usa los elementos después de establecer sus valores y finalmente libera el bloque.
La variable local p deja de existir cuando la función retorna. El bloque sobrevive porque la duración de su almacenamiento es asignada, no automática. Esta distinción también funciona a la inversa: un puntero local puede apuntar a un objeto global y un puntero global puede apuntar a un bloque del montículo.
Los datos estáticos sobreviven a las llamadas
int next_id(void) {
static int id = 0;
return ++id;
}
Las llamadas sucesivas devuelven 1, 2 y 3. La inicialización se realiza una vez y id existe durante toda la ejecución del programa. Al quitar static se crea un nuevo id automático, inicializado a cero, en cada llamada; entonces cada llamada devuelve 1.
El espacio de la pila es limitado
Un array local grande y las llamadas muy anidadas consumen espacio de la pila. Superar el límite produce un desbordamiento de pila, que normalmente termina el programa. Muchas configuraciones de Linux permiten una pila de 8 MiB; la reserva predeterminada de la pila del hilo principal de Microsoft suele ser de 1 MiB. Son ajustes del sistema, no reglas de C. Un array local de 4 000 000 valores int necesita 16 000 000 bytes cuando un int ocupa 4 bytes, unos 15,3 MiB.
La reserva de memoria en el montículo tiene costes de gestión al reservar y liberar bloques. Leer un elemento no es necesariamente más lento solo porque su array esté en el montículo. Además, el conjunto de memoria del montículo no está relacionado con la estructura de datos montículo que se usa en una cola de prioridad.
Puntos clave
- La duración del almacenamiento de un objeto determina su tiempo de vida.
- Las variables locales automáticas terminan con su bloque; los objetos estáticos duran toda la ejecución del programa.
- Un puntero local y el bloque del montículo al que apunta tienen tiempos de vida independientes.
- Los arrays locales grandes y las llamadas profundas pueden agotar la pila.
- C especifica los tiempos de vida, mientras que la pila y el montículo describen implementaciones habituales.
1.2 Elegir el almacenamiento adecuado
Elige el almacenamiento preguntándote cuánto espacio se necesita, cuándo se conoce ese tamaño y cuánto tiempo deben durar los datos. El diseño adecuado más sencillo suele ser un array local ordinario.
Adapta el almacenamiento a los datos
| Necesidad | Opción inicial adecuada |
|---|---|
| Un array pequeño de tamaño fijo usado durante una llamada | Array local |
| Una cantidad leída de la entrada | Reserva comprobada en el montículo |
| Un resultado que sobrevive a la función que lo crea | Reserva en el montículo o búfer propiedad de la función llamadora |
| Datos que crecen a medida que llegan | Búfer redimensionable en el montículo |
| Un array muy grande | Reserva en el montículo con comprobación de fallo |
Las temperaturas diarias de una semana necesitan siete valores, por lo que double temps[7]; es un array local natural. Una calificación por estudiante necesita un tamaño obtenido en tiempo de ejecución. Con n estudiantes, usa double *grades = malloc(n * sizeof *grades); después de comprobar el tamaño. Para 250 estudiantes y valores double de 8 bytes, se solicitan 2000 bytes. Después comprueba el resultado antes de llenar el array.
Por qué falla devolver un búfer local
char *greeting(void) {
char buf[16] = "hello";
return buf; /* error: buf ends when this call returns */
}
La dirección devuelta no puede prolongar el tiempo de vida de buf. La función llamadora recibe un puntero colgante aunque los bytes antiguos parezcan no haber cambiado. GCC y Clang pueden advertir de este patrón.
Una solución es dejar que la función llamadora proporcione el almacenamiento:
#include <string.h>
void greeting(char out[16]) {
strcpy(out, "hello");
}
/* In the caller: */
char message[16];
greeting(message);
Aquí el array de la función llamadora sigue existiendo durante toda la llamada y después de ella. El parámetro sigue comportándose como un puntero; la función llamadora debe proporcionar espacio suficiente. Un segundo diseño reserva un resultado en el montículo y hace responsable a la función llamadora de liberarlo. Un tercero devuelve un búfer estático. Ese búfer permanece, pero todas las funciones llamadoras lo comparten y una llamada posterior puede sobrescribir su contenido. Elige según si las funciones llamadoras necesitan resultados independientes.
Un tamaño en tiempo de ejecución no siempre implica usar el montículo
C99 introdujo los arrays de longitud variable, como int a[n];. Tienen almacenamiento automático, normalmente en la pila. C11 hizo opcional su compatibilidad y el compilador de Microsoft no los admite. Una entrada grande puede agotar la pila y declarar el array no proporciona un resultado nulo que se pueda comprobar. La reserva en el montículo ofrece una vía de fallo comprobable y permite cambiar el tamaño más adelante.
Un puntero no indica la propiedad
int value = 3;
int *p = &value;
p apunta a un objeto automático. Pasarlo a free no es válido. La misma regla excluye liberar un array estático o un literal de cadena. La dirección debe pertenecer a un bloque reservado que siga existiendo y del que el programa sea propietario.
Del mismo modo, char *s = "hello"; apunta a un literal de cadena; no crea una copia modificable. Modificar el literal es comportamiento indefinido. Usa char s[] = "hello"; para una copia local modificable o reserva espacio para una copia independiente en el montículo.
Puntos clave
- Elige el almacenamiento según el tamaño de los datos, el tiempo de vida requerido y la necesidad de crecer.
- Una dirección devuelta por una función solo puede usarse si aquello a lo que apunta sigue existiendo.
- Un búfer proporcionado por la función llamadora sigue siendo propiedad de esta.
- Un búfer estático de resultado se comparte entre llamadas.
- Los punteros pueden apuntar a objetos locales, estáticos o reservados; solo pueden liberarse bloques reservados que sigan existiendo.
- Los literales de cadena no son copias modificables.
2Reservar y liberar memoria del montículo
Una solicitud de reserva necesita un número de bytes, una ruta para gestionar el fallo y un propietario. Parte del número de elementos para calcular el almacenamiento necesario, decide si sus valores iniciales deben ser cero y usa cada bloque solo después de una solicitud exitosa. La propiedad vincula el último uso con la liberación de recursos, también en reservas anidadas y funciones que solo completan parte de su trabajo.
2.1 Solicitar el número correcto de bytes
malloc reserva un bloque contiguo de bytes y devuelve su dirección inicial. Incluye <stdlib.h> para disponer de su declaración. Una reserva exitosa proporciona almacenamiento, pero no lo llena con valores útiles.
Cuenta los elementos y calcula el tamaño de cada uno
int *nums = malloc(n * sizeof *nums);
n es un número de elementos; sizeof *nums es el número de bytes por elemento. Al multiplicarlos se obtiene el número de bytes. Cuando un int ocupa 4 bytes, diez valores int necesitan 40 bytes. Diez valores double de 8 bytes necesitan 80 bytes. Diez punteros necesitan 80 bytes cuando cada puntero ocupa 8 bytes. Un char ocupa siempre un byte de C.
La forma sizeof *nums se adapta al tipo apuntado si cambia la declaración. Para este tipo ordinario que no es de longitud variable, sizeof no evalúa *nums, por lo que no desreferencia un puntero sin inicializar. En C, el void * devuelto se convierte al tipo de puntero a objeto sin una conversión explícita.
malloc(n) reserva n bytes, no espacio para n valores int. sizeof nums mide la variable de tipo puntero, no un elemento ni todo el bloque. En un sistema con punteros de 8 bytes y valores int de 4 bytes, malloc(n * sizeof nums) solicita el doble del espacio necesario, lo que oculta el error en la expresión. Con otros tipos puede reservar demasiado poco.
Comprueba el tamaño y los fallos antes de usar el resultado
Este programa completo rechaza una solicitud vacía, protege la multiplicación contra el desbordamiento, comprueba la reserva e inicializa cada elemento:
#include <stdint.h>
#include <stdlib.h>
int main(void) {
size_t n = 5;
int *nums;
if (n == 0 || n > SIZE_MAX / sizeof *nums) {
return EXIT_FAILURE;
}
nums = malloc(n * sizeof *nums);
if (nums == NULL) return EXIT_FAILURE;
for (size_t i = 0; i < n; ++i) {
nums[i] = (int)(i * i);
}
free(nums);
return EXIT_SUCCESS;
}
SIZE_MAX, de <stdint.h>, es el mayor valor de size_t. Comprueba mediante división antes de multiplicar. Si el producto se reduce modularmente primero, comprobar el resultado reducido no permite recuperar el tamaño deseado. Una solicitud de tamaño distinto de cero puede fallar aunque el cálculo quepa en el tipo, por lo que la comprobación del puntero nulo es un paso independiente.
Los valores inicializados aquí son 0, 1, 4, 9 y 16. nums[i] accede al mismo elemento que *(nums + i). La aritmética de punteros avanza en elementos, por lo que el código no multiplica i por el tamaño en bytes.
Conserva la cantidad junto al puntero
No existe una operación estándar de C que recupere el número de elementos de una reserva a partir de su puntero. Guarda n por separado y pasa tanto el puntero como la cantidad a las funciones. La cantidad controla los bucles y las comprobaciones de índices.
Una copia de una cadena necesita sus caracteres y el terminador '\0'. La cadena «Amsterdam» tiene nueve caracteres y necesita diez bytes, así que solicita strlen(s) + 1 para copiarla. <string.h> proporciona strlen.
Una solicitud a malloc de tamaño cero puede devolver un puntero nulo o uno que se puede liberar, pero no desreferenciar. Trata las cantidades cero como un caso vacío explícito en vez de considerar que ese puntero es un almacenamiento de array utilizable.
Puntos clave
- Multiplica el número de elementos por
sizeof *pointerpara obtener el número de bytes de un array. - Comprueba si la multiplicación desborda antes de llamar a
malloc. - Comprueba si el resultado es
NULLantes de usarlo. - Inicializa cada elemento de
mallocantes de leerlo. - Conserva el número de elementos por separado;
sizeof pointerno permite recuperarlo. - Incluye el carácter nulo de terminación al reservar una copia de una cadena.
- Trata explícitamente las cantidades cero.
2.2 Inicializar un array a cero
Un contador necesita un valor inicial definido antes del primer incremento. calloc combina la reserva con el rellenado de ceros, por lo que un array de contadores enteros está listo para contar después de comprobar que la reserva no falló.
Dos argumentos describen el array
Incluye <stdlib.h> y escribe:
int *tally = calloc(26, sizeof *tally);
if (tally == NULL) return EXIT_FAILURE;
El primer argumento es el número de elementos y el segundo es el tamaño de un elemento. Cuando un int ocupa 4 bytes, esto reserva 104 bytes. Todos los bytes comienzan en cero, por lo que cada contador entero comienza en cero. Llamar a calloc con un único argumento combinado no compila; su interfaz requiere dos argumentos.
Para la palabra «banana», incrementa el contador correspondiente a cada letra. Después de procesarla, los contadores de a, b y n contienen 3, 1 y 2. Todos los demás permanecen en cero. En una codificación de caracteres con letras minúsculas consecutivas, la expresión del índice es c - 'a'. La entrada debe ser una letra minúscula antes de usar esa expresión como índice del array.
#include <stdlib.h>
int main(void) {
int *counts = calloc(3, sizeof *counts);
if (counts == NULL) return EXIT_FAILURE;
counts[1]++;
counts[1]++;
/* The counters now hold 0, 2, 0. */
free(counts);
return EXIT_SUCCESS;
}
El código de recuento solo escribió en el contador central. Sus dos vecinos siguen teniendo valores útiles porque la reserva los puso a cero.
Qué significa rellenar con ceros
calloc establece todos los bits en cero. Esto produce valores cero para los elementos enteros y char. Por tanto, un búfer de caracteres de tamaño positivo rellenado con ceros comienza como una cadena vacía: su primer carácter es '\0'.
En las plataformas más comunes, todos los bits a cero también representan el cero de coma flotante y los punteros nulos. C portable no garantiza esas representaciones para los tipos de coma flotante y de puntero. Si el código necesita esos valores en todas las implementaciones de C, asigna 0.0 o NULL explícitamente a los elementos.
Elegir entre las funciones de reserva
| Uso inicial | Opción natural |
|---|---|
| Incrementar contadores enteros | calloc |
| Establecer indicadores enteros gradualmente desde un estado de todos ceros | calloc |
| Sobrescribir inmediatamente todos los elementos | malloc |
Una llamada exitosa a malloc seguida de memset puede poner un bloque a cero, pero la multiplicación del tamaño en ese código sigue necesitando una comprobación de desbordamiento. Las implementaciones más comunes de calloc rechazan un producto demasiado grande para size_t; C23 lo exige explícitamente. Aun así, hay que comprobar si el resultado es nulo, porque una solicitud de tamaño correcto puede fallar.
El rellenado con ceros se aplica a esta reserva. Si realloc amplía después el bloque, los bytes añadidos no están inicializados. Establece esos elementos antes de leerlos o usarlos para contar. Por último, libera el bloque con free, igual que con un bloque obtenido mediante malloc.
Puntos clave
- Pasa a
callocun número de elementos y un tamaño de elemento. - Los contadores enteros rellenados con ceros pueden incrementarse inmediatamente después de comprobar la reserva.
- Los bytes puestos a cero no son un sustituto portable de asignar el cero de coma flotante o punteros nulos.
- La ampliación con
reallocno pone a cero los bytes añadidos.
2.3 Un propietario y una liberación final
Un bloque reservado necesita una respuesta clara a «¿quién lo libera?». Esa responsabilidad es la propiedad de la memoria. Otro código puede tomar prestada la dirección sin obtener permiso para liberarla.
Libera después del último uso
free, declarado en <stdlib.h>, deja disponible para su reutilización un bloque reservado que sigue existiendo. Pasa su dirección inicial exactamente una vez. free(NULL) no hace nada. Pasar una dirección local, un literal de cadena, una dirección interior como p + 1 o un bloque ya liberado es comportamiento indefinido.
free(p) no asigna ningún valor a la variable p ni garantiza que borre los bytes. Después de la liberación, el valor anterior del puntero es inválido. Si la variable sigue en uso, asigna p = NULL; sin inspeccionar su valor anterior. Esto solo cambia p; los alias también deben dejar de usar el bloque liberado.
Una función que devuelve memoria recién reservada transfiere la propiedad a la función llamadora:
#include <stdlib.h>
#include <string.h>
char *copy_string(const char *s) {
char *copy = malloc(strlen(s) + 1);
if (copy == NULL) return NULL;
strcpy(copy, s);
return copy;
}
La función llamadora comprueba el resultado, usa la copia y la libera. La cadena de entrada se toma prestada; esta función no la libera. POSIX y C23 ofrecen strdup con el mismo patrón de propiedad; la función anterior también funciona en C17.
Libera las reservas anidadas de dentro hacia fuera
Una cuadrícula formada por un array de punteros a filas tiene bloques separados. Tres punteros a filas y tres filas de cuatro valores int requieren cuatro reservas. Con punteros de 8 bytes y valores int de 4 bytes, el bloque exterior usa 24 bytes y las filas usan 48 bytes, con un total de 72 bytes.
Libera cada fila mientras su dirección siga disponible y después libera el array de punteros. Liberar el bloque exterior no libera recursivamente sus filas. Acceder a grid[i] después de liberar grid supone usar almacenamiento liberado.
Libera los recursos cuando solo parte de la operación tiene éxito
Este ejemplo completo lleva la cuenta de cuántas filas existen realmente:
#include <stdlib.h>
int main(void) {
size_t rows = 3, made = 0;
int **grid = malloc(rows * sizeof *grid);
if (grid == NULL) return EXIT_FAILURE;
for (; made < rows; ++made) {
grid[made] = malloc(4 * sizeof *grid[made]);
if (grid[made] == NULL) break;
}
int status = made == rows ? EXIT_SUCCESS : EXIT_FAILURE;
/* Use the grid here only if every row was allocated. */
for (size_t i = 0; i < made; ++i) free(grid[i]);
free(grid);
return status;
}
Si falla la segunda fila, made es 1, por lo que solo se libera la fila cero. No se lee ningún puntero a fila sin inicializar. Otro diseño útil inicializa los punteros independientes a NULL y dirige cada fallo a una única sección de liberación de recursos; free(NULL) hace que esa sección sea segura después de un éxito parcial.
Cuenta los periodos de propiedad, no las llamadas de reserva. Un búfer creado por malloc y redimensionado con éxito dos veces necesita una única llamada final a free aplicada al resultado más reciente; una llamada exitosa a realloc ya pone fin al tiempo de vida del bloque anterior. El asignador puede conservar el espacio liberado para reservas posteriores, por lo que un monitor del sistema no tiene por qué mostrar una disminución inmediata de la memoria del proceso.
Puntos clave
- El propietario libera cada bloque después de su último uso.
- Solo la dirección inicial de un bloque reservado que siga existiendo, o
NULL, es un argumento válido parafree. - Restablecer un puntero no corrige los alias.
- Libera los bloques interiores antes que el bloque exterior que contiene sus direcciones.
- Las rutas de fallo deben liberar todos los bloques obtenidos anteriormente.
- Tras cambiar el tamaño con éxito, se necesita una llamada final a
freesobre el bloque más reciente.
3Cambiar el tamaño de los búferes con realloc
La entrada no siempre llega con un tamaño final conocido. Un búfer redimensionable permite a un programa reservar más espacio a medida que descubre cuánto necesita. Una ampliación segura conserva tanto los valores existentes como la capacidad de recuperarse de un fallo. Mantener juntos un puntero, una longitud y una capacidad hace concretas estas reglas, mientras que el crecimiento geométrico limita la cantidad de copias repetidas.
3.1 Cambiar el tamaño sin perder el bloque anterior
realloc cambia el tamaño de un bloque reservado. Puede ampliar o reducir el almacenamiento, y la dirección devuelta puede cambiar en ambos casos. Incluye <stdlib.h> y trata el éxito y el fallo como eventos diferentes para el tiempo de vida.
Qué permanece y qué cambia
Cuando un bloque crece, el contenido existente se conserva hasta el tamaño anterior. Los bytes añadidos no están inicializados. Cuando se reduce, solo se conserva la parte inicial que cabe en el nuevo tamaño. Un array que contiene 3, 1, 4 y 1 conserva esos cuatro valores al ampliarse a seis valores int; los dos últimos elementos necesitan que se les asigne un valor antes de usarlos. Con valores int de 4 bytes, la solicitud pasa de 16 a 24 bytes.
El asignador puede ampliar el bloque en el mismo lugar o reservarlo en otro lugar, copiar los bytes conservados y liberar el bloque anterior. Tu código debe funcionar con cualquiera de los dos resultados.
Recibe el resultado en un puntero temporal
int *tmp = realloc(nums, 6 * sizeof *nums);
if (tmp == NULL) {
/* nums still owns its original block. */
} else {
nums = tmp;
nums[4] = 5;
nums[5] = 9;
}
Para esta solicitud de tamaño positivo, un fallo deja el bloque original reservado y sin cambios. El programa puede seguir usándolo o liberarlo antes de comunicar el fallo. Si la operación tiene éxito, la propiedad pasa al bloque devuelto. Asigna el nuevo resultado antes de acceder al array redimensionado.
La asignación directa nums = realloc(nums, bytes); pierde esta protección. Si la llamada falla, sobrescribe la única dirección guardada con NULL. La reserva anterior sigue ocupada, pero el programa ya no puede liberarla.
Hay que reconstruir los punteros guardados a elementos
Supón que el código guarda int *selected = &nums[2];. Un cambio de tamaño exitoso termina el tiempo de vida del objeto anterior, incluso si la dirección devuelta coincide. selected deja de ser válido tras ese éxito. Conserva el índice 2 durante el cambio de tamaño y después calcula selected = &nums[2]; a partir del nuevo puntero si ese índice todavía existe.
No compares el puntero anterior con el resultado para decidir si hay que actualizar los alias. Usa el nuevo resultado siempre que la operación tenga éxito. Si la reducción eliminó el elemento seleccionado, no hay ningún puntero a ese elemento que reconstruir.
Un ejemplo completo de ampliación y liberación
#include <stdlib.h>
int main(void) {
int *nums = malloc(4 * sizeof *nums);
if (nums == NULL) return EXIT_FAILURE;
for (size_t i = 0; i < 4; ++i) nums[i] = (int)i;
int *tmp = realloc(nums, 6 * sizeof *nums);
if (tmp == NULL) {
free(nums);
return EXIT_FAILURE;
}
nums = tmp;
nums[4] = 4;
nums[5] = 5;
free(nums);
return EXIT_SUCCESS;
}
Para una cantidad variable, comprueba su multiplicación para obtener los bytes antes de esta llamada, igual que con malloc. Solicita bytes, no elementos: realloc(nums, 6) solicita seis bytes.
realloc(NULL, positive_size) se comporta como malloc, lo que permite que un búfer vacío comience con un puntero nulo. Para liberar un búfer, llama a free explícitamente. Evita cambiar el tamaño a cero: el comportamiento histórico de las bibliotecas difiere y cambiar a cero el tamaño de un bloque existente es comportamiento indefinido en C23. Ni los arrays locales ni los bloques previamente liberados son destinos válidos para cambiar su tamaño.
Puntos clave
- Usa un puntero temporal para que un fallo al cambiar a un tamaño positivo conserve la dirección del propietario.
- La ampliación conserva el contenido anterior, pero deja sin inicializar los bytes añadidos.
- La reducción conserva solo la parte que sigue cabiendo.
- Tras una operación exitosa, usa el puntero devuelto y reconstruye los punteros guardados a elementos.
- Usa
freepara liberar almacenamiento; trata explícitamente los tamaños cero.
3.2 Un array que crece con la entrada
Un array que puede crecer necesita tres datos de estado: su puntero, su longitud y su capacidad. La longitud cuenta los elementos inicializados que están en uso. La capacidad cuenta los espacios reservados para elementos. Mantén la longitud igual o inferior a la capacidad.
Añade elementos en un orden seguro
Empieza con int *data = NULL; size_t len = 0, cap = 0;. Antes de añadir un elemento, comprueba si len == cap. Un array lleno debe crecer antes de escribir en data[len]. El puntero y la capacidad solo pueden cambiar después de que el cambio de tamaño tenga éxito. Guarda el nuevo valor y después aumenta la longitud.
El siguiente ejemplo completo añade diez valores mediante crecimiento geométrico:
#include <stdint.h>
#include <stdlib.h>
int main(void) {
int *data = NULL;
size_t len = 0, cap = 0;
for (int value = 0; value < 10; ++value) {
if (len == cap) {
size_t next;
if (cap == 0) {
next = 4;
} else {
if (cap > SIZE_MAX / 2 / sizeof *data) {
free(data);
return EXIT_FAILURE;
}
next = cap * 2;
}
int *tmp = realloc(data, next * sizeof *data);
if (tmp == NULL) {
free(data);
return EXIT_FAILURE;
}
data = tmp;
cap = next;
}
data[len++] = value;
}
free(data);
return EXIT_SUCCESS;
}
La comprobación de desbordamiento verifica tanto la duplicación como la conversión a bytes antes de que cualquiera de los productos pueda reducirse modularmente. La capacidad inicial de cuatro también cabe para este tipo de elemento. Ante un fallo, el programa libera la reserva anterior, de la que sigue siendo propietario, y se detiene; no actualiza la capacidad indebidamente.
Sigue la longitud y la capacidad por separado
Con una capacidad inicial de 4, las primeras cuatro inserciones usan el espacio existente. Antes de la quinta, la capacidad se duplica a 8. Antes de la novena, se duplica a 16. Diez inserciones terminan con longitud 10 y capacidad 16. Si ambas ampliaciones desplazan el bloque, copian como máximo cuatro y ocho elementos, doce en total.
La capacidad sobrante es intencional. Crecer un elemento por cada inserción provoca copias repetidas. Desde una capacidad inicial de 1, llegar a 1000 elementos puede requerir 999 cambios de tamaño y 499 500 elementos copiados. Duplicar alcanza una capacidad de 1024 después de diez cambios de tamaño y como máximo 1023 copias. Un cambio de tamaño concreto puede ser costoso, pero el trabajo promedio por inserción permanece constante con el crecimiento geométrico.
Un factor como 1,5 deja menos espacio sobrante, pero amplía el array con más frecuencia. Si la entrada ha terminado, el programa puede reducir el tamaño a la longitud utilizada, gestionando el fallo con el mismo patrón del puntero temporal. Un array vacío se libera con free, no con un cambio de tamaño a cero.
Los búferes de caracteres necesitan un espacio adicional
Para una cadena que crece, reserva espacio para '\0'. Antes de añadir un carácter, amplía si len + 1 >= cap. Con capacidad 8, caben siete caracteres dejando espacio para el terminador; «hello world» provoca una ampliación a 16 y necesita doce bytes incluido el terminador.
Un puntero guardado a un elemento deja de ser válido después de una ampliación exitosa. Conserva un índice y vuelve a acceder al elemento usando la nueva dirección inicial. Al restablecer un array para seguir usándolo, libera su bloque y establece data = NULL; len = cap = 0;.
Puntos clave
- La longitud cuenta los elementos usados; la capacidad cuenta los espacios disponibles.
- Amplía antes de añadir un elemento cuando la longitud sea igual a la capacidad.
- Actualiza el puntero y la capacidad solo después de un cambio de tamaño exitoso.
- El crecimiento geométrico limita las copias repetidas a costa de espacio sobrante.
- Comprueba los cálculos de ampliación antes de multiplicar.
- Reserva un byte adicional para el carácter nulo de terminación de una cadena.
- Conserva los índices durante la ampliación y reconstruye después los punteros a elementos.
4Reconocer y prevenir errores de memoria
Los errores de memoria surgen cuando se incumplen reglas sobre propiedad, tiempo de vida, límites o inicialización. Cada uno deja un rastro diferente: una dirección perdida, un acceso después de la liberación, un índice fuera del bloque o un valor leído demasiado pronto. Algunos errores provocan el cierre inmediato del programa, mientras que otros corrompen datos y aparecen más tarde. Leer el código pensando en los bloques y su historia ayuda a identificar la causa detrás del síntoma.
4.1 Encontrar la ruta que pierde memoria
Una fuga de memoria es almacenamiento reservado que un programa ya no necesita pero nunca libera. A menudo desaparece el último puntero, lo que hace imposible una llamada posterior a free. Una fuga puede pasar inadvertida mientras aumenta el uso de memoria.
Sigue las direcciones, no los nombres de las variables
char *p = malloc(100);
if (p == NULL) return EXIT_FAILURE;
p = malloc(200); /* loses the only address of the first block */
if (p == NULL) return EXIT_FAILURE;
free(p);
La llamada final a free solo libera el segundo bloque. Se reutilizó el nombre p, pero no la primera reserva. Si la segunda solicitud falla, el bloque de 100 bytes sigue perdido. Libera el primer bloque antes de sustituirlo, cuando ya no se necesite, o conserva su dirección en otra variable propietaria.
Asignar NULL a un puntero propietario antes de liberarlo causa la misma pérdida. La asignación cambia una variable de tipo puntero; no indica al asignador que libere nada. Las variables locales automáticas desaparecen al terminar una llamada, pero los bloques reservados a los que apuntaban permanecen.
Examina todas las salidas de una función
Un retorno anticipado puede omitir la liberación de recursos:
char *line = malloc(100);
if (line == NULL) return EXIT_FAILURE;
if (input_is_bad) {
free(line);
return EXIT_FAILURE;
}
/* Use line on the successful path. */
free(line);
Ambas rutas liberan el bloque. En una función con más recursos, una única sección de liberación reduce el número de rutas que hay que examinar. Una función que devuelve un resultado reservado transfiere la responsabilidad de su propiedad; una función que simplemente abandona un puntero local propietario provoca una fuga.
La asignación directa del resultado de una llamada fallida a realloc es otra vía de pérdida. Las reservas anidadas se pierden cuando su array exterior de punteros se libera antes de usar las direcciones interiores para liberar los recursos.
La repetición hace costosas las fugas pequeñas
Supón que un bucle reserva 100 bytes en cada pasada y nunca los libera. Después de 1000 pasadas ha abandonado 100 000 bytes. Libera cada búfer al final de su pasada o reserva una vez antes del bucle, reutiliza el bloque y libéralo después. La reutilización también evita el trabajo repetido del asignador cuando basta con la misma capacidad.
Un servicio que pierde 64 bytes por solicitud pierde 640 000 bytes después de 10 000 solicitudes, exactamente 625 KiB. Después de un millón de solicitudes ha perdido 64 000 000 bytes, unos 61 MiB. Los programas de larga duración pueden acabar sin poder reservar memoria o ralentizar el sistema. El sistema operativo normalmente recupera la memoria del proceso al terminar, pero eso no ayuda al servicio mientras se ejecuta.
Perdido y todavía accesible son informes diferentes
Si un puntero global sigue conteniendo la dirección inicial de una reserva al terminar, Valgrind llama al bloque «still reachable». La dirección no se ha perdido; el programa simplemente no liberó el bloque. «Definitely lost» significa que no queda ningún puntero utilizable. Libera los recursos de forma deliberada para que estos informes muestren claramente las reservas sin explicar.
Puntos clave
- Sobrescribir o poner a nulo el último puntero propietario puede causar la pérdida de un bloque.
- Las rutas normales, de retorno anticipado y de fallo necesitan liberar los recursos.
- Las pequeñas fugas repetidas se acumulan mientras se ejecuta el programa.
- Reutilizar un búfer adecuado puede evitar tanto el trabajo de reservas repetidas como las fugas en cada iteración.
4.2 Cuando un puntero sobrevive al objeto al que apunta
Un puntero se convierte en puntero colgante cuando termina el tiempo de vida del objeto al que apunta. La variable puede seguir existiendo, pero leer o escribir el objeto anterior mediante ese puntero no es válido. Tanto devolver una dirección local como liberar un bloque reservado pueden crear este problema.
Encuentra el último uso antes de la liberación
int *scores = malloc(3 * sizeof *scores);
if (scores == NULL) return EXIT_FAILURE;
scores[0] = 90;
scores[1] = 75;
scores[2] = 82;
free(scores);
printf("%d\n", scores[0]); /* use-after-free */
La asignación a scores[0] ocurrió durante el tiempo de vida del bloque. La lectura final no. Mueve free(scores) después de la impresión, una vez completados todos los usos. Limitarse a asignar scores = NULL antes de imprimir convierte el acceso inválido en una desreferenciación nula; no proporciona los datos que faltan.
Esto es comportamiento indefinido: C no impone requisitos sobre el resultado. El programa puede mostrar un valor antiguo, cerrarse inesperadamente o dañar datos sin avisar. Los bytes liberados suelen permanecer en su lugar hasta que el asignador los reutiliza. Por tanto, un resultado aparentemente razonable no valida una lectura después de la liberación.
Los alias comparten el problema del tiempo de vida
int *a = malloc(3 * sizeof *a);
if (a == NULL) return EXIT_FAILURE;
int *b = a;
free(a);
a = NULL;
b[0] = 1; /* b also lost its live target */
Había un bloque y dos variables de tipo puntero que contenían su dirección. Liberarlo mediante a termina su tiempo de vida para ambas. Poner a a nulo solo asigna un valor a a; no puede actualizar b. El código que usa alias prestados debe terminar antes de que el propietario libere el bloque, y esos alias no deben usarse después.
La doble liberación también usa un objeto cuyo tiempo de vida ha terminado
Llamar a free dos veces para un bloque es una doble liberación, también cuando la segunda llamada se hace mediante un alias. Puede corromper los registros del asignador o hacer que falle una reserva posterior. Algunas bibliotecas la detectan de inmediato y detienen el programa, pero esa detección inmediata no está garantizada.
Asignar NULL a una variable propietaria después de la liberación ayuda si se repite la llamada de liberación, porque free(NULL) es válido. No hace que free(b) sea seguro cuando b todavía se refiere al bloque anterior. Un único propietario debe decidir cuándo se libera el bloque.
Cambiar el tamaño también termina el tiempo de vida anterior
Una llamada exitosa a realloc sustituye el objeto anterior aunque la dirección numérica devuelta no cambie. Los punteros guardados al interior del bloque anterior deben volver a calcularse a partir del puntero devuelto. Guarda las posiciones como índices mientras pueda producirse una ampliación. En cambio, una llamada fallida a realloc con tamaño positivo deja la reserva anterior activa.
Una función que devuelve la dirección de una variable local automática crea un error similar de tiempo de vida sin llamar a free: el objeto deja de existir cuando la función retorna. Usa almacenamiento propiedad de la función llamadora o un resultado en el montículo con un propietario claramente definido. El uso después de la liberación y la doble liberación también pueden convertirse en defectos de seguridad cuando un atacante puede influir en el almacenamiento reutilizado.
Puntos clave
- Un puntero colgante apunta a un objeto cuyo tiempo de vida ha terminado.
- Termina todas las lecturas y escrituras antes de liberar el bloque.
- Una liberación invalida todos los alias del objeto anterior.
- Poner a nulo un puntero propietario ayuda con la liberación repetida, pero no corrige los alias prestados.
- Una doble liberación puede dañar los registros del asignador.
- Una llamada exitosa a
reallocinvalida los punteros guardados al interior del bloque anterior.
4.3 Límites válidos y valores inicializados
Un acceso válido a un elemento requiere tanto espacio suficiente como un valor que se haya establecido antes de leerlo. Un bloque puede tener el tamaño correcto y contener datos sin inicializar, o contener valores útiles y ser accedido fuera de sus límites.
Comprueba el rango de elementos
Para n elementos, los índices válidos van de cero a n - 1. Un bucle con i <= n realiza un acceso adicional. Con diez valores int de 4 bytes, la reserva es de 40 bytes; p[10] comienza justo después de esos bytes. Usa i < n para recorrer los diez elementos.
Un índice procedente de la entrada también necesita una comprobación de rango antes de usarse. Una entrada con signo debe ser mayor o igual que cero e inferior al número de elementos. Conserva la cantidad junto al puntero y pasa ambos a las funciones. Un puntero por sí solo no puede describir el rango permitido.
Dimensiona las cadenas para incluir su terminador
char *copy = malloc(strlen(s));
strcpy(copy, s); /* writes the terminator beyond the allocation */
Este fragmento incorrecto también necesita una comprobación de fallo. La corrección del tamaño es malloc(strlen(s) + 1), seguido de la comprobación y después la copia. «Amsterdam» tiene nueve caracteres, pero necesita diez bytes con '\0'. El terminador es un byte almacenado real, aunque no se imprima como parte del texto.
Un array de punteros no es almacenamiento para cadenas
char **names = malloc(n * sizeof *names);
Esto reserva n espacios para punteros. No crea búferes de caracteres a los que puedan apuntar esos punteros. Antes de usar names[i] como cadena, asígnale una dirección de cadena válida. Puede tomar prestado un array existente o un literal con un tiempo de vida suficiente, o ser propietario de una copia de cadena reservada por separado. Solo se liberan las cadenas propias durante la liberación de los bloques anidados.
El mismo razonamiento sobre el tamaño se aplica a los arrays numéricos: malloc(n) cuenta bytes y sizeof p mide un puntero. Usa n * sizeof *p, con la comprobación de desbordamiento y la comprobación del puntero nulo, para expresar correctamente un número de elementos.
Escribe antes de leer
int *t = malloc(3 * sizeof *t);
if (t == NULL) return EXIT_FAILURE;
t[0] = 5;
t[1] = 7;
printf("%d\n", t[0] + t[1] + t[2]); /* t[2] was never set */
free(t);
t[2] está dentro del bloque reservado, pero su valor no está inicializado. Asígnale un valor antes de la suma o usa calloc cuando cero sea el valor inicial deseado. Los bytes añadidos durante una ampliación con realloc necesitan la misma atención. Estar dentro de los límites no inicializa la memoria.
Por qué el cierre puede ocurrir más tarde
Una escritura fuera de los límites puede dañar datos cercanos o los registros del asignador. Una lectura fuera de los límites puede exponer datos ajenos. Ambas son comportamiento indefinido. Un pequeño desbordamiento puede acceder a memoria asignada y escapar a la detección inmediata; una llamada posterior a free puede ser el primer fallo visible.
Comprueba por separado el tamaño de la reserva, el rango del índice y el historial de inicialización. Una reserva mayor no puede proporcionar un valor significativo a un elemento sin inicializar, y poner a cero la memoria no puede hacer válido un índice fuera de rango.
Puntos clave
- Los índices válidos para
nelementos van de cero an - 1. - El almacenamiento de una cadena incluye un byte para el carácter nulo de terminación.
- Un array de punteros a cadenas no reserva las cadenas mismas.
- Inicializa todos los valores que lee un cálculo, incluidos los elementos añadidos al ampliar el bloque.
- Una escritura inválida puede corromper datos mucho antes de un cierre visible.
4.4 Rastrear un cierre inesperado hasta su causa
Un fallo de segmentación es una respuesta del sistema operativo a un acceso prohibido a memoria. Entre las causas habituales están desreferenciar un puntero nulo o sin inicializar, escribir en un literal de cadena, acceder muy lejos de los límites de un array, usar almacenamiento liberado y agotar la pila.
Qué indica el mensaje
En Linux, SIGSEGV suele detener el proceso y un intérprete de comandos puede imprimir «Segmentation fault (core dumped)». Un estado de salida habitual del intérprete es 139, según la convención de señales de 128 más la señal 11. macOS puede informar de SIGSEGV o SIGBUS para accesos inválidos. Windows comunica el fallo comparable como una violación de acceso, a menudo con el código de excepción 0xC0000005.
Estos mensajes identifican un acceso fallido, no toda su historia. Un pequeño desbordamiento puede corromper un puntero al principio y provocar un cierre mucho más tarde. Muchos accesos inválidos permanecen dentro de la memoria que el proceso puede direccionar, por lo que corrompen datos sin avisar en vez de generar un fallo.
Empieza con información de línea
Compila un ejecutable de depuración con advertencias:
cc -std=c17 -g -O0 -Wall -Wextra program.c -o program
Después ejecútalo con un depurador o un comprobador de memoria. Una traza de la pila de llamadas enumera las llamadas a funciones activas en el momento del fallo. Encuentra el marco situado más arriba que pertenezca a tu propio código. Las funciones de biblioteca como free pueden ser el lugar donde se hace visible un estado incorrecto, mientras que una llamada anterior de tu programa lo creó.
En la línea indicada, identifica cada puntero que se desreferencia. Para cada uno, rastrea su valor hasta su origen. ¿Se comprobó la reserva? ¿Sigue existiendo el objeto al que apunta? ¿Está el índice dentro de los límites del bloque? ¿Se asignó siquiera una dirección válida al puntero?
Analiza un resultado nulo
int *p = NULL;
printf("%d\n", *p); /* invalid dereference */
La impresión requiere una lectura mediante p, pero no existe ningún objeto entero en esa dirección. Si p procede de una función de reserva, comprueba el resultado antes de esta línea y sigue una ruta de error si falla. Si se puso a nulo después de la liberación, mueve el uso necesario antes de liberar o cambia el diseño del lugar donde se guardan los datos.
Un puntero sin inicializar, como en int *p; *p = 5;, no tiene asignado ningún objeto válido al que apuntar. Una reserva, la dirección de un objeto que siga existiendo o un puntero prestado y comprobado deben establecer ese destino antes de desreferenciarlo.
Sigue la causa cuando cambien los síntomas
Una función con recursión infinita puede agotar los marcos de pila en una versión sin optimizar. Una condición de parada corrige la lógica. Un compilador con optimización puede convertir algunas llamadas recursivas en un bucle, por lo que la ausencia de un cierre por desbordamiento de pila no hace útil el cálculo sin límite.
Los mensajes de progreso impresos también son pruebas imperfectas. stdout se almacena en búfer: la salida puede quedar sin escribir cuando un proceso se cierra inesperadamente, especialmente si está redirigida. Para un mensaje de diagnóstico, usa stderr o llama a fflush(stdout) después de imprimir un mensaje de progreso. Añadir una impresión puede cambiar la disposición de la memoria y ocultar un error; no ha reparado el acceso inválido. Vuelve a ejecutar con un comprobador para encontrar la propia operación incorrecta.
Puntos clave
- Un fallo de segmentación indica un acceso prohibido; la corrupción original puede haber ocurrido antes.
- Compila con información de depuración y localiza el primer marco relevante de tu propio código.
- Rastrea el origen de cada puntero, el tiempo de vida del objeto al que apunta y el rango del índice.
- La salida almacenada en búfer puede ocultar una impresión ejecutada antes de un cierre inesperado.
- Si el síntoma cambia al añadir una impresión, aún es necesario investigar.
5Detectar errores de memoria con Valgrind y sanitizadores
Los comprobadores de memoria hacen visibles los errores ocultos al informar del acceso y de la historia del bloque. Valgrind observa un ejecutable de depuración; AddressSanitizer añade comprobaciones al compilarlo. Aprende a relacionar sus mensajes con los límites de los elementos, los puntos de reserva y los puntos de liberación, y después realiza una corrección que mantenga la propiedad correcta. Tras una nueva ejecución sin errores, prueba entradas que recorran otras rutas.
5.1 Leer un informe de Valgrind
La herramienta predeterminada de Valgrind, Memcheck, observa un programa compilado mientras se ejecuta. Sigue los límites de los bloques del montículo, los tiempos de vida de las reservas y si los valores se han inicializado. No necesita una compilación con sanitizadores, pero la información de depuración facilita mucho el uso de sus informes.
Compila y ejecuta
cc -std=c17 -g -O0 -Wall -Wextra program.c -o program
valgrind --leak-check=full ./program
Coloca los argumentos del propio programa después de ./program. Añade --show-leak-kinds=all para enumerar todas las categorías de fugas y --track-origins=yes cuando no esté claro el origen de un valor sin inicializar.
Valgrind funciona en Linux y en algunos otros sistemas de tipo Unix, pero no de forma nativa en Windows ni en Mac con Apple silicon. Memcheck suele ralentizar la ejecución aproximadamente entre 20 y 30 veces. Usa primero una entrada pequeña que reproduzca el fallo y después realiza más pruebas. Una prueba sin errores solo cubre las rutas que ejecuta realmente.
Lee por separado el tamaño del acceso y el del bloque
Un informe encabezado por «Invalid write of size 4» describe una escritura de cuatro bytes. Una dirección «0 bytes after a block of size 40 alloc'd» comienza justo después de una reserva de 40 bytes. Si ese bloque contiene diez valores int de 4 bytes, el acceso incorrecto comienza en el elemento 10; el último elemento válido es el 9.
El informe también indica la línea del acceso y la pila de llamadas de la reserva. Úsalas juntas: la reserva indica los límites previstos y el acceso indica qué índice los vulneró. «Size 4» no significa que el programa se haya excedido en cuatro elementos.
«Invalid read of size N» describe de forma similar una lectura inválida. Una dirección dentro de un bloque descrito como «free'd» señala un problema de tiempo de vida; compara los lugares de reserva, liberación y acceso. «Invalid free() / delete / delete[] / realloc()» puede indicar una liberación duplicada o una dirección que nunca fue el inicio válido de una reserva.
Para una condición que usa datos no establecidos, Memcheck puede indicar «Conditional jump or move depends on uninitialised value(s)». Esa grafía pertenece al mensaje de la herramienta. La condición indicada consumió el valor; --track-origins=yes ayuda a localizar dónde se originó el almacenamiento sin inicializar.
Lee la categoría de la fuga
| Categoría del informe | Qué significa |
|---|---|
| Definitely lost | No queda ningún puntero al bloque |
| Indirectly lost | Solo se puede acceder al bloque a través de otro bloque perdido |
| Possibly lost | Solo se encontró un puntero interior |
| Still reachable | Queda un puntero al terminar, pero el bloque no se liberó |
Supón que una función auxiliar reserva espacio para diez valores int de 4 bytes en un puntero local y retorna sin liberar ni devolver la dirección. Su bloque de 40 bytes está definitivamente perdido. En cambio, un puntero global que conservara la dirección inicial haría que un bloque sin liberar siguiera siendo accesible. Los totales de reservas y liberaciones del resumen del montículo describen eventos, incluidos los cambios de tamaño; no sustituyen el razonamiento sobre la propiedad.
Una salida sin errores puede mostrar «All heap blocks were freed -- no leaks are possible» y «ERROR SUMMARY: 0 errors from 0 contexts». Memcheck por lo general no detecta todos los accesos fuera de los límites de arrays locales o globales que permanecen dentro de memoria circundante válida. Corrige el primer error, vuelve a ejecutar y prueba más entradas, incluidas la entrada vacía y las rutas con retorno anticipado.
Puntos clave
- Usa información de depuración para que Memcheck pueda identificar las líneas de tu código fuente.
- Lee conjuntamente el tamaño del acceso, los límites del bloque y el historial de reserva.
- Usa
--track-origins=yespara rastrear los valores sin inicializar. - Distingue los bloques perdidos de los bloques todavía accesibles al terminar.
- Corrige el primer error antes de interpretar las consecuencias posteriores.
- Ejecuta varias entradas porque un comprobador solo observa las rutas ejecutadas.
5.2 Incorporar comprobaciones al programa
AddressSanitizer, o ASan, añade comprobaciones de acceso a memoria durante la compilación. El ejecutable resultante se comprueba a sí mismo mientras se ejecuta. Es útil para las pruebas habituales de desarrollo porque su ralentización típica es de unas dos veces, mucho menor que la de Memcheck.
Compila y enlaza con el sanitizador
cc -std=c17 -g -fsanitize=address -fno-omit-frame-pointer \
program.c -o program-asan
./program-asan
Usa la opción del sanitizador tanto en la compilación como en el enlazado final cuando los realices en pasos separados. -g proporciona información de línea y -fno-omit-frame-pointer ayuda a conservar pilas de llamadas útiles. El compilador de Microsoft usa /fsanitize=address.
ASan detecta desbordamientos de arrays en el montículo, la pila y los datos globales, usos después de la liberación, liberaciones inválidas y dobles liberaciones. Las comprobaciones del tiempo de vida del ámbito y las comprobaciones opcionales de uso después del retorno también pueden detectar punteros a variables locales que ya no existen. Requiere recompilar y usar memoria adicional. Mantén esta instrumentación en las versiones de desarrollo y prueba.
Lee las tres partes de la historia
Un informe de uso después de la liberación puede comenzar con «ERROR: AddressSanitizer: heap-use-after-free». Una línea posterior, «READ of size 4», describe la operación inválida. El informe muestra después dónde se liberó la memoria con «freed by thread T0 here:» y dónde se reservó con «previously allocated by thread T0 here:».
Empieza por la primera línea relevante de tu código en cada pila de llamadas. La reserva establece el bloque; la liberación termina su tiempo de vida; la lectura indicada ocurre después de ese final. Decide si debes mover la liberación después de la lectura o dejar de acceder al bloque después de liberarlo. El informe proporciona la historia, mientras que la propiedad prevista determina la corrección.
Para «heap-buffer-overflow», una dirección «located 0 bytes after 20-byte region» comienza justo después de la región. Convierte bytes a elementos solo después de comprobar el tamaño del tipo usado por el programa. ASan normalmente se detiene en el primer error, por lo que corregirlo puede revelar otro error en la siguiente ejecución.
Las comprobaciones de fugas y de valores sin inicializar son diferentes
LeakSanitizer comprueba al terminar si hay reservas sin liberar. Normalmente está activado con ASan en Linux. En cadenas de herramientas compatibles de macOS se puede activar con ASAN_OPTIONS=detect_leaks=1; algunas cadenas de herramientas de Mac no lo proporcionan. La herramienta independiente leaks de Apple es otra opción para comprobar fugas en ese sistema.
ASan no sigue si se ha inicializado un valor dentro de los límites. Para ese problema, usa Memcheck o MemorySanitizer de Clang en una configuración compatible de Linux. UndefinedBehaviorSanitizer comprueba otros tipos de comportamiento indefinido, como el desbordamiento de enteros con signo, y puede combinarse con ASan mediante -fsanitize=address,undefined.
Elige una ejecución independiente para cada comprobador
| Necesidad | Herramienta útil |
|---|---|
| Comprobaciones rápidas durante el desarrollo, incluidos los límites de arrays locales | Versión compilada con ASan |
| Seguimiento de valores sin inicializar sin instrumentación de sanitizadores | Memcheck en un sistema compatible |
| Fugas de reservas al terminar | LeakSanitizer o Memcheck |
No ejecutes un ejecutable de ASan con Valgrind. Compila un ejecutable de depuración ordinario independiente para Memcheck y no combines ASan y MemorySanitizer en una misma compilación. Después de una ejecución sin errores, cambia la entrada para que se ejecuten otras rutas de reserva, ampliación y liberación de recursos.
Puntos clave
- ASan requiere compilar y enlazar con su opción de instrumentación.
- Lee las pilas del acceso incorrecto, la liberación y la reserva para reconstruir un error de tiempo de vida.
- LeakSanitizer comprueba las fugas al terminar cuando la plataforma lo admite.
- ASan no detecta las lecturas ordinarias de memoria sin inicializar.
- Usa ejecuciones independientes y compilaciones adecuadas para ASan y Memcheck.
5.3 Convertir un informe en una corrección verificada
Un informe de memoria identifica un error observado y partes de su historia. Una corrección útil restablece el tamaño, los límites, la inicialización y la propiedad correctos, y después vuelve a comprobar el programa corregido.
Reduce y reproduce
Encuentra la entrada más pequeña que siga produciendo el error. Compila con -g, advertencias y poca optimización, y después usa Memcheck o una versión compilada con ASan. Lee el primer informe antes de investigar los posteriores; un estado corrupto puede generar una cadena de fallos engañosos.
Supón que un programa contiene:
size_t n = 5;
int *v = malloc(n * sizeof *v);
for (size_t i = 0; i <= n; ++i) v[i] = (int)(i * i);
printf("%d\n", v[2]);
return 0;
En un sistema con valores int de 4 bytes, ASan informa de una escritura de cuatro bytes en el primer byte posterior a una reserva de 20 bytes. La escritura está en el bucle. Memcheck también puede informar del desbordamiento y después de una reserva sin liberar; que el bloque esté perdido o todavía accesible depende de qué punteros queden al terminar.
Relaciona el informe con el código
Cinco valores int ocupan los índices de cero a cuatro. La condición i <= n llega al índice cinco. El tamaño de la reserva es correcto; el límite del bucle no. Ampliar el bloque ocultaría el síntoma mientras el recorrido seguiría siendo incorrecto.
Tampoco hay una comprobación de la reserva y el bloque propio no se libera. Esto requiere sus propias correcciones, aunque el primer informe haya detenido el programa antes de llegar a ellas.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
size_t n = 5;
int *v = malloc(n * sizeof *v);
if (v == NULL) return EXIT_FAILURE;
for (size_t i = 0; i < n; ++i) v[i] = (int)(i * i);
printf("%d\n", v[2]);
free(v);
return EXIT_SUCCESS;
}
El programa corregido inicializa cinco elementos válidos, imprime 4 mientras el bloque sigue existiendo y lo libera. La pequeña cantidad fija cabe en el tipo; una cantidad obtenida de la entrada también necesita comprobar el desbordamiento del tamaño antes de la reserva.
Usa la historia del informe para elegir la corrección adecuada
Para un uso después de la liberación, compara las pilas de liberación y acceso para encontrar el último uso legítimo. Mueve la liberación después de ese uso o elimina el acceso posterior inválido. Asignar NULL a un puntero no puede hacer que sus datos anteriores vuelvan a estar disponibles.
Para una fuga, la pila de reserva indica dónde comenzó el bloque. No identifica la línea adecuada para liberarlo. Sigue la propiedad a través de los retornos exitosos y los fallos para encontrar dónde termina el último uso. Para una doble liberación, elimina la liberación duplicada o corrige los conflictos entre propietarios. Eliminar la única liberación válida crea una fuga.
Para datos sin inicializar, añade --track-origins=yes de Memcheck. La línea que consume los datos puede estar lejos de la reserva que proporcionó los bytes sin establecer. Inicializa cada valor que lee el cálculo o usa calloc cuando cero sea el estado inicial correcto.
Verifica la corrección en más de una ruta
Vuelve a compilar y ejecuta de nuevo la entrada original que reproducía el fallo. Continúa hasta que su informe no muestre errores y después prueba la entrada vacía, los límites de crecimiento, una entrada ordinaria y los retornos por error. Las advertencias del compilador detectan algunos problemas antes de la ejecución; las herramientas de tiempo de ejecución comprueban las rutas que alcanzan estas pruebas.
Puntos clave
- Reproduce el fallo con una entrada pequeña y lee el primer error.
- Usa el tamaño del bloque y la posición del acceso para identificar el error de límites real.
- Comprueba los fallos de reserva y la liberación de recursos aunque el primer error de acceso detenga la ejecución.
- Los informes de fugas muestran los puntos de reserva; la propiedad determina los puntos de liberación.
- Después de corregir, vuelve a compilar, ejecuta de nuevo el caso que reproduce el fallo y prueba otras rutas.
Ejemplos de Gestión dinámica de memoria en C
1Seguir un objeto a través de llamadas a funciones
9 pasos
- 1IntroducciónUna dirección necesita un objeto que siga existiendo
Un puntero permite que una parte de un programa encuentre datos creados en otro lugar. Su utilidad depende de que esos datos sigan existiendo cuando se use el puntero.
Sigue la ejecución de una función auxiliar que crea un array de enteros, devuelve su dirección y permite que la función llamadora use el resultado. Mantén separados el puntero local y el array mientras sigues la llamada: no tienen por qué tener el mismo tiempo de vida.
- 2ExplicaciónIdentifica los tres tiempos de vida
Una variable local ordinaria tiene duración automática del almacenamiento y deja de existir cuando termina su bloque. Una variable global o local
staticdura toda la ejecución del programa. Un bloque reservado dura hasta su liberación.En los sistemas más comunes, los objetos automáticos ocupan la pila de llamadas y las reservas usan el montículo. Estos nombres describen implementaciones habituales; las reglas de C definen los tiempos de vida independientemente de la disposición de la memoria.
- 3EjemploCrea el puntero local y el bloque
Dentro de
make_array,int *p = malloc(5 * sizeof *p);crea dos objetos separados: el puntero local y, si la reserva tiene éxito, un bloque para cinco valores int.Cuando un
intocupa 4 bytes, el bloque tiene 20 bytes. Compruebapantes de usarlo. La variable de tipo puntero pertenece a la llamada a la función, pero el bloque reservado no forma parte del almacenamiento automático de esa llamada. - 4EjemploDevuelve la dirección
La función auxiliar devuelve
py la función llamadora guarda el resultado enint *nums. Se copia la dirección; no se copia el array. Las variables locales de la función auxiliar dejan entonces de existir.Si la reserva tuvo éxito,
numssigue identificando el array reservado. La función llamadora comprueba el resultado, inicializa y usa sus elementos y finalmente lo libera. El retorno no liberó el bloque porque la duración de su almacenamiento es asignada. - 5Desafío¿Qué sobrevive al retorno?
Una función auxiliar crea un
int countautomático y un array independiente en el montículo. Devuelve la dirección del array. ¿Cuál de estos dos objetos puede seguir usando la función llamadora?---reveal---
El array del montículo sigue existiendo hasta que se libera. El
countautomático de la función auxiliar dejó de existir al retornar. La dirección copiada solo permite acceder al array; conservar una dirección no mantendría vivocountsi la función auxiliar hubiera devuelto&count. - 6EjemploPrueba a devolver un array local
Ahora cambia la función auxiliar para que declare
int values[5];y devuelvavalues. Ese array es automático. Deja de existir cuando la función auxiliar retorna, por lo que la función llamadora se queda sin un objeto al que pueda acceder.El tamaño sigue siendo de cinco valores int, pero el tiempo de vida es diferente. En su lugar, se puede colocar un resultado utilizable en un búfer proporcionado por la función llamadora o reservarlo y transferirlo a esta con una responsabilidad de liberación clara.
- 7ConexiónElige un acuerdo de uso del búfer
Un búfer estático de resultado sobrevive al retorno, pero todas las llamadas lo comparten. Una llamada posterior puede sobrescribir un resultado anterior. Un resultado en el montículo puede dar a cada llamada su propio almacenamiento independiente, con la función llamadora como responsable de liberarlo.
Para el trabajo temporal, un array local pequeño de tamaño fijo es más sencillo. Los arrays cuyo tamaño depende de la entrada, los muy grandes o los que pueden crecer requieren una reserva comprobada, para que el programa pueda gestionar los fallos y controlar el tiempo de vida.
- 8DesafíoDos resultados deben seguir siendo independientes
Una función llamadora necesita dos resultados modificables de llamadas sucesivas y debe conservar ambos. No proporciona ningún búfer. ¿Satisfaría esa necesidad un único array estático de resultado? ¿Qué diseño es adecuado?
---reveal---
Un único array estático sería compartido y se sobrescribiría. Devuelve un resultado reservado independiente en cada llamada, comprueba cada resultado y deja que la función llamadora libere ambos después de sus últimos usos. El tiempo de vida en el montículo permite que sobrevivan; los bloques separados garantizan su independencia.
- 9RepasoDistingue la dirección del tiempo de vida
Sigue el objeto al que apunta un puntero: las variables locales automáticas dejan de existir al terminar su bloque, los objetos estáticos duran toda la ejecución y los bloques reservados necesitan una liberación deliberada.
Para un resultado devuelto, pregunta si el almacenamiento sobrevive y si las funciones llamadoras necesitan copias independientes. Para el trabajo temporal, pregunta si basta con un array local pequeño de tamaño fijo. El tamaño y el tiempo de vida requeridos determinan el diseño antes de escribir ninguna llamada de reserva.
2Reservar una cuadrícula y liberar los recursos en todas las rutas
13 pasos
Guarda esta unidad para seguir cada Lección Interactiva paso a paso.
- 1IntroducciónPlanifica tanto el éxito como el fallo
- 2ExplicaciónConvierte cantidades en bytes
- 3ConsejoComprueba antes de multiplicar
- 4Desafío¿Tamaño del elemento o del puntero?
- 5ExplicaciónDa un valor inicial a los contadores
- 6EjemploUsa la fila rellenada con ceros
- 7Desafío¿Qué ocurre si crece después?
- 8ExplicaciónCuenta bloques, no nombres de punteros
- 9EjemploDetén la construcción ante un fallo parcial
- 10DesafíoElige el orden de liberación
- 11Idea claveUn propietario puede prestar el acceso
- 12DesafíoUna ruta se detiene antes de terminar
- 13RepasoPlanifica la liberación junto con la construcción
3Añadir valores sin perder datos
12 pasos
Guarda esta unidad para seguir cada Lección Interactiva paso a paso.
- 1IntroducciónAmplía antes de almacenar el siguiente valor
- 2ExplicaciónEl cambio de tamaño puede sustituir el objeto
- 3EjemploDistingue el fallo del éxito
- 4Desafío¿Qué puede conservarse después de reducir?
- 5ExplicaciónLa longitud y la capacidad responden a preguntas diferentes
- 6EjemploSupera el primer límite de crecimiento
- 7EjemploSupera el siguiente límite
- 8Idea claveEl espacio sobrante reduce las copias
- 9Desafío¿Qué estado deja una inserción fallida?
- 10ConexiónUna cadena reserva un espacio para el terminador
- 11DesafíoUna selección prestada durante la ampliación
- 12RepasoActualiza el estado solo después del éxito
4Examinar toda la historia de un bloque
15 pasos
Guarda esta unidad para seguir cada Lección Interactiva paso a paso.
- 1IntroducciónCuatro preguntas para cada acceso
- 2EjemploPierde una reserva al sustituir su dirección
- 3Desafío¿Poner el puntero a nulo libera la memoria?
- 4ConsejoExamina tanto las salidas como los bucles
- 5EjemploLibera una vez e invalida todos los alias
- 6DesafíoCorrige el último uso
- 7Idea claveUn código inválido puede producir la salida esperada
- 8EjemploCuenta el terminador nulo
- 9EjemploReservado no significa inicializado
- 10DesafíoIdentifica dos límites diferentes
- 11ExplicaciónUn fallo es un síntoma
- 12EjemploExamina el acceso que provoca el cierre
- 13DesafíoUna impresión hace desaparecer el cierre
- 14DesafíoCombina propiedad y límites
- 15RepasoIdentifica la regla incumplida
5Leer un informe y verificar la corrección
14 pasos
Guarda esta unidad para seguir cada Lección Interactiva paso a paso.
- 1IntroducciónConvierte un mensaje en la historia de un bloque
- 2ExplicaciónUsa una versión de depuración ordinaria para Memcheck
- 3EjemploInterpreta la descripción de la dirección
- 4DesafíoInterpreta los bytes como bytes
- 5ConsejoElige una opción de informe para el problema
- 6ExplicaciónCompila un ejecutable de ASan por separado
- 7EjemploSigue un uso después de la liberación mediante tres pilas
- 8DesafíoUna ejecución sin errores de ASan no detecta un error de valor
- 9EjemploRelaciona el informe con el bucle
- 10EjemploMantén el bloque activo durante el trabajo
- 11Desafío¿Dónde debe corregirse una fuga?
- 12ConexiónComprueba lo que ocurre al terminar y la compatibilidad de la plataforma
- 13DesafíoCompleta el ciclo de corrección
- 14RepasoUsa las pruebas, corrige y vuelve a ejecutar
Ejercicios de Gestión dinámica de memoria en C
Set 1: Tiempos de vida, recuentos de bytes y primeras comprobaciones10 Preguntas de Práctica
Pregunta 1 de 10
Mostrar respuestaOcultar respuesta
Pregunta 2 de 10
Mostrar respuestaOcultar respuesta
Pregunta 3 de 10
sizeof(int) es 4. ¿Cuántos bytes debe solicitar la reserva?Mostrar respuestaOcultar respuesta
Pregunta 4 de 10
int *hits = calloc(6, sizeof *hits);, ¿qué valor tiene hits[4] antes de cualquier asignación posterior?Mostrar respuestaOcultar respuesta
calloc pone todos los bytes a cero, lo que da a los elementos enteros el valor cero. El índice 4 está dentro del array de seis elementos, por lo que puede leerse como cero después de comprobar la reserva. El almacenamiento recién obtenido con malloc estaría sin inicializar; esa regla no se aplica a esta reserva exitosa con calloc.Pregunta 5 de 10
Mostrar respuestaOcultar respuesta
Pregunta 6 de 10
int *tmp = realloc(data, bytes); y devuelve NULL. El bloque original era válido. ¿Qué puede hacer ahora el propietario?Mostrar respuestaOcultar respuesta
realloc con tamaño positivo deja la reserva original y su contenido sin cambios. Como el resultado se recibió en tmp, data sigue conteniendo su dirección válida. Liberar el puntero temporal nulo no hace nada; no puede liberar el bloque que sigue siendo propiedad del programa y que identifica data.Pregunta 7 de 10
Mostrar respuestaOcultar respuesta
Pregunta 8 de 10
int *view = owner; free(owner); owner = NULL;. ¿Qué ocurre si después lee view[0]?Mostrar respuestaOcultar respuesta
view era un alias de la misma reserva, cuyo tiempo de vida terminó en free(owner). Asignar NULL a owner solo actualiza esa variable de tipo puntero. No cambia view ni restaura el bloque anterior, por lo que la lectura posterior es un uso después de la liberación con comportamiento indefinido.Pregunta 9 de 10
strcpy en un búfer recién reservado en el montículo. ¿Cuál es la reserva mínima necesaria en bytes de C?Mostrar respuestaOcultar respuesta
strcpy también almacena el carácter nulo de terminación. Por tanto, el búfer mínimo tiene ocho bytes. Siete bytes contendrían las letras visibles, pero dejarían el terminador fuera de la reserva, causando una escritura fuera de los límites aunque la impresión pareciera funcionar.Pregunta 10 de 10
Una variable global int *saved es inicialmente igual a NULL. Una función auxiliar ejecuta este código y retorna. Los valores int ocupan 4 bytes. Memcheck informa de una escritura inválida justo después de la reserva. ¿Qué error de recorrido y qué categoría de fuga al terminar se producen?
int *p = malloc(7 * sizeof *p);
if (p == NULL) return;
for (size_t i = 0; i <= 7; ++i) p[i] = (int)i;
saved = p;
Mostrar respuestaOcultar respuesta
Set 2: Seguir las rutas de reserva y liberación10 Preguntas de Práctica
Guarda esta unidad para ver todas las respuestas y practicar cada set.
Pregunta 1 de 10
p. La función llamadora guarda el resultado en values. ¿Qué sigue existiendo inmediatamente después de que retorne la función auxiliar?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 2 de 10
n distinta de cero, ¿qué condición detecta antes de la reserva que n * sizeof *p no cabe en size_t?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 3 de 10
a, lo comprueba y después intenta reservar b. La segunda reserva falla. La función es propietaria de a y retornará inmediatamente con un fallo. ¿Qué liberación de recursos se necesita?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 4 de 10
static int visits = 0; y ejecuta return ++visits;. ¿Qué valor devuelve su tercera llamada?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 5 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 6 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 7 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 8 de 10
free después de un bucle que llena un array del montículo. ¿Qué debería comprobar la investigación antes de cambiar la llamada de liberación?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 9 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 10 de 10
Un programa comienza con este bloque comprobado y después toma una rama según p[5]. Un cambio a un tamaño positivo tuvo éxito. ASan no informa de nada, mientras que Memcheck informa de un valor no establecido en la condición. ¿Qué corrección se ajusta a toda la historia?
int *p = calloc(4, sizeof *p);
if (p == NULL) return EXIT_FAILURE;
int *tmp = realloc(p, 6 * sizeof *p);
if (tmp == NULL) { free(p); return EXIT_FAILURE; }
p = tmp;
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Set 3: Crecimiento, tiempos de vida y pruebas de los informes10 Preguntas de Práctica
Guarda esta unidad para ver todas las respuestas y practicar cada set.
Pregunta 1 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 2 de 10
int *p = malloc(11 * sizeof p);. ¿Cuántos bytes solicita y qué tiene de incorrecto la expresión?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 3 de 10
calloc con tamaño positivo para un búfer de caracteres, ¿qué cadena de C contiene inicialmente?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 4 de 10
p. El código guarda el índice 4 y un puntero a ese elemento. Un intento de ampliación a nueve valores int falla al usar un puntero temporal. Una reducción posterior a tres tiene éxito y se actualiza el puntero propietario. ¿Puede usarse después la selección?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 5 de 10
old, establece cap = 16 y asigna data = realloc(data, cap * sizeof *data). Esa solicitud de tamaño positivo falla antes de almacenar otro valor. ¿Qué estado restaura el array sin cambios?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 6 de 10
buffer. Ejecuta buffer = NULL; free(buffer); después de su último uso. ¿Qué ocurre con el bloque original?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 7 de 10
p y después llama a free(alias), donde alias se copió de p antes de la liberación. ¿Qué error es este?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 8 de 10
stdout no termina con un salto de línea y después el programa se cierra inesperadamente. Su texto nunca aparece. ¿Por qué esto no permite determinar con fiabilidad hasta dónde llegó la ejecución?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 9 de 10
program-asan con AddressSanitizer y quiere una ejecución independiente con Memcheck. ¿Qué ejecutable debe recibir Memcheck?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 10 de 10
items[n] = 0;, justo después de una reserva para n valores int. El bucle usa i <= n, el resultado no se comprueba y no se liberan los recursos. ¿Qué corrección aborda la causa y los demás defectos de propiedad?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Set 4: Corregir la propiedad en condiciones cambiantes10 Preguntas de Práctica
Guarda esta unidad para ver todas las respuestas y practicar cada set.
Pregunta 1 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 2 de 10
calloc y después amplió el bloque con éxito a siete contadores. ¿Qué debe ocurrir antes de incrementar por primera vez el contador 6?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 3 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 4 de 10
p. Un intento de cambiar su tamaño a siete valores int falla mediante tmp y deja p sin cambios. Después, el código ejecuta p = realloc(p, 9 * sizeof *p) y esa solicitud también falla. No existen alias. ¿Qué reserva se ha perdido ahora?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 5 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 6 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 7 de 10
char **labels = malloc(4 * sizeof *labels);, comprueba el resultado y después ejecuta inmediatamente strcpy(labels[0], "east");. ¿Qué diseño corrige la primera entrada y deja clara su propiedad?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 8 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 9 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 10 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Set 5: Seguir historias completas de memoria10 Preguntas de Práctica
Guarda esta unidad para ver todas las respuestas y practicar cada set.
Pregunta 1 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 2 de 10
double *samples ordinario, ¿qué mide sizeof samples?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 3 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 4 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 5 de 10
X con éxito sin ampliarse y actualiza su terminador. El siguiente carácter Y provoca una ampliación mediante un puntero temporal, pero la solicitud falla. La API promete conservar la cadena en caso de fallo. ¿Qué estado queda?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 6 de 10
a y 40 bytes en la variable local b. En la ruta de error libera a, establece b = NULL y retorna. No quedan alias. ¿Qué tamaño total de reservas se pierde y qué cambio lo evita?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 7 de 10
Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 8 de 10
int *p = malloc(6 * sizeof p);, comprueba que tuvo éxito, inicializa los índices de 0 a 5 y después suma los índices de 0 a 11. ¿Qué diagnóstico se ajusta a toda la suma?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 9 de 10
p[index]. ¿Qué debe comprobarse sobre ese acceso?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
Pregunta 10 de 10
display, una liberación anterior en reset y una reserva en load. La función llamadora invoca load, reset y después display sobre el mismo resultado. Necesita esa impresión. ¿Qué corrección se deduce de la historia del bloque?Mostrar respuestaOcultar respuesta
Guarda esta unidad para ver esta respuesta.
¿Te atascas con una pregunta tuya? Ayuda con Tareas
Términos Clave
A
AddressSanitizer
Alias
NULL después de la liberación deja inutilizables para lecturas posteriores los alias guardados.Almacenamiento automático
Almacenamiento de salida en búfer
fflush(stdout) después de una impresión de progreso hace que su texto almacenado en búfer esté disponible antes de la siguiente operación.Almacenamiento estático
static.Array de longitud variable
int a[n] no proporciona la comprobación de fallo mediante un resultado nulo que ofrece la reserva en el montículo.Array dinámico
B
Bloque de memoria reservado perdido
NULL sin llamar primero a free hace que se pierda la dirección de la reserva.Búfer estático compartido
Búfer proporcionado por la función llamadora
C
calloc
calloc reserva un array a partir de una cantidad y un tamaño de elemento, pone sus bytes a cero y devuelve un puntero nulo si falla la reserva.calloc comienzan en cero antes del primer incremento.Capacidad
Comportamiento indefinido
Crecimiento geométrico
D
Definitivamente perdido
Desbordamiento de búfer en el montículo
Desbordamiento de pila
Desbordamiento en el cálculo del tamaño
size_t y se reduce modularmente a un valor representable menor.SIZE_MAX / sizeof *p antes de multiplicarlo por el tamaño del elemento.Doble liberación
free sobre lo que era una única reserva activa compartida.Duración del almacenamiento
E
Ejecución de verificación
Entrada que reproduce el fallo
Error de una unidad
i <= n por i < n hace que un recorrido que comienza en cero visite exactamente los n elementos previstos.F
Fallo de segmentación
free
free libera un bloque reservado que sigue existiendo para que pueda reutilizarse cuando se le pasa su dirección inicial, y no hace nada cuando se le pasa NULL.Fuga de memoria
I
Inicialización
Instrumentación
-fsanitize=address durante la compilación y el enlazado crea un ejecutable instrumentado con ASan.L
LeakSanitizer
Lectura de memoria sin inicializar
Literal de cadena
char word[] = "hello"; cuando necesites una copia local modificable en vez de un puntero al literal.Longitud
M
malloc
malloc reserva un número solicitado de bytes y devuelve la dirección de un bloque o un puntero nulo, sin inicializar los valores almacenados.malloc necesita asignaciones antes de que sus elementos se sumen o comparen.Memcheck
Memoria rellenada con ceros
calloc comienza con un carácter nulo y contiene una cadena vacía.MemorySanitizer
Montículo
P
Perdido indirectamente
Pila de llamadas
Posiblemente perdido
Primer error detectado
Propiedad de la memoria
Puntero colgante
Puntero interior
Puntero nulo
Puntero prestado
Puntero temporal
tmp = realloc(p, bytes), un resultado nulo deja p disponible para seguir usándolo o liberar los recursos.Punto de reserva de memoria
R
realloc
realloc sustituye un bloque reservado por un objeto del tamaño solicitado, conserva la parte inicial que cabe y puede cambiar su dirección.Reserva de memoria
Reserva de memoria anidada
Resumen del montículo
Reutilización de búferes
Ruta de liberación de recursos
S
Seguimiento del origen
--track-origins=yes de Memcheck puede explicar por qué una condición posterior depende de datos del montículo no establecidos.size_t
size_t es el tipo entero sin signo usado para los tamaños de objetos y los recuentos de bytes de las reservas, incluido el resultado de sizeof.size_t para su longitud y capacidad, de modo que puedan describir tamaños de objetos.sizeof
sizeof proporciona el tamaño de un tipo u objeto en bytes de C; aplicado a un puntero, mide el puntero en vez de la reserva a la que apunta.n * sizeof *p se adapta al tipo de elemento, mientras que sizeof p no puede recuperar la longitud del array.T
Terminador nulo
Tiempo de vida de un objeto
realloc termina el tiempo de vida del objeto anterior aunque la dirección devuelta parezca no haber cambiado.Todavía accesible
Traza de la pila de llamadas
U
UndefinedBehaviorSanitizer
-fsanitize=address,undefined para activar los dos conjuntos compatibles de comprobaciones.Uso después de la liberación
Sobre esta unidad
Crea tus propios Materiales de Estudio
Convierte tus notas, diapositivas y documentos en Guías de Estudio, Preguntas de Práctica, Flashcards y 6 herramientas más.