← Programación en C para Sistemas Operativos Bloque 7 · Memoria dinámica · Tema 28 de 42

Tema 28

malloc, calloc, realloc y free

Llevamos veintisiete temas con una promesa pendiente: la memoria que el programa pide mientras corre. Las variables locales desaparecen al volver de la función, las globales tienen un tamaño fijo desde que se compila, y muchas veces ninguna de las dos sirve: el tamaño se conoce recién al leer la entrada, o el dato tiene que sobrevivir a la función que lo creó. Para eso existe la memoria dinámica. Se pide con malloc y compañía, se usa como cualquier arreglo, y se devuelve con free, y de por medio el programa asume todas las responsabilidades que hasta ahora llevaba el compilador. Este tema enseña las cuatro funciones, qué hacer cuando fallan, cómo crecer un arreglo sin perder lo que había, quién debe liberar cada bloque, y qué hay por dentro de uno. Los errores que se cometen con ellas son el tema siguiente.

1. Para qué sirve la memoria dinámica

Hasta aquí vimos tres duraciones para los datos, en el tema 6: la automática, de las variables locales, que vive lo que dura la llamada; la estática, de las globales, que vive todo el programa; y una tercera que quedó anunciada, la dinámica, que vive desde que el programa la pide hasta que la devuelve. Se necesita en tres situaciones:

La memoria dinámica sale del montículo, la región [heap] y las regiones de mmap que vimos en el tema 27. Las funciones de este tema, declaradas en stdlib.h, son la interfaz con el administrador de memoria de la biblioteca de C, que a su vez pide más al núcleo con brk y mmap cuando lo necesita. El ciclo siempre tiene tres pasos: pedir un bloque, usarlo con la dirección que se recibe, y devolverlo. A diferencia de las variables locales, nadie hace los dos últimos pasos por el programador. Olvidar devolver un bloque es una fuga; devolverlo dos veces o usarlo después de devolverlo es un error grave; y ninguna de las dos cosas la detecta el compilador.

2. malloc y free

void *malloc(size_t n) reserva un bloque de n bytes y devuelve su dirección, o NULL si no pudo. void free(void *p) lo devuelve. El bloque tiene el contenido que haya tenido la memoria antes: malloc no lo inicializa. Este programa reserva un arreglo de enteros del tamaño que se le indique:

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

int main(int argc, char *argv[])
{
    if (argc != 2) {
        fprintf(stderr, "uso: %s cantidad\n", argv[0]);
        return 2;
    }
    size_t n = strtoul(argv[1], NULL, 10);
    int *v = malloc(n * sizeof *v);       /* n enteros: sizeof *v sigue al tipo de v */

    if (v == NULL) {
        fprintf(stderr, "%s: sin memoria para %zu enteros\n", argv[0], n);
        return 1;
    }
    long suma = 0;
    for (size_t i = 0; i < n; i++) {
        v[i] = (int) (i * i);
        suma += v[i];
    }
    printf("%zu enteros en %p, suma = %ld\n", n, (void *) v, suma);
    free(v);
    return 0;
}
terminal
$ gcc -std=c17 -Wall -Wextra -g -o basico basico.c
$ ./basico 10
10 enteros en 0x56e42a8fe010, suma = 285
$ ./basico 1000000
1000000 enteros en 0x76a326c2f010, suma = 16866920716384
$ ./basico 99999999999999; echo $?
./basico: sin memoria para 99999999999999 enteros
1

Varios detalles de las pocas líneas que importan:

3. Comprobar cada pedido

Un pedido puede fallar, y entonces malloc devuelve NULL y deja errno en ENOMEM. Desreferenciar ese NULL termina el programa con Segmentation fault, como vimos en el tema 27. Por eso todo pedido se comprueba, siempre, aunque «nunca falle». Este programa intenta siete tamaños, desde 1 MiB hasta el máximo posible:

pedidos.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <stdint.h>

static void probar(const char *que, size_t n)
{
    errno = 0;
    void *p = malloc(n);

    if (p != NULL)
        printf("%-22s -> %p\n", que, p);
    else
        printf("%-22s -> NULL (errno = %d: %s)\n", que, errno, strerror(errno));
    free(p);
}

int main(void)
{
    probar("malloc(1 MiB)", (size_t) 1 << 20);
    probar("malloc(1 GiB)", (size_t) 1 << 30);
    probar("malloc(64 GiB)", (size_t) 64 << 30);
    probar("malloc(1 TiB)", (size_t) 1 << 40);
    probar("malloc(1 << 62)", (size_t) 1 << 62);
    probar("malloc(SIZE_MAX)", SIZE_MAX);
    probar("malloc(0)", 0);
    return 0;
}
terminal · la máquina tiene 16 GB de memoria y 4 GB de intercambio
$ ./pedidos
malloc(1 MiB)          -> 0x7032550ff010
malloc(1 GiB)          -> 0x7032151ff010
malloc(64 GiB)         -> NULL (errno = 12: Cannot allocate memory)
malloc(1 TiB)          -> NULL (errno = 12: Cannot allocate memory)
malloc(1 << 62)        -> NULL (errno = 12: Cannot allocate memory)
malloc(SIZE_MAX)       -> NULL (errno = 12: Cannot allocate memory)
malloc(0)              -> 0x5eb7449b2020
$ (ulimit -v 200000; ./pedidos)
malloc(1 MiB)          -> 0x7b38b6e66010
malloc(1 GiB)          -> NULL (errno = 12: Cannot allocate memory)
malloc(64 GiB)         -> NULL (errno = 12: Cannot allocate memory)
malloc(1 TiB)          -> NULL (errno = 12: Cannot allocate memory)
malloc(1 << 62)        -> NULL (errno = 12: Cannot allocate memory)
malloc(SIZE_MAX)       -> NULL (errno = 12: Cannot allocate memory)
malloc(0)              -> 0x5bfc5f429040

El resultado enseña cómo falla de verdad la memoria en Linux:

¿Qué se hace cuando el pedido falla? Depende del programa. Una biblioteca no decide por quien la usa: libera lo que haya reservado hasta ese punto y devuelve un error (un código o un puntero nulo), como hace contador_crear en la sección 9. Una herramienta de línea de órdenes chica, en cambio, no tiene cómo continuar sin memoria, y es habitual centralizar la decisión en una función propia que avisa y termina:

un envoltorio habitual para programas chicos
static void *reservar(size_t n)
{
    void *p = malloc(n);

    if (p == NULL && n != 0) {
        fprintf(stderr, "sin memoria al pedir %zu bytes\n", n);
        exit(EXIT_FAILURE);
    }
    return p;
}

El programa pierde la oportunidad de limpiar, pero cada llamada queda sin comprobación propia, y el mensaje sale siempre. Lo que no se admite es la tercera opción, ignorar el resultado: además de terminar el programa, desreferenciar un puntero nulo con un desplazamiento grande, como en p[1000000], puede alcanzar memoria que sí está mapeada y escribir donde no debe.

4. calloc y la multiplicación peligrosa

void *calloc(size_t n, size_t tam) reserva espacio para n elementos de tam bytes cada uno, y lo pone todo en cero. Se diferencia de malloc(n * tam) en dos cosas. La primera es evidente: el contenido inicial es conocido. La segunda no lo parece, y es la razón principal para preferirla cuando n o tam vienen de afuera: calloc comprueba que la multiplicación no se desborde.

mult.c
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <errno.h>
#include <string.h>

struct registro {
    long a, b;                            /* 16 bytes */
};

int main(int argc, char *argv[])
{
    if (argc != 2) {
        fprintf(stderr, "uso: %s cantidad\n", argv[0]);
        return 2;
    }
    size_t n = strtoull(argv[1], NULL, 10);   /* una cantidad que llega de afuera */

    printf("n = %zu registros de %zu bytes\n", n, sizeof(struct registro));
    printf("n * sizeof(struct registro) = %zu\n", n * sizeof(struct registro));

    void *a = malloc(n * sizeof(struct registro));
    printf("malloc(n * tamaño) -> %s\n", a != NULL ? "un bloque" : "NULL");

    errno = 0;
    void *b = calloc(n, sizeof(struct registro));
    printf("calloc(n, tamaño)  -> %s (errno = %d: %s)\n",
           b != NULL ? "un bloque" : "NULL", errno, strerror(errno));
    free(a);
    free(b);
    return 0;
}
terminal
$ ./mult 1000
n = 1000 registros de 16 bytes
n * sizeof(struct registro) = 16000
malloc(n * tamaño) -> un bloque
calloc(n, tamaño)  -> un bloque (errno = 0: Success)
$ ./mult 1152921504606846977
n = 1152921504606846977 registros de 16 bytes
n * sizeof(struct registro) = 16
malloc(n * tamaño) -> un bloque
calloc(n, tamaño)  -> NULL (errno = 12: Cannot allocate memory)

Con una cantidad razonable los dos pedidos tienen éxito. Con n = 2⁶⁰ + 1, el producto con 16 se da la vuelta en la aritmética sin signo del tema 7 y vale 16: malloc recibe un pedido de 16 bytes, lo cumple sin quejarse, y devuelve un bloque con lugar para un registro, mientras el programa cree que tiene más de mil billones. Un programa que luego escriba n registros, o los primeros millones, escribe fuera del bloque. Este es un desbordamiento de entero que termina en un desbordamiento de memoria, un error clásico de seguridad: la cantidad de elementos llega de un archivo o de la red, y quien la envía elige un valor que haga dar la vuelta a la cuenta. calloc hace la multiplicación con comprobación, y ante el desbordamiento devuelve NULL con ENOMEM. Cuando los valores son constantes, gcc avisa del problema al compilar: product ‘1152921504606846977 * 16’ of arguments 1 and 2 exceeds ‘SIZE_MAX’ [-Walloc-size-larger-than=].

Hay una función para el caso en que no se quiere el relleno con ceros, reallocarray (de stdlib.h en glibc, con _DEFAULT_SOURCE): hace realloc con la misma comprobación. reallocarray(NULL, 10, sizeof *v) equivale a malloc de 10 elementos sin riesgo de desborde, y con una cantidad absurda devuelve NULL con ENOMEM y deja intacto el bloque original, lo que verificamos en una prueba con SIZE_MAX / 2 elementos de 4 bytes.

Queda el costo del relleno. Poner en cero 64 MiB parece caro, pero calloc es más inteligente que malloc más memset:

perezoso.c
/* campo() e informe() son los de bss.c del tema 27 */
int main(void)
{
    informe("al empezar");

    char *a = calloc(1, TAM);
    informe("calloc(1, 64 MiB)");
    printf("  a[0] = %d, a[TAM - 1] = %d  (ceros, y casi sin gastar memoria)\n", a[0], a[TAM - 1]);

    char *b = malloc(TAM);
    informe("malloc(64 MiB)");
    memset(b, 0, TAM);
    informe("... y memset(b, 0, 64 MiB)");

    free(a);
    free(b);
    informe("después de los free");
    return 0;
}
terminal
$ ./perezoso
al empezar                         VmSize    2764 KiB  VmRSS    1408 KiB  fallos     82
calloc(1, 64 MiB)                  VmSize   68304 KiB  VmRSS    1664 KiB  fallos     94
  a[0] = 0, a[TAM - 1] = 0  (ceros, y casi sin gastar memoria)
malloc(64 MiB)                     VmSize  133844 KiB  VmRSS    1664 KiB  fallos     96
... y memset(b, 0, 64 MiB)         VmSize  133844 KiB  VmRSS   67200 KiB  fallos  16480
después de los free                VmSize    2764 KiB  VmRSS    1764 KiB  fallos  16480

El calloc de 64 MiB aumentó VmSize en 64 MiB y VmRSS en 256 KiB: casi nada. Para un bloque tan grande, malloc usa mmap, y la memoria que entrega el núcleo ya viene en cero, así que calloc sabe que no necesita escribir nada, y las páginas se asignan recién al tocarlas. En cambio el memset escribe en las 16 384 páginas y las vuelve todas reales: 16 384 fallos de página y 64 MiB más de VmRSS. Es la respuesta a la práctica 3 del tema 27. Y la última línea muestra que al hacer free de los bloques grandes el espacio de direcciones vuelve al tamaño del principio, porque malloc los devuelve al núcleo con munmap.

5. realloc: crecer sin perder lo que había

void *realloc(void *p, size_t n) cambia el tamaño de un bloque que se reservó antes: devuelve un bloque de n bytes con el mismo contenido que p, hasta el menor de los dos tamaños. Puede hacerlo de dos maneras: extender el bloque donde está, si hay lugar a continuación, o reservar otro bloque, copiar y liberar el viejo. El programa no sabe cuál ocurrió, así que debe usar siempre el puntero que devuelve, y nunca el anterior. Las reglas son cuatro:

La segunda regla tiene una consecuencia para la forma de escribir la llamada. Este programa agranda un bloque hasta que realloc falla, con el espacio de direcciones limitado a unos 585 MiB para forzarlo, y lo hace de dos maneras:

fallo_realloc.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char *argv[])
{
    if (argc != 2) {
        fprintf(stderr, "uso: %s mal|bien\n", argv[0]);
        return 2;
    }
    int bien = strcmp(argv[1], "bien") == 0;
    size_t tam = 1 << 20;
    char *p = malloc(tam);

    if (p == NULL)
        return 1;
    strcpy(p, "datos importantes");

    for (;;) {
        size_t nuevo = tam * 2;

        if (bien) {
            char *tmp = realloc(p, nuevo);        /* el puntero viejo sigue en p */
            if (tmp == NULL)
                break;
            p = tmp;
        } else {
            p = realloc(p, nuevo);                /* si falla, p vale NULL */
            if (p == NULL)
                break;
        }
        tam = nuevo;
    }
    printf("realloc falló al pasar de %zu MiB a %zu MiB\n", tam >> 20, (tam * 2) >> 20);
    if (p != NULL)
        printf("p sigue siendo válido: \"%s\"\n", p);
    else
        printf("p vale NULL: se perdió el bloque de %zu MiB, que nadie puede liberar\n", tam >> 20);
    free(p);
    return 0;
}
terminal
$ (ulimit -v 600000; ./fallo_realloc mal)
realloc falló al pasar de 512 MiB a 1024 MiB
p vale NULL: se perdió el bloque de 512 MiB, que nadie puede liberar
$ (ulimit -v 600000; ./fallo_realloc bien)
realloc falló al pasar de 512 MiB a 1024 MiB
p sigue siendo válido: "datos importantes"

La forma p = realloc(p, nuevo) es la más escrita y la más peligrosa. Cuando falla, asigna NULL a p y destruye la única copia de la dirección del bloque viejo, que sigue reservado y que nadie puede ya usar ni liberar: es una fuga de 512 MiB, y además se pierde el contenido, los «datos importantes». La forma segura usa un puntero temporal: tmp = realloc(p, nuevo);, se comprueba, y solo si salió bien se hace p = tmp;. Si falla, p conserva el bloque y el programa puede guardar sus datos, liberar o seguir con lo que tiene. Es una de las reglas que se aprenden una vez y se aplican siempre.

La segunda cuestión es cuánto crecer. Este programa agrega 200 000 enteros de a uno a un arreglo, y compara cuatro estrategias, con y sin otros pedidos de memoria intercalados que impidan extender el bloque en el lugar:

crecer.c
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

enum { N = 200000 };

static void *vecinos[N];                  /* bloques que otros pedirían en el medio */

static double segundos(void)
{
    struct timespec t;

    clock_gettime(CLOCK_MONOTONIC, &t);
    return (double) t.tv_sec + (double) t.tv_nsec / 1e9;
}

/* agregar N enteros de a uno; ¿cuánto crece el arreglo en cada ampliación? */
static void probar(const char *estrategia, int duplicar, int con_vecinos)
{
    int *v = NULL;
    size_t cantidad = 0, capacidad = 0, nv = 0;
    long llamadas = 0, movidas = 0;
    double t0 = segundos();

    for (size_t i = 0; i < N; i++) {
        if (cantidad == capacidad) {
            size_t nueva = duplicar ? (capacidad == 0 ? 8 : capacidad * 2) : capacidad + 1;
            int *tmp = realloc(v, nueva * sizeof *v);

            if (tmp == NULL) {
                free(v);
                fprintf(stderr, "sin memoria\n");
                exit(1);
            }
            llamadas++;
            if (tmp != v && v != NULL)
                movidas++;
            v = tmp;
            capacidad = nueva;
            if (con_vecinos)
                vecinos[nv++] = malloc(16);       /* alguien más pide memoria */
        }
        v[cantidad++] = (int) i;
    }
    double t = segundos() - t0;

    printf("%-34s %7ld llamadas, %6ld movieron el bloque, %.4f s\n",
           estrategia, llamadas, movidas, t);
    free(v);
    for (size_t k = 0; k < nv; k++)
        free(vecinos[k]);
}

int main(void)
{
    probar("de a 1", 0, 0);
    probar("de a 1, con otros pedidos", 0, 1);
    probar("duplicando", 1, 0);
    probar("duplicando, con otros pedidos", 1, 1);
    return 0;
}
terminal
$ ./crecer
de a 1                              200000 llamadas,     27 movieron el bloque, 0.0020 s
de a 1, con otros pedidos           200000 llamadas,     76 movieron el bloque, 0.0064 s
duplicando                              16 llamadas,      1 movieron el bloque, 0.0007 s
duplicando, con otros pedidos           16 llamadas,      0 movieron el bloque, 0.0004 s

El resultado es menos dramático de lo que la teoría anuncia, y vale la pena entender por qué. Crecer de a uno obliga a copiar todo el contenido cada vez que el bloque se mueve, un trabajo proporcional al cuadrado de la cantidad de elementos; pero aquí el bloque casi nunca se movió, porque glibc lo extiende en el lugar cuando está al final del montículo, y para los bloques grandes usa mremap, que reubica páginas sin copiar sus datos (con strace se cuentan unas 160 llamadas a mremap). Aun así, la cuenta es clara: 200 000 llamadas contra 16, tiempo entre 3 y 10 veces mayor, y 76 movimientos cuando alguien más pide memoria en el medio. Y lo más importante, el resultado de la estrategia ingenua depende de la suerte del asignador: en otro sistema, o con bloques chicos, cada llamada puede copiar todo.

La estrategia que funciona siempre es el crecimiento geométrico: cuando el arreglo se llena, se pide el doble. Cada elemento se copia, en promedio, una o dos veces en toda la vida del arreglo, y el costo por elemento agregado es constante. Para eso hay que llevar dos números, que no son lo mismo: la cantidad de elementos que el arreglo tiene, y su capacidad, los que caben sin pedir más. El tema 30 construye un arreglo dinámico completo con esta técnica.

6. Las reglas de free

free(p) devuelve el bloque al administrador, que lo ofrecerá a un próximo pedido. Las reglas, todas sobre el puntero que se le pasa:

Se le pasa…Resultado
El puntero que devolvió malloc, calloc o reallocCorrecto, una sola vez.
NULLCorrecto: no hace nada. Permite limpiar sin comprobar.
El mismo puntero por segunda vezDoble liberación: comportamiento indefinido.
Un puntero al interior del bloque (p + 1)Indefinido.
La dirección de una variable local o globalIndefinido.
Una cadena literalIndefinido.
Un puntero a memoria que ya no es del programaIndefinido.

Todos los casos «indefinidos» son errores graves, y glibc detecta algunos de ellos y aborta el programa. Este programa los provoca, uno por cada argumento:

mal_free.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char *argv[])
{
    char local[16] = "en la pila";
    char *p = malloc(32);

    if (argc != 2 || p == NULL)
        return 2;
    if (strcmp(argv[1], "nulo") == 0) {
        free(NULL);                       /* permitido: no hace nada */
        printf("free(NULL) no hizo nada\n");
    } else if (strcmp(argv[1], "pila") == 0) {
        free(local);
    } else if (strcmp(argv[1], "literal") == 0) {
        free((void *) "literal");
    } else if (strcmp(argv[1], "doble") == 0) {
        free(p);
        free(p);
    } else if (strcmp(argv[1], "medio") == 0) {
        free(p + 1);
    }
    printf("seguimos\n");
    return 0;
}
terminal
$ gcc -std=c17 -Wall -Wextra -g -o mal_free mal_free.c
mal_free.c: In function ‘main’:
mal_free.c:21:9: warning: pointer ‘p’ used after ‘free’ [-Wuse-after-free]
mal_free.c:16:9: warning: ‘free’ called on unallocated object ‘local’ [-Wfree-nonheap-object]
mal_free.c:18:9: warning: ‘free’ called on a pointer to an unallocated object ‘"literal"’ [-Wfree-nonheap-object]
mal_free.c:23:9: warning: ‘free’ called on pointer ‘p’ with nonzero offset 1 [-Wfree-nonheap-object]
$ ./mal_free nulo
free(NULL) no hizo nada
seguimos
$ ./mal_free pila
double free or corruption (out)
Aborted
$ ./mal_free literal
free(): invalid pointer
Aborted
$ ./mal_free doble
free(): double free detected in tcache 2
Aborted
$ ./mal_free medio
free(): invalid pointer
Aborted

Las cuatro formas incorrectas se detectan, y por dos caminos. gcc las ve al compilar cuando las ve a la vista, con -Wuse-after-free y -Wfree-nonheap-object, ambos en -Wall: son los avisos que más vale no ignorar. Y glibc las ve al ejecutar, con comprobaciones internas que terminan el programa con SIGABRT y un mensaje. Pero hay que leer esos mensajes con cuidado: liberar una variable de la pila produce double free or corruption (out), que no menciona la pila, y free(): invalid pointer sirve igual para una cadena literal que para un puntero al medio de un bloque. Y lo que la biblioteca detecta es una fracción de lo que puede fallar: la mayoría de los errores con la memoria, como usar un bloque después de liberarlo, no producen ningún mensaje. De eso trata el tema 29.

Una costumbre defensiva es asignar NULL al puntero justo después de liberarlo, free(p); p = NULL;. Convierte la doble liberación en una llamada inocua, porque free(NULL) no hace nada, y hace que un uso posterior termine en Segmentation fault inmediato en lugar de leer memoria ajena. No es una solución, porque solo anula esa copia del puntero: si la dirección se guardó en otro lugar, esa otra copia sigue colgante.

7. Cadenas en memoria dinámica

Hasta ahora, las funciones que arman cadenas, como snprintf, necesitaban que quien llamaba aportara el buffer, con su tamaño. Con memoria dinámica, la función puede reservar el lugar justo y entregarlo, y el problema del tamaño desaparece. Quedaban dos promesas pendientes: strdup, anunciada en el tema 21, y la medición con snprintf(NULL, 0, …), del tema 22. Este programa tiene las dos, más una tercera opción:

cadenas.c
#define _GNU_SOURCE                       /* asprintf */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>

/* como sprintf, pero reserva el lugar justo; quien llama debe hacer free */
static char *formato(const char *fmt, ...)
{
    va_list ap, copia;

    va_start(ap, fmt);
    va_copy(copia, ap);
    int n = vsnprintf(NULL, 0, fmt, ap);          /* primera pasada: medir */
    va_end(ap);
    if (n < 0) {
        va_end(copia);
        return NULL;
    }
    char *s = malloc((size_t) n + 1);
    if (s != NULL)
        vsnprintf(s, (size_t) n + 1, fmt, copia); /* segunda pasada: escribir */
    va_end(copia);
    return s;
}

/* strdup hecha a mano */
static char *copiar(const char *origen)
{
    size_t n = strlen(origen) + 1;                /* con el '\0' */
    char *copia = malloc(n);

    if (copia != NULL)
        memcpy(copia, origen, n);
    return copia;
}

int main(void)
{
    char original[] = "/proc/self/maps";
    char *a = copiar(original);
    char *b = strdup(original);
    char *c = formato("%s tiene %zu caracteres", original, strlen(original));
    char *d = NULL;

    if (asprintf(&d, "pid %d", 1532) < 0)
        d = NULL;
    if (a == NULL || b == NULL || c == NULL || d == NULL) {
        fprintf(stderr, "sin memoria\n");
        return 1;
    }

    original[0] = 'X';                    /* las copias son independientes */
    printf("original: %s\n", original);
    printf("copiar:   %s\n", a);
    printf("strdup:   %s\n", b);
    printf("formato:  %s\n", c);
    printf("asprintf: %s\n", d);
    free(a);
    free(b);
    free(c);
    free(d);
    return 0;
}
terminal
$ ./cadenas
original: Xproc/self/maps
copiar:   /proc/self/maps
strdup:   /proc/self/maps
formato:  /proc/self/maps tiene 15 caracteres
asprintf: pid 1532

8. De quién es cada bloque

En C, el lenguaje no sabe quién es el dueño de un bloque. La pregunta «¿quién lo libera?» se responde por convención, y la respuesta tiene que estar escrita en la documentación de cada función. Es la información más importante de una interfaz que maneja memoria, y la que más errores causa cuando falta. Casi todas las funciones que ya usamos caen en uno de tres casos:

La función devuelve…Quién liberaEjemplos
Un bloque nuevo, reservado para quien llamaQuien llama, con free.malloc, strdup, asprintf, getline (el buffer), getcwd(NULL, 0), realpath(…, NULL)
Un puntero a datos que son de la función o del sistemaNadie: no se libera. Puede cambiar en la próxima llamada.getenv, strerror
Un puntero adentro de lo que se le pasóQuien lo reservó antes.strchr, strstr, strtok (sus campos), fgets
duenos.c
#define _XOPEN_SOURCE 700                 /* realpath es de XSI */
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main(void)
{
    char *dir = getcwd(NULL, 0);          /* POSIX: reserva el lugar necesario */
    char *real = realpath("..", NULL);    /* idem: quien llama libera */
    char *linea = NULL;
    size_t capacidad = 0;
    FILE *f = fopen("/proc/self/status", "r");

    printf("getcwd:   %s\n", dir);
    printf("realpath: %s\n", real);
    if (f != NULL && getline(&linea, &capacidad, f) > 0)
        printf("getline:  %s", linea);
    printf("(la línea ocupó %zu bytes de memoria dinámica)\n", capacidad);

    free(dir);
    free(real);
    free(linea);
    if (f != NULL)
        fclose(f);
    return 0;
}
terminal · ejecutado en ~/c/tema28
$ ./duenos
getcwd:   /home/ana/c/tema28
realpath: /home/ana/c
getline:  Name:	duenos
(la línea ocupó 120 bytes de memoria dinámica)

Tres funciones, tres bloques que son del programa. getcwd(NULL, 0) y realpath(…, NULL) son extensiones de POSIX que, ante un buffer nulo, reservan lo que haga falta; evitan adivinar el tamaño con PATH_MAX, un límite que en Linux no es real. getline, vista en el tema 10, es la misma idea con un detalle: recibe la dirección del puntero y la de la capacidad, y los actualiza. Al llamarla en un bucle, el mismo buffer se reutiliza y solo se agranda si una línea no cabe; se libera una vez, al final. Reservó 120 bytes aunque la línea fuera mucho más corta, porque pide un lugar inicial generoso.

Con las estructuras aparece una pregunta más: una estructura que contiene punteros a bloques propios, como el nombre de contador_t en la sección siguiente, posee esos bloques, y quien destruye la estructura debe liberarlos. La regla general para no perderse:

9. Un módulo con tipo opaco

En el tema 26 vimos los tipos opacos y quedó pendiente un ejemplo con memoria dinámica, porque quien usa un tipo opaco no puede declarar variables de ese tipo, y solo recibe punteros que el módulo reserva. Este módulo guarda un contador con un nombre propio, en tres archivos:

contador.h
#ifndef CONTADOR_H
#define CONTADOR_H

#include <stddef.h>

/* tipo opaco: quien incluye este archivo no ve los campos */
typedef struct contador contador_t;

/* reserva un contador con un nombre propio; NULL si no hay memoria */
contador_t *contador_crear(const char *nombre);

/* suma 1 y devuelve el valor nuevo */
long contador_incrementar(contador_t *c);

/* el nombre, que sigue siendo del contador: no se libera ni se guarda */
const char *contador_nombre(const contador_t *c);

/* libera el contador y su nombre; acepta NULL, como free */
void contador_destruir(contador_t *c);

#endif
contador.c
#define _POSIX_C_SOURCE 200809L           /* strdup */
#include <stdlib.h>
#include <string.h>
#include "contador.h"

struct contador {
    char *nombre;                         /* memoria propia, copiada de la que recibió */
    long valor;
};

contador_t *contador_crear(const char *nombre)
{
    contador_t *c = malloc(sizeof *c);

    if (c == NULL)
        return NULL;
    c->nombre = strdup(nombre);
    if (c->nombre == NULL) {              /* si falla la segunda, hay que deshacer la primera */
        free(c);
        return NULL;
    }
    c->valor = 0;
    return c;
}

long contador_incrementar(contador_t *c)
{
    return ++c->valor;
}

const char *contador_nombre(const contador_t *c)
{
    return c->nombre;
}

void contador_destruir(contador_t *c)
{
    if (c == NULL)
        return;
    free(c->nombre);                      /* primero lo que cuelga... */
    free(c);                              /* ...y después el contenedor */
}
uso.c
#include <stdio.h>
#include "contador.h"

int main(void)
{
    char texto[] = "visitas";
    contador_t *c = contador_crear(texto);

    if (c == NULL) {
        fprintf(stderr, "sin memoria\n");
        return 1;
    }
    texto[0] = 'X';                       /* el contador tiene su propia copia */
    contador_incrementar(c);
    contador_incrementar(c);
    printf("%s = %ld\n", contador_nombre(c), contador_incrementar(c));
    contador_destruir(c);
    return 0;
}
terminal · y uso_mal.c, que intenta mirar adentro del tipo opaco
$ gcc -std=c17 -Wall -Wextra -g -o uso uso.c contador.c && ./uso
visitas = 3
$ gcc -std=c17 -Wall -Wextra -g -o uso_mal uso_mal.c contador.c
uso_mal.c: In function ‘main’:
uso_mal.c:7:16: error: storage size of ‘copia’ isn’t known
    7 |     contador_t copia;                     /* no se puede: tamaño desconocido */
      |                ^~~~~
uso_mal.c:9:22: error: invalid use of incomplete typedef ‘contador_t’ {aka ‘struct contador’}
    9 |     printf("%ld\n", c->valor);            /* ni mirar adentro */
      |                      ^~

El módulo reúne las ideas de la sección 8. La estructura posee su nombre: contador_crear hace una copia con strdup, de modo que modificar el texto original después (la línea texto[0] = 'X') no afecta al contador. El éxito parcial se deshace: si malloc funciona y strdup falla, la función libera el contador antes de devolver NULL, para no dejar un bloque perdido. Destruir es el inverso de crear, libera primero el nombre y luego la estructura, y acepta NULL como free, lo que simplifica los caminos de error de quien lo usa. El nombre que devuelve contador_nombre no se libera: sigue siendo del contador, y deja de ser válido cuando éste se destruye; quien lo necesite más tiempo debe copiarlo con strdup. Y las dos líneas rechazadas por el compilador en uso_mal.c demuestran la opacidad: ni se puede declarar una variable contador_t, porque su tamaño es desconocido, ni acceder a un campo. Todo pasa por las funciones del módulo, y el módulo puede cambiar su implementación sin que uso.c lo note.

10. Matrices dinámicas

En el tema 23 dejamos pendiente cómo crear una matriz cuyas dimensiones se conocen en tiempo de ejecución. Hay dos formas, y la diferencia es cuántos bloques se piden:

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

/* una sola reserva: un puntero a filas de 'cols' enteros */
static int suma_tipada(size_t filas, size_t cols)
{
    int (*m)[cols] = calloc(filas, sizeof *m);     /* sizeof *m = cols * sizeof(int) */

    if (m == NULL)
        return -1;
    for (size_t i = 0; i < filas; i++)
        for (size_t j = 0; j < cols; j++)
            m[i][j] = (int) (i * cols + j);
    int s = m[filas - 1][cols - 1];
    free(m);                                       /* un solo free */
    return s;
}

/* una reserva por fila: filas + 1 reservas y filas + 1 liberaciones */
static int suma_filas(size_t filas, size_t cols)
{
    int **m = calloc(filas, sizeof *m);
    int s = -1;

    if (m == NULL)
        return -1;
    for (size_t i = 0; i < filas; i++) {
        m[i] = calloc(cols, sizeof **m);
        if (m[i] == NULL)
            goto limpiar;
    }
    for (size_t i = 0; i < filas; i++)
        for (size_t j = 0; j < cols; j++)
            m[i][j] = (int) (i * cols + j);
    s = m[filas - 1][cols - 1];
limpiar:
    for (size_t i = 0; i < filas; i++)
        free(m[i]);                                /* free(NULL) es válido */
    free(m);
    return s;
}

int main(void)
{
    printf("último elemento, una reserva:  %d\n", suma_tipada(1000, 500));
    printf("último elemento, por filas:    %d\n", suma_filas(1000, 500));
    return 0;
}
terminal
$ ./matriz
último elemento, una reserva:  499999
último elemento, por filas:    499999
Un solo bloque, int (*m)[cols]Un bloque por fila, int **m
Reservas y liberaciones1 y 1filas + 1 y filas + 1
MemoriaContigua: la matriz del tema 23, en orden por filas, amable con la caché.Cada fila en un lugar distinto, con la sobrecarga de cada bloque (sección 12).
Accesom[i][j], con una multiplicación por la fila.m[i][j], con una lectura de puntero más.
Filas de distinto largoNo.Sí (arreglos «irregulares»).
Si falla a mediasNo hay «a medias».Hay que liberar las filas ya reservadas.

La primera usa un puntero a arreglo con una dimensión variable: sizeof *m es cols * sizeof(int), calculado al ejecutar, así que calloc(filas, sizeof *m) reserva todas las filas juntas y, de paso, con la comprobación del desbordamiento de la sección 4. Es la forma preferible cuando la matriz es rectangular. En la segunda, calloc inicializa los punteros en NULL, lo que permite que el camino de error libere sin saber hasta dónde se había llegado: free(NULL) no hace nada. Cuando hay una sola etiqueta de limpieza, el goto hacia abajo es el modismo de C para centralizar la liberación; es el uso disciplinado del goto para la limpieza de recursos que describió el tema 9.

11. Memoria alineada

malloc devuelve direcciones alineadas a 16 bytes, la alineación de max_align_t, como vimos en el tema 25. Si hace falta más, por ejemplo 64 bytes para que una estructura ocupe una línea de caché, hay dos funciones:

alineada.c
#define _POSIX_C_SOURCE 200809L
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    void *a = malloc(100);
    void *b = aligned_alloc(64, 256);     /* C11: el tamaño debe ser múltiplo de la alineación */
    void *c = NULL;
    int err = posix_memalign(&c, 4096, 10000);      /* POSIX: devuelve el error, no usa errno */

    printf("malloc(100)           %p  (mod 16 = %ju, mod 64 = %ju)\n", a,
           (uintmax_t) ((uintptr_t) a % 16), (uintmax_t) ((uintptr_t) a % 64));
    printf("aligned_alloc(64,256) %p  (mod 64 = %ju)\n", b, (uintmax_t) ((uintptr_t) b % 64));
    printf("posix_memalign(4096)  %p  (mod 4096 = %ju, err = %d)\n", c,
           (uintmax_t) ((uintptr_t) c % 4096), err);
    free(a);
    free(b);
    free(c);                              /* todas se liberan con free */
    return 0;
}
terminal
$ ./alineada
malloc(100)           0x5c8bca7d5010  (mod 16 = 0, mod 64 = 16)
aligned_alloc(64,256) 0x5c8bca7d5080  (mod 64 = 0)
posix_memalign(4096)  0x5c8bca7d6000  (mod 4096 = 0, err = 0)

aligned_alloc(alineación, tamaño) es del estándar C11, y exige que el tamaño sea múltiplo de la alineación. posix_memalign(&p, alineación, tamaño) es de POSIX, deja la dirección en p y devuelve 0 o el código de error, sin tocar errno; la alineación debe ser una potencia de 2 y múltiplo de sizeof(void *). Con las dos, el bloque se libera con el free normal. La alineación de 4096 es la de una página, que se pide para memoria que se va a pasar al núcleo o a un dispositivo.

12. Por dentro: un bloque de malloc

Un bloque de malloc no es solo los bytes que se piden. La biblioteca guarda información de administración junto a cada uno, y mirarla explica varias conductas. Este programa pide bloques de distintos tamaños y consulta tres cosas: cuánto es realmente utilizable (malloc_usable_size, una extensión de glibc), la palabra de 8 bytes que hay justo antes del bloque, y la distancia entre un bloque y el anterior:

por_dentro.c
#define _DEFAULT_SOURCE                   /* malloc_usable_size */
#include <malloc.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* campo() es el de bss.c del tema 27 */
int main(void)
{
    size_t pedidos[] = {1, 8, 24, 25, 40, 100, 1000};
    void *anterior = NULL;

    printf("pedido  usable  encabezado  distancia al bloque anterior\n");
    for (size_t i = 0; i < sizeof pedidos / sizeof pedidos[0]; i++) {
        void *p = malloc(pedidos[i]);
        size_t encabezado = ((size_t *) p)[-1];       /* solo para mirar: no se hace en un programa */

        printf("%6zu  %6zu  0x%-8zx  %s%td\n", pedidos[i], malloc_usable_size(p), encabezado,
               anterior ? "+" : "", anterior ? (char *) p - (char *) anterior : 0);
        anterior = p;                                 /* no se libera: se quiere ver la secuencia */
    }

    void *a = malloc(24);
    printf("\nmalloc(24) -> %p\n", a);
    free(a);
    void *b = malloc(24);
    printf("free y otro malloc(24) -> %p  (%s)\n", b, a == b ? "el mismo bloque" : "otro bloque");
    free(b);

    long antes = campo("VmRSS:");
    enum { CUANTOS = 1000000 };
    void **bloques = malloc(CUANTOS * sizeof *bloques);
    for (long i = 0; i < CUANTOS; i++)
        bloques[i] = malloc(8);
    long despues = campo("VmRSS:");

    printf("\n%d bloques de 8 bytes: %ld KiB de VmRSS, unos %ld bytes por bloque (con el puntero)\n",
           CUANTOS, despues - antes, (despues - antes) * 1024 / CUANTOS);
    for (long i = 0; i < CUANTOS; i++)
        free(bloques[i]);
    free(bloques);
    return 0;
}
terminal
$ ./por_dentro
pedido  usable  encabezado  distancia al bloque anterior
     1      24  0x21        0
     8      24  0x21        +32
    24      24  0x21        +32
    25      40  0x31        +32
    40      40  0x31        +48
   100     104  0x71        +48
  1000    1000  0x3f1       +112

malloc(24) -> 0x58494e0b8540
free y otro malloc(24) -> 0x58494e0b8540  (el mismo bloque)

1000000 bloques de 8 bytes: 39296 KiB de VmRSS, unos 40 bytes por bloque (con el puntero)
Un bloque de malloc de 100 bytes pedidos ocupa 112 bytes en el montículo: 8 bytes de encabezado con el tamaño 0x71, que es 112 más el bit de bloque anterior en uso, seguidos de 104 bytes utilizables, de los cuales el programa pidió 100. El puntero que recibe el programa apunta al primer byte utilizable. El siguiente bloque empieza 112 bytes después malloc(100) · un bloque de 112 bytes en el montículo 0x71 104 bytes utilizables (100 pedidos) siguiente… encabezado · 8 bytes + 112 p ← lo que recibe el programa: la dirección del primer byte utilizable 0x71 = 0x70 (112, el tamaño del bloque entero) + 1 (el bit «el bloque anterior está en uso»). Tamaño del bloque = pedido + 8, redondeado a múltiplo de 16, con un mínimo de 32.
Un bloque de malloc en glibc: un encabezado de 8 bytes con el tamaño, y el espacio utilizable a continuación.

La salida deja ver cómo está hecho el administrador de glibc, que es el de la mayoría de los Linux:

El programa leyó el encabezado con ((size_t *) p)[-1] solo para mirar; no es un dato que se pueda usar, porque su formato es un detalle interno de glibc que cambia entre versiones y entre bibliotecas, y escribir en él corrompe el administrador. Con gdb se puede mirar lo mismo, y también qué pasa con un bloque cuando se lo libera:

terminal · gdb sobre basico 10, detenido justo antes y justo después del free
$ gdb -q -batch -ex 'break basico.c:23' -ex 'break basico.c:24' -ex 'run 10' -ex 'p v' -ex 'p *v@10' \
      -ex 'p n * sizeof(*v)' -ex 'x/2gx (long *) v - 2' -ex continue -ex 'p *v@10' \
      -ex 'x/4gx (long *) v - 2' ./basico
Breakpoint 1, main (argc=2, argv=0x7fffffffd8b8) at basico.c:23
23	    free(v);
$1 = (int *) 0x555555559010
$2 = {0, 1, 4, 9, 16, 25, 36, 49, 64, 81}
$3 = 40
0x555555559000:	0x0000000000000000	0x0000000000000031

Breakpoint 2, main (argc=2, argv=0x7fffffffd8b8) at basico.c:24
24	    return 0;
$4 = {1431655769, 5, 106306542, -770954042, 16, 25, 36, 49, 64, 81}
0x555555559000:	0x0000000000000000	0x0000000000000031
0x555555559010:	0x0000000555555559	0xd20c2cc606561bee

Antes del free, el bloque contiene los diez cuadrados, y la palabra de 8 bytes anterior a él, 0x31, dice que el bloque entero mide 48. Después del free el bloque no se borra: seis de los diez enteros siguen ahí, 16, 25, 36… Pero los primeros 16 bytes, cuatro enteros, ya no son los del programa: el administrador los usó para su propia contabilidad, uno con un puntero al siguiente bloque libre, codificado con la dirección del bloque, y otro con una clave al azar que sirve para detectar la doble liberación, y que es la que produjo el mensaje double free detected in tcache 2 de la sección 6. Es un adelanto del tema siguiente: la memoria liberada sigue siendo legible y a veces parece intacta, y esa es justamente la trampa. En ningún programa correcto se lee un bloque después de liberarlo.

13. Errores frecuentes

ErrorQué pasaCorrección
No comprobar el resultado de mallocSegmentation fault al usar NULL, o algo peor con un desplazamiento grande.if (p == NULL) en cada pedido, o un envoltorio.
Calcular el tamaño con sizeof(int) en vez de sizeof *pUn cambio de tipo deja el pedido chico: desbordamiento.malloc(n * sizeof *p).
sizeof p para el tamaño del bloqueEs el tamaño del puntero, 8.Guardar el tamaño en una variable.
malloc(n * tam) con n de afueraSi el producto se desborda, el bloque es mucho menor de lo previsto.calloc(n, tam) o reallocarray.
Leer un bloque de malloc sin inicializarloBasura o restos de datos anteriores; valores distintos entre ejecuciones.Escribir antes de leer, o calloc.
p = realloc(p, n)Si falla, se pierde el bloque y se pierden los datos.Un puntero temporal.
Seguir usando el puntero viejo después de reallocSi el bloque se movió, es un puntero colgante.Usar siempre el devuelto; recalcular los punteros interiores.
Crecer un arreglo de a un elementoUna llamada por elemento; copias del bloque entero.Duplicar la capacidad.
Olvidar el + 1 del \0 al copiar una cadenaUn byte fuera del bloque.strlen(s) + 1, o strdup.
Liberar dos veces, o un puntero que no vino de mallocAborto de glibc, o corrupción silenciosa.Un dueño por bloque; p = NULL después de free.
Usar un bloque después de freeLee datos del administrador o de otro bloque.No se usa; AddressSanitizer lo detecta (tema 29).
No liberar en los caminos de errorFuga en cada falla.Un único punto de limpieza (goto).
Liberar el contenedor antes de lo que cuelgaSe pierde acceso a los bloques internos: fuga.Primero los internos, después el contenedor.
Reservar millones de bloques chicosCinco veces la memoria necesaria y mucho tiempo.Un arreglo, o un solo bloque grande.
Creer que un malloc exitoso garantiza memoriaEl OOM killer puede matar al proceso al usarla.Limitar el consumo del programa; probar con ulimit -v.

14. Resumen

15. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. ¿Por qué se escribe malloc(n * sizeof *p) y no malloc(n * sizeof(int))?

Porque sizeof *p toma el tamaño del tipo de la variable que recibirá el bloque, y sigue correcto si ese tipo cambia. Con sizeof(int), un cambio de int *p a long *p deja el pedido más chico que lo necesario y produce un desbordamiento. sizeof no evalúa su operando, así que *p es válido aunque p aún no apunte a nada.

2. ¿Qué contiene el bloque que devuelve malloc, y qué devuelve sizeof p sobre él?

El contenido es indeterminado: basura o restos de usos anteriores, a veces ceros por casualidad. sizeof p vale 8, el tamaño de un puntero: el tamaño del bloque no se puede consultar, y el programa debe guardarlo aparte.

3. ¿Por qué un malloc que tuvo éxito no garantiza que la memoria exista?

Porque Linux, por omisión, solo reserva espacio de direcciones; la memoria física se asigna al tocar cada página. Si el programa usa más de lo que el sistema puede dar, el fallo ocurre en una escritura cualquiera, sin ningún código de error: el OOM killer termina un proceso con SIGKILL.

4. ¿Cómo se prueban los caminos de error de un programa que casi nunca falla por falta de memoria?

Limitando su espacio de direcciones con ulimit -v (por ejemplo, en un subshell, (ulimit -v 200000; ./programa)), con lo que malloc devuelve NULL con ENOMEM de verdad. Sin un límite, esas ramas nunca se ejecutan, y los errores que tengan no se descubren.

5. ¿Qué ventajas tiene calloc(n, tam) sobre malloc(n * tam)?

Entrega el bloque en cero, y comprueba que la multiplicación no se desborde: si n * tam no cabe en un size_t, devuelve NULL en lugar de un bloque más chico que el pedido. Para bloques grandes, además, no cuesta el relleno: el núcleo ya entrega páginas de ceros.

6. ¿Por qué p = realloc(p, n) es peligroso, y cuál es la forma correcta?

Si realloc falla, devuelve NULL y deja el bloque original intacto; la asignación pone NULL en p y destruye la única copia de la dirección, con lo que el bloque se pierde (fuga) junto con sus datos. La forma correcta guarda el resultado en un temporal, tmp = realloc(p, n), lo comprueba, y solo si no es NULL hace p = tmp.

7. Después de un realloc exitoso, ¿qué ocurre con los punteros al interior del bloque anterior?

Si el bloque se movió, quedan colgantes: apuntan a memoria que ya se liberó. Aunque el puntero principal no haya cambiado, no se puede asegurar de antemano que el bloque no se vaya a mover. Hay que recalcular los punteros interiores a partir del nuevo, o usar índices, que sobreviven al movimiento.

8. ¿Por qué se duplica la capacidad al crecer un arreglo, en lugar de sumar un elemento?

Porque duplicar hace que el costo total de agregar n elementos sea proporcional a n: cada elemento se copia pocas veces en toda la vida del arreglo. Sumar de a uno obliga a una llamada a realloc por elemento y, si el bloque se mueve, a copiar todo cada vez, un trabajo proporcional al cuadrado. Que glibc lo disimule en algunos casos no lo vuelve portable.

9. ¿Qué se puede pasar a free, y qué pasa con lo demás?

Se puede pasar un puntero devuelto por malloc, calloc o realloc, una sola vez, o NULL, que no hace nada. Todo lo demás, un puntero al interior del bloque, una variable local, una cadena literal o un bloque ya liberado, es comportamiento indefinido; glibc detecta algunos casos y aborta con mensajes como free(): invalid pointer.

10. ¿Qué diferencia hay entre strdup(s) y char *t = s;?

strdup reserva un bloque nuevo y copia la cadena, incluido el \0; el resultado es independiente de s y debe liberarse con free. La asignación de punteros copia solo la dirección: las dos variables apuntan a la misma cadena, y modificarla por una afecta a la otra.

11. ¿Cómo hace formato para reservar exactamente el lugar necesario?

Llama dos veces a vsnprintf: la primera con tamaño 0 y destino NULL, que no escribe nada y devuelve cuántos caracteres haría falta; con ese número reserva n + 1 bytes con malloc, y la segunda llamada escribe el texto. Como los argumentos variables se recorren dos veces, usa va_copy.

12. En contador_crear, ¿por qué se libera c cuando falla strdup?

Porque la función ya reservó la estructura con éxito; si no la liberara antes de devolver NULL, ese bloque quedaría reservado sin que nadie tuviera su dirección: una fuga en cada fallo. Un éxito parcial se deshace antes de informar el error.

13. ¿Qué es un tipo opaco, y por qué sus funciones tienen que reservar memoria?

Es un tipo declarado en la cabecera pero definido solo en el archivo fuente del módulo: quien lo usa no ve los campos ni su tamaño. Como el compilador no sabe cuánto mide, quien lo usa no puede declarar variables de ese tipo, solo punteros, y es el módulo el que reserva los objetos con malloc y los destruye con una función propia.

14. ¿Por qué malloc(25) ocupa más que malloc(24) en la memoria, y qué guarda el encabezado?

En glibc, cada bloque tiene un encabezado de 8 bytes y el tamaño total se redondea a un múltiplo de 16, con un mínimo de 32: 24 bytes pedidos caben en un bloque de 32 (24 utilizables), pero 25 requieren uno de 48. El encabezado guarda el tamaño del bloque, con un bit que indica si el anterior está en uso; free lo lee para saber cuánto devuelve.

Para practicar

  1. Escribe int *crear_serie(size_t n), que devuelva un arreglo dinámico con los primeros n cuadrados, o NULL si no hay memoria. Llámala desde main, muestra los valores y libera el arreglo. Pruébala con ulimit -v para ejecutar el camino de error.
  2. Escribe una función char *leer_archivo(const char *ruta, size_t *largo) que lea un archivo entero a un bloque dinámico, agrandándolo con realloc y la estrategia de duplicar, y lo termine con \0. Pruébala con un archivo vacío, con uno de 1 byte y con /proc/self/maps (cuyo tamaño según stat es 0).
  3. Corrige este fragmento de manera que no pierda memoria si realloc falla: buf = realloc(buf, tam * 2); if (buf == NULL) return -1;. Verifica tu solución con ulimit -v.
  4. Escribe una función char **dividir(const char *texto, char sep, size_t *cantidad) que devuelva un arreglo dinámico de cadenas dinámicas, con los campos del texto separados por sep (incluidos los vacíos), y otra liberar_campos que libere todo. ¿Quién es dueño de cada bloque? Escríbelo en un comentario.
  5. Agrega a contador_t una función contador_copiar que devuelva un contador nuevo con el mismo nombre y valor. ¿Qué pasaría si copiara solo la estructura, sin duplicar el nombre?
  6. Modifica por_dentro.c para ver cuántos bloques de 32 bytes caben antes de que brk cambie, y compara con la región [heap] de /proc/self/maps. ¿Qué pasa con el encabezado de un bloque grande, de 1 MiB, que malloc obtiene con mmap?
  7. Mide con clock_gettime el tiempo de reservar y liberar un millón de bloques de 32 bytes con malloc, frente a reservar un solo arreglo de un millón de estructuras de 32 bytes con calloc. ¿Cuál es la diferencia de memoria (VmRSS) y de tiempo?
  8. Escribe una versión de suma_filas en la que una fila falle a propósito (por ejemplo, cuando i == 3), y verifica con valgrind --leak-check=full o con -fsanitize=address que el camino de limpieza libera todo.