Orientación a objetos en C

¿Qué sería un conjunto de improvisados ​​hacks de preprocesadores (compatible con ANSI C89 / ISO C90) que permiten algún tipo de orientación de objetos desagradables (pero utilizables) en C?

Estoy familiarizado con algunos lenguajes diferentes orientados a objetos, así que no respondas con respuestas como “¡Aprende C ++!”. He leído ” Progtwigción orientada a objetos con ANSI C ” (cuidado: formato PDF ) y muchas otras soluciones interesantes, pero estoy interesado principalmente en las suyas :-)!


Véase también ¿Se puede escribir código orientado a objetos en C?

C Object System (COS) suena prometedor (todavía está en versión alfa). Trata de mantener mínimos los conceptos disponibles por simplicidad y flexibilidad: progtwigción uniforme orientada a objetos que incluye clases abiertas, metaclases, metaclases de propiedades, generics, multimétodos, delegación, propiedad, excepciones, contratos y cierres. Hay un borrador (PDF) que lo describe.

La excepción en C es una implementación C89 de TRY-CATCH-FINALMENTE encontrada en otros lenguajes OO. Viene con una suite de pruebas y algunos ejemplos.

Ambos por Laurent Deniau, que está trabajando mucho en OOP en C.

Aconsejaría contra el uso del preprocesador (ab) para tratar de hacer que la syntax C sea más parecida a la de otro lenguaje más orientado a objetos. En el nivel más básico, solo usa estructuras simples como objetos y las pasa por punteros:

struct monkey { float age; bool is_male; int happiness; }; void monkey_dance(struct monkey *monkey) { /* do a little dance */ } 

Para obtener cosas como la herencia y el polymorphism, debes trabajar un poco más duro. Puede hacer herencia manual haciendo que el primer miembro de una estructura sea una instancia de la superclase, y luego puede convertir punteros a clases base y derivadas libremente:

 struct base { /* base class members */ }; struct derived { struct base super; /* derived class members */ }; struct derived d; struct base *base_ptr = (struct base *)&d; // upcast struct derived *derived_ptr = (struct derived *)base_ptr; // downcast 

Para obtener el polymorphism (es decir, funciones virtuales), utiliza punteros de función y, opcionalmente, tablas de puntero de función, también conocidas como tablas virtuales o tablas virtuales:

 struct base; struct base_vtable { void (*dance)(struct base *); void (*jump)(struct base *, int how_high); }; struct base { struct base_vtable *vtable; /* base members */ }; void base_dance(struct base *b) { b->vtable->dance(b); } void base_jump(struct base *b, int how_high) { b->vtable->jump(b, how_high); } struct derived1 { struct base super; /* derived1 members */ }; void derived1_dance(struct derived1 *d) { /* implementation of derived1's dance function */ } void derived1_jump(struct derived1 *d, int how_high) { /* implementation of derived 1's jump function */ } /* global vtable for derived1 */ struct base_vtable derived1_vtable = { &derived1_dance, /* you might get a warning here about incompatible pointer types */ &derived1_jump /* you can ignore it, or perform a cast to get rid of it */ }; void derived1_init(struct derived1 *d) { d->super.vtable = &derived1_vtable; /* init base members d->super.foo */ /* init derived1 members d->foo */ } struct derived2 { struct base super; /* derived2 members */ }; void derived2_dance(struct derived2 *d) { /* implementation of derived2's dance function */ } void derived2_jump(struct derived2 *d, int how_high) { /* implementation of derived2's jump function */ } struct base_vtable derived2_vtable = { &derived2_dance, &derived2_jump }; void derived2_init(struct derived2 *d) { d->super.vtable = &derived2_vtable; /* init base members d->super.foo */ /* init derived1 members d->foo */ } int main(void) { /* OK! We're done with our declarations, now we can finally do some polymorphism in C */ struct derived1 d1; derived1_init(&d1); struct derived2 d2; derived2_init(&d2); struct base *b1_ptr = (struct base *)&d1; struct base *b2_ptr = (struct base *)&d2; base_dance(b1_ptr); /* calls derived1_dance */ base_dance(b2_ptr); /* calls derived2_dance */ base_jump(b1_ptr, 42); /* calls derived1_jump */ base_jump(b2_ptr, 42); /* calls derived2_jump */ return 0; } 

Y así es como se hace el polymorphism en C. No es bonito, pero cumple su función. Hay algunos problemas persistentes que involucran lanzamientos de punteros entre clases base y derivadas, que son seguros siempre que la clase base sea el primer miembro de la clase derivada. La herencia múltiple es mucho más difícil; en ese caso, para el caso entre clases base distintas de la primera, debe ajustar manualmente los punteros en función de los desplazamientos adecuados, lo que es realmente complicado y propenso a errores.

Otra cosa (difícil) que puedes hacer es cambiar el tipo dynamic de un objeto en tiempo de ejecución. Usted acaba de reasignar un nuevo puntero vtable. Incluso puede cambiar selectivamente algunas de las funciones virtuales mientras mantiene otras, creando nuevos tipos híbridos. Solo tenga cuidado de crear un nuevo vtable en lugar de modificar el vtable global, de lo contrario, afectará accidentalmente a todos los objetos de un tipo determinado.

Una vez trabajé con una biblioteca C que se implementó de una manera que me pareció bastante elegante. Habían escrito, en C, una forma de definir objetos, y luego heredar de ellos para que fueran tan extensibles como un objeto de C ++. La idea básica fue esta:

  • Cada objeto tenía su propio archivo
  • Las funciones públicas y las variables se definen en el archivo .h para un objeto
  • Las variables y funciones privadas solo se ubicaron en el archivo .c
  • Para “heredar” una nueva estructura se crea con el primer miembro de la estructura siendo el objeto heredado de

Heredar es difícil de describir, pero básicamente fue esto:

 struct vehicle { int power; int weight; } 

Luego en otro archivo:

 struct van { struct vehicle base; int cubic_size; } 

Luego, podría tener una camioneta creada en la memoria y ser utilizada por un código que solo conocía sobre vehículos:

 struct van my_van; struct vehicle *something = &my_van; vehicle_function( something ); 

Funcionó muy bien, y los archivos .h definieron exactamente lo que debería poder hacer con cada objeto.

El escritorio de GNOME para Linux está escrito en C orientado a objetos, y tiene un modelo de objeto llamado ” GObject ” que admite propiedades, herencia, polymorphism, así como algunos otros extras como referencias, manejo de eventos (llamados “señales”), tiempo de ejecución tipeo, datos privados, etc.

Incluye hacks de preprocesador para hacer cosas como typecasting en la jerarquía de clases, etc. Aquí hay una clase de ejemplo que escribí para GNOME (cosas como gchar son typedefs):

Fuente de clase

Encabezado de clase

Dentro de la estructura de GObject hay un entero de GType que se usa como un número mágico para el sistema de tipado dynamic de GLib (puedes convertir toda la estructura en un “GType” para encontrar su tipo).

Si piensa en métodos llamados a los objetos como métodos estáticos que pasan un ” this ” implícito en la función, puede hacer que sea más fácil pensar en OO en C.

Por ejemplo:

 String s = "hi"; System.out.println(s.length()); 

se convierte en:

 string s = "hi"; printf(length(s)); // pass in s, as an implicit this 

O algo así.

Solía ​​hacer este tipo de cosas en C, antes de saber qué era OOP.

A continuación se muestra un ejemplo que implementa un buffer de datos que crece según demanda, dado un tamaño mínimo, incremento y tamaño máximo. Esta implementación en particular se basó en “elementos”, lo que quiere decir que fue diseñada para permitir una colección tipo lista de cualquier tipo C, no solo un byte-buffer de longitud variable.

La idea es que el objeto se instancia con el xxx_crt () y se borra con xxx_dlt (). Cada uno de los métodos “miembros” toma un puntero específicamente tipeado para operar.

Implementé una lista vinculada, un búfer cíclico y varias otras cosas de esta manera.

Debo confesar que nunca he pensado en cómo implementar la herencia con este enfoque. Me imagino que una combinación de lo que ofrece Kieveli podría ser un buen camino.

dtb.c:

 #include  #include  #include  static void dtb_xlt(void *dst, const void *src, vint len, const byte *tbl); DTABUF *dtb_crt(vint minsiz,vint incsiz,vint maxsiz) { DTABUF *dbp; if(!minsiz) { return NULL; } if(!incsiz) { incsiz=minsiz; } if(!maxsiz || maxsizmaxsiz) { incsiz=maxsiz-minsiz; } if((dbp=(DTABUF*)malloc(sizeof(*dbp))) == NULL) { return NULL; } memset(dbp,0,sizeof(*dbp)); dbp->min=minsiz; dbp->inc=incsiz; dbp->max=maxsiz; dbp->siz=minsiz; dbp->cur=0; if((dbp->dta=(byte*)malloc((vuns)minsiz)) == NULL) { free(dbp); return NULL; } return dbp; } DTABUF *dtb_dlt(DTABUF *dbp) { if(dbp) { free(dbp->dta); free(dbp); } return NULL; } vint dtb_adddta(DTABUF *dbp,const byte *xlt256,const void *dtaptr,vint dtalen) { if(!dbp) { errno=EINVAL; return -1; } if(dtalen==-1) { dtalen=(vint)strlen((byte*)dtaptr); } if((dbp->cur + dtalen) > dbp->siz) { void *newdta; vint newsiz; if((dbp->siz+dbp->inc)>=(dbp->cur+dtalen)) { newsiz=dbp->siz+dbp->inc; } else { newsiz=dbp->cur+dtalen; } if(newsiz>dbp->max) { errno=ETRUNC; return -1; } if((newdta=realloc(dbp->dta,(vuns)newsiz))==NULL) { return -1; } dbp->dta=newdta; dbp->siz=newsiz; } if(dtalen) { if(xlt256) { dtb_xlt(((byte*)dbp->dta+dbp->cur),dtaptr,dtalen,xlt256); } else { memcpy(((byte*)dbp->dta+dbp->cur),dtaptr,(vuns)dtalen); } dbp->cur+=dtalen; } return 0; } static void dtb_xlt(void *dst,const void *src,vint len,const byte *tbl) { byte *sp,*dp; for(sp=(byte*)src,dp=(byte*)dst; len; len--,sp++,dp++) { *dp=tbl[*sp]; } } vint dtb_addtxt(DTABUF *dbp,const byte *xlt256,const byte *format,...) { byte textÝ501¨; va_list ap; vint len; va_start(ap,format); len=sprintf_len(format,ap)-1; va_end(ap); if(len<0 || len>=sizeof(text)) { sprintf_safe(text,sizeof(text),"STRTOOLNG: %s",format); len=(int)strlen(text); } else { va_start(ap,format); vsprintf(text,format,ap); va_end(ap); } return dtb_adddta(dbp,xlt256,text,len); } vint dtb_rmvdta(DTABUF *dbp,vint len) { if(!dbp) { errno=EINVAL; return -1; } if(len > dbp->cur) { len=dbp->cur; } dbp->cur-=len; return 0; } vint dtb_reset(DTABUF *dbp) { if(!dbp) { errno=EINVAL; return -1; } dbp->cur=0; if(dbp->siz > dbp->min) { byte *newdta; if((newdta=(byte*)realloc(dbp->dta,(vuns)dbp->min))==NULL) { free(dbp->dta); dbp->dta=null; dbp->siz=0; return -1; } dbp->dta=newdta; dbp->siz=dbp->min; } return 0; } void *dtb_elmptr(DTABUF *dbp,vint elmidx,vint elmlen) { if(!elmlen || (elmidx*elmlen)>=dbp->cur) { return NULL; } return ((byte*)dbp->dta+(elmidx*elmlen)); } 

dtb.h

 typedef _Packed struct { vint min; /* initial size */ vint inc; /* increment size */ vint max; /* maximum size */ vint siz; /* current size */ vint cur; /* current data length */ void *dta; /* data pointer */ } DTABUF; #define dtb_dtaptr(mDBP) (mDBP->dta) #define dtb_dtalen(mDBP) (mDBP->cur) DTABUF *dtb_crt(vint minsiz,vint incsiz,vint maxsiz); DTABUF *dtb_dlt(DTABUF *dbp); vint dtb_adddta(DTABUF *dbp,const byte *xlt256,const void *dtaptr,vint dtalen); vint dtb_addtxt(DTABUF *dbp,const byte *xlt256,const byte *format,...); vint dtb_rmvdta(DTABUF *dbp,vint len); vint dtb_reset(DTABUF *dbp); void *dtb_elmptr(DTABUF *dbp,vint elmidx,vint elmlen); 

PD: vint fue simplemente un tipo de int. Lo usé para recordarme que su longitud era variable de una plataforma a otra (para portar).

Un poco fuera de tema, pero el comstackdor original de C ++, Cfront , compiló C ++ a C y luego al ensamblador.

Conservado aquí .

ffmpeg (un conjunto de herramientas para el procesamiento de video) está escrito en C directo (y lenguaje ensamblador), pero utilizando un estilo orientado a objetos. Está lleno de estructuras con indicadores de función. Hay un conjunto de funciones de fábrica que inicializan las estructuras con los punteros de “método” apropiados.

Si realmente piensas de forma cauta, incluso la biblioteca C estándar usa OOP – considera FILE * como un ejemplo: fopen() inicializa un objeto FILE * , y lo usas usa los métodos member fscanf() , fprintf() , fread() , fwrite() y otros, y finalmente finalizarlo con fclose() .

También puede ir con la forma pseudo-Objective-C, que tampoco es difícil:

 typedef void *Class; typedef struct __class_Foo { Class isa; int ivar; } Foo; typedef struct __meta_Foo { Foo *(*alloc)(void); Foo *(*init)(Foo *self); int (*ivar)(Foo *self); void (*setIvar)(Foo *self); } meta_Foo; meta_Foo *class_Foo; void __meta_Foo_init(void) __attribute__((constructor)); void __meta_Foo_init(void) { class_Foo = malloc(sizeof(meta_Foo)); if (class_Foo) { class_Foo = {__imp_Foo_alloc, __imp_Foo_init, __imp_Foo_ivar, __imp_Foo_setIvar}; } } Foo *__imp_Foo_alloc(void) { Foo *foo = malloc(sizeof(Foo)); if (foo) { memset(foo, 0, sizeof(Foo)); foo->isa = class_Foo; } return foo; } Foo *__imp_Foo_init(Foo *self) { if (self) { self->ivar = 42; } return self; } // ... 

Usar:

 int main(void) { Foo *foo = (class_Foo->init)((class_Foo->alloc)()); printf("%d\n", (foo->isa->ivar)(foo)); // 42 foo->isa->setIvar(foo, 60); printf("%d\n", (foo->isa->ivar)(foo)); // 60 free(foo); } 

Esto es lo que puede resultar de algún código de Objective-C como este, si se usa un traductor de Objective-C-to-C bastante viejo:

 @interface Foo : NSObject { int ivar; } - (int)ivar; - (void)setIvar:(int)ivar; @end @implementation Foo - (id)init { if (self = [super init]) { ivar = 42; } return self; } @end int main(void) { Foo *foo = [[Foo alloc] init]; printf("%d\n", [foo ivar]); [foo setIvar:60]; printf("%d\n", [foo ivar]); [foo release]; } 

Creo que lo que Adam Rosenfield publicó es la forma correcta de hacer OOP en C. Me gustaría añadir que lo que muestra es la implementación del objeto. En otras palabras, la implementación real se colocará en el archivo .c , mientras que la interfaz se colocará en el archivo .h encabezado. Por ejemplo, usando el ejemplo de mono arriba:

La interfaz se vería así:

 //monkey.h struct _monkey; typedef struct _monkey monkey; //memory management monkey * monkey_new(); int monkey_delete(monkey *thisobj); //methods void monkey_dance(monkey *thisobj); 

Puede ver en el archivo .h interfaz que solo está definiendo prototipos. A continuación, puede comstackr la parte de implementación “archivo .c ” en una biblioteca estática o dinámica. Esto crea encapsulación y también puede cambiar la implementación a voluntad. El usuario de su objeto no necesita saber casi nada sobre su implementación. Esto también enfoca el diseño general del objeto.

Personalmente creo que oop es una forma de conceptualizar la estructura del código y la reutilización, y realmente no tiene nada que ver con esas otras cosas que se agregan a C ++, como la sobrecarga o las plantillas. Sí, esas son funciones útiles muy útiles, pero no son representativas de lo que realmente es la progtwigción orientada a objetos.

Mi recomendación: mantenlo simple. Uno de los mayores problemas que tengo es mantener un software más antiguo (a veces más de 10 años). Si el código no es simple, puede ser difícil. Sí, uno puede escribir OOP muy útil con polymorphism en C, pero puede ser difícil de leer.

Prefiero objetos simples que encapsulan algunas funcionalidades bien definidas. Un buen ejemplo de esto es GLIB2 , por ejemplo, una tabla hash:

 GHastTable* my_hash = g_hash_table_new(g_str_hash, g_str_equal); int size = g_hash_table_size(my_hash); ... g_hash_table_remove(my_hash, some_key); 

Las claves son:

  1. Arquitectura simple y patrón de diseño
  2. Logra la encapsulación básica de OOP.
  3. Fácil de implementar, leer, comprender y mantener

Si fuera a escribir OOP en CI probablemente iría con un diseño pseudo- Pimpl . En lugar de pasar punteros a las estructuras, terminas pasando punteros a punteros para construir estructuras. Esto hace que el contenido sea opaco y facilita el polymorphism y la herencia.

El problema real con OOP en C es qué sucede cuando las variables salen del scope. No hay destructores generados por el comstackdor y eso puede causar problemas. Las macros pueden ayudar, pero siempre va a ser feo observarlo.

 #include "triangle.h" #include "rectangle.h" #include "polygon.h" #include  int main() { Triangle tr1= CTriangle->new(); Rectangle rc1= CRectangle->new(); tr1->width= rc1->width= 3.2; tr1->height= rc1->height= 4.1; CPolygon->printArea((Polygon)tr1); printf("\n"); CPolygon->printArea((Polygon)rc1); } 

Salida:

 6.56 13.12 

Aquí hay una muestra de lo que es la progtwigción de OO con C.

Esta es C real, pura, sin macros de preprocesador. Tenemos herencia, polymorphism y encapsulación de datos (incluidos datos privados para clases u objetos). No hay ninguna posibilidad de que el calificador protegido sea equivalente, es decir, los datos privados también son privados en la cadena de innumerancias. Pero esto no es un inconveniente porque no creo que sea necesario.

CPolygon no se CPolygon una instancia porque solo lo usamos para manipular objetos de la cadena de innderitance que tienen aspectos comunes pero una implementación diferente de ellos (polymorphism).

@Adam Rosenfield tiene una muy buena explicación de cómo lograr OOP con C

Además, te recomendaría que leas

1) pjsip

Una muy buena biblioteca de C para VoIP. Puede aprender cómo logra OOP a través de estructuras y tablas de punteros de función

2) iOS Runtime

Descubra cómo iOS Runtime potencia el Objetivo C. Logra OOP a través de un puntero isa, clase meta

Para mí, la orientación de objetos en C debería tener estas características:

  1. Encapsulación y ocultación de datos (se puede lograr usando structs / punteros opacos)

  2. Herencia y soporte para el polymorphism (la herencia individual se puede lograr usando estructuras – asegúrese de que la base abstracta no sea instanciable)

  3. Funcionalidad de constructor y destructor (no es fácil de lograr)

  4. Comprobación de tipos (al menos para los tipos definidos por el usuario, ya que C no impone ninguno)

  5. Recuento de referencias (o algo para implementar RAII )

  6. Soporte limitado para manejo de excepciones (setjmp y longjmp)

Además de lo anterior, debe confiar en las especificaciones ANSI / ISO y no debe confiar en la funcionalidad específica del comstackdor.

Mira http://ldeniau.web.cern.ch/ldeniau/html/oopc/oopc.html . Si nada más leer la documentación es una experiencia esclarecedora.

Llego un poco tarde a la fiesta aquí, pero me gusta evitar los dos extremos: demasiados o demasiados códigos ofuscados, pero un par de macros obvias pueden hacer que el código OOP sea más fácil de desarrollar y leer:

 /* * OOP in C * * gcc -o oop oop.c */ #include  #include  #include  struct obj2d { float x; // object center x float y; // object center y float (* area)(void *); }; #define X(obj) (obj)->b1.x #define Y(obj) (obj)->b1.y #define AREA(obj) (obj)->b1.area(obj) void * _new_obj2d(int size, void * areafn) { struct obj2d * x = calloc(1, size); x->area = areafn; // obj2d constructor code ... return x; } // -------------------------------------------------------- struct rectangle { struct obj2d b1; // base class float width; float height; float rotation; }; #define WIDTH(obj) (obj)->width #define HEIGHT(obj) (obj)->height float rectangle_area(struct rectangle * self) { return self->width * self->height; } #define NEW_rectangle() _new_obj2d(sizeof(struct rectangle), rectangle_area) // -------------------------------------------------------- struct triangle { struct obj2d b1; // deliberately unfinished to test error messages }; #define NEW_triangle() _new_obj2d(sizeof(struct triangle), triangle_area) // -------------------------------------------------------- struct circle { struct obj2d b1; float radius; }; #define RADIUS(obj) (obj)->radius float circle_area(struct circle * self) { return M_PI * self->radius * self->radius; } #define NEW_circle() _new_obj2d(sizeof(struct circle), circle_area) // -------------------------------------------------------- #define NEW(objname) (struct objname *) NEW_##objname() int main(int ac, char * av[]) { struct rectangle * obj1 = NEW(rectangle); struct circle * obj2 = NEW(circle); X(obj1) = 1; Y(obj1) = 1; // your decision as to which of these is clearer, but note above that // macros also hide the fact that a member is in the base class WIDTH(obj1) = 2; obj1->height = 3; printf("obj1 position (%f,%f) area %f\n", X(obj1), Y(obj1), AREA(obj1)); X(obj2) = 10; Y(obj2) = 10; RADIUS(obj2) = 1.5; printf("obj2 position (%f,%f) area %f\n", X(obj2), Y(obj2), AREA(obj2)); // WIDTH(obj2) = 2; // error: struct circle has no member named width // struct triangle * obj3 = NEW(triangle); // error: triangle_area undefined } 

Creo que esto tiene un buen equilibrio, y los errores que genera (al menos con las opciones predeterminadas de gcc 6.3) para algunos de los errores más probables son útiles en lugar de confusos. El objective es mejorar la productividad del progtwigdor, ¿no?

Si necesita escribir un pequeño código, intente esto: https://github.com/fulminati/class-framework

 #include "class-framework.h" CLASS (People) { int age; }; int main() { People *p = NEW (People); p->age = 10; printf("%d\n", p->age); }