servicio seguridad que ejemplo diseño arquitectura c design api memory-management malloc

c - que - seguridad api rest



Diseño de API C: ¿Quién debería asignar? (11)

¿Cuál es la forma correcta / preferida de asignar memoria en una API de C?

Puedo ver, al principio, dos opciones:

1) Permita que quien llama haga todo el manejo de la memoria (externa):

myStruct *s = malloc(sizeof(s)); myStruct_init(s); myStruct_foo(s); myStruct_destroy(s); free(s);

Las funciones _init y _destroy son necesarias ya que se puede asignar más memoria interna, y debe manejarse en alguna parte.

Esto tiene la desventaja de ser más largo, pero también el malloc se puede eliminar en algunos casos (por ejemplo, se puede pasar una estructura asignada por la pila:

int bar() { myStruct s; myStruct_init(&s); myStruct_foo(&s); myStruct_destroy(&s); }

Además, es necesario que la persona que llama sepa el tamaño de la estructura.

2) Ocultar malloc s en _init y s free en _destroy .

Ventajas: código más corto, ya que las funciones se llamarán de todos modos. Estructuras completamente opacas

Desventajas: no se puede pasar una estructura asignada de una manera diferente.

myStruct *s = myStruct_init(); myStruct_foo(s); myStruct_destroy(foo);

Actualmente me estoy inclinando por el primer caso; nuevamente, no sé sobre el diseño de la API de C


¿Por qué no proporcionar ambos, para obtener lo mejor de ambos mundos?

Use las funciones _init y _terminate para usar el método n. ° 1 (o el nombre que considere oportuno).

Use las funciones _create y _destroy adicionales para la asignación dinámica. Como _init y _terminate ya existen, de hecho se reduce a:

myStruct *myStruct_create () { myStruct *s = malloc(sizeof(*s)); if (s) { myStruct_init(s); } return (s); } void myStruct_destroy (myStruct *s) { myStruct_terminate(s); free(s); }

Si desea que sea opaco, establezca _init y _terminate static y no los exponga en la API, solo proporcione _create y _destroy. Si necesita otras asignaciones, por ejemplo, con una devolución de llamada dada, proporcione otro conjunto de funciones para esto, por ejemplo, _createllado, _destroyllamado.

Lo importante es hacer un seguimiento de las asignaciones, pero debes hacerlo de todos modos. Siempre debe usar la contraparte del asignador usado para la desasignación.


Ambas formas están bien, tiendo a hacer de la primera manera, ya que una gran parte del CI lo hace para los sistemas integrados y toda la memoria está compuesta por pequeñas variables en la pila o está asignada estáticamente. De esta forma no puede quedarse sin memoria, ya sea que tengas suficiente al principio o que estés jodido desde el principio. Es bueno saber cuándo tienes 2K de Ram :-) Así que todas mis bibliotecas son como # 1 donde se supone que la memoria está asignada.

Pero este es un caso límite del desarrollo de C.

Habiendo dicho eso, probablemente iría con el # 1 todavía. Tal vez usando init y finalize / dispose (en lugar de destruir) para nombres.


Ambos son aceptables, hay intercambios entre ellos, como habrás notado.

Hay grandes ejemplos del mundo real de ambos, como dice Dean Harding , GTK + usa el segundo método; OpenSSL es un ejemplo que usa el primero.


Ambos son funcionalmente equivalentes. Pero, en mi opinión, el método # 2 es más fácil de usar. Algunas razones para preferir 2 sobre 1 son:

  1. Es mas intuitivo ¿Por qué debería tener que llamar free al objeto después de que (aparentemente) lo myStruct_Destroy usando myStruct_Destroy ?

  2. Oculta los detalles de myStruct del usuario. Él no tiene que preocuparse por su tamaño, etc.

  3. En el método n. ° 2, myStruct_init no tiene que preocuparse por el estado inicial del objeto.

  4. No tiene que preocuparse por las pérdidas de memoria del usuario que se olvida de llamar free .

Sin embargo, si su implementación API se envía como una biblioteca compartida separada, el método n.º 2 es obligatorio. Para aislar su módulo de cualquier discrepancia en las implementaciones de malloc / new y free / delete todas las versiones del compilador, debe mantener la asignación de memoria y la desasignación para usted. Tenga en cuenta que esto es más cierto de C ++ que de C.


El problema que tengo con el primer método no es tanto que sea más largo para la persona que llama, sino que la API ahora está esposada para poder expandir la cantidad de memoria que está usando precisamente porque no sabe cómo la memoria recibido fue asignado. La persona que llama no siempre sabe por adelantado cuánta memoria necesitará (imagínese si estaba tratando de implementar un vector).

Otra opción que no mencionó, que será excesiva la mayoría de las veces, es pasar un puntero de función que la API utiliza como asignador. Esto no le permite usar la pila, pero le permite hacer algo como reemplazar el uso de malloc con un grupo de memoria, que aún mantiene el control de la API cuando quiere asignar.

En cuanto a qué método es el diseño de API adecuado, se hace de ambas maneras en la biblioteca estándar de C. strdup () y stdio usan el segundo método, mientras que sprintf y strcat usan el primer método. Personalmente prefiero el segundo método (o tercero) a menos que 1) Sé que nunca necesitaré realloc y 2) Espero que la vida de mis objetos sea corta y, por lo tanto, utilizar la pila es muy conveniente.

editar: en realidad hay 1 opción más, y es una mala con un precedente prominente. Podrías hacerlo del mismo modo que strtok () lo hace con estática. No es bueno, solo lo mencioné por completo.


Eso podría dar algún elemento de reflexión:

El caso n. ° 1 imita el esquema de asignación de memoria de C ++, con más o menos los mismos beneficios:

  • fácil asignación de temporales en la pila (o en matrices estáticas o similares para escribir su propio asignador de struct reemplazando malloc).
  • fácil de memoria si algo sale mal en init

El caso n. ° 2 oculta más información sobre la estructura utilizada y también puede usarse para estructuras opacas, generalmente cuando la estructura vista por el usuario no es exactamente la misma que la utilizada internamente (digamos que podría haber más campos ocultos al final de la estructura) )

La API mixta entre el caso n.º 1 y el caso n.º 2 también es común: hay un campo utilizado para pasar un puntero a una estructura ya inicializada, si es nulo se asigna (y el puntero siempre se devuelve). Con dicha API, la libre suele ser responsabilidad de la persona que llama, incluso si init realizó la asignación.

En la mayoría de los casos, probablemente iría por el caso n. ° 1.


Iría por (1) con una extensión simple, es decir, que tu función _init siempre devuelva el puntero al objeto. La inicialización de su puntero puede simplemente leer:

myStruct *s = myStruct_init(malloc(sizeof(myStruct)));

Como puede ver en el lado derecho, solo tiene una referencia al tipo y no a la variable. Una macro simple luego te da (2) al menos parcialmente

#define NEW(T) (T ## _init(malloc(sizeof(T))))

y la inicialización de su puntero se lee

myStruct *s = NEW(myStruct);


Mi ejemplo favorito de una API C de diseño correcto es GTK+ que utiliza el método n. ° 2 que usted describe.

Aunque otra ventaja de su método n. ° 1 no es solo que puede asignar el objeto en la pila, sino que también puede reutilizar la misma instancia varias veces. Si no va a ser un caso de uso común, entonces la simplicidad de # 2 es probablemente una ventaja.

Por supuesto, esa es solo mi opinión :)


Otra desventaja del n. ° 2 es que la persona que llama no tiene control sobre cómo se asignan las cosas. Esto puede solucionarse proporcionando una API para que el cliente registre sus propias funciones de asignación / desasignación (como ocurre con SDL), pero incluso eso puede no ser lo suficientemente detallado.

La desventaja del n. ° 1 es que no funciona bien cuando los búferes de salida no son de tamaño fijo (por ejemplo, cadenas). En el mejor de los casos, deberá proporcionar otra función para obtener la longitud del búfer primero para que la persona que llama pueda asignarlo. En el peor de los casos, es simplemente imposible hacerlo de manera eficiente (es decir, la longitud de la informática en una ruta separada es demasiado costosa en computación y copia de una sola vez).

La ventaja del n. ° 2 es que le permite exponer su tipo de datos estrictamente como un puntero opaco (es decir, declarar la estructura pero no definirla, y usar punteros de forma coherente). Luego puede cambiar la definición de la estructura como mejor le parezca en futuras versiones de su biblioteca, mientras que los clientes permanecen compatibles a nivel binario. Con el n. ° 1, debe hacerlo solicitando al cliente que especifique la versión dentro de la estructura de alguna forma (por ejemplo, todos los campos cbSize en la API de Win32) y luego escriba código manualmente que pueda manejar versiones anteriores y posteriores de la estructura para seguir siendo compatible con binarios a medida que su biblioteca evoluciona.

En general, si sus estructuras son datos transparentes que no cambiarán con futuras revisiones menores de la biblioteca, iría con el n. ° 1. Si se trata de un objeto de datos más o menos complicado y desea una encapsulación completa para hacerlo a prueba de fallas para el futuro desarrollo, vaya al n. ° 2.


Vea su método # 2 dice

myStruct *s = myStruct_init(); myStruct_foo(s); myStruct_destroy(s);

Ahora vea si myStruct_init() necesita devolver algún código de error por varios motivos, entonces déjelo ir de esta manera.

myStruct *s; int ret = myStruct_init(&s); // int myStruct_init(myStruct **s); myStruct_foo(s); myStruct_destroy(s);


Método número 2 todo el tiempo.

¿Por qué? porque con el método número 1 tienes que filtrar detalles de implementación a la persona que llama. La persona que llama tiene que saber al menos qué tan grande es la estructura. No puede cambiar la implementación interna del objeto sin volver a compilar ningún código que lo use.