Gestión dinámica de memoria en C

Aprende cómo un programa en C obtiene memoria mientras se ejecuta y la devuelve. Compara la pila y el montículo, reserva bloques con malloc y calloc, cambia su tamaño con realloc y libéralos con free. Reconoce las fugas de memoria, los punteros colgantes, las dobles liberaciones y los accesos fuera de los límites, comprende por qué causan cierres inesperados como los fallos de segmentación y detecta y corrige estos errores con Valgrind y AddressSanitizer.

PrincipianteUnidad 5 de 8 del curso Curso de Programación en C

Lo que aprenderás

  1. 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ático
      Describe 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ículo
      Elige 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.
  2. 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 sizeof
      Calcula 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 calloc
      Usa 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 propiedad
      Libera 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.
  3. 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 realloc
      Llama 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 crecer
      Manté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.
  4. 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 memoria
      Explica 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 liberaciones
      Reconoce 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 inicializar
      Manté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 origen
      Explica qué es un fallo de segmentación, enumera sus causas habituales y rastrea el fallo hasta la línea que contiene el error.
  5. 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 Memcheck
      Compila 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 LeakSanitizer
      Compila un programa con AddressSanitizer, lee sus informes y elige entre los sanitizadores y Valgrind.
    • Corregir errores de memoria a partir del informe de una herramienta
      Sigue 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.

Guía de estudio de Gestión dinámica de memoria en C

5 capítulos

1

Explicar 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 almacenamientoObjetos típicosEl tiempo de vida termina
AutomáticaVariables locales ordinarias y parámetrosCuando termina su bloque
EstáticaVariables globales y variables locales declaradas staticCuando termina el programa
AsignadaBloques obtenidos de funciones de reservaCuando 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
NecesidadOpción inicial adecuada
Un array pequeño de tamaño fijo usado durante una llamadaArray local
Una cantidad leída de la entradaReserva comprobada en el montículo
Un resultado que sobrevive a la función que lo creaReserva en el montículo o búfer propiedad de la función llamadora
Datos que crecen a medida que lleganBúfer redimensionable en el montículo
Un array muy grandeReserva 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.
2

Reservar 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 *pointer para 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 NULL antes de usarlo.
  • Inicializa cada elemento de malloc antes de leerlo.
  • Conserva el número de elementos por separado; sizeof pointer no 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 inicialOpción natural
Incrementar contadores enteroscalloc
Establecer indicadores enteros gradualmente desde un estado de todos ceroscalloc
Sobrescribir inmediatamente todos los elementosmalloc

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 calloc un 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 realloc no 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 para free.
  • 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 free sobre el bloque más reciente.
3

Cambiar 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 free para 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.
4

Reconocer 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 realloc invalida 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 n elementos van de cero a n - 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.
5

Detectar 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 informeQué significa
Definitely lostNo queda ningún puntero al bloque
Indirectly lostSolo se puede acceder al bloque a través de otro bloque perdido
Possibly lostSolo se encontró un puntero interior
Still reachableQueda 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=yes para 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
NecesidadHerramienta útil
Comprobaciones rápidas durante el desarrollo, incluidos los límites de arrays localesVersión compilada con ASan
Seguimiento de valores sin inicializar sin instrumentación de sanitizadoresMemcheck en un sistema compatible
Fugas de reservas al terminarLeakSanitizer 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

5 Lecciones Interactivas

1

Seguir un objeto a través de llamadas a funciones

9 pasos
Sigue los datos locales, estáticos y reservados, y después elige el almacenamiento para un resultado devuelto.
  1. 1
    Introducció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.

  2. 2
    Explicació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 static dura 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.

  3. 3
    EjemploCrea 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 int ocupa 4 bytes, el bloque tiene 20 bytes. Comprueba p antes 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.

  4. 4
    EjemploDevuelve la dirección

    La función auxiliar devuelve p y la función llamadora guarda el resultado en int *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, nums sigue 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.

  5. 5
    Desafío¿Qué sobrevive al retorno?

    Una función auxiliar crea un int count automá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 count automá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 vivo count si la función auxiliar hubiera devuelto &count.

  6. 6
    EjemploPrueba a devolver un array local

    Ahora cambia la función auxiliar para que declare int values[5]; y devuelva values. 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.

  7. 7
    Conexió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.

  8. 8
    Desafí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.

  9. 9
    RepasoDistingue 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.

2

Reservar una cuadrícula y liberar los recursos en todas las rutas

13 pasos
Parte de los tamaños de los elementos para realizar solicitudes comprobadas, inicializar valores y liberar recursos tras un éxito parcial.

Guarda esta unidad para seguir cada Lección Interactiva paso a paso.

  1. 1
    IntroducciónPlanifica tanto el éxito como el fallo
  2. 2
    ExplicaciónConvierte cantidades en bytes
  3. 3
    ConsejoComprueba antes de multiplicar
  4. 4
    Desafío¿Tamaño del elemento o del puntero?
  5. 5
    ExplicaciónDa un valor inicial a los contadores
  6. 6
    EjemploUsa la fila rellenada con ceros
  7. 7
    Desafío¿Qué ocurre si crece después?
  8. 8
    ExplicaciónCuenta bloques, no nombres de punteros
  9. 9
    EjemploDetén la construcción ante un fallo parcial
  10. 10
    DesafíoElige el orden de liberación
  11. 11
    Idea claveUn propietario puede prestar el acceso
  12. 12
    DesafíoUna ruta se detiene antes de terminar
  13. 13
    RepasoPlanifica la liberación junto con la construcción
3

Añadir valores sin perder datos

12 pasos
Sigue la longitud, la capacidad y los fallos de cambio de tamaño de un array dinámico, incluido el caso del terminador de cadena.

Guarda esta unidad para seguir cada Lección Interactiva paso a paso.

  1. 1
    IntroducciónAmplía antes de almacenar el siguiente valor
  2. 2
    ExplicaciónEl cambio de tamaño puede sustituir el objeto
  3. 3
    EjemploDistingue el fallo del éxito
  4. 4
    Desafío¿Qué puede conservarse después de reducir?
  5. 5
    ExplicaciónLa longitud y la capacidad responden a preguntas diferentes
  6. 6
    EjemploSupera el primer límite de crecimiento
  7. 7
    EjemploSupera el siguiente límite
  8. 8
    Idea claveEl espacio sobrante reduce las copias
  9. 9
    Desafío¿Qué estado deja una inserción fallida?
  10. 10
    ConexiónUna cadena reserva un espacio para el terminador
  11. 11
    DesafíoUna selección prestada durante la ampliación
  12. 12
    RepasoActualiza el estado solo después del éxito
4

Examinar toda la historia de un bloque

15 pasos
Distingue fugas, alias inválidos, errores de límites y valores no establecidos, y después rastrea un cierre inesperado hasta su causa.

Guarda esta unidad para seguir cada Lección Interactiva paso a paso.

  1. 1
    IntroducciónCuatro preguntas para cada acceso
  2. 2
    EjemploPierde una reserva al sustituir su dirección
  3. 3
    Desafío¿Poner el puntero a nulo libera la memoria?
  4. 4
    ConsejoExamina tanto las salidas como los bucles
  5. 5
    EjemploLibera una vez e invalida todos los alias
  6. 6
    DesafíoCorrige el último uso
  7. 7
    Idea claveUn código inválido puede producir la salida esperada
  8. 8
    EjemploCuenta el terminador nulo
  9. 9
    EjemploReservado no significa inicializado
  10. 10
    DesafíoIdentifica dos límites diferentes
  11. 11
    ExplicaciónUn fallo es un síntoma
  12. 12
    EjemploExamina el acceso que provoca el cierre
  13. 13
    DesafíoUna impresión hace desaparecer el cierre
  14. 14
    DesafíoCombina propiedad y límites
  15. 15
    RepasoIdentifica la regla incumplida
5

Leer un informe y verificar la corrección

14 pasos
Elige un comprobador, interpreta los bytes y las pilas de llamadas y corrige un desbordamiento y una liberación omitida.

Guarda esta unidad para seguir cada Lección Interactiva paso a paso.

  1. 1
    IntroducciónConvierte un mensaje en la historia de un bloque
  2. 2
    ExplicaciónUsa una versión de depuración ordinaria para Memcheck
  3. 3
    EjemploInterpreta la descripción de la dirección
  4. 4
    DesafíoInterpreta los bytes como bytes
  5. 5
    ConsejoElige una opción de informe para el problema
  6. 6
    ExplicaciónCompila un ejecutable de ASan por separado
  7. 7
    EjemploSigue un uso después de la liberación mediante tres pilas
  8. 8
    DesafíoUna ejecución sin errores de ASan no detecta un error de valor
  9. 9
    EjemploRelaciona el informe con el bucle
  10. 10
    EjemploMantén el bloque activo durante el trabajo
  11. 11
    Desafío¿Dónde debe corregirse una fuga?
  12. 12
    ConexiónComprueba lo que ocurre al terminar y la compatibilidad de la plataforma
  13. 13
    DesafíoCompleta el ciclo de corrección
  14. 14
    RepasoUsa las pruebas, corrige y vuelve a ejecutar

Ejercicios de Gestión dinámica de memoria en C

50 Preguntas de Práctica en 5 sets

Set 1: Tiempos de vida, recuentos de bytes y primeras comprobaciones10 Preguntas de Práctica

Pregunta 1 de 10

Un búfer contiene seis valores int inicializados. El código guarda el índice 4, reduce el bloque con éxito a tres valores int y después necesita el elemento seleccionado anteriormente. ¿Cuál es un siguiente paso válido?
  1. Reconstruir el puntero seleccionado como &data[4].
  2. Descartar la selección porque se eliminó el índice 4.
  3. Leer mediante el puntero anterior antes de otra reserva.
  4. Leer el índice 4 si la dirección del bloque no cambió.
Mostrar respuestaOcultar respuesta
Respuesta: B
La reducción conserva solo los tres primeros elementos, en los índices de cero a dos. El índice 4 ya no designa un elemento del bloque redimensionado, por lo que no puede recuperarse volviendo a calcular un puntero. El objeto anterior también dejó de existir al tener éxito la operación, aunque la dirección no cambiara; los punteros guardados no pueden recuperar los datos descartados.

Pregunta 2 de 10

Una función debe producir un resultado de texto modificable que sobreviva a su retorno. La función llamadora no proporciona un búfer y las distintas llamadas deben conservar resultados independientes. ¿Qué diseño de almacenamiento es adecuado?
  1. Devolver un array local; la función llamadora guarda su dirección.
  2. Devolver un búfer estático; la función llamadora lo libera.
  3. Devolver el literal; la función llamadora cambia sus letras.
  4. Devolver una copia reservada; la función llamadora la libera.
Mostrar respuestaOcultar respuesta
Respuesta: D
Una copia reservada tiene un tiempo de vida que supera la llamada que la crea y proporciona almacenamiento independiente a cada resultado. Un búfer estático también sobrevive, pero comparte un único resultado entre llamadas y la función llamadora no puede liberarlo. Un array local deja de existir al retornar y un literal de cadena no puede proporcionar almacenamiento modificable.

Pregunta 3 de 10

Un array necesita 13 valores int. En este sistema sizeof(int) es 4. ¿Cuántos bytes debe solicitar la reserva?
  1. 52 bytes
  2. 13 bytes
  3. 17 bytes
  4. 104 bytes
Mostrar respuestaOcultar respuesta
Respuesta: A
La solicitud necesita 13 elementos de 4 bytes cada uno, lo que da 52 bytes. Una solicitud de 13 bytes confunde el número de elementos con el número de bytes; no puede contener trece valores int en este sistema.

Pregunta 4 de 10

Después de una llamada exitosa int *hits = calloc(6, sizeof *hits);, ¿qué valor tiene hits[4] antes de cualquier asignación posterior?
  1. Un valor sin inicializar
  2. El entero 6
  3. 0
  4. El entero 4
Mostrar respuestaOcultar respuesta
Respuesta: C
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

Una función reserva un array de punteros y cuatro bloques de filas separados. Las cinco reservas tienen éxito y la función es propietaria de todas. ¿Cómo debe liberar la cuadrícula?
  1. Liberar el array de punteros y después leer sus punteros a filas.
  2. Liberar solo el array de punteros; también es propietario de las filas.
  3. Liberar las cuatro filas y después el array de punteros.
  4. Liberar cada fila dos veces y después el array de punteros.
Mostrar respuestaOcultar respuesta
Respuesta: C
Las filas son cuatro reservas separadas y sus direcciones se almacenan en el array exterior. Libéralas mientras esas direcciones sigan disponibles y después libera el bloque exterior. Liberar solo el array de punteros deja sin liberar las reservas de las filas; liberarlo primero también invalida las lecturas posteriores de sus entradas.

Pregunta 6 de 10

Un cambio a un tamaño positivo usa int *tmp = realloc(data, bytes); y devuelve NULL. El bloque original era válido. ¿Qué puede hacer ahora el propietario?
  1. Seguir usando o liberar el bloque original sin cambios.
  2. Usar los nuevos bytes mediante tmp después de ponerlo a nulo.
  3. Liberar tmp para liberar de inmediato el bloque original.
  4. Usar data solo después de reservarlo de nuevo.
Mostrar respuestaOcultar respuesta
Respuesta: A
Una llamada fallida a 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

Un array dinámico comienza con longitud 0 y capacidad 3. Duplica su capacidad solo cuando está lleno. Después de ocho inserciones exitosas, ¿cuáles son su longitud y su capacidad?
  1. Longitud 8, capacidad 8
  2. Longitud 12, capacidad 12
  3. Longitud 8, capacidad 9
  4. Longitud 8, capacidad 12
Mostrar respuestaOcultar respuesta
Respuesta: D
La cuarta inserción aumenta la capacidad de 3 a 6 y la séptima la aumenta de 6 a 12. Ahora hay ocho valores en uso, por lo que la longitud es 8. Una capacidad de 8 describiría un crecimiento ajustado exactamente, no la regla de duplicación indicada; la capacidad sobrante no aumenta el número de valores almacenados.

Pregunta 8 de 10

Después de una reserva exitosa, el código ejecuta int *view = owner; free(owner); owner = NULL;. ¿Qué ocurre si después lee view[0]?
  1. Lee almacenamiento válido porque view no es nulo.
  2. Accede a un objeto cuyo tiempo de vida ha terminado.
  3. Lee cero porque owner se ha puesto a nulo.
  4. Crea una nueva reserva en la dirección anterior.
Mostrar respuestaOcultar respuesta
Respuesta: B
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

Un programa copia la cadena «network» con strcpy en un búfer recién reservado en el montículo. ¿Cuál es la reserva mínima necesaria en bytes de C?
  1. 7 bytes
  2. 9 bytes
  3. 16 bytes
  4. 8 bytes
Mostrar respuestaOcultar respuesta
Respuesta: D
Las siete letras requieren siete bytes y 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;
  1. Se escribe un valor int de más; el bloque está todavía accesible.
  2. Se escriben cuatro valores int de más; el bloque está definitivamente perdido.
  3. Se escribe un valor int de más; el bloque está definitivamente perdido.
  4. Se escriben cuatro valores int de más; el bloque está todavía accesible.
Mostrar respuestaOcultar respuesta
Respuesta: A
El bloque de siete valores int permite los índices de cero a seis, pero el bucle también escribe en el índice siete. El tamaño del acceso es el de un valor int de cuatro bytes. La variable global conserva la dirección inicial después de que la función auxiliar retorne, por lo que el bloque sin liberar está todavía accesible, no definitivamente perdido.
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

Una función auxiliar devuelve un array reservado con éxito mediante un puntero local p. La función llamadora guarda el resultado en values. ¿Qué sigue existiendo inmediatamente después de que retorne la función auxiliar?
  1. La variable p de la función auxiliar y el array reservado.
  2. La variable p de la función auxiliar y todos sus parámetros.
  3. El array reservado y la variable values de la función llamadora.
  4. La variable values de la función llamadora, pero no el array reservado.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 2 de 10

Para una cantidad n distinta de cero, ¿qué condición detecta antes de la reserva que n * sizeof *p no cabe en size_t?
  1. n > SIZE_MAX / sizeof *p
  2. n * sizeof *p > SIZE_MAX
  3. sizeof p > n / SIZE_MAX
  4. n > SIZE_MAX * sizeof *p
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 3 de 10

Una función reserva el búfer 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?
  1. Liberar b y retornar sin tocar a.
  2. Poner a a nulo y retornar con un fallo.
  3. Retornar con un fallo; liberar el puntero local libera a.
  4. Liberar a antes de retornar con un fallo.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 4 de 10

Una función declara static int visits = 0; y ejecuta return ++visits;. ¿Qué valor devuelve su tercera llamada?
  1. 1
  2. 3
  3. 0
  4. 2
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 5 de 10

Un búfer de cadena tiene una capacidad de 12 bytes y contiene 11 caracteres más su terminador. ¿Qué debe ocurrir antes de añadir otro carácter?
  1. Sobrescribir el terminador y mantener el tamaño sin cambios.
  2. Aumentar la longitud dos veces y ampliar tras la siguiente inserción.
  3. Escribir en el índice 12 y mantener el terminador anterior.
  4. Ampliar el búfer y escribir después el carácter y el terminador.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 6 de 10

Un bucle reserva correctamente 48 bytes en cada una de 750 iteraciones y pierde el único puntero de cada bloque sin liberarlo. ¿Cuánto almacenamiento se pierde?
  1. 36 000 bytes
  2. 750 bytes
  3. 48 000 bytes
  4. 36 048 bytes
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 7 de 10

Un array reservado con éxito contiene nueve elementos. ¿Cuál es su índice de elemento válido más alto?
  1. 9
  2. 8
  3. 10
  4. 7
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 8 de 10

Un programa empieza a cerrarse inesperadamente dentro de 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?
  1. Escrituras anteriores que puedan haber cruzado los límites del array.
  2. Si eliminar toda la liberación de recursos permite terminar el programa.
  3. Si una impresión cambia la dirección de liberación indicada.
  4. Si reservar el mismo array localmente oculta el cierre.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 9 de 10

Una reserva sin liberar todavía tiene su dirección inicial guardada en un puntero global al terminar. ¿Qué categoría de fuga de Valgrind la describe?
  1. Definitivamente perdido
  2. Perdido indirectamente
  3. Todavía accesible
  4. Posiblemente perdido
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;
  1. Volver a inicializar los elementos de 0 a 3 antes de la condición.
  2. Inicializar el elemento 4, pero dejar el elemento 5 sin cambios.
  3. Inicializar los elementos 4 y 6 antes de la condición.
  4. Inicializar los elementos 4 y 5 antes de usar sus valores.
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

Una función auxiliar tiene un espacio de trabajo local fijo de 20 valores int, usado solo durante la llamada y sin necesidad de cambiar su tamaño. ¿Qué opción de almacenamiento evita un propietario y una ruta de liberación de recursos innecesarios?
  1. Un bloque del montículo cuya dirección se pierde.
  2. Un puntero devuelto a un array local.
  3. Un bloque del montículo detrás de un global sin propietario.
  4. Un array local ordinario de 20 valores int.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 2 de 10

En un sistema con punteros de 8 bytes y valores int de 4 bytes, el código usa int *p = malloc(11 * sizeof p);. ¿Cuántos bytes solicita y qué tiene de incorrecto la expresión?
  1. 44 bytes; mide un elemento en vez del puntero.
  2. 88 bytes; mide el puntero en vez de un elemento.
  3. 11 bytes; sizeof elimina por completo la multiplicación.
  4. 88 bytes; reserva once bloques con propietarios independientes.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 3 de 10

Después de una llamada exitosa a calloc con tamaño positivo para un búfer de caracteres, ¿qué cadena de C contiene inicialmente?
  1. Una cadena vacía
  2. Una cadena de espacios
  3. Una cadena de «0»
  4. Una cadena desconocida
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 4 de 10

Un bloque de seis valores int es propiedad de 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?
  1. Sí; la ampliación fallida conservó la selección y el tiempo de vida.
  2. Sí; reconstruye el índice 4 desde el puntero final de tres valores int.
  3. No; el puntero anterior es inválido y el índice 4 ya no cabe.
  4. No; la ampliación fallida ya había liberado el bloque de seis valores int.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 5 de 10

Un array tiene longitud 6 y capacidad 8. Se añaden dos valores con éxito. Después, el código guarda su puntero propietario en 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?
  1. Restaurar data = old, longitud 9 y capacidad 16.
  2. Restaurar data = old, longitud 8 y capacidad 8.
  3. Mantener data = NULL, longitud 8 y capacidad 8.
  4. Restaurar data = old, longitud 8 y capacidad 16.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 6 de 10

Una función conserva su única reserva en buffer. Ejecuta buffer = NULL; free(buffer); después de su último uso. ¿Qué ocurre con el bloque original?
  1. Se libera porque la variable contenía antes su dirección.
  2. Se libera y la asignación lo rellena con bytes nulos.
  3. Permanece reservado y se pierde su dirección.
  4. Permanece reservado y aún se puede acceder a él mediante buffer.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 7 de 10

Después de una reserva comprobada, una función libera su bloque mediante p y después llama a free(alias), donde alias se copió de p antes de la liberación. ¿Qué error es este?
  1. Una doble liberación mediante dos alias
  2. Una fuga de dos bloques activos separados
  3. Una liberación válida de dos punteros
  4. Una lectura sin inicializar de almacenamiento nuevo
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 8 de 10

Una impresión de diagnóstico en 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?
  1. La ausencia del texto demuestra que la impresión nunca se ejecutó.
  2. La impresión evitó necesariamente el propio error de memoria.
  3. La falta del salto de línea causó necesariamente el fallo de memoria.
  4. El texto todavía podía estar en el búfer de salida.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 9 de 10

Una persona ha compilado program-asan con AddressSanitizer y quiere una ejecución independiente con Memcheck. ¿Qué ejecutable debe recibir Memcheck?
  1. La misma versión con ASan y la detección de fugas desactivada.
  2. Una versión de depuración independiente sin AddressSanitizer.
  3. La misma versión con ASan y los punteros de marco omitidos.
  4. Un archivo fuente proporcionado directamente en vez de un ejecutable.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 10 de 10

ASan se detiene en 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?
  1. Comprobar la reserva, recorrer con i < n y liberar después del uso.
  2. Reservar n + 1 valores int, mantener el límite y omitir la liberación.
  3. Comprobar la reserva, mantener i <= n y liberar antes del bucle.
  4. Mantener el tamaño y el límite, poner el bloque a cero y liberar después del uso.
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

¿Qué duración del almacenamiento tiene un array global ordinario con ámbito de archivo?
  1. Duración estática del almacenamiento
  2. Duración automática del almacenamiento
  3. Duración asignada del almacenamiento
  4. Una nueva duración por llamada
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 2 de 10

Una biblioteca reservó cuatro contadores enteros con calloc y después amplió el bloque con éxito a siete contadores. ¿Qué debe ocurrir antes de incrementar por primera vez el contador 6?
  1. Incrementarlo directamente porque toda ampliación conserva el rellenado con ceros.
  2. Liberar y recrear los siete contadores para conservar los valores anteriores.
  3. Asignarle el valor inicial deseado antes de incrementarlo.
  4. Establecer su dirección en NULL antes de usar la reserva ampliada.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 3 de 10

El array exterior de una cuadrícula contiene punteros a filas de propiedad separada. Su creador ha creado las filas 0 y 1. Una función lectora toma prestada la fila 0 hasta que retorna. La creación de la fila 2 falla mientras esa función lectora está activa. Las entradas posteriores nunca se establecieron. ¿Qué liberación de recursos ante el fallo respeta todos los tiempos de vida?
  1. Liberar las filas 0 y 1, dejar que termine la función lectora y liberar el array exterior.
  2. Dejar que termine la función lectora, liberar el array exterior y después leer y liberar sus filas.
  3. Dejar que termine la función lectora, liberar todas las filas previstas y después el array exterior.
  4. Dejar que termine la función lectora, liberar las filas 0 y 1 y después el array exterior.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 4 de 10

Un bloque reservado para cuatro valores int es propiedad de 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?
  1. El primer bloque solicitado para siete valores int sigue reservado, pero se ha perdido.
  2. El bloque original de cuatro valores int sigue reservado, pero se ha perdido.
  3. El segundo bloque solicitado para nueve valores int sigue reservado, pero se ha perdido.
  4. No queda ningún bloque reservado porque fallaron ambas solicitudes de cambio de tamaño.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 5 de 10

La capacidad de un array dinámico se duplica a partir de 5 a medida que se llena. Comienza vacío con esa capacidad y recibe 18 valores. Si cada ampliación desplaza el bloque, ¿qué capacidad y qué número máximo total de copias de elementos resultan de las ampliaciones?
  1. Capacidad 20; como máximo 15 elementos copiados.
  2. Capacidad 20; como máximo 18 elementos copiados.
  3. Capacidad 18; como máximo 23 elementos copiados.
  4. Capacidad 40; como máximo 35 elementos copiados.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 6 de 10

Una función reserva un resultado, lo llena, lo libera y después devuelve el puntero a la función llamadora. La función llamadora necesita leer ese resultado. ¿Qué corrección de la propiedad es adecuada?
  1. Devolver el resultado liberado y hacer que la función llamadora compruebe que no es nulo.
  2. Poner el puntero a nulo antes de retornar y mantener la misma interfaz.
  3. Liberar el resultado dos veces para que no quede ninguna reserva obsoleta.
  4. Devolver el resultado activo y dejar que la función llamadora lo libere después.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 7 de 10

El código reserva 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?
  1. Reservar cinco posiciones para punteros y copiar mediante la entrada 0 tal como está.
  2. Asignar a la entrada 0 el literal «east» y después sobrescribirlo con strcpy.
  3. Reservar cinco caracteres para la entrada 0, comprobarla, copiar y liberarla después.
  4. Asignar NULL a la entrada 0 y después copiar cinco caracteres mediante ella.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 8 de 10

Un cambio de tamaño exitoso reduce un bloque de nueve valores int inicializados a cuatro. ¿Qué contenido está garantizado que se conserve?
  1. Los valores originales en los índices de 5 a 8.
  2. Los valores originales en los índices de 0 a 3.
  3. Los nueve valores, siempre que no se produzca otra reserva.
  4. Cuatro nuevos valores cero que sustituyen la parte inicial original.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 9 de 10

Una prueba presenta un desbordamiento de un elemento de un array local que permanece dentro de memoria de pila válida por lo demás. Una ejecución de Memcheck no lo detecta. ¿Qué comprobación posterior se ajusta mejor a estas pruebas?
  1. Repetir Memcheck mostrando las categorías de fugas.
  2. Volver a compilar con ASan y ejecutar la misma entrada.
  3. Considerar válido el acceso tras una ejecución sin errores.
  4. Sustituirlo por un bloque mayor del montículo sin comprobarlo.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 10 de 10

Un comprobador informa de una doble liberación. Eliminar ambas llamadas de liberación hace que la ejecución deje de cerrarse inesperadamente, pero deja una reserva sin liberar. ¿Qué corrección debería sustituir ese cambio?
  1. No mantener ninguna liberación y confiar en el sistema operativo al terminar.
  2. Mantener ambas liberaciones, pero poner a nulo un alias prestado.
  3. Mover ambas liberaciones antes del último uso para acortar el tiempo de vida.
  4. Mantener una liberación después del último uso bajo un único propietario.
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

Una función devuelve la dirección de un array local de caracteres. La función llamadora a veces imprime el texto esperado y otras veces se cierra inesperadamente. ¿Qué explica ambos resultados?
  1. El array local se volvió estático porque su dirección salió de la función.
  2. El retorno transfirió la propiedad de una reserva válida del montículo.
  3. El texto esperado demuestra que el array seguía existiendo en esa ejecución.
  4. El array dejó de existir al retornar; el acceso de la función llamadora es inválido.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 2 de 10

Para un double *samples ordinario, ¿qué mide sizeof samples?
  1. El tamaño de un valor double al que apunta.
  2. El número de bytes de toda la reserva.
  3. El tamaño de la variable de tipo puntero.
  4. El número de elementos inicializados del array.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 3 de 10

Una función devuelve una cadena recién reservada. La función llamadora la pasa a una función de impresión que solo la toma prestada; esta la libera por error. Después, la función llamadora la lee y la libera. ¿Qué cambio de propiedad corrige toda la ruta?
  1. Eliminar la liberación de la función llamadora y mantener su lectura después de la impresión.
  2. Eliminar la liberación de la función de impresión y mantener la liberación final de la llamadora.
  3. Mantener ambas liberaciones y poner a nulo solo el puntero local de la función de impresión.
  4. Mover la liberación de la función llamadora antes de la impresión para terminar antes la propiedad.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 4 de 10

Memcheck informa de que un salto condicional depende de un valor sin inicializar. ¿Qué opción ayuda a localizar dónde se originó ese valor?
  1. --track-origins=yes
  2. --leak-check=no
  3. --show-leak-kinds=all
  4. --error-limit=no
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 5 de 10

Un búfer de texto comienza con capacidad 6, longitud 4 y un terminador en el índice 4. Añade 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?
  1. Longitud 4, capacidad 6; se descartan tanto X como Y.
  2. Longitud 5, capacidad 6; se conserva X y el terminador está en 5.
  3. Longitud 6, capacidad 6; Y sustituye al terminador en 5.
  4. Longitud 5, capacidad 12; se conserva X y el terminador está en 5.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 6 de 10

Un servicio procesa tres trabajos. Cada trabajo reserva correctamente 24 bytes en la variable local 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?
  1. 192 bytes; liberar ambas variables de tipo puntero después de que retornen.
  2. 72 bytes; dejar de liberar a en la ruta de error.
  3. 40 bytes; liberar solo b del último trabajo después del bucle.
  4. 120 bytes; liberar cada bloque de b antes de poner su puntero a nulo.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 7 de 10

Un array tiene un puntero prestado al elemento 2. El código amplía el array con éxito mediante un puntero temporal y actualiza el puntero propietario, y después escribe mediante el puntero prestado anterior. La dirección no cambió. ¿Qué regla hace que este acceso siga siendo inválido?
  1. Todo cambio de tamaño descarta el contenido de la parte inicial original.
  2. El puntero propietario y el prestado deben diferir en la dirección.
  3. El cambio de tamaño exitoso terminó el tiempo de vida del objeto anterior.
  4. La ampliación solo conserva los valores a los que se llega mediante el índice cero.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 8 de 10

Los valores int ocupan 4 bytes y los punteros 8 bytes. El código usa 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?
  1. Sus últimas seis lecturas están dentro de los límites, pero no están inicializadas.
  2. Sus últimas seis lecturas están fuera de una reserva de 24 bytes.
  3. Las doce lecturas son válidas porque la reserva las inicializa.
  4. Solo su última lectura cruza el final de la reserva excesiva.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 9 de 10

Un programa se detiene con un fallo de segmentación en una línea que lee p[index]. ¿Qué debe comprobarse sobre ese acceso?
  1. El origen del puntero, el tiempo de vida del objeto y el rango del índice.
  2. Solo el índice, porque cualquier puntero no nulo apunta a un objeto activo.
  3. Solo la línea de liberación, porque todos los fallos son dobles liberaciones.
  4. Solo el almacenamiento de salida en búfer, porque una lectura de array no puede fallar.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

Pregunta 10 de 10

El informe de una herramienta muestra una lectura en 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?
  1. Aumentar el tamaño en load y mantener el orden de las llamadas.
  2. Poner a nulo solo el puntero local de reset y mantener la lectura posterior.
  3. Mantener activo el resultado durante display y después liberarlo.
  4. Ignorar reset porque el error indicado ocurrió en display.
Mostrar respuestaOcultar respuesta

Guarda esta unidad para ver esta respuesta.

¿Te atascas con una pregunta tuya? Ayuda con Tareas

Términos Clave

63 Términos Clave, cada uno con su definición

A

AddressSanitizer
AddressSanitizer es instrumentación añadida por el compilador para detectar accesos y liberaciones de memoria inválidos mientras se ejecuta un programa instrumentado.
Una versión compilada con ASan puede identificar un acceso fuera de los límites de un array local que permanece dentro de memoria de pila válida por lo demás.
Alias
Un alias es otro puntero al mismo objeto, que comparte el tiempo de vida de ese objeto sin actualizarse cuando cambia otra variable de tipo puntero.
Poner el puntero propietario a NULL después de la liberación deja inutilizables para lecturas posteriores los alias guardados.
Almacenamiento automático
El almacenamiento automático corresponde a los objetos locales ordinarios y a los parámetros cuyo tiempo de vida termina cuando la ejecución sale de su bloque.
El array local de una función auxiliar deja de existir cuando esta retorna, aunque la función llamadora haya guardado su dirección.
Almacenamiento de salida en búfer
El almacenamiento de salida en búfer conserva temporalmente el texto impreso en memoria antes de escribirlo, por lo que un cierre inesperado puede impedir que aparezcan mensajes de progreso recientes.
Llamar a 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
El almacenamiento estático dura toda la ejecución del programa y lo usan los objetos globales y las variables locales declaradas con static.
Un contador estático dentro de una función conserva su valor actualizado cuando comienza la siguiente llamada.
Array de longitud variable
Un array de longitud variable es un array cuyo tamaño se establece durante la ejecución y que tiene un tiempo de vida automático; habitualmente ocupa almacenamiento de la pila.
Declarar 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
Un array dinámico es almacenamiento reservado para elementos que se controla mediante una longitud y una capacidad, de modo que pueda ampliarse cuando lleguen más valores.
Un array dinámico puede contener diez valores en dieciséis espacios, lo que deja capacidad libre para inserciones posteriores.

B

Bloque de memoria reservado perdido
Un bloque de memoria reservado perdido es un bloque que sigue reservado pero cuya dirección utilizable ya no se conserva, lo que impide al programa liberarlo normalmente.
Poner el único puntero propietario a NULL sin llamar primero a free hace que se pierda la dirección de la reserva.
Búfer estático compartido
Un búfer estático compartido es almacenamiento persistente de una función para sus resultados, reutilizado por las llamadas, de modo que una llamada posterior puede sobrescribir un resultado anterior.
Dos punteros devueltos por llamadas sucesivas pueden referirse al mismo array estático en vez de a textos independientes.
Búfer proporcionado por la función llamadora
Un búfer proporcionado por la función llamadora es almacenamiento suministrado a una función para su salida, lo que permite a la función llamadora conservar el control de su tamaño y tiempo de vida.
Una función llamadora puede pasar su propio array de 16 bytes a una función auxiliar que lo rellena con un saludo breve.

C

calloc
La función 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.
Los contadores enteros de una solicitud comprobada a calloc comienzan en cero antes del primer incremento.
Capacidad
La capacidad de un array es el número de espacios para elementos reservados en su bloque, incluidos los espacios sin usar que todavía no pertenecen a su contenido lógico.
Una solicitud de ampliación fallida debe dejar la capacidad sin cambios porque no se obtuvieron espacios adicionales.
Comportamiento indefinido
El comportamiento indefinido ocurre cuando C no impone requisitos sobre el resultado del programa tras una infracción de las reglas, lo que permite cierres inesperados o corrupción silenciosa.
Una escritura fuera de los límites puede parecer inofensiva en una prueba y corromper otro valor con una entrada diferente.
Crecimiento geométrico
El crecimiento geométrico aumenta la capacidad mediante un factor multiplicativo, como la duplicación, para limitar las copias repetidas durante muchas inserciones.
Un array lleno de capacidad cuatro crece a ocho y después a dieciséis cuando alcanza el siguiente límite de crecimiento.

D

Definitivamente perdido
Una reserva está definitivamente perdida en Memcheck cuando no queda ningún puntero, por lo que su almacenamiento reservado no puede liberarse mediante ninguna dirección guardada.
Una función auxiliar que abandona su único puntero propietario puede dejar un bloque definitivamente perdido en el informe al terminar.
Desbordamiento de búfer en el montículo
Un desbordamiento de búfer en el montículo lee o escribe antes del inicio o después del final de un bloque reservado, aunque la dirección circundante sea accesible.
Un bucle que llega al índice diez en una reserva de diez elementos cruza el rango válido de elementos del bloque.
Desbordamiento de pila
Un desbordamiento de pila agota el espacio disponible para llamadas activas y objetos automáticos, a menudo debido a una recursión profunda o a un array local enorme.
Una función recursiva sin optimizar y sin condición de parada puede seguir añadiendo marcos hasta que falla.
Desbordamiento en el cálculo del tamaño
Un desbordamiento en el cálculo del tamaño ocurre cuando un cálculo que convierte una cantidad en bytes supera el rango de size_t y se reduce modularmente a un valor representable menor.
Compara el número de elementos con SIZE_MAX / sizeof *p antes de multiplicarlo por el tamaño del elemento.
Doble liberación
Una doble liberación intenta liberar un bloque cuyo tiempo de vida ya terminó mediante una liberación anterior, también si se hace mediante otro alias.
Un propietario y quien toma prestado un bloque no pueden llamar ambos a free sobre lo que era una única reserva activa compartida.
Duración del almacenamiento
La duración del almacenamiento de un objeto determina durante cuánto tiempo se reserva su almacenamiento, incluido si termina con un bloque, con la liberación o al terminar el programa.
Un puntero local y su array reservado pueden tener distintas duraciones del almacenamiento aunque compartan una dirección.

E

Ejecución de verificación
Una ejecución de verificación ejecuta el programa corregido con un comprobador para confirmar la ruta probada y va seguida de entradas que ejercitan otras rutas.
Después de ejecutar sin errores el caso que reproducía el fallo, prueba la entrada vacía, los límites de crecimiento y las salidas anticipadas para detectar liberaciones de recursos omitidas.
Entrada que reproduce el fallo
Una entrada que reproduce el fallo es un caso de prueba que alcanza de forma fiable el problema observado y proporciona un punto de partida concreto para la depuración y la verificación.
Reduce una entrada que falla conservando su error para que el primer informe relevante sea más fácil de interpretar.
Error de una unidad
Un error de una unidad hace que un límite sea una posición demasiado grande o pequeña, a menudo al incluir el número de elementos como índice final inválido.
Cambiar i <= n por i < n hace que un recorrido que comienza en cero visite exactamente los n elementos previstos.

F

Fallo de segmentación
Un fallo de segmentación es un fallo del sistema operativo causado por un acceso prohibido a memoria, como una desreferenciación nula habitual o una escritura en memoria de solo lectura.
Una escritura incorrecta puede corromper datos primero y causar un fallo de segmentación más tarde en otra función.
free
La función 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.
La función llamadora libera una copia de cadena devuelta solo después de que todos quienes la toman prestada hayan terminado de leerla.
Fuga de memoria
Una fuga de memoria es una reserva que ya no se necesita pero permanece sin liberar, a menudo porque el programa perdió su último puntero a ella.
Sobrescribir el único puntero propietario con una nueva reserva puede abandonar permanentemente el bloque anterior.

I

Inicialización
La inicialización establece un valor antes de que se lea, mediante una asignación inicial o una reserva rellenada con ceros adecuada.
Un contador debe tener un valor inicial definido porque incrementarlo primero lee su valor anterior.
Instrumentación
La instrumentación añade código de comprobación durante la compilación para que el programa resultante pueda detectar determinados errores mientras se ejecutan las rutas probadas.
Añadir -fsanitize=address durante la compilación y el enlazado crea un ejecutable instrumentado con ASan.

L

LeakSanitizer
LeakSanitizer comprueba si hay fugas de reservas al terminar y puede acompañar a ASan; su disponibilidad y configuración predeterminada dependen de la plataforma.
Las versiones de ASan en Linux normalmente activan las comprobaciones de fugas; algunas cadenas de herramientas de macOS no pueden proporcionarlas.
Lectura de memoria sin inicializar
Una lectura de memoria sin inicializar consume un valor que nunca se estableció, incluido un elemento dentro de los límites de almacenamiento recién reservado o ampliado.
Sumar tres valores int reservados después de asignar solo dos sigue leyendo un tercer valor no establecido.
Literal de cadena
Un literal de cadena es texto escrito entre comillas en el código fuente de C, con almacenamiento estático que no debe modificarse mediante un puntero.
Usa char word[] = "hello"; cuando necesites una copia local modificable en vez de un puntero al literal.
Longitud
La longitud de un array es el número de elementos inicializados que están actualmente en uso, que determina el rango para los cálculos sobre sus valores almacenados.
Después de añadir un quinto valor en el almacenamiento disponible, la longitud pasa a ser cinco aunque la capacidad siga siendo ocho.

M

malloc
La función 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.
Un array de enteros obtenido con malloc necesita asignaciones antes de que sus elementos se sumen o comparen.
Memcheck
Memcheck es el comprobador de memoria predeterminado de Valgrind, que sigue los accesos inválidos, los tiempos de vida de las reservas, los valores no establecidos y los bloques sin liberar durante una ejecución.
Ejecuta un ejecutable de depuración ordinario con Memcheck para obtener líneas del código fuente junto con sus historiales de memoria.
Memoria rellenada con ceros
La memoria rellenada con ceros tiene todos sus bits establecidos en cero, lo que da el cero entero, pero no garantiza de forma portable el cero de coma flotante ni los punteros nulos.
Un búfer de caracteres de tamaño positivo obtenido con calloc comienza con un carácter nulo y contiene una cadena vacía.
MemorySanitizer
MemorySanitizer es el comprobador de Clang para el uso de valores sin inicializar en sistemas compatibles y aborda un tipo de error que el ASan ordinario no sigue.
Úsalo en una compilación compatible independiente en vez de combinar su instrumentación con AddressSanitizer.
Montículo
El montículo es el conjunto de memoria del que el asignador proporciona bloques cuyos tiempos de vida se controlan mediante la reserva y la liberación.
Devolver la dirección de un array reservado deja el bloque en el montículo para que la función llamadora lo use y lo libere.

P

Perdido indirectamente
Una reserva está perdida indirectamente cuando solo se puede acceder a ella a través de otro bloque que también se ha perdido, según informa Memcheck.
Perder un array exterior de direcciones de filas propias también puede dejar las reservas de las filas perdidas indirectamente.
Pila de llamadas
La pila de llamadas es la región que se usa habitualmente para llevar el control de las llamadas a funciones activas, sus objetos locales, parámetros e información de retorno.
Una función recursiva sin optimizar puede seguir añadiendo marcos hasta agotar el espacio disponible de la pila.
Posiblemente perdido
Una reserva está posiblemente perdida en Memcheck cuando solo se encontró un puntero interior, lo que deja incertidumbre sobre la dirección inicial de la reserva.
Examina si un cursor que avanza sustituyó al puntero propietario en vez de conservar un puntero inicial independiente.
Primer error detectado
El primer error detectado es el hallazgo más temprano del comprobador en una ejecución y suele ser el mejor punto de partida porque los fallos posteriores pueden derivarse de su daño.
Corrige el desbordamiento inicial antes de investigar cada fallo posterior del asignador en el mismo informe.
Propiedad de la memoria
La propiedad de la memoria es la responsabilidad de liberar un bloque reservado después de su último uso o de transferir deliberadamente esa obligación a otra parte del programa.
Una función que produce una copia transfiere la propiedad a la función llamadora cuando devuelve un resultado recién reservado.
Puntero colgante
Un puntero colgante se refiere a un objeto cuyo tiempo de vida ha terminado, por lo que ya no queda un objeto válido al que acceder mediante ese puntero.
Un puntero al antiguo array local de una función auxiliar queda colgante inmediatamente después de que esta retorne.
Puntero interior
Un puntero interior apunta a una posición dentro de un objeto, como un elemento de un array, en vez de a la dirección inicial de su reserva.
Guarda el índice de un elemento durante un cambio de tamaño exitoso y reconstruye el puntero interior a partir del resultado.
Puntero nulo
Un puntero nulo es un valor de puntero que no designa ningún objeto y que las funciones de reserva devuelven cuando no pueden proporcionar un bloque solicitado de tamaño distinto de cero.
Una comprobación del puntero nulo dirige la ejecución a una ruta de error antes de que el código intente acceder a una reserva fallida.
Puntero prestado
Un puntero prestado da acceso a un objeto que sigue existiendo mientras otra parte del programa conserva la responsabilidad de decidir cuándo liberarlo.
Una función de impresión puede tomar prestada una cadena durante su llamada sin obtener permiso para liberar el bloque de la función llamadora.
Puntero temporal
Un puntero temporal recibe el resultado de un cambio de tamaño antes de actualizar el puntero propietario, conservando la dirección de la reserva anterior si la solicitud falla.
Con tmp = realloc(p, bytes), un resultado nulo deja p disponible para seguir usándolo o liberar los recursos.
Punto de reserva de memoria
Un punto de reserva de memoria es el lugar del programa que solicita un bloque y que aparece en los informes de las herramientas para establecer dónde comenzó ese almacenamiento.
El punto de reserva de un informe de fuga puede estar dentro de una función auxiliar, mientras que la liberación correcta corresponde a la función llamadora.

R

realloc
La función realloc sustituye un bloque reservado por un objeto del tamaño solicitado, conserva la parte inicial que cabe y puede cambiar su dirección.
Ampliar un bloque de cuatro valores int a seis conserva los cuatro primeros valores, pero deja sin establecer dos elementos nuevos.
Reserva de memoria
Una reserva de memoria es una solicitud de un bloque de memoria reservado; una solicitud exitosa proporciona su dirección inicial para usarlo y liberarlo después.
Un programa comprueba si su reserva tuvo éxito antes de almacenar el primer elemento del array solicitado.
Reserva de memoria anidada
Una reserva de memoria anidada es un diseño con bloques interiores reservados por separado cuyas direcciones se guardan en un contenedor exterior reservado.
Una cuadrícula con filas separadas necesita liberar las filas antes de liberar el array exterior de punteros.
Resumen del montículo
Un resumen del montículo informa de la actividad de reservas y del almacenamiento del montículo restante al terminar, lo que ayuda a investigar los bloques que no se liberaron.
Lee los totales de bloques restantes junto con las categorías de fugas en vez de considerar perdido cada bloque.
Reutilización de búferes
La reutilización de búferes conserva una reserva adecuada durante operaciones repetidas y sustituye su contenido antes de liberar finalmente el bloque.
Un bucle puede reutilizar un espacio de trabajo de 100 bytes en vez de reservar y abandonar un bloque en cada pasada.
Ruta de liberación de recursos
Una ruta de liberación de recursos es código que libera los recursos obtenidos antes de que termine una función, también cuando el trabajo se detiene después de un éxito parcial.
Si falla una segunda reserva, la ruta de error libera el primer bloque antes de retornar con un fallo.

S

Seguimiento del origen
El seguimiento del origen rastrea un valor sin inicializar hasta el almacenamiento que lo proporcionó, lo que ayuda a localizar la asignación de valor que falta.
--track-origins=yes de Memcheck puede explicar por qué una condición posterior depende de datos del montículo no establecidos.
size_t
El tipo 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.
Un array dinámico usa variables size_t para su longitud y capacidad, de modo que puedan describir tamaños de objetos.
sizeof
El operador 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.
La solicitud n * sizeof *p se adapta al tipo de elemento, mientras que sizeof p no puede recuperar la longitud del array.

T

Terminador nulo
Un terminador nulo es el carácter de valor cero que marca el final de una cadena de C y ocupa un byte después de sus caracteres visibles.
La cadena de cuatro caracteres «west» necesita cinco bytes cuando se copia junto con su terminador.
Tiempo de vida de un objeto
El tiempo de vida de un objeto es el periodo durante el cual existe en almacenamiento reservado y se puede acceder a él mediante punteros válidos.
Una llamada exitosa a realloc termina el tiempo de vida del objeto anterior aunque la dirección devuelta parezca no haber cambiado.
Todavía accesible
Una reserva está todavía accesible en Valgrind cuando queda un puntero al terminar, aunque el programa no haya liberado el bloque.
Un puntero global que conserva la dirección inicial de una reserva sin liberar hace que esta sea accesible para la comprobación al terminar.
Traza de la pila de llamadas
Una traza de la pila de llamadas enumera las llamadas a funciones activas en un cierre inesperado o error, lo que ayuda a localizar el acceso relevante y la ruta que llegó a él.
Empieza por el primer marco relevante de tu propio código y después examina cómo obtuvo su valor el puntero.

U

UndefinedBehaviorSanitizer
UndefinedBehaviorSanitizer añade comprobaciones para determinados comportamientos indefinidos ajenos a las direcciones, como el desbordamiento de enteros con signo, y puede acompañar a ASan.
Una versión de desarrollo puede usar -fsanitize=address,undefined para activar los dos conjuntos compatibles de comprobaciones.
Uso después de la liberación
Un uso después de la liberación accede a un objeto reservado después de liberarlo, lo que puede devolver datos aparentemente antiguos, corromper memoria o provocar un cierre inesperado.
Imprimir una cadena liberada mediante un alias guardado es inválido aunque aparezcan las palabras esperadas.

Sobre esta unidad

Por StudeliInformar de un error

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.

Comenzar