C Diseño de API: ¿Quién debe asignar?


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

Puedo ver, al principio, dos opciones:

1) Deje que la persona que 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 dentro, y debe manejarse en algún lugar.

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 pila asignada estructura:

int bar() {
    myStruct s;
    myStruct_init(&s);

    myStruct_foo(&s);

    myStruct_destroy(&s);
}

Además, es necesario que el llamante conozca el tamaño de la estructura.

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

Ventajas: código más corto, ya que las funciones se van a llamar 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 inclino por el primer caso; por otra parte, no conozco el diseño de la API de C.

Author: Tordek, 2010-07-21

11 answers

Mi ejemplo favorito de una API C bien diseñada es GTK+ que utiliza el método #2 que usted describe.

Aunque otra ventaja de su método #1 no es solo que podría asignar el objeto en la pila, sino también que podría reutilizar la misma instancia varias veces. Si eso 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:)

 9
Author: Dean Harding,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-07-21 04:38:32

Método número 2 cada vez.

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

 15
Author: JeremyP,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-07-21 10:31:35

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

La desventaja de #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, calcular la longitud en una ruta separada es demasiado caro en comparación con la computación y la copia de una sola vez).

La ventaja de #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 consistentemente). A continuación, puede cambiar la definición de la estructura como mejor le parezca en futuras versiones de su biblioteca, mientras que los clientes siendo compatible a nivel binario. Con #1, debe hacerlo requiriendo que el cliente especifique la versión dentro de la estructura de alguna manera (por ejemplo, todos los campos cbSize en la API Win32), y luego escribir manualmente código que pueda manejar tanto versiones anteriores como nuevas de la estructura para seguir siendo compatible con binarios a medida que su biblioteca evolucione.

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

 14
Author: Pavel Minaev,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-07-21 04:49:33

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

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

Utilice funciones adicionales _create y _destroy para la asignación dinámica. Dado que _init y _terminate ya existen, efectivamente 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, haga _init y _terminate static y no los exponga en la API, solo proporcione _create y _destroy. Si usted necesita otro las asignaciones, por ejemplo, con una devolución de llamada dada, proporcionan otro conjunto de funciones para esto, por ejemplo, _createcalled, _destroycalled.

Lo importante es hacer un seguimiento de las asignaciones, pero tienes que hacer esto de todos modos. Siempre debe usar la contraparte del asignador utilizado para la desasignación.

 11
Author: Secure,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-07-21 05:51:26

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 más intuitivo. ¿Por qué debería tener que llamar free al objeto después de haberlo destruido (aparentemente) usando myStruct_Destroy?

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

  3. En el método #2, myStruct_init no tiene que preocuparse por el estado inicial de la objeto.

  4. No tiene que preocuparse por las fugas de memoria del usuario que olvida llamar a free.

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

 4
Author: 341008,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-07-21 06:00:15

El problema que tengo con el primer método no es tanto que sea más largo para el que llama, es que la api ahora está esposada al poder expandir la cantidad de memoria que está usando precisamente porque no sabe cómo se asignó la memoria que recibió. La persona que llama no siempre sabe de antemano cuánta memoria necesitará (imagínese si estuviera tratando de implementar un vector).

Otra opción que no mencionaste, que va a ser exagerada la mayor parte del tiempo, es pasar en 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 la api en control de cuándo 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é reasignar y 2) Espero que la vida útil de mis objetos sea corta y, por lo tanto, usar la pila es muy conveniente

Editar: En realidad hay 1 otra opción, y es una mala con un precedente prominente. Podrías hacerlo de la misma manera que strtok() lo hace con estática. No es bueno, solo lo mencioné por completo.

 3
Author: frankc,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-07-21 13:15:41

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

Pero este es un caso extremo de desarrollo de C.

Teniendo dicho eso, probablemente iría con el #1 todavía. Tal vez usando init y finalizar/disponer (en lugar de destruir) para los nombres.

 2
Author: Keith Nicholas,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-07-21 05:12:53

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

Caso #1 imita el esquema de asignación de memoria de C++, con más o menos los mismos beneficios:

  • fácil asignación de temporarios en la pila (o en arrays estáticos o tal para escribir su propio struct allocator reemplazando malloc).
  • fácil sin memoria si algo sale mal en init

El caso # 2 oculta más información sobre la estructura usada y también se puede usar para estructuras opacas, típicamente cuando la estructura es vista por el usuario no es exactamente el mismo que el utilizado internamente por la lib (digamos que podría haber algunos campos más ocultos al final de la estructura).

API mixta entre el caso#1 y el caso #2 también es común : hay un campo utilizado para pasar un puntero a alguna estructura ya inicializada, si es null se asigna (y el puntero siempre se devuelve). Con tal API el libre es generalmente 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 #1.

 2
Author: kriss,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-07-21 05:39:09

Ambos son aceptables - hay compensaciones entre ellos, como usted ha señalado.

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.

 1
Author: caf,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 11:46:58

Me gustaría ir para (1) con una extensión simple, que es hacer que su función _init siempre devuelva el puntero al objeto. La inicialización del puntero puede decir:

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

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

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

Y la inicialización del puntero dice

myStruct *s = NEW(myStruct);
 1
Author: Jens Gustedt,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-07-21 06:13:39

Vea su método #2 dice

myStruct *s = myStruct_init();

myStruct_foo(s);

myStruct_destroy(s);

Ahora veamos si myStruct_init() necesita devolver algún código de error por varias razones, entonces vayamos por este camino.

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

myStruct_foo(s);

myStruct_destroy(s);
 0
Author: Jeegar Patel,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-12-16 10:17:54