Orientação a objetos em C

157

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

Eu estou familiarizado com algumas linguagens orientadas a objetos diferentes, portanto, não responda com respostas como "Aprenda C ++!". Eu li " Programação Orientada a Objetos com ANSI C " (cuidado: formato PDF ) e várias outras soluções interessantes, mas estou principalmente interessado na sua :-)!


Consulte também Você pode escrever código orientado a objetos em C?

Anthony Cuozzo
fonte
1
Posso responder para aprender D e usar abi compatível c para onde você realmente precisa C. digitalmars.com/d
Tim Matthews
2
@ Dinah: Obrigado pelo "Veja também". Esse post foi interessante.
1
A questão interessante parece ser por que você quer um corte pré-processador de OOP em C.
Calyth
3
@ Calyth: Acho que o OOP é útil e "eu trabalho com alguns sistemas embarcados que realmente têm apenas um compilador C disponível" (acima). Além disso, você não acha interessantes os hacks do pré-processador?
2
Possível duplicata de Você pode escrever código orientado a objeto em C?
Ciro Santilli escreveu:

Respostas:

31

O C Object System (COS) parece promissor (ainda está na versão alfa). Ele tenta manter o mínimo de conceitos disponíveis por questões de simplicidade e flexibilidade: programação uniforme orientada a objetos, incluindo classes abertas, metaclasses, metaclasses de propriedades, genéricos, métodos múltiplos, delegação, propriedade, exceções, contratos e fechamentos. Há um rascunho (PDF) que o descreve.

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

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

filante
fonte
O @vonbrand COS migrou para o github, onde a última confirmação é no verão passado. A maturidade pode explicar a falta de confirmação.
Philant
185

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

struct monkey
{
    float age;
    bool is_male;
    int happiness;
};

void monkey_dance(struct monkey *monkey)
{
    /* do a little dance */
}

Para obter coisas como herança e polimorfismo, você precisa trabalhar um pouco mais. Você pode fazer herança manual fazendo com que o primeiro membro de uma estrutura seja uma instância da superclasse e, em seguida, você pode usar ponteiros para basear e derivar classes livremente:

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 polimorfismo (ou seja, funções virtuais), use ponteiros de função e, opcionalmente, tabelas de ponteiros de função, 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 se faz polimorfismo em C. Não é bonito, mas faz o trabalho. Existem alguns problemas complicados que envolvem a conversão de ponteiros entre as classes base e derivada, que são seguros desde que a classe base seja o primeiro membro da classe derivada. A herança múltipla é muito mais difícil - nesse caso, para alternar entre classes base diferentes da primeira, você precisa ajustar manualmente seus ponteiros com base nos deslocamentos adequados, o que é realmente complicado e propenso a erros.

Outra coisa (complicada) que você pode fazer é alterar o tipo dinâmico de um objeto em tempo de execução! Você acabou de atribuir a ele um novo ponteiro de tabela. Você pode alterar seletivamente algumas das funções virtuais enquanto mantém outras, criando novos tipos 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 objetos de um determinado tipo.

Adam Rosenfield
fonte
6
Adam, a diversão de mudar o vtable mundial de um tipo é simular pato-digitando C. :)
jmucchiello
Agora tenho pena do C ++ ... Bem, é claro que a sintaxe do C ++ é mais clara, mas como não é uma sintaxe trivial, estou atenuada. Gostaria de saber se algo híbrido entre C ++ e C poderia ser alcançado, então void * ainda seria do tipo castable válido. A parte com struct derived {struct base super;};é óbvia para adivinhar como funciona, pois pela ordem dos bytes está correta.
Jokoon # 1
2
+1 para código elegante, bem escrito. Era exatamente isso que eu estava procurando!
Homunculus Reticulli
3
Bem feito. É exatamente assim que eu venho fazendo isso e é a maneira correta também. Em vez de exigir um ponteiro para a estrutura / objeto em mente, você deve apenas passar um ponteiro para um número inteiro (endereço). Isso permitiria que você passasse qualquer tipo de objeto para chamadas de método polimórficas ilimitadas. Além disso, a única coisa que falta é uma função para inicializar suas estruturas (objetos / classes). Isso incluiria uma função malloc e retornaria um ponteiro. Talvez eu vou adicionar um pedaço de como fazer a passagem de mensagens (Objective-C) em C.
1
Esta é a palha me quebrou de C ++, e usar C mais (antes eu só usei C ++ para a herança) Obrigado
Anne Quinn
31

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

  • Cada objeto tinha seu próprio arquivo
  • Funções e variáveis ​​públicas são definidas no arquivo .h para um objeto
  • 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 objeto para herdar de

Herdar é difícil de descrever, mas basicamente era o seguinte:

struct vehicle {
   int power;
   int weight;
}

Em outro arquivo:

struct van {
   struct vehicle base;
   int cubic_size;
}

Então você poderia ter uma van criada na memória e sendo usada por código que sabia apenas sobre 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 poder fazer com cada objeto.

Kieveli
fonte
Eu realmente gosto dessa solução, exceto que todos os internos do "objeto" são públicos.
Lawrence Dol
6
@ Monkey Software: C não tem controle de acesso. A única maneira de ocultar os detalhes da implementação é interagir por meio de ponteiros opacos, o que pode ser bastante doloroso, pois todos os campos precisariam ser acessados ​​por métodos acessadores que provavelmente não podem ser incorporados.
238 Adam Adam Rosenfield
1
@ Adam: Compiladores apoio otimizações link-tempo irão in-line-los muito bem ...
Christoph
9
Se você fizer isso, também deverá garantir que todas as funções no arquivo .c que não são definidas como públicas sejam definidas como estáticas, para que não acabem como funções nomeadas nos arquivos de objetos. Isso garante que ninguém possa encontrar seus nomes na fase do link.
jmucchiello
2
@Marcel: C foi usado porque o código foi implantado em placas de baixo nível executando uma variedade de processadores para sistemas autônomos. Todos eles apoiaram a compilação de C para seus respectivos binários nativos. A abordagem tornou o código muito fácil de ler quando você percebeu o que eles estavam tentando fazer.
precisa saber é o seguinte
18

A área de trabalho do GNOME para Linux é escrita em C orientado a objetos e possui um modelo de objeto chamado " GObject ", que suporta propriedades, herança, polimorfismo, além de outras vantagens como referências, manipulação de eventos (chamados "sinais"), tempo de execução digitação, dados privados etc.

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

Origem da Classe

Cabeçalho da classe

Dentro da estrutura GObject, há um número 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 em um "GType" para descobrir seu tipo).

James Cape
fonte
infelizmente, o arquivo leia-me / tutorial (link wiki) não está funcionando e só existe um manual de referência para isso (estou falando do GObject e não do GTK). forneça alguns arquivos do tutorial para o mesmo ...
FL4SOF
Os links foram corrigidos.
James Cape
4
Os links estão quebrados novamente.
SeanRamey 29/09
6

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

A seguir, é apresentado um exemplo, que implementa um buffer de dados que cresce sob demanda, considerando um tamanho mínimo, um incremento e um tamanho máximo. Essa implementação específica foi baseada em "elemento", ou seja, foi projetada para permitir uma coleção semelhante a uma lista de qualquer tipo C, não apenas um buffer de bytes de tamanho variável.

A idéia é que o objeto seja instanciado usando xxx_crt () e excluído usando xxx_dlt (). Cada um dos métodos "membro" usa um ponteiro de tipo específico para operar.

Eu implementei uma lista vinculada, buffer cíclico e várias outras coisas dessa maneira.

Devo confessar que nunca pensei em como implementar herança com essa abordagem. Imagino que alguma mistura daquilo oferecido por Kieveli possa ser um bom caminho.

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

PS: vint era simplesmente um typedef do int - usei-o para me lembrar que seu comprimento era variável de plataforma para plataforma (para portar).

Lawrence Dol
fonte
7
Santo Moly, isso poderia ganhar um concurso C ofuscado! eu gosto disso! :)
horseyguy 29/07/2009
@horseyguy Não, não podia. Foi publicado. Também consideram a inclusão de abuso de arquivos de cabeçalho na ferramenta iocccsize. Também não é um programa completo. O ano de 2009 não teve concurso, portanto não se pode comparar o tamanho do iocccs. O CPP foi abusado muitas vezes também, por isso é bastante antigo. Etc. Desculpe. Não estou tentando ser negativo, mas sou realista. Eu meio que entendo o seu significado e é uma boa leitura e eu votei positivamente. (E sim, eu participo e sim, também ganho.)
Pryftan
6

Ligeiramente fora de tópico, mas o compilador C ++ original, Cfront , compilou C ++ em C e depois no assembler.

Preservado aqui .

zebrabox
fonte
Eu já vi isso antes. Eu acredito que foi um bom trabalho.
@ Anthony Cuozzo: Stan Lippman escreveu um ótimo livro chamado 'C ++ - Inside the object model', onde relatou muitas de suas experiências e decisões de design ao escrever e manter o c-front. Ainda é um bom ler e me ajudou imensamente quando a transição de C para C ++ muitos anos atrás
zebrabox
5

Se você pensa em métodos chamados objetos como métodos estáticos que passam um ' this' implícito para a função, isso pode facilitar o pensamento 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.

jjnguy
fonte
6
@ Artelius: Claro, mas às vezes o óbvio não é, até que seja declarado. +1 para isso.
Lawrence Dol
1
melhor ainda seriastring->length(s);
OozeMeister
4

O ffmpeg (um kit de ferramentas para processamento de vídeo) é escrito em C direto (e linguagem assembly), mas usando um estilo orientado a objetos. Está cheio de estruturas com ponteiros de função. Há um conjunto de funções de fábrica que inicializam as estruturas com os ponteiros de "método" apropriados.

Mr Fooz
fonte
Eu não vejo nenhuma função de fábrica nele (ffmpeg), mas parece que não está usando polimorfismo / herança (maneira trivial sugerida acima).
FL4SOF
avcodec_open é uma função de fábrica. Ele coloca ponteiros de função em uma estrutura AVCodecContext (como draw_horiz_band). Se você observar o uso da macro FF_COMMON_FRAME no avcodec.h, verá algo semelhante à herança dos membros dos dados. IMHO, ffmpeg prova para mim que OOP é o melhor feito em C ++, não C.
Sr. Fooz
3

Se você realmente pensa catefully, mesmo padrão de uso da biblioteca C OOP - considerar FILE *como um exemplo: fopen()inicializa um FILE *objeto, e você usá-lo usar métodos membros fscanf(), fprintf(), fread(), fwrite()e outros, e, eventualmente, finalizá-lo com fclose().

Você também pode seguir a maneira pseudo-objetiva-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 resultar de um código Objective-C como este, se um tradutor de Objective-C-to-C bastante antigo 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];
}
Maxthon Chan
fonte
O que __attribute__((constructor))faz void __meta_Foo_init(void) __attribute__((constructor))?
AE de Drew
1
Essa é uma extensão do GCC que garantirá que a função marcada seja chamada quando o binário for carregado na memória. @AEDrew
Maxthon Chan
popen(3)também retorna a FILE *para outro exemplo.
Pryftan
3

Acho que o que Adam Rosenfield postou é a maneira correta de fazer POO em C. Gostaria de acrescentar que o que ele mostra é a implementação do objeto. Em outras palavras, a implementação real seria colocada no .carquivo, enquanto a interface seria colocada no .harquivo de cabeçalho . Por exemplo, usando o exemplo de macaco acima:

A interface ficaria assim:

//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 .harquivo de interface que você está apenas definindo protótipos. Você pode então compilar a parte " .carquivo" da implementação em uma biblioteca estática ou dinâmica. Isso cria encapsulamento e você também pode alterar a implementação à vontade. O usuário do seu objeto não precisa saber quase nada sobre sua implementação. Isso também enfatiza o design geral do objeto.

É minha convicção pessoal que oop é uma maneira de conceituar sua estrutura e reutilização de código e realmente não tem nada a ver com as outras coisas adicionadas ao c ++, como sobrecarga ou modelos. Sim, esses são recursos úteis muito agradáveis, mas não representam o que realmente é a programação orientada a objetos.

user2074102
fonte
Você pode declarar uma estrutura com typedef struct Monkey {} Monkey; Qual é o objetivo de digitar após a criação?
MarcusJ
1
@ MarcusJ O struct _monkeyé simplesmente um protótipo. A definição de tipo real é definida no arquivo de implementação (o arquivo .c). Isso cria o efeito de encapsulamento e permite que o desenvolvedor da API redefina a estrutura do macaco no futuro sem modificar a API. Os usuários da API precisam se preocupar apenas com os métodos reais. O designer da API cuida da implementação, incluindo como o objeto / estrutura é organizado. Portanto, os detalhes do objeto / estrutura estão ocultos do usuário (um tipo opaco).
Eu defino minhas estruturas nos cabeçalhos, isso não é padrão? Bem, faço dessa maneira porque ocasionalmente preciso acessar membros da estrutura fora dessa biblioteca.
MarcusJ
1
@ MarcusJ Você pode definir suas estruturas nos cabeçalhos, se desejar (não há padrão). Mas se você quiser alterar sua estrutura interna no futuro, poderá quebrar seu código. Encapsulamento é apenas um estilo de codificação que facilita a alteração de uma implementação sem quebrar seu código. Você sempre pode acessar seus membros através de métodos de acesso, como int getCount(ObjectType obj)etc, se optar por definir a estrutura no arquivo de implementação.
2

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

Prefiro objetos simples que encapsulem 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. Atinge o encapsulamento básico de POO.
  3. Fácil de implementar, ler, entender e manter
Wadester
fonte
1

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

O verdadeiro problema com OOP em C é o que acontece quando as variáveis ​​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 ver.

jmucchiello
fonte
1
Ao programar em C, trato do escopo usando ifinstruções e liberando-as no final. Por exemploif ( (obj = new_myObject()) ) { /* code using myObject */ free_myObject(obj); }
1

Outra maneira de programar em um estilo orientado a objeto com C é usar um gerador de código que transforma uma linguagem específica de domínio em C. Como é feito com TypeScript e JavaScript para trazer OOP para js.

Luca Wienert
fonte
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);
}

Resultado:

6.56
13.12

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

É C real, puro, sem macros de pré-processador. Temos herança, polimorfismo e encapsulamento de dados (incluindo dados privados de classes ou objetos). Não há chance para o equivalente qualificado do qualificador, ou seja, os dados privados também são privados na cadeia de herança. Mas isso não é um inconveniente, porque não acho necessário.

CPolygon não é instanciado porque apenas o usamos para manipular objetos da cadeia de herança que possuem aspectos em comum, mas diferentes implementações (Polimorfismo).

rogergc
fonte
0

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

Além disso, eu recomendo que você leia

1) pjsip

Uma biblioteca C muito boa para VoIP. Você pode aprender como ele alcança OOP através de estruturas e tabelas de ponteiros de funções

2) Tempo de execução do iOS

Aprenda como o iOS Runtime potencializa o Objetivo C. Ele alcança OOP por meio de um apontador, meta classe

onmyway133
fonte
0

Para mim, a orientação a objetos em C deve ter os seguintes recursos:

  1. Encapsulamento e ocultação de dados (pode ser alcançado usando structs / ponteiros opacos)

  2. Herança e suporte ao polimorfismo (a herança única pode ser obtida usando estruturas - verifique se a base abstrata não é instanciada)

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

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

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

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

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

FL4SOF
fonte
Para o número (5) - Você não pode implementar o RAII em uma linguagem sem destruidores (o que significa que o RAII não é uma técnica suportada por compilador em C ou Java).
Tom
construtores e destruidores podem ser escritos para objetos baseados em c - acho que o GObject faz isso. e, é claro, RAAI (não é direto, pode ser feio e nem precisa ser pragmático) - tudo o que eu estava procurando era identificar a semântica baseada em C para obter o que foi dito acima.
FL4SOF
C não suporta destruidores. Você precisa digitar algo para fazê-los funcionar. Isso significa que eles não se limpam. GObject não altera o idioma.
Tom
0

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

Tim
fonte
3
Forneça o contexto para o link que você está compartilhando. Embora o link que você compartilhou possa realmente ser muito útil, é aconselhável capturar os aspectos principais do artigo compartilhado que respondem à pergunta. Dessa forma, mesmo se o link for removido, sua resposta ainda será relevante e útil.
IshmaelMakitla
0

Estou um pouco atrasado para a festa aqui, mas gosto de evitar os dois extremos da macro - muitos ou muitos códigos ofuscam, mas algumas macros óbvias podem facilitar o desenvolvimento e a leitura do código OOP:

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

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

duanev
fonte
0

Se você precisar escrever um pequeno código, tente o seguinte: 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);
}
cicciodarkast
fonte
2
Não basta postar alguma ferramenta ou biblioteca como resposta. Pelo menos demonstre como ele resolve o problema na própria resposta.
Baum mit Augen
0

Também estou trabalhando nisso com base em uma solução macro. Portanto, é apenas para os mais corajosos, eu acho ;-) Mas já é muito bom, e já estou trabalhando em alguns projetos. Funciona para que você defina primeiro um arquivo de cabeçalho separado para cada classe. Como isso:

#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 a classe, você cria um arquivo de cabeçalho para ele e um arquivo C em que implementa os métodos:

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

No cabeçalho que você criou para a classe, você inclui outros cabeçalhos necessários e define tipos etc. relacionados à classe. No cabeçalho da classe e no arquivo C, você inclui o arquivo de especificação da classe (consulte o primeiro exemplo de código) e uma macro X. Essas macros X ( 1 , 2 , 3 etc.) expandirão o código para as estruturas de classe reais e outras declarações.

Para herdar uma classe #define SUPER supernamee adicionar supername__define \como a primeira linha na definição de classe. Ambos devem estar lá. Também há suporte a JSON, sinais, classes abstratas etc.

Para criar um objeto, basta usar W_NEW(classname, .x=1, .y=2,...). A inicialização é baseada na inicialização da estrutura introduzida no C11. Funciona bem e tudo o que não está listado é definido como zero.

Para chamar um método, use W_CALL(o,method)(1,2,3). Parece uma chamada de função de ordem superior, mas é apenas uma macro. Ele se expande, o ((o)->klass->method(o,1,2,3))que é um truque muito bom.

Consulte a documentação e o próprio código .

Como a estrutura precisa de algum código padrão, escrevi um script Perl (wobject) que faz o trabalho. Se você usar isso, basta escrever

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

e criará o arquivo de especificação da classe, o cabeçalho da classe e um arquivo C, que inclui Point_impl.conde você implementa a classe. Isso economiza bastante trabalho, se você tiver muitas classes simples, mas ainda assim tudo estiver em C. wobject é um scanner muito simples, baseado em expressões regulares, fácil de se adaptar a necessidades específicas ou de ser reescrito do zero.

J.P.
fonte
0

Você pode experimentar o COOP , uma estrutura amigável para programadores para OOP em C, que apresenta classes, exceções, polimorfismo e gerenciamento de memória (importante para o código incorporado). É uma sintaxe relativamente leve, veja o tutorial no Wiki lá.

Shmuel Fine
fonte