Cada vez que veo una "clase" de C (cualquier estructura que esté destinada a ser utilizada para acceder a funciones que toman un puntero como primer argumento) las veo implementadas de esta manera:
typedef struct { int member_a; float member_b; } CClass; CClass* CClass_create(); void CClass_destroy(CClass *self); void CClass_someFunction(CClass *self, ...); ... Y en este caso CClass_create siempre malloc s su memoria y devuelve un puntero a eso.
Cada vez que veo que aparece algo new en C++ innecesariamente, por lo general parece volver locos a los programadores de C++, pero esta práctica parece aceptable en C. ¿Qué sucede? ¿Hay alguna razón detrás de por qué las "clases" de estructura asignadas al montón son tan comunes?
Hay varias razones para esto.
Discutámoslos brevemente.
Para punteros opacos , le permite hacer algo como:
struct CClass_; typedef struct CClass_ CClass; // the rest as in your example Por lo tanto, el usuario no ve la definición de struct CClass_ , lo que lo aísla de los cambios y permite otras cosas interesantes, como implementar la clase de manera diferente para diferentes plataformas.
Por supuesto, esto prohíbe el uso de variables de pila de CClass . Pero, OTOH, uno puede ver que esto no prohíbe la asignación de objetos CClass estáticamente (de algún grupo), devueltos por CClass_create o tal vez otra función como CClass_create_static .
Falta de destructores : dado que el compilador C no destruirá automáticamente los objetos de la pila CClass , debe hacerlo usted mismo (llamando manualmente a la función destructora). Entonces, el único beneficio que queda es el hecho de que la asignación de pila es, en general, más rápida que la asignación de montón. OTOH, no tiene que usar el montón: puede asignar desde un grupo, una arena o algo así, y eso puede ser casi tan rápido como la asignación de pilas, sin los problemas potenciales de asignación de pilas discutidos a continuación.
Sistemas integrados : Stack no es un recurso "infinito", ya sabes. Claro, para la mayoría de las aplicaciones en los sistemas operativos "regulares" actuales (POSIX, Windows...), casi lo es. Pero, en los sistemas integrados, la pila puede ser tan baja como unos pocos KB. Eso es extremo, pero incluso los sistemas integrados "grandes" tienen una pila que está en MB. Por lo tanto, se agotará si se usa en exceso. Cuando lo hace, en su mayoría no hay garantía de lo que sucederá: AFAIK, tanto en C como en C ++ eso es "Comportamiento indefinido". OTOH, CClass_create() puede devolver un puntero NULL cuando no tiene memoria, y puede manejar eso.
Contenedores : a los usuarios de C++ les gusta la asignación de pilas, pero si crea un std::vector en la pila, su contenido se asignará en montón. Puede modificar eso, por supuesto, pero ese es el comportamiento predeterminado, y hace que la vida sea mucho más fácil decir "todos los miembros de un contenedor están asignados al montón" en lugar de tratar de averiguar cómo manejar si no lo están.
Inercia : bueno, el OO vino de SmallTalk. Todo es dinámico allí, por lo que la traducción "natural" a C es la forma de "poner todo en el montón". Entonces, los primeros ejemplos fueron así e inspiraron a otros durante muchos años.
" Pereza ": si sabe que solo quiere apilar objetos, necesita algo como:
CClass CClass_make(); void CClass_deinit(CClass *me);Pero, si desea permitir tanto la pila como el montón, debe agregar:
CClass *CClass_create(); void CClass_destroy(CClass *me);Esto es más trabajo para el implementador, pero también es confuso para el usuario. Se pueden crear interfaces ligeramente diferentes, pero eso no cambia el hecho de que necesita dos conjuntos de funciones.
Por supuesto, la razón de los "contenedores" también es parcialmente una razón de "pereza".
Suponiendo, como en su pregunta, CClass_create y CClass_destroy usan malloc/free , entonces para mí hacer lo siguiente es una mala práctica:
void Myfunc() { CClass* myinstance = CClass_create(); ... CClass_destroy(myinstance); }porque podríamos evitar un malloc y un free fácilmente:
void Myfunc() { CClass myinstance; // no malloc needed here, myinstance is on the stack CClass_Initialize(&myinstance); ... CClass_Uninitialize(&myinstance); // no free needed here because myinstance is on the stack }con
CClass* CClass_create() { CClass *self= malloc(sizeof(CClass)); CClass_Initialize(self); return self; } void CClass_destroy(CClass *self); { CClass_Uninitialize(self); free(self); } void CClass_Initialize(CClass *self) { // initialize stuff ... } void CClass_Uninitialize(CClass *self); { // uninitialize stuff ... }En C++ también preferimos hacer esto:
void Myfunc() { CClass myinstance; ... }que esto:
void Myfunc() { CClass* myinstance = new CCLass; ... delete myinstance; } Para evitar un new / delete innecesario.
En C, cuando algún componente proporciona una función de "creación", el implementador del componente también controla cómo se inicializa el componente. Por lo tanto, no solo emula el operator new de C++, sino también el constructor de clases.
Renunciar a este control sobre la inicialización significa muchas más comprobaciones de errores en las entradas, por lo que mantener el control facilita proporcionar un comportamiento consistente y predecible.
También me opongo a que malloc siempre se use para asignar memoria. Esto puede ser a menudo el caso, pero no siempre. Por ejemplo, en algunos sistemas integrados, encontrará que malloc / free no se usa en absoluto. Las funciones X_create pueden asignarse de otras formas, por ejemplo, desde una matriz cuyo tamaño se fija en tiempo de compilación.