La asignación de memoria dinámica en C se refiere a la realización de una gestión manual de memoria para la asignación de memoria dinámica en el lenguaje de programación C a través de un grupo de funciones en la biblioteca estándar de C , a saber, malloc , realloc , calloc , adjusted_alloc y free . [1] [2] [3]
El lenguaje de programación C++ incluye estas funciones; sin embargo, los operadores new y delete brindan una funcionalidad similar y son recomendados por los autores de ese lenguaje. [4] Aún así, hay varias situaciones en las que el uso de / no es aplicable, como código de recolección de basura o código sensible al rendimiento, y puede requerirse una combinación de la ubicación y en lugar del operador de nivel superior.new
delete
malloc
new
new
Existen muchas implementaciones diferentes del mecanismo de asignación de memoria real que utiliza malloc . Su rendimiento varía tanto en el tiempo de ejecución como en la memoria requerida.
El lenguaje de programación C administra la memoria de forma estática , automática o dinámica . Las variables de duración estática se asignan en la memoria principal, generalmente junto con el código ejecutable del programa, y persisten durante la vida útil del programa; las variables de duración automática se asignan en la pila y aparecen y desaparecen cuando se invocan y retornan funciones. Para las variables de duración estática y automática, el tamaño de la asignación debe ser constante en tiempo de compilación (excepto en el caso de matrices automáticas de longitud variable [5] ). Si el tamaño requerido no se conoce hasta el tiempo de ejecución (por ejemplo, si se están leyendo datos de tamaño arbitrario del usuario o de un archivo de disco), entonces el uso de objetos de datos de tamaño fijo es inadecuado.
La duración de la memoria asignada también puede ser motivo de preocupación. Ni la memoria de duración estática ni la de duración automática son adecuadas para todas las situaciones. Los datos asignados automáticamente no pueden persistir en múltiples llamadas de función, mientras que los datos estáticos persisten durante la vida del programa, independientemente de que sean necesarios o no. En muchas situaciones, el programador requiere una mayor flexibilidad para gestionar la duración de la memoria asignada.
Estas limitaciones se evitan mediante el uso de la asignación dinámica de memoria , en la que la memoria se gestiona de forma más explícita (pero más flexible), normalmente asignándola desde el almacén libre (informalmente llamado "montón"), [ cita requerida ] un área de memoria estructurada para este propósito. En C, la función de biblioteca malloc
se utiliza para asignar un bloque de memoria en el montón. El programa accede a este bloque de memoria a través de un puntero que malloc
retorna. Cuando la memoria ya no es necesaria, se pasa el puntero a free
que desasigna la memoria para que pueda usarse para otros fines.
La descripción original de C indicaba que calloc
y cfree
estaban en la biblioteca estándar, pero no . Se proporcionó malloc
el código para una implementación de modelo simple de un administrador de almacenamiento para Unixalloc
con y free
como funciones de interfaz de usuario, y utilizando la sbrk
llamada al sistema para solicitar memoria del sistema operativo. [6] La documentación de Unix de la 6.ª edición proporciona alloc
y free
como funciones de asignación de memoria de bajo nivel. [7] Las rutinas malloc
y free
en su forma moderna se describen completamente en el manual de Unix de la 7.ª edición. [8] [9]
Algunas plataformas proporcionan llamadas a funciones intrínsecas o de biblioteca que permiten la asignación dinámica en tiempo de ejecución desde la pila C en lugar del montón (por ejemplo, alloca()
[10] ). Esta memoria se libera automáticamente cuando finaliza la función que realiza la llamada.
Las funciones de asignación de memoria dinámica de C se definen en stdlib.h
el encabezado ( cstdlib
encabezado en C++). [1]
malloc()
toma un solo argumento (la cantidad de memoria a asignar en bytes), mientras que calloc()
toma dos argumentos: la cantidad de elementos y el tamaño de cada elemento.malloc()
sólo asigna memoria, mientras que calloc()
asigna y establece los bytes en la región asignada a cero. [11]Crear una matriz de diez números enteros con alcance automático es sencillo en C:
int matriz [ 10 ];
Sin embargo, el tamaño de la matriz se fija en el momento de la compilación. Si se desea asignar una matriz similar de forma dinámica sin utilizar una matriz de longitud variable , lo cual no se garantiza que sea compatible con todas las implementaciones de C11 , se puede utilizar el siguiente código:
int * matriz = malloc ( 10 * tamaño de ( int ));
Esto calcula la cantidad de bytes que diez números enteros ocupan en la memoria, luego solicita esa cantidad de bytes malloc
y asigna el resultado a un puntero llamado array
(debido a la sintaxis de C, los punteros y las matrices se pueden usar indistintamente en algunas situaciones).
Debido a malloc
que es posible que no pueda atender la solicitud, podría devolver un puntero nulo y es una buena práctica de programación verificar esto:
int * matriz = malloc ( 10 * tamaño de ( int )); si ( matriz == NULL ) { fprintf ( stderr , "malloc falló \n " ); devolver -1 ; }
Cuando el programa ya no necesita la matriz dinámica , eventualmente debe llamar free
para devolver la memoria que ocupa al almacén libre:
libre ( matriz );
La memoria reservada por malloc
no se inicializa y puede contener restos de datos previamente utilizados y descartados. Después de la asignación con malloc
, los elementos de la matriz son variables no inicializadas . El comando calloc
devolverá una asignación que ya se ha borrado:
int * matriz = calloc ( 10 , tamañoof ( int ));
Con realloc podemos cambiar el tamaño de la cantidad de memoria a la que apunta un puntero. Por ejemplo, si tenemos un puntero que actúa como una matriz de tamaño y queremos cambiarlo a una matriz de tamaño , podemos usar realloc.
int * arr = malloc ( 2 * tamañode ( int )); arr [ 0 ] = 1 ; arr [ 1 ] = 2 ; arr = realloc ( arr , 3 * tamañode ( int )); arr [ 2 ] = 3 ;
Tenga en cuenta que se debe asumir que realloc ha cambiado la dirección base del bloque (es decir, si no ha podido ampliar el tamaño del bloque original y, por lo tanto, ha asignado un nuevo bloque más grande en otro lugar y ha copiado el contenido anterior en él). Por lo tanto, cualquier puntero a direcciones dentro del bloque original ya no es válido.
malloc
devuelve un puntero void ( void *
), que indica que es un puntero a una región de tipo de datos desconocido. El uso de conversión es necesario en C++ debido al fuerte sistema de tipos, mientras que este no es el caso en C. Se puede "convertir" (ver conversión de tipos ) este puntero a un tipo específico:
int * ptr , * ptr2 ; ptr = malloc ( 10 * sizeof ( * ptr )); /* sin conversión */ ptr2 = ( int * ) malloc ( 10 * sizeof ( * ptr )); /* con conversión */
Realizar este tipo de yeso tiene sus ventajas y desventajas.
malloc
las que originalmente devolvieron un char *
. [12]malloc()
llamada (aunque los compiladores modernos y los analizadores estáticos pueden advertir sobre dicho comportamiento sin requerir la conversión [13] ).stdlib.h
, en el que se encuentra el prototipo de función para . [12] [14] En ausencia de un prototipo para , el estándar C90 requiere que el compilador de C asuma que devuelve un . Si no hay conversión, C90 requiere un diagnóstico cuando este entero se asigna al puntero; sin embargo, con la conversión, este diagnóstico no se produciría, ocultando un error. En ciertas arquitecturas y modelos de datos (como LP64 en sistemas de 64 bits, donde los punteros y son de 64 bits y es de 32 bits), este error puede resultar en un comportamiento indefinido, ya que la función declarada implícitamente devuelve un valor de 32 bits mientras que la función realmente definida devuelve un valor de 64 bits. Dependiendo de las convenciones de llamada y el diseño de la memoria, esto puede resultar en una destrucción de pila . Es menos probable que este problema pase desapercibido en los compiladores modernos, ya que C99 no permite declaraciones implícitas, por lo que el compilador debe producir un diagnóstico incluso si asume que devuelve.malloc
malloc
malloc
int
long
int
malloc
int
malloc
se llama y se realiza la conversión.El uso inadecuado de la asignación dinámica de memoria puede ser con frecuencia una fuente de errores, como errores de seguridad o fallos del programa, generalmente debidos a fallos de segmentación .
Los errores más comunes son los siguientes: [15]
free
se genera una acumulación de memoria no reutilizable, que el programa ya no utiliza. Esto desperdicia recursos de memoria y puede provocar fallas de asignación cuando estos recursos se agotan.malloc
, uso para almacenar datos, desasignación mediante free
. Los fallos en el cumplimiento de este patrón, como el uso de memoria después de una llamada a free
( puntero colgante ) o antes de una llamada a malloc
( puntero salvaje ), la llamada free
dos veces ("doble liberación"), etc., normalmente provoca un fallo de segmentación y da como resultado un bloqueo del programa. Estos errores pueden ser transitorios y difíciles de depurar; por ejemplo, el sistema operativo no suele recuperar inmediatamente la memoria liberada y, por lo tanto, los punteros colgantes pueden persistir durante un tiempo y parecer que funcionan.Además, como interfaz que precede a la estandarización ANSI C, malloc
y sus funciones asociadas tienen comportamientos que se dejaron intencionalmente a la implementación para que los definiera por sí mismos. Uno de ellos es la asignación de longitud cero, que es más problemática realloc
ya que es más común cambiar el tamaño a cero. [16] Aunque tanto POSIX como la Especificación Única de Unix requieren un manejo adecuado de las asignaciones de tamaño cero ya sea devolviendo NULL
o algo más que se pueda liberar de manera segura, [17] no todas las plataformas están obligadas a cumplir con estas reglas. Entre los muchos errores de doble liberación a los que ha dado lugar, el RCE de WhatsApp de 2019 fue especialmente destacado. [18] Una forma de encapsular estas funciones para hacerlas más seguras es simplemente verificar las asignaciones de tamaño 0 y convertirlas en asignaciones de tamaño 1. (El retorno NULL
tiene sus propios problemas: de lo contrario, indica una falla por falta de memoria. En el caso de que realloc
hubiera señalado que la memoria original no se movió ni se liberó, lo que nuevamente no es el caso para el tamaño 0, lo que lleva a la doble liberación). [19]
La implementación de la gestión de memoria depende en gran medida del sistema operativo y la arquitectura. Algunos sistemas operativos proporcionan un asignador para malloc, mientras que otros proporcionan funciones para controlar ciertas regiones de datos. El mismo asignador de memoria dinámica se utiliza a menudo para implementar tanto malloc
el operador como new
en C++ . [20]
La implementación de los asignadores heredados se hacía comúnmente utilizando el segmento de montón . El asignador generalmente expandía y contraía el montón para cumplir con las solicitudes de asignación.
El método del montón presenta algunos defectos inherentes:
Doug Lea ha desarrollado el dlmalloc de dominio público ("Doug Lea's Malloc") como un asignador de propósito general, a partir de 1987. La biblioteca GNU C (glibc) se deriva de ptmalloc ("pthreads malloc") de Wolfram Gloger, una bifurcación de dlmalloc con mejoras relacionadas con los subprocesos. [21] [22] [23] A partir de noviembre de 2023, la última versión de dlmalloc es la versión 2.8.6 de agosto de 2012. [24]
dlmalloc es un asignador de etiquetas de límites. La memoria en el montón se asigna como "fragmentos", una estructura de datos alineada de 8 bytes que contiene un encabezado y memoria utilizable. La memoria asignada contiene una sobrecarga de 8 o 16 bytes para el tamaño del fragmento y los indicadores de uso (similar a un vector dope ). Los fragmentos no asignados también almacenan punteros a otros fragmentos libres en el área de espacio utilizable, lo que hace que el tamaño mínimo del fragmento sea de 16 bytes en sistemas de 32 bits y 24/32 (depende de la alineación) bytes en sistemas de 64 bits. [22] [24] : 2.8.6, Tamaño mínimo asignado
La memoria no asignada se agrupa en " bins " de tamaños similares, implementados mediante el uso de una lista de fragmentos con doble enlace (con punteros almacenados en el espacio no asignado dentro del fragmento). Los bins se clasifican por tamaño en tres clases: [22] [24] : Estructuras de datos superpuestas
El desarrollador de juegos Adrian Stone sostiene que dlmalloc
, como asignador de etiquetas de límites, no es amigable para los sistemas de consola que tienen memoria virtual pero no tienen paginación por demanda . Esto se debe a que sus devoluciones de llamadas de reducción y crecimiento de grupo ( sysmalloc
/ systrim
) no se pueden usar para asignar y confirmar páginas individuales de memoria virtual. En ausencia de paginación por demanda, la fragmentación se convierte en una preocupación mayor. [27]
Desde FreeBSD 7.0 y NetBSD 5.0, la malloc
implementación anterior ( phkmalloc
por Poul-Henning Kamp ) fue reemplazada por jemalloc, escrita por Jason Evans. La razón principal de esto fue la falta de escalabilidad phkmalloc
en términos de multihilo. Para evitar la contención de bloqueos, jemalloc
utiliza "arenas" separadas para cada CPU . Los experimentos que miden la cantidad de asignaciones por segundo en aplicaciones multihilo han demostrado que esto hace que se escale linealmente con la cantidad de hilos, mientras que para phkmalloc y dlmalloc el rendimiento fue inversamente proporcional a la cantidad de hilos. [28]
malloc
La implementación de la función de OpenBSD hace uso de mmap . Para solicitudes mayores en tamaño que una página, la asignación completa se recupera usando mmap
; tamaños más pequeños se asignan de grupos de memoria mantenidos por malloc
dentro de un número de "páginas de depósito", también asignadas con mmap
. [29] [ se necesita una mejor fuente ] En una llamada a free
, la memoria se libera y se desasigna del espacio de direcciones del proceso usando munmap
. Este sistema está diseñado para mejorar la seguridad al aprovechar la aleatorización del diseño del espacio de direcciones y las características de página de espacio libre implementadas como parte de mmap
la llamada al sistema de OpenBSD , y para detectar errores de uso después de liberación: como una asignación de memoria grande se desasigna completamente después de que se libera, el uso posterior causa un error de segmentación y la finalización del programa.
El proyecto GrapheneOS comenzó inicialmente portando el asignador de memoria de OpenBSD a la biblioteca C Bionic de Android. [30]
Hoard es un asignador cuyo objetivo es el rendimiento escalable de la asignación de memoria. Al igual que el asignador de OpenBSD, Hoard utiliza mmap
exclusivamente, pero administra, la memoria en fragmentos de 64 kilobytes llamados superbloques. El montón de Hoard está dividido lógicamente en un único montón global y una cantidad de montones por procesador. Además, hay una caché local de subprocesos que puede contener una cantidad limitada de superbloques. Al asignar solo desde superbloques en el montón local por subproceso o por procesador, y mover los superbloques casi vacíos al montón global para que puedan ser reutilizados por otros procesadores, Hoard mantiene baja la fragmentación al tiempo que logra una escalabilidad casi lineal con la cantidad de subprocesos. [31]
Un asignador de memoria compacto de propósito general, de código abierto, de Microsoft Research centrado en el rendimiento. [32] La biblioteca tiene aproximadamente 11 000 líneas de código .
Cada subproceso tiene un almacenamiento local para pequeñas asignaciones. Para asignaciones grandes, se puede utilizar mmap o sbrk . TCMalloc, un malloc desarrollado por Google, [33] tiene recolección de basura para el almacenamiento local de subprocesos inactivos. Se considera que TCMalloc es más del doble de rápido que ptmalloc de glibc para programas multiproceso. [34] [35]
Los núcleos de los sistemas operativos necesitan asignar memoria de la misma manera que lo hacen los programas de aplicación. malloc
Sin embargo, la implementación de dentro de un núcleo a menudo difiere significativamente de las implementaciones utilizadas por las bibliotecas de C. Por ejemplo, los búferes de memoria podrían necesitar cumplir con restricciones especiales impuestas por DMA , o la función de asignación de memoria podría ser llamada desde el contexto de interrupción. [36] Esto requiere una malloc
implementación estrechamente integrada con el subsistema de memoria virtual del núcleo del sistema operativo.
Debido a que malloc
y sus parientes pueden tener un fuerte impacto en el rendimiento de un programa, no es raro anular las funciones de una aplicación específica mediante implementaciones personalizadas que están optimizadas para los patrones de asignación de la aplicación. El estándar C no proporciona ninguna forma de hacer esto, pero los sistemas operativos han encontrado varias formas de hacerlo explotando la vinculación dinámica. Una forma es simplemente vincular una biblioteca diferente para anular los símbolos. Otra forma, empleada por Unix System V.3 , es crear punteros malloc
de free
función que una aplicación puede restablecer a funciones personalizadas. [37]
La forma más común en sistemas tipo POSIX es configurar la variable de entorno LD_PRELOAD con la ruta del asignador, de modo que el enlazador dinámico utilice esa versión de malloc/calloc/free en lugar de la implementación de libc.
El bloque de memoria más grande que malloc
se puede asignar depende del sistema host, particularmente del tamaño de la memoria física y de la implementación del sistema operativo.
En teoría, el número más grande debería ser el valor máximo que se puede almacenar en un size_t
tipo, que es un entero sin signo que depende de la implementación y que representa el tamaño de un área de memoria. En el estándar C99 y posteriores, está disponible como la SIZE_MAX
constante de . Aunque no está garantizado por ISO C , normalmente es .<stdint.h>
2^(CHAR_BIT * sizeof(size_t)) - 1
En los sistemas glibc, el bloque de memoria más grande que malloc
se puede asignar es solo la mitad de este tamaño, es decir , . [38]2^(CHAR_BIT * sizeof(ptrdiff_t) - 1) - 1
Las implementaciones de la biblioteca C que se incluyen con varios sistemas operativos y compiladores pueden incluir alternativas y extensiones a la malloc
interfaz estándar. Entre ellas, se destacan:
alloca
, que asigna una cantidad solicitada de bytes en la pila de llamadas . No existe una función de desasignación correspondiente, ya que normalmente la memoria se desasigna tan pronto como regresa la función de llamada. alloca
estuvo presente en sistemas Unix ya en 32/V (1978), pero su uso puede ser problemático en algunos contextos (por ejemplo, integrados). [39] Aunque muchos compiladores lo admiten, no forma parte del estándar ANSI-C y, por lo tanto, puede que no siempre sea portable. También puede causar problemas menores de rendimiento: conduce a marcos de pila de tamaño variable, por lo que es necesario gestionar tanto los punteros de pila como de marco (con marcos de pila de tamaño fijo, uno de estos es redundante). [40] Las asignaciones más grandes también pueden aumentar el riesgo de un comportamiento indefinido debido a un desbordamiento de pila . [41] C99 ofrecía matrices de longitud variable como un mecanismo alternativo de asignación de pila; sin embargo, esta característica se relegó a opcional en el estándar C11 posterior .posix_memalign
que asigna memoria con la alineación especificada por el autor de la llamada. Sus asignaciones se desasignan con free
, [42] por lo que la implementación normalmente debe ser parte de la biblioteca malloc.calloc
y cfree
, y la Sección 8.7 (página 173) describe una implementación para alloc
y free
.man
página correspondiente a malloc
etc. se encuentra en la página 275.