Orientação a objects em C

O que seria um conjunto de hacks de pré-processamento bacana (compatível com ANSI C89 / ISO C90) que permitem algum tipo de orientação a object feia (mas utilizável) em C?

Estou familiarizado com algumas linguagens orientadas a object, então, por favor, não responda com respostas como “Aprenda C ++!”. Eu li ” Programação orientada a object com ANSI C ” (cuidado: formato PDF ) e várias outras soluções interessantes, mas estou mais interessado no seu :-)!


Veja também Você pode escrever código orientado a objects em C?

C Object System (COS) parece promissor (ainda está na versão alfa). Ele tenta manter o mínimo dos conceitos disponíveis por uma questão de simplicidade e flexibilidade: programação uniforme orientada a objects, incluindo classs abertas, metaclasss, metaclasss de propriedade, genéricos, multimethods, delegação, propriedade, exceções, contratos e encerramentos. Existe um documento de rascunho (PDF) que o descreve.

A exceção em C é uma implementação do C89 do TRY-CATCH-FINALLY encontrada em outros idiomas OO. Vem com um testuite e alguns exemplos.

Ambos por Laurent Deniau, que está trabalhando muito em OOP em C.

Eu aconselharia o uso do pré-processador (ab) para tentar tornar a syntax C mais parecida com a de outra linguagem orientada a objects. No nível mais básico, você apenas usa estruturas simples como objects e as passa por pointers:

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

Para obter coisas como inheritance e polymorphism, você precisa trabalhar um pouco mais. Você pode fazer inheritance manual fazendo com que o primeiro membro de uma estrutura seja uma instância da superclass, e então você pode girar livremente em torno de pointers para classs base e derivadas:

 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 obter polymorphism (ou seja, funções virtuais), você usa pointers de function e, opcionalmente, tabelas de ponteiro de function, também conhecidas como tabelas virtuais ou 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; } 

E é assim que você faz o polymorphism em C. Não é bonito, mas faz o trabalho. Há alguns problemas persistentes que envolvem conversões de ponteiro entre classs base e derivadas, que são seguras, desde que a class base seja o primeiro membro da class derivada. A inheritance múltipla é muito mais difícil – nesse caso, para se situar entre classs base diferentes da primeira, você precisa ajustar manualmente seus pointers com base nos deslocamentos adequados, o que é realmente complicado e propenso a erros.

Outra coisa (complicada) que você pode fazer é mudar o tipo dynamic de um object em tempo de execução! Você acabou de reatribuir um novo ponteiro vtable. Você pode até mesmo alterar seletivamente algumas das funções virtuais enquanto mantém outras, criando novos tipos de híbridos. Apenas tenha cuidado para criar uma nova vtable em vez de modificar a vtable global, caso contrário, você afetará acidentalmente todos os objects de um determinado tipo.

Certa vez trabalhei com uma biblioteca C que foi implementada de uma maneira que me pareceu bastante elegante. Eles tinham escrito, em C, uma maneira de definir objects e, em seguida, herdá-los para que fossem tão extensíveis quanto um object C ++. A ideia básica era esta:

  • Cada object tinha seu próprio arquivo
  • Funções e variables ​​públicas são definidas no arquivo .h para um object
  • Variáveis ​​e funções privadas foram localizadas apenas no arquivo .c
  • Para “herdar” uma nova estrutura é criada com o primeiro membro da estrutura sendo o object a herdar de

Herdar é difícil de descrever, mas basicamente foi isso:

 struct vehicle { int power; int weight; } 

Então em outro arquivo:

 struct van { struct vehicle base; int cubic_size; } 

Então você poderia ter uma van criada na memory e ser usada por um código que só conhecia veículos:

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

Funcionou lindamente, e os arquivos .h definiram exatamente o que você deveria ser capaz de fazer com cada object.

A área de trabalho GNOME para Linux é escrita em C orientado a objects, e possui um modelo de object chamado ” GObject ” que suporta propriedades, inheritance, polymorphism, bem como alguns outros resources como referências, manipulação de events (chamada “sinais”), tempo de execução digitação, dados privados, etc.

Inclui hacks pré-processador para fazer coisas como typecasting na hierarquia de classs, etc. Aqui está um exemplo de class que escrevi para o GNOME (coisas como gchar são typedefs):

Classe de origem

Cabeçalho de class

Dentro da estrutura do GObject há um inteiro GType que é usado como um número mágico para o sistema de digitação dinâmica do GLib (você pode converter toda a estrutura para um “GType” para encontrar seu tipo).

Se você pensar em methods chamados em objects como methods estáticos que passam um ‘ this ‘ implícito para a function, pode tornar mais fácil pensar no OO em C.

Por exemplo:

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

torna-se:

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

Ou algo assim.

Eu costumava fazer esse tipo de coisa em C, antes de saber o que era OOP.

A seguir está um exemplo, que implementa um buffer de dados que cresce sob demanda, dado um tamanho mínimo, incremento e tamanho máximo. Essa implementação em particular foi baseada em “elementos”, ou seja, foi projetada para permitir uma coleção em forma de lista de qualquer tipo C, não apenas um buffer de byte de tamanho variável.

A idéia é que o object seja instanciado usando o xxx_crt () e excluído usando xxx_dlt (). Cada um dos methods “membro” usa um ponteiro especificamente typescript para operar.

Implementei uma linked list, buffer cíclico e várias outras coisas dessa maneira.

Devo confessar, nunca pensei em como implementar a inheritance com essa abordagem. Imagino que alguma mistura do que oferece Kieveli seja um bom caminho.

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); 

PS: vint era simplesmente um typedef de int – eu usei isso para me lembrar que o comprimento era variável de plataforma para plataforma (para portabilidade).

Ligeiramente off-topic, mas o compilador C ++ original, Cfront , compilou C ++ para C e depois para assembler.

Preservado aqui .

ffmpeg (um kit de ferramentas para processamento de vídeo) é escrito em C (e linguagem de assembly), mas usando um estilo orientado a objects. Está cheio de estruturas com pointers de function. Há um conjunto de funções de fábrica que inicializam as estruturas com os pointers apropriados de “método”.

Se você realmente pensa catefully, mesmo a biblioteca C padrão usa OOP – considere FILE * como um exemplo: fopen() inicializa um object FILE * , e você o usa usando methods membro fscanf() , fprintf() , fread() , fwrite() e outros, e eventualmente finalizá-lo com fclose() .

Você também pode usar o modo pseudo-Objetivo-C que também não é 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); } 

Isto é o que pode ser resultado de algum código Objective-C como este, se um velho tradutor Objective-C-to-C for usado:

 @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]; } 

Eu acho que Adam Rosenfield postou a maneira correta de fazer OOP em C. Eu gostaria de acrescentar que o que ele mostra é a implementação do object. Em outras palavras, a implementação real seria colocada no arquivo .c , enquanto a interface seria colocada no arquivo .h do header. Por exemplo, usando o exemplo do macaco acima:

A interface seria semelhante a:

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

Você pode ver no arquivo .h da interface que você está apenas definindo protótipos. Você pode então compilar a parte de implementação ” .c file” em uma biblioteca estática ou dinâmica. Isso cria encapsulamento e também você pode alterar a implementação à vontade. O usuário do seu object precisa saber quase nada sobre a implementação dele. Isso também coloca o foco no design geral do object.

É minha convicção pessoal que oop é uma maneira de conceituar sua estrutura de código e reusabilidade e realmente não tem nada a ver com essas outras coisas que são adicionadas ao c ++ como sobrecarga ou modelos. Sim, esses são resources úteis muito bons, mas eles não são representativos de qual programação orientada a objects realmente é.

Minha recomendação: seja simples. Um dos maiores problemas que tenho é manter software antigo (às vezes com mais de 10 anos). Se o código não for simples, pode ser difícil. Sim, pode-se escrever OOP muito útil com polymorphism em C, mas pode ser difícil de ler.

Eu prefiro objects simples que encapsulam algumas funcionalidades bem definidas. Um ótimo exemplo disso é o GLIB2 , por exemplo, uma tabela de 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); 

As chaves são:

  1. Arquitetura simples e padrão de design
  2. Alcança o encapsulamento OOP básico.
  3. Fácil de implementar, ler, entender e manter

Se eu fosse escrever OOP no CI provavelmente iria com um design pseudo- Pimpl . Em vez de passar pointers para structs, você acaba passando pointers para pointers para structs. Isso torna o conteúdo opaco e facilita o polymorphism e a inheritance.

O problema real com o OOP em C é o que acontece quando as variables ​​saem do escopo. Não há destruidores gerados pelo compilador e isso pode causar problemas. As macros podem ajudar, mas sempre será feio de se olhar.

 #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); } 

Saída:

 6.56 13.12 

Aqui está um show do que é programação OO com C.

Isso é real, puro C, sem macros de pré-processamento. Temos inheritance, polymorphism e encapsulamento de dados (incluindo dados privados para classs ou objects). Não há chance de um equivalente de qualificador protegido, ou seja, os dados privados também são privados da cadeia de inheritance. Mas isso não é um inconveniente porque não acho que seja necessário.

CPolygon não é instanciado porque só o usamos para manipular objects da cadeia de inheritance que têm aspectos comuns, mas diferentes implementações deles (polymorphism).

@Adam Rosenfield tem uma explicação muito boa de como conseguir OOP com C

Além disso, eu recomendo que você leia

1) pjsip

Uma ótima biblioteca C para VoIP. Você pode aprender como ele alcança o OOP através de estruturas e tabelas de pointers de function

2) Tempo de Execução do iOS

Aprenda como o iOS Runtime potencializa o Objective C. Ele alcança o OOP através do ponteiro isa, meta class

Para mim, a orientação a objects em C deve ter esses resources:

  1. Encapsulamento e ocultação de dados (pode ser obtido usando structs / opaque pointer)

  2. Herança e suporte para polymorphism (inheritance única pode ser obtida usando structs – certifique-se de que a base abstrata não seja instanciável)

  3. Funcionalidade de construtor e destruidor (não é fácil de conseguir)

  4. Verificação de tipo (pelo menos para tipos definidos pelo usuário, pois o C não aplica nenhum)

  5. Contagem de referência (ou algo para implementar o RAII )

  6. Suporte limitado para tratamento de exceções (setjmp e longjmp)

Além disso, ele deve basear-se nas especificações ANSI / ISO e não deve depender da funcionalidade específica do compilador.

Veja em http://ldeniau.web.cern.ch/ldeniau/html/oopc/oopc.html . Se nada mais lendo a documentação é uma experiência esclarecedora.

Estou um pouco atrasado para a festa aqui, mas gosto de evitar extremos macro – muitos ou muito ofuscar código, mas algumas macros óbvias podem tornar o código OOP mais fácil de desenvolver e ler:

 /* * 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 } 

Eu acho que isso tem um bom equilíbrio, e os erros que ele gera (pelo menos com as opções padrão do gcc 6.3) para alguns dos erros mais prováveis ​​são úteis em vez de confusos. O ponto inteiro é melhorar a produtividade do programador não?

Se você precisar escrever um pequeno código, tente isto: 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); }