Orientación a objetos en C

157

¿Qué sería un conjunto de ingeniosos hacks de preprocesador (compatible con ANSI C89 / ISO C90) que permiten algún tipo de orientación de objeto feo (pero utilizable) en C?

Estoy familiarizado con algunos lenguajes diferentes orientados a objetos, así que no responda con respuestas como "¡Aprenda C ++!". He leído " Programación orientada a objetos con ANSI C " (cuidado: formato PDF ) y varias otras soluciones interesantes, ¡pero estoy interesado principalmente en la suya :-)!


Consulte también ¿Puede escribir código orientado a objetos en C?

Anthony Cuozzo
fuente
1
¿Puedo responder para aprender D y usar el abi compatible con c para donde realmente necesitas C. digitalmars.com/d
Tim Matthews
2
@Dinah: Gracias por el "Ver también". Esa publicación fue interesante.
1
La pregunta interesante parece ser ¿por qué quiere un corte pre-procesador de programación orientada a objetos en C
Calyth
3
@Calyth: Encuentro que OOP es útil y "Trabajo con algunos sistemas integrados que realmente solo tienen un compilador de C disponible" (desde arriba). Además, ¿no te parecen interesantes los ingeniosos hacks de preprocesadores?
2
Posible duplicado de ¿Puedes escribir código orientado a objetos en C?
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

Respuestas:

31

C Object System (COS) suena prometedor (todavía está en versión alfa). Trata de minimizar los conceptos disponibles en aras de la simplicidad y la flexibilidad: programación orientada a objetos uniforme que incluye clases abiertas, metaclases, metaclases de propiedad, genéricos, métodos múltiples, 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-FINALLY que se encuentra en otros lenguajes OO. Viene con una suite de prueba y algunos ejemplos.

Tanto por Laurent Deniau, que está trabajando mucho en la programación orientada a objetos en C .

philant
fuente
@vonbrand COS migró a github donde el último commit es el verano pasado. La madurez puede explicar la falta de compromiso.
frágil
185

Aconsejaría contra el uso del preprocesador (ab) para tratar de hacer que la sintaxis de 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 herencia y polimorfismo, debes trabajar un poco más duro. Puede hacer una herencia manual haciendo que el primer miembro de una estructura sea una instancia de la superclase, y luego puede lanzar punteros alrededor de las 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 polimorfismo (es decir, funciones virtuales), utiliza punteros de función y, opcionalmente, tablas de puntero de función, también conocidas como tablas virtuales o vtables:

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 polimorfismo en C. No es bonito, pero hace el trabajo. Hay algunos problemas difíciles relacionados con la conversión de punteros entre las clases base y derivada, 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 diferenciar entre clases base distintas de la primera, debe ajustar manualmente los punteros en función de las compensaciones adecuadas, lo que es realmente complicado y propenso a errores.

¡Otra cosa (difícil) que puede hacer es cambiar el tipo dinámico de un objeto en tiempo de ejecución! Simplemente reasigne 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 una nueva vtable en lugar de modificar la vtable global, de lo contrario, afectará accidentalmente a todos los objetos de un tipo determinado.

Adam Rosenfield
fuente
66
Adam, la diversión de cambiar la tabla global de un tipo es simular la escritura del pato en C. :)
jmucchiello
Ahora me da pena C ++ ... Bueno, por supuesto, la sintaxis de C ++ es más clara, pero como no es una sintaxis trivial, estoy mitigada. Me pregunto si se podría lograr algo híbrido entre C ++ y C, por lo que void * seguiría siendo un tipo de molde válido. La parte con struct derived {struct base super;};es obvia para adivinar cómo funciona, ya que por orden de bytes es correcta.
jokoon
2
+1 para código elegante, bien escrito. ¡Esto es exactamente lo que estaba buscando!
Homunculus Reticulli
3
Bien hecho. Así es exactamente como lo he estado haciendo y es la forma correcta también. En lugar de requerir un puntero a la estructura / objeto en mente, solo debe pasar un puntero a un entero (dirección). Esto le permitiría pasar cualquier tipo de objeto para llamadas de método polimórfico ilimitadas. Además, lo único que falta es una función para inicializar sus estructuras (objetos / clases). Esto incluiría una función malloc y devolvería un puntero. Tal vez
1
Esta es la gota que colmó el vaso de C ++, y usar C más (antes solo usaba C ++ como herencia) Gracias
Anne Quinn
31

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, luego heredaron de ellos para que fueran tan extensibles como un objeto C ++. La idea básica era esta:

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

La herencia 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;
}

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

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

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

Kieveli
fuente
Realmente me gusta esta solución, excepto que todas las partes internas del "objeto" son públicas.
Lawrence Dol el
66
@Software Monkey: C no tiene control de acceso. La única forma de ocultar los detalles de la implementación es interactuar a través de punteros opacos, lo que puede ser bastante doloroso, ya que sería necesario acceder a todos los campos a través de métodos de acceso que probablemente no se pueden insertar.
Adam Rosenfield el
1
@ Adam: los compiladores que admiten optimizaciones de tiempo de enlace los alinearán bien ...
Christoph
9
Si hace esto, también debe asegurarse de que todas las funciones en el archivo .c que no están definidas como públicas se definan como estáticas para que no terminen como funciones con nombre en sus archivos de objeto. Eso asegura que nadie pueda encontrar sus nombres en la fase de enlace.
jmucchiello
2
@Marcel: C se usó porque el código se implementó en placas de bajo nivel que ejecutan una variedad de procesadores para sistemas autónomos. Todos soportaron la compilación de C a sus respectivos binarios nativos. El enfoque hizo que el código fuera muy fácil de leer una vez que se dio cuenta de lo que estaban tratando de hacer.
Kieveli
18

El escritorio GNOME para Linux está escrito en C orientado a objetos y tiene un modelo de objeto llamado " GObject " que admite propiedades, herencia, polimorfismo, así como algunas otras ventajas como referencias, manejo de eventos (llamadas "señales"), tiempo de ejecución mecanografía, datos privados, etc.

Incluye hacks de preprocesador para hacer cosas como la conversión de tipos 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 GObject hay un número entero GType que se utiliza como un número mágico para el sistema de escritura dinámica de GLib (puede convertir toda la estructura en un "GType" para encontrar su tipo).

James Cape
fuente
desafortunadamente, el archivo read me / tutorial (enlace wiki) no funciona y solo hay un manual de referencia para eso (estoy hablando de GObject y no GTK). por favor proporcione algunos archivos del tutorial para el mismo ...
FL4SOF
Los enlaces han sido corregidos.
James Cape
44
Los enlaces están rotos de nuevo.
SeanRamey
6

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

El siguiente es un ejemplo, que implementa un búfer de datos que crece bajo demanda, dado un tamaño mínimo, incremento y tamaño máximo. Esta implementación particular se basó en "elementos", es decir, fue diseñada para permitir una colección tipo lista de cualquier tipo C, no solo un búfer de bytes de longitud variable.

La idea es que el objeto se instancia usando xxx_crt () y se elimina usando xxx_dlt (). Cada uno de los métodos "miembro" toma un puntero específicamente tipado para operar.

Implementé una lista vinculada, un búfer cíclico y 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 ofrecido por Kieveli podría ser un buen camino.

dtb.c:

#include <limits.h>
#include <string.h>
#include <stdlib.h>

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 || maxsiz<minsiz) { maxsiz=minsiz;        }
    if(minsiz+incsiz>maxsiz)     { 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 era simplemente un typedef de int: lo usé para recordarme que su longitud era variable de una plataforma a otra (para portar).

Lawrence Dol
fuente
77
Holy Moly, ¡esto podría ganar un concurso de C ofuscado! ¡me gusta! :)
horseyguy
@horseyguy No, no podría. Ha sido publicado. También consideran la inclusión del abuso de archivos de encabezado contra la herramienta iocccsize. Tampoco es un programa completo. 2009 no tuvo concurso, así que no puedo comparar el iocccsize. El CPP ha sido abusado muchas veces también, por lo que es bastante antiguo. Etc. Lo siento. No estoy tratando de ser negativo, pero soy realista. Sin embargo, entiendo tu significado y es una buena lectura y lo he votado. (Y sí, participo en él y sí, yo también gano.)
Pryftan
6

Ligeramente fuera de tema, pero el compilador original de C ++, Cfront , compiló C ++ a C y luego al ensamblador.

Preservado aquí .

zebrabox
fuente
En realidad lo he visto antes. Creo que fue un buen trabajo.
@Anthony Cuozzo: Stan Lippman escribió un gran libro llamado 'C ++ - Inside the object model' donde relató muchas de sus experiencias y decisiones de diseño al escribir y mantener c-front. Es todavía una buena lectura y me ayudó enormemente cuando la transición de C a C ++ muchos años atrás
zebrabox
5

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

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í.

jjnguy
fuente
66
@Artelius: Claro, pero a veces lo obvio no lo es, hasta que se indique. +1 por esto.
Lawrence Dol el
1
mejor aún seríastring->length(s);
OozeMeister
4

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

Señor fooz
fuente
no veo ninguna función de fábrica en él (ffmpeg), más bien no parece estar usando polimorfismo / herencia (forma trivial sugerida anteriormente).
FL4SOF
avcodec_open es una función de fábrica. Rellena los punteros de función en una estructura AVCodecContext (como draw_horiz_band). Si observa el uso de la macro FF_COMMON_FRAME en avcodec.h, verá algo similar a la herencia de los miembros de datos. En mi humilde opinión, ffmpeg me demuestra que la OOP se hace mejor en C ++, no C.
Mr Fooz
3

Si realmente piensa catefully, incluso estándar de uso de la biblioteca C POO - considerar FILE *como un ejemplo: fopen()inicializa un FILE *objeto, y lo usa utilizar métodos miembros fscanf(), fprintf(), fread(), fwrite()y otros, y finalmente finalizar con fclose().

También puede usar la forma pseudo-Objetivo-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 Objective-C como este, si se usa un traductor Objective-C-to-C bastante antiguo:

@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];
}
Maxthon Chan
fuente
¿Qué hace __attribute__((constructor))en void __meta_Foo_init(void) __attribute__((constructor))?
AE Drew
1
Esta es una extensión de GCC que asegurará que se llame a la función marcada cuando el binario se carga en la memoria. @AEDrew
Maxthon Chan
popen(3)también devuelve un FILE *para otro ejemplo.
Pryftan
3

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

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 .harchivo de interfaz que solo está definiendo prototipos. Luego puede compilar la parte de implementación " .carchivo" en una biblioteca estática o dinámica. Esto crea la encapsulación y también puede cambiar la implementación a voluntad. El usuario de su objeto no necesita saber casi nada acerca de su implementación. Esto también pone el foco en el diseño general del objeto.

Creo personalmente que oop es una forma de conceptualizar la estructura y la reutilización de su código 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 características útiles muy buenas, pero no son representativas de lo que realmente es la programación orientada a objetos.

usuario2074102
fuente
Puede declarar una estructura con typedef struct Monkey {} Monkey; ¿Cuál es el punto de escribirla después de haberla creado?
MarcusJ
1
@MarcusJ The struct _monkeyes simplemente un prototipo. La definición de tipo real se define en el archivo de implementación (el archivo .c). Esto crea el efecto de encapsulación y permite al desarrollador de API redefinir la estructura del mono en el futuro sin modificar la API. Los usuarios de la API solo deben preocuparse por los métodos reales. El diseñador de API se encarga de la implementación, incluida la forma en que se presenta el objeto / estructura. Por lo tanto, los detalles del objeto / estructura están ocultos para el usuario (un tipo opaco).
Defino mis estructuras en los encabezados, ¿no es esto estándar? Bueno, lo hago así porque ocasionalmente necesito acceder a miembros de la estructura fuera de esa biblioteca.
MarcusJ
1
@ MarcusJ Puede definir sus estructuras en los encabezados si lo desea (no hay un estándar). Pero si desea cambiar su estructura interna en el futuro, puede romper su código. La encapsulación es simplemente un estilo de codificación que hace que sea más fácil cambiar una implementación sin romper el código. Siempre puede acceder a sus miembros a través de métodos de acceso como int getCount(ObjectType obj)etc. si elige definir la estructura en el archivo de implementación.
2

Mi recomendación: que sea sencillo. Uno de los mayores problemas que tengo es el mantenimiento de software 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 polimorfismo en C, pero puede ser difícil de leer.

Prefiero objetos simples que encapsulan una funcionalidad bien definida. Un gran 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
Wadester
fuente
1

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

El verdadero problema con OOP en C es lo que sucede cuando las variables salen del alcance. No hay destructores generados por el compilador y eso puede causar problemas. Las macros posiblemente pueden ayudar, pero siempre será feo mirarlas.

jmucchiello
fuente
1
Cuando programo en C, trato con el alcance mediante el uso de ifdeclaraciones y su liberación al final. Por ejemploif ( (obj = new_myObject()) ) { /* code using myObject */ free_myObject(obj); }
1

Otra forma de programar en un estilo orientado a objetos con C es usar un generador de código que transforma un lenguaje específico de dominio en C. Como se hace con TypeScript y JavaScript para llevar OOP a js.

Luca Wienert
fuente
0
#include "triangle.h"
#include "rectangle.h"
#include "polygon.h"

#include <stdio.h>

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 un programa de lo que es la programación OO con C.

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

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

rogergc
fuente
0

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

Además, te recomendaría que leyeras

1) pjsip

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

2) iOS Runtime

Aprenda cómo iOS Runtime impulsa el Objetivo C. Alcanza la POO a través de un puntero isa, metaclase

onmyway133
fuente
0

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 utilizando estructuras / punteros opacos)

  2. Herencia y soporte para polimorfismo (la herencia única 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. Verificació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 el manejo de excepciones (setjmp y longjmp)

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

FL4SOF
fuente
Para el número (5): no puede implementar RAII en un lenguaje sin destructores (lo que significa que RAII no es una técnica compatible con el compilador en C o Java).
Tom
se pueden escribir constructores y destructores para objetos basados ​​en c, supongo que GObject lo hace. y, por supuesto, RAAI (no es sencillo, puede ser feo y no necesita ser pragmático): todo lo que estaba buscando es identificar la semántica basada en C para lograr lo anterior.
FL4SOF
C no admite destructores. Tienes que escribir algo para que funcionen. Eso significa que no se limpian ellos mismos. GObject no cambia el idioma.
Tom
0

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

Tim
fuente
3
Proporcione contexto para el enlace que está compartiendo. Aunque el enlace que compartió puede ser muy útil, es recomendable capturar los aspectos clave del artículo compartido que responden a la pregunta. De esta manera, incluso si se elimina el enlace, su respuesta seguirá siendo relevante y útil.
ishmaelMakitla
0

Llego un poco tarde a la fiesta aquí, pero me gusta evitar ambos extremos de macro: demasiados o demasiado código ofuscado, 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 <stdio.h>
#include <stdlib.h>
#include <math.h>

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 punto es mejorar la productividad del programador, ¿no?

revs duanev
fuente
0

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);
}
revs cicciodarkast
fuente
2
Por favor, no solo publique alguna herramienta o biblioteca como respuesta. Al menos demuestre cómo resuelve el problema en la respuesta misma.
Baum mit Augen
0

También estoy trabajando en esto basado en una solución macro. Supongo que es solo para los más valientes ;-) Pero ya es bastante agradable y ya estoy trabajando en algunos proyectos. Funciona para que primero defina un archivo de encabezado separado para cada clase. Me gusta esto:

#define CLASS Point
#define BUILD_JSON

#define Point__define                            \
    METHOD(Point,public,int,move_up,(int steps)) \
    METHOD(Point,public,void,draw)               \
                                                 \
    VAR(read,int,x,JSON(json_int))               \
    VAR(read,int,y,JSON(json_int))               \

Para implementar la clase, crea un archivo de encabezado y un archivo C donde implementa los métodos:

METHOD(Point,public,void,draw)
{
    printf("point at %d,%d\n", self->x, self->y);
}

En el encabezado que creó para la clase, incluye otros encabezados que necesita y define los tipos, etc. relacionados con la clase. Tanto en el encabezado de la clase como en el archivo C incluye el archivo de especificación de la clase (consulte el primer ejemplo de código) y una macro X. Estas macros X ( 1 , 2 , 3 etc.) expandirán el código a las estructuras de clase reales y otras declaraciones.

Para heredar una clase #define SUPER supernamey agregarsupername__define \ como la primera línea en la definición de clase. Ambos deben estar ahí. También hay soporte JSON, señales, clases abstractas, etc.

Para crear un objeto, solo use W_NEW(classname, .x=1, .y=2,...) . La inicialización se basa en la inicialización de estructura introducida en C11. Funciona bien y todo lo que no está en la lista se establece en cero.

Para llamar a un método, use W_CALL(o,method)(1,2,3). Parece una llamada de función de orden superior, pero es solo una macro. Se expande a((o)->klass->method(o,1,2,3)) que es un truco realmente agradable.

Ver documentación y el código en sí .

Como el marco necesita un código repetitivo, escribí un script de Perl (wobject) que hace el trabajo. Si usas eso, puedes escribir

class Point
    public int move_up(int steps)
    public void draw()
    read int x
    read int y

y creará el archivo de especificación de clase, el encabezado de clase y un archivo C, que incluye Point_impl.cdónde implementa la clase. Ahorra bastante trabajo, si tiene muchas clases simples pero aún así todo está en C. wobject es un escáner basado en expresiones regulares muy simple que es fácil de adaptar a necesidades específicas, o reescribir desde cero.

J.P.
fuente
0

Puede probar COOP , un marco amigable para programadores para OOP en C, que presenta clases, excepciones, polimorfismo y administración de memoria (importante para el código incrustado). Es una sintaxis relativamente ligera, vea el tutorial en la Wiki allí.

Shmuel Fine
fuente