¿Es malo escribir C orientado a objetos? [cerrado]

14

Parece que siempre escribo código en C que está mayormente orientado a objetos, así que digamos que tengo un archivo fuente o algo que crearía una estructura y luego pasar el puntero a esta estructura a las funciones (métodos) propiedad de esta estructura:

struct foo {
    int x;
};

struct foo* createFoo(); // mallocs foo

void destroyFoo(struct foo* foo); // frees foo and its things

¿Es esta mala práctica? ¿Cómo aprendo a escribir C de la "manera correcta"?

    
pregunta mosmo 28.01.2016 - 15:08

5 respuestas

24

No, esto no es una mala práctica, incluso se recomienda hacerlo, aunque incluso se podrían usar convenciones como struct foo *foo_new(); y void foo_free(struct foo *foo);

Por supuesto, como dice un comentario, solo haz esto cuando sea apropiado. No tiene sentido usar un constructor para un int .

El prefijo foo_ es una convención seguida por una gran cantidad de bibliotecas, ya que evita el choque con el nombramiento de otras bibliotecas. Otras funciones a menudo tienen la convención de usar foo_<function>(struct foo *foo, <parameters>); . Esto permite que su struct foo sea un tipo opaco.

Eche un vistazo a la documentación de libcurl para la convención, especialmente con "subnamespaces", de modo que llame a una función curl_multi_* se ve mal a primera vista cuando el primer parámetro fue devuelto por curl_easy_init() .

Hay incluso más enfoques genéricos, consulte Programación orientada a objetos con ANSI-C

    
respondido por el Residuum 28.01.2016 - 15:46
2

No está mal, es excelente. La programación orientada a objetos es una buena cosa (a menos que te dejes llevar, puedes tener demasiada cosa buena). C no es el lenguaje más adecuado para la POO, pero eso no debería impedirle sacar lo mejor de él.

    
respondido por el gnasher729 28.01.2016 - 17:47
0

No está mal. Suscribe el uso de RAII que previene muchos errores (pérdidas de memoria, uso de variables no inicializadas, uso después de la liberación, 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 atributo cleanup , que destruirá sus objetos correctamente. Si declara su objeto así:

my_str __attribute__((cleanup(my_str_destructor))) ptr;

Luego se ejecutará my_str_destructor(ptr) cuando ptr se salga del ámbito. Solo tenga en cuenta que no se puede usar con argumentos de función .

Además, recuerde usar my_str_ en los nombres de sus métodos, porque C no tiene espacios de nombres, y es fácil colisionar con algún otro nombre de función.

    
respondido por el Marqin 28.01.2016 - 16:26
-2

Este código puede tener muchas ventajas, pero desafortunadamente no se escribió el Estándar C para facilitarlo. Los compiladores históricamente han ofrecido garantías de comportamiento efectivas más allá de lo que exigía el Estándar que permitía escribir dicho código de forma mucho más limpia de lo que es posible en el Estándar C, pero los compiladores últimamente han comenzado a revocar tales garantías en nombre de la optimización.

Más notablemente, 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 cualquier tipo para acceder a los miembros de esa secuencia común, incluso si los tipos no están relacionados, y además, a los efectos de establecer una secuencia inicial común, todos los punteros a las estructuras son equivalentes. El código que hace uso de tal comportamiento puede ser mucho más limpio y más seguro con respecto al tipo que el código que no lo hace, pero desafortunadamente, aunque la Norma requiere que las estructuras que comparten una secuencia inicial común se distribuyan 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.

Por consiguiente, si desea escribir código orientado a objetos en C, tendrá que decidir (y debe tomar esta decisión desde el principio) saltar por un montón de aros para cumplir con las reglas de tipo puntero de C y ser preparado para que los compiladores modernos generen código sin sentido si uno se desliza hacia arriba, incluso si los compiladores más antiguos hubieran generado un código que funcione según lo previsto, o bien documente un requisito de que el código solo se podrá utilizar con compiladores que estén configurados para admitir el comportamiento de punteros de estilo antiguo (por ejemplo, usar un "-fno-strict-aliasing") Algunas personas consideran que "-fno-strict-aliasing" es malvado, pero sugeriría que es más útil pensar que "-fno-strict-aliasing" C es un lenguaje que ofrece mayor poder semántico para algunos propósitos que el C estándar, pero a expensas de optimizaciones que podrían ser importantes para otros propósitos.

Por ejemplo, en los compiladores tradicionales, los compiladores históricos interpretarían el siguiente código:

struct pair { int i1,i2; };
struct trio { int i1,i2,i3; };

void hey(struct pair *p, struct trio *t)
{
  p->i1++;
  t->i1^=1;
  p->i1--;
  t->i1^=1;
}

siguiendo los siguientes pasos en orden: aumente 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 de primer miembro de *t . Los compiladores modernos reorganizarán la secuencia de operaciones de manera tal que el código sea más eficiente si p y t identifican diferentes objetos, pero cambiarán el comportamiento si no lo hacen.

Por supuesto, este ejemplo está diseñado deliberadamente, y en la práctica, el código que usa un puntero de un tipo para acceder a los miembros que forman parte de la secuencia inicial común de otro tipo funcionará, por lo general, generalmente , pero desafortunadamente desde no hay forma de saber cuándo puede fallar ese código, no es posible usarlo de forma segura, excepto si se deshabilita el análisis de aliasing basado en tipos.

Un ejemplo un tanto menos artificial ocurriría si uno quisiera escribir una función para hacer algo como intercambiar dos punteros a tipos arbitrarios. En la gran mayoría de los compiladores de la "C de 1990", eso se podría lograr a través de:

void swap_pointers(void **p1, void **p2)
{
  void *temp = *p1;
  *p1 = *p2;
  *p2 = temp;
}

Sin embargo, en el Estándar C, uno tendría que usar:

#include "string.h"
#include "stdlib.h"
void swap_pointers2(void **p1, void **p2)
{
  void **temp = malloc(sizeof (void*));
  memcpy(temp, p1, sizeof (void*));
  memcpy(p1, p2, sizeof (void*));
  memcpy(p2, temp, sizeof (void*));
  free(temp);
}

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 tipo de puntero temporal invocará el comportamiento indefinido. Es seguro que es extremadamente improbable que un compilador se dé cuenta de algo así, pero como 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 para escribir el código anterior sin usar el almacenamiento asignado .

    
respondido por el supercat 28.01.2016 - 21:06
-3

El siguiente paso es ocultar la declaración de estructura. Pones esto en el archivo .h:

typedef struct foo_s foo_t;

foo_t * foo_new(...);
void foo_destroy(foo_t *foo);
some_type foo_whatever(foo_t *foo, ...);
...

Y luego en el archivo .c:

struct foo_s {
    ...
};
    
respondido por el Solomon Slow 28.01.2016 - 18:13

Lea otras preguntas en las etiquetas