Siempre parece que escribo código en C que está orientado principalmente a objetos, así que digamos que tenía un archivo fuente o algo para crear una estructura y luego pasar el puntero a esta estructura a las funciones (métodos) que posee esta estructura:
struct foo {
int x;
};
struct foo* createFoo(); // mallocs foo
void destroyFoo(struct foo* foo); // frees foo and its things
¿Es esta una mala práctica? ¿Cómo aprendo a escribir C de la "manera correcta"?
Respuestas:
No, esto no es una mala práctica, incluso se recomienda hacerlo, aunque incluso se podrían usar convenciones como
struct foo *foo_new();
yvoid foo_free(struct foo *foo);
Por supuesto, como dice un comentario, solo haga esto cuando sea apropiado. No tiene sentido usar un constructor para un
int
.El prefijo
foo_
es una convención seguida de muchas bibliotecas, ya que protege contra choques con el nombre de otras bibliotecas. Otras funciones a menudo tienen la convención de usarfoo_<function>(struct foo *foo, <parameters>);
. Esto le permitestruct foo
ser de tipo opaco.Eche un vistazo a la documentación de libcurl para la convención, especialmente con "subnombres de espacios", de modo que llamar a una función se
curl_multi_*
vea mal a primera vista cuando el primer parámetro fue devuelto porcurl_easy_init()
.Hay enfoques aún más genéricos, consulte Programación orientada a objetos con ANSI-C
fuente
std::string
, ¿no podrías tenerfoo::create
? No uso C. ¿Tal vez eso es solo en C ++?No está mal, es excelente. Programación Orientada a Objetos es una buena cosa (a menos que se deje llevar, que puede tener demasiado de algo bueno). C no es el lenguaje más adecuado para OOP, pero eso no debería impedir que aproveches al máximo.
fuente
No está mal. Respalda el uso de RAII que evita muchos errores (pérdidas de memoria, uso de variables no inicializadas, uso después de free, etc., que pueden causar problemas de seguridad).
Entonces, si desea compilar su código solo con GCC o Clang (y no con el compilador de MS), puede usar el
cleanup
atributo, que destruirá adecuadamente sus objetos. Si declaras tu objeto así:Luego
my_str_destructor(ptr)
se ejecutará cuando ptr esté fuera de alcance. Solo tenga en cuenta que no se puede usar con argumentos de función .Además, recuerde usar
my_str_
los nombres de sus métodos, porqueC
no tiene espacios de nombres y es fácil colisionar con algún otro nombre de función.fuente
#define
sus nombres de tipo__attribute__((cleanup(my_str_destructor)))
, lo obtendrá como implícito en todo el#define
alcance (se agregará a todas sus declaraciones de variables).#define
tipo 'd o matrices de este). En resumen: no es el estándar C, y paga con mucha inflexibilidad en uso.__attribute__((cleanup()))
un cuasi-estándar. Sin embargo, b) yc) siguen en pie ...Puede haber muchas ventajas en dicho código, pero desafortunadamente el Estándar C no fue escrito para facilitarlo. Históricamente, los compiladores han ofrecido garantías de comportamiento efectivas más allá de lo que requería el Estándar, lo que hizo posible escribir dicho código de manera mucho más limpia de lo que es posible en el Estándar C, pero los compiladores han comenzado recientemente a revocar tales garantías en nombre de la optimización.
En particular, muchos compiladores de C han garantizado históricamente (por diseño, si no documentación) que si dos tipos de estructura contienen la misma secuencia inicial, se puede usar un puntero a cualquiera de los tipos para acceder a los miembros de esa secuencia común, incluso si los tipos no están relacionados, y además que para propósitos de establecer una secuencia inicial común, todos los apuntadores a estructuras son equivalentes. El código que hace uso de este comportamiento puede ser mucho más limpio y más seguro de tipo que el código que no lo hace, pero desafortunadamente, aunque el Estándar requiere que las estructuras que comparten una secuencia inicial común deben establecerse de la misma manera, prohíbe que el código realmente use un puntero de un tipo para acceder a la secuencia inicial de otro.
En consecuencia, si desea escribir código orientado a objetos en C, tendrá que decidir (y debe tomar esta decisión desde el principio) saltar entre muchos aros para cumplir con las reglas de tipo puntero de C y estar preparado para tener los compiladores modernos generan código sin sentido si uno se desliza hacia arriba, incluso si los compiladores más antiguos hubieran generado un código que funciona según lo previsto, o de lo contrario documentan el requisito de que el código solo se pueda usar con compiladores que estén configurados para admitir el comportamiento del puntero de estilo antiguo (por ejemplo, usando un "-fno-estricto-alias") Algunas personas consideran que "-fno-estricto-aliasing" es malo, pero sugeriría que es más útil pensar en "-fno-estricto-aliasing" C como un lenguaje que ofrece mayor poder semántico para algunos propósitos que el "estándar" C,pero a expensas de optimizaciones que pueden ser importantes para otros propósitos.
A modo de ejemplo, en los compiladores tradicionales, los compiladores históricos interpretarían el siguiente código:
siguiendo los siguientes pasos en orden: incremente el primer miembro de
*p
, complemente el bit más bajo del primer miembro de*t
, luego disminuya el primer miembro de*p
y complemente el bit más bajo del primer miembro de*t
. Los compiladores modernos reorganizarán la secuencia de operaciones de una manera que codifique, que será más eficientep
et
identificará diferentes objetos, pero cambiará el comportamiento si no lo hacen.Este ejemplo es, por supuesto, deliberadamente ideado, y en la práctica el código que usa un puntero de un tipo para acceder a miembros que son parte de la secuencia inicial común de otro tipo generalmente funcionará, pero desafortunadamente ya que no hay forma de saber cuándo podría fallar dicho código no es posible usarlo de manera segura, excepto deshabilitando el análisis de alias basado en tipo.
Un ejemplo algo menos artificial ocurriría si uno quisiera escribir una función para hacer algo como cambiar dos punteros a tipos arbitrarios. En la gran mayoría de los compiladores "C de la década de 1990", eso podría lograrse mediante:
En el Estándar C, sin embargo, uno debería usar:
Si
*p2
se mantiene en el almacenamiento asignado y el puntero temporal no se mantiene en el almacenamiento asignado, el tipo efectivo de*p2
se convertirá en el tipo del puntero temporal y el código que intenta usar*p2
como cualquier tipo que no coincida con el puntero temporal type invocará Comportamiento indefinido. Es seguro que es extremadamente improbable que un compilador se dé cuenta de tal cosa, pero dado que la filosofía moderna del compilador requiere que los programadores eviten el Comportamiento Indefinido a toda costa, no puedo pensar en ningún otro medio seguro de escribir el código anterior sin usar el almacenamiento asignado .fuente
foo_function(foo*, ...)
pseudo-OO en C es solo un estilo de API particular que parece clases, pero debería llamarse más adecuadamente programación modular con tipos de datos abstractos.El siguiente paso es ocultar la declaración de estructura. Pones esto en el archivo .h:
Y luego en el archivo .c:
fuente