Orientation objet en C

157

Quel serait un ensemble de hacks de préprocesseur astucieux (compatibles ANSI C89 / ISO C90) qui permettent une sorte d'orientation objet laide (mais utilisable) en C?

Je connais quelques langages orientés objet différents, alors ne répondez pas avec des réponses telles que "Apprendre C ++!". J'ai lu " Programmation orientée objet avec ANSI C " (attention: format PDF ) et plusieurs autres solutions intéressantes, mais je m'intéresse surtout à la vôtre :-)!


Voir aussi Pouvez-vous écrire du code orienté objet en C?

Anthony Cuozzo
la source
1
Puis-je répondre pour apprendre D et utiliser le abi compatible c là où vous avez vraiment besoin C. digitalmars.com/d
Tim Matthews
2
@Dinah: Merci pour le "Voir aussi". Ce message était intéressant.
1
La question intéressante semble être de savoir pourquoi voudriez-vous un hack pré-processeur de la POO sur C.
Calyth
3
@Calyth: Je trouve que la POO est utile et "je travaille avec certains systèmes embarqués qui n'ont vraiment qu'un compilateur C disponible" (ci-dessus). De plus, ne trouvez-vous pas les astuces de préprocesseurs intéressantes à regarder?
2
Possible duplication de Pouvez-vous écrire du code orienté objet en C?
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

Réponses:

31

C Object System (COS) semble prometteur (il est toujours en version alpha). Il essaie de minimiser les concepts disponibles dans un souci de simplicité et de flexibilité: programmation orientée objet uniforme comprenant des classes ouvertes, des métaclasses, des métaclasses de propriétés, des génériques, des méthodes multiples, la délégation, la propriété, les exceptions, les contrats et les fermetures. Il existe un projet de papier (PDF) qui le décrit.

L'exception en C est une implémentation C89 de TRY-CATCH-FINALLY trouvée dans d'autres langages OO. Il est livré avec une suite de tests et quelques exemples.

À la fois par Laurent Deniau, qui travaille beaucoup sur POO en C .

philant
la source
@vonbrand COS a migré vers github où le dernier commit est l'été dernier. La maturité peut expliquer le manque d'engagement.
philant
185

Je déconseillerais l'utilisation du préprocesseur (ab) pour essayer de rendre la syntaxe C plus semblable à celle d'un autre langage plus orienté objet. Au niveau le plus élémentaire, vous utilisez simplement des structures simples comme objets et les passez par des pointeurs:

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

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

Pour obtenir des choses comme l'héritage et le polymorphisme, vous devez travailler un peu plus dur. Vous pouvez faire l'héritage manuel en faisant en sorte que le premier membre d'une structure soit une instance de la superclasse, puis vous pouvez convertir librement des pointeurs vers les classes de base et dérivées:

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

Pour obtenir le polymorphisme (c'est-à-dire les fonctions virtuelles), vous utilisez des pointeurs de fonction et éventuellement des tables de pointeurs de fonction, également appelées tables virtuelles 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;
}

Et c'est comme ça que vous faites le polymorphisme en C. Ce n'est pas joli, mais ça fait le travail. Il y a des problèmes persistants impliquant des casts de pointeurs entre les classes de base et dérivées, qui sont sûrs tant que la classe de base est le premier membre de la classe dérivée. L'héritage multiple est beaucoup plus difficile - dans ce cas, pour faire le cas entre des classes de base autres que la première, vous devez ajuster manuellement vos pointeurs en fonction des décalages appropriés, ce qui est vraiment délicat et sujet aux erreurs.

Une autre chose (délicate) que vous pouvez faire est de changer le type dynamique d'un objet au moment de l'exécution! Vous venez de lui réaffecter un nouveau pointeur vtable. Vous pouvez même modifier sélectivement certaines des fonctions virtuelles tout en en conservant d'autres, créant de nouveaux types hybrides. Veillez simplement à créer une nouvelle vtable au lieu de modifier la vtable globale, sinon vous affecterez accidentellement tous les objets d'un type donné.

Adam Rosenfield
la source
6
Adam, le plaisir de changer la vtable globale d'un type est de simuler le typage canard en C. :)
jmucchiello
Maintenant, je plains C ++ ... Bien sûr, la syntaxe C ++ est plus claire, mais comme ce n'est pas une syntaxe triviale, je suis mitigé. Je me demande si quelque chose d'hybride entre C ++ et C pourrait être réalisé, donc void * serait toujours un type castable valide. La partie avec struct derived {struct base super;};est évidente pour deviner comment cela fonctionne, car dans l'ordre des octets, c'est correct.
jokoon le
2
+1 pour un code élégant, bien écrit. Ceci est exactement ce que je cherchais!
Homunculus Reticulli
3
Bien joué. C'est exactement ce que je fais et c'est la bonne manière aussi. Au lieu d'exiger un pointeur vers la structure / objet à l'esprit, vous devez simplement passer un pointeur vers un entier (adresse). Cela vous permettrait de passer n'importe quel type d'objet pour des appels de méthode polymorphes illimités. De plus, la seule chose qui manque est une fonction pour initialiser vos structures (objets / classes). Cela inclurait une fonction malloc et retournerait un pointeur. Peut-être que j'ajouterai un morceau sur la façon de faire passer un message (objectif-c) dans C.
1
C'est la paille m'a cassé du C ++, et utiliser C plus (avant je n'utilisais que C ++ pour l'héritage) Merci
Anne Quinn
31

Une fois, j'ai travaillé avec une bibliothèque C qui a été implémentée d'une manière qui m'a paru assez élégante. Ils avaient écrit, en C, une façon de définir des objets, puis en héritent pour qu'ils soient aussi extensibles qu'un objet C ++. L'idée de base était la suivante:

  • Chaque objet avait son propre fichier
  • Les fonctions et variables publiques sont définies dans le fichier .h pour un objet
  • Les variables et fonctions privées ne se trouvaient que dans le fichier .c
  • Pour "hériter", une nouvelle structure est créée avec le premier membre de la structure étant l'objet à hériter

L'héritage est difficile à décrire, mais c'était essentiellement ceci:

struct vehicle {
   int power;
   int weight;
}

Puis dans un autre fichier:

struct van {
   struct vehicle base;
   int cubic_size;
}

Ensuite, vous pourriez avoir une camionnette créée en mémoire et utilisée par un code qui ne connaissait que les véhicules:

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

Cela fonctionnait à merveille et les fichiers .h définissaient exactement ce que vous devriez pouvoir faire avec chaque objet.

Kieveli
la source
J'aime beaucoup cette solution, sauf que tous les composants internes de "l'objet" sont publics.
Lawrence Dol
6
@Software Monkey: C n'a pas de contrôle d'accès. Le seul moyen de masquer les détails de l'implémentation est d'interagir via des pointeurs opaques, ce qui peut devenir assez pénible, car tous les champs devraient être accessibles via des méthodes d'accesseur qui ne peuvent probablement pas être insérées.
Adam Rosenfield
1
@Adam: Les compilateurs prenant en charge les optimisations au moment des liens les intégreront très bien ...
Christoph
9
Si vous faites cela, vous devez également vous assurer que toutes les fonctions du fichier .c qui ne sont pas définies comme publiques sont définies comme statiques afin qu'elles ne finissent pas comme des fonctions nommées dans vos fichiers objets. Cela garantit que personne ne peut trouver ses noms dans la phase de liaison.
jmucchiello
2
@Marcel: C a été utilisé parce que le code a été déployé sur des cartes de bas niveau exécutant une variété de processeurs pour des systèmes autonomes. Ils ont tous pris en charge la compilation de C vers leurs binaires natifs respectifs. L'approche a rendu le code très facile à lire une fois que vous avez réalisé ce qu'ils essayaient de faire.
Kieveli
18

Le bureau GNOME pour Linux est écrit en C orienté objet, et il a un modèle objet appelé " GObject " qui prend en charge les propriétés, l'héritage, le polymorphisme, ainsi que d'autres goodies comme les références, la gestion des événements (appelés "signaux"), le runtime saisie, données privées, etc.

Il inclut des hacks de préprocesseur pour faire des choses comme le typage dans la hiérarchie des classes, etc. Voici un exemple de classe que j'ai écrit pour GNOME (des choses comme gchar sont des typedefs):

Source de classe

En-tête de classe

Dans la structure GObject, il y a un entier GType qui est utilisé comme nombre magique pour le système de typage dynamique de GLib (vous pouvez convertir la structure entière en un "GType" pour trouver son type).

James Cape
la source
malheureusement, le fichier read me / tutorial (lien wiki) ne fonctionne pas et il n'y a qu'un manuel de référence pour cela (je parle de GObject et non de GTK). s'il vous plaît fournir quelques fichiers de tutoriel pour le même ...
FL4SOF
Les liens ont été corrigés.
James Cape
4
Les liens sont à nouveau rompus.
SeanRamey
6

J'avais l'habitude de faire ce genre de chose en C, avant de savoir ce qu'était la POO.

Voici un exemple, qui implémente un tampon de données qui se développe à la demande, étant donné une taille minimale, un incrément et une taille maximale. Cette implémentation particulière était basée sur des "éléments", c'est-à-dire qu'elle était conçue pour permettre une collection de type liste de n'importe quel type C, pas seulement un tampon d'octets de longueur variable.

L'idée est que l'objet est instancié à l'aide de xxx_crt () et supprimé à l'aide de xxx_dlt (). Chacune des méthodes "membres" utilise un pointeur spécifiquement typé pour fonctionner.

J'ai implémenté une liste chaînée, un tampon cyclique et un certain nombre d'autres choses de cette manière.

Je dois avouer que je n'ai jamais réfléchi à la manière de mettre en œuvre l'héritage avec cette approche. J'imagine qu'un mélange de celui offert par Kieveli pourrait être une bonne voie.

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 était simplement un typedef de int - je l'ai utilisé pour me rappeler que sa longueur était variable d'une plateforme à l'autre (pour le portage).

Lawrence Dol
la source
7
Holy Moly, cela pourrait gagner un concours C obscurci! je l'aime! :)
horseyguy
@horseyguy Non, ça ne pouvait pas. Il a été publié. Ils envisagent également l'inclusion d'abus de fichiers d'en-tête contre l'outil iocccsize. Ce n'est pas non plus un programme complet. 2009 n'a pas eu de concours, donc je ne peux pas comparer la taille de l'ioccc. Le RPC a été abusé à maintes reprises et il est donc assez ancien. Etc. Désolé. Je n'essaye pas d'être négatif, je suis cependant réaliste. Je comprends en quelque sorte votre sens et c'est une bonne lecture et je l'ai voté à la hausse. (Et oui j'y participe et oui je gagne aussi.)
Pryftan
6

Légèrement hors sujet, mais le compilateur C ++ d'origine, Cfront , a compilé C ++ en C puis en assembleur.

Conservé ici .

bœuf zébré
la source
Je l'ai déjà vu. Je pense que c'était un beau travail.
@Anthony Cuozzo: Stan Lippman a écrit un excellent livre intitulé 'C ++ - Inside the object model' où il a raconté beaucoup de ses expériences et décisions de conception en écrivant et en maintenant c-front. C'est toujours une bonne lecture et m'a énormément aidé lors de la transition du C au C ++ il y a de nombreuses années
zebrabox
5

Si vous pensez aux méthodes appelées sur des objets comme des méthodes statiques qui passent un 'implicite'this ' dans la fonction, cela peut faciliter la réflexion OO en C.

Par exemple:

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

devient:

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

Ou quelque chose comme ça.

jjnguy
la source
6
@Artelius: Bien sûr, mais parfois l'évidence ne l'est pas, jusqu'à ce qu'elle soit déclarée. +1 pour cela.
Lawrence Dol
1
mieux encore serait string->length(s);
OozeMeister
4

ffmpeg (une boîte à outils pour le traitement vidéo) est écrit en C pur (et en langage d'assemblage), mais en utilisant un style orienté objet. Il est plein de structures avec des pointeurs de fonction. Il existe un ensemble de fonctions d'usine qui initialisent les structures avec les pointeurs de "méthode" appropriés.

Monsieur Fooz
la source
Je ne vois aucune fonction d'usine (ffmpeg), mais il ne semble pas utiliser le polymorphisme / héritage (manière triviale suggérée ci-dessus).
FL4SOF
avcodec_open est une fonction d'usine. Il fourre les pointeurs de fonction dans une structure AVCodecContext (comme draw_horiz_band). Si vous regardez l'utilisation de la macro FF_COMMON_FRAME dans avcodec.h, vous verrez quelque chose qui ressemble à l'héritage des membres de données. À mon humble avis, ffmpeg me prouve que la POO est mieux faite en C ++, pas en C.
M. Fooz
3

Si vous pense vraiment catefully, même l' utilisation de la bibliothèque standard C OOP - considèrent FILE *comme un exemple: fopen()initialise un FILE *objet, et vous l' utilisez utiliser des méthodes membres fscanf(), fprintf(), fread(), fwrite()et d' autres, et éventuellement mettre la dernière main avec fclose().

Vous pouvez également utiliser la méthode pseudo-Objective-C qui n'est pas difficile non plus:

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;
}
// ...

Utiliser:

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

C'est ce qui peut résulter d'un code Objective-C comme celui-ci, si un assez vieux traducteur Objective-C-to-C est utilisé:

@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
la source
Que fait __attribute__((constructor))-on void __meta_Foo_init(void) __attribute__((constructor))?
AE a attiré le
1
C'est une extension GCC qui s'assurera que la fonction marquée est appelée lorsque le binaire est chargé en mémoire. @AEDrew
Maxthon Chan
popen(3)renvoie également a FILE *pour un autre exemple.
Pryftan le
3

Je pense que ce qu'Adam Rosenfield a posté est la manière correcte de faire la POO en C. Je voudrais ajouter que ce qu'il montre est l'implémentation de l'objet. En d'autres termes, l'implémentation réelle serait placée dans le .cfichier, tandis que l'interface serait placée dans l'en-tête.h fichier d' . Par exemple, en utilisant l'exemple de singe ci-dessus:

L'interface ressemblerait à:

//monkey.h

    struct _monkey;

    typedef struct _monkey monkey;

    //memory management
    monkey * monkey_new();
    int monkey_delete(monkey *thisobj);
    //methods
    void monkey_dance(monkey *thisobj);

Vous pouvez voir dans le .hfichier d' interface que vous ne définissez que des prototypes. Vous pouvez ensuite compiler le « .cfichier» de la partie d'implémentation dans une bibliothèque statique ou dynamique. Cela crée une encapsulation et vous pouvez également modifier l'implémentation à volonté. L'utilisateur de votre objet n'a besoin de rien savoir de sa mise en œuvre. Cela met également l'accent sur la conception globale de l'objet.

C'est ma conviction personnelle que oop est un moyen de conceptualiser votre structure de code et votre réutilisabilité et n'a vraiment rien à voir avec les autres choses qui sont ajoutées au C ++ comme la surcharge ou les modèles. Oui, ce sont de très belles fonctionnalités utiles mais elles ne sont pas représentatives de ce qu'est réellement la programmation orientée objet.

utilisateur2074102
la source
Vous pouvez déclarer une structure avec typedef struct Monkey {} Monkey; Quel est l'intérêt de la saisir après sa création?
MarcusJ
1
@MarcusJ The struct _monkeyest simplement un prototype. La définition de type réelle est définie dans le fichier d'implémentation (le fichier .c). Cela crée l'effet d'encapsulation et permet au développeur d'API de redéfinir la structure du singe à l'avenir sans modifier l'API. Les utilisateurs de l'API doivent uniquement se préoccuper des méthodes réelles. Le concepteur d'API s'occupe de l'implémentation, y compris de la disposition de l'objet / de la structure. Ainsi, les détails de l'objet / structure sont cachés à l'utilisateur (un type opaque).
Je définis mes structures dans les en-têtes, n'est-ce pas standard? Eh bien, je le fais de cette façon parce que j'ai parfois besoin d'accéder aux membres de la structure en dehors de cette bibliothèque.
MarcusJ
1
@MarcusJ Vous pouvez définir vos structures dans les en-têtes si vous le souhaitez (il n'y a pas de standard). Mais si vous voulez changer sa structure interne plus tard, vous risquez de casser votre code. L'encapsulation est simplement un style de codage qui facilite la modification d'une implémentation sans casser votre code. Vous pouvez toujours accéder à vos membres via des méthodes d'accesseur comme int getCount(ObjectType obj)etc. si vous choisissez de définir la structure dans le fichier d'implémentation.
2

Ma recommandation: restez simple. L'un des plus gros problèmes que j'ai est de maintenir des logiciels plus anciens (parfois plus de 10 ans). Si le code n'est pas simple, cela peut être difficile. Oui, on peut écrire une POO très utile avec un polymorphisme en C, mais cela peut être difficile à lire.

Je préfère les objets simples qui encapsulent des fonctionnalités bien définies. Un bon exemple de ceci est GLIB2 , par exemple une table de hachage:

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

Les clés sont:

  1. Architecture simple et modèle de conception
  2. Réalise l'encapsulation de base de la POO.
  3. Facile à implémenter, lire, comprendre et maintenir
Wadester
la source
1

Si j'allais écrire la POO en CI, j'irais probablement avec un pseudo- Pimpl conception . Au lieu de passer des pointeurs vers des structures, vous finissez par passer des pointeurs vers des pointeurs vers des structures. Cela rend le contenu opaque et facilite le polymorphisme et l'héritage.

Le vrai problème avec la POO en C est ce qui se passe lorsque les variables quittent la portée. Il n'y a pas de destructeurs générés par le compilateur et cela peut causer des problèmes. Les macros peuvent éventuellement aider, mais ce sera toujours moche à regarder.

jmucchiello
la source
1
Lors de la programmation en C, je gère la portée en utilisant des ifinstructions et en les libérant à la fin. Par exempleif ( (obj = new_myObject()) ) { /* code using myObject */ free_myObject(obj); }
1

Une autre façon de programmer dans un style orienté objet avec C est d'utiliser un générateur de code qui transforme un langage spécifique à un domaine en C. Comme c'est le cas avec TypeScript et JavaScript pour amener la POO à js.

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

Production:

6.56
13.12

Voici un spectacle de ce qu'est la programmation OO avec C.

C'est du vrai C pur, pas de macros de préprocesseur. Nous avons l'héritage, le polymorphisme et l'encapsulation des données (y compris les données privées aux classes ou aux objets). Il n'y a aucune chance pour l'équivalent de qualificatif protégé, c'est-à-dire que les données privées sont également privées tout au long de la chaîne d'héritage. Mais ce n'est pas un inconvénient car je ne pense pas que ce soit nécessaire.

CPolygon n'est pas instancié car nous ne l'utilisons que pour manipuler des objets en aval de la chaîne d'héritage qui ont des aspects communs mais une implémentation différente de ceux-ci (polymorphisme).

rogergc
la source
0

@Adam Rosenfield a une très bonne explication sur la façon de réaliser la POO avec C

D'ailleurs, je vous recommande de lire

1) pjsip

Une très bonne bibliothèque C pour la VoIP. Vous pouvez apprendre comment il réalise la POO via des structures et des tables de pointeurs de fonction

2) Exécution iOS

Découvrez comment iOS Runtime alimente l'Objectif C.Il réalise la POO via isa pointer, méta-classe

onmyway133
la source
0

Pour moi, l'orientation objet en C devrait avoir ces caractéristiques:

  1. Encapsulation et masquage des données (peut être réalisé à l'aide de structs / pointeurs opaques)

  2. Héritage et prise en charge du polymorphisme (l'héritage unique peut être obtenu à l'aide de structs - assurez-vous que la base abstraite n'est pas instanciable)

  3. Fonctionnalité du constructeur et du destructeur (pas facile à réaliser)

  4. Vérification de type (au moins pour les types définis par l'utilisateur, car C n'en applique aucun)

  5. Comptage de références (ou quelque chose pour implémenter RAII )

  6. Prise en charge limitée de la gestion des exceptions (setjmp et longjmp)

En plus de ce qui précède, il doit s'appuyer sur les spécifications ANSI / ISO et ne doit pas s'appuyer sur des fonctionnalités spécifiques au compilateur.

FL4SOF
la source
Pour le numéro (5) - Vous ne pouvez pas implémenter RAII dans un langage sans destructeurs (ce qui signifie que RAII n'est pas une technique prise en charge par le compilateur en C ou Java).
Tom
les constructeurs et les destructeurs peuvent être écrits pour un objet basé sur c - je suppose que GObject le fait. et bien sûr RAAI (ce n'est pas simple, peut être moche et n'a pas besoin d'être pragmatique du tout) - tout ce que je cherchais est d'identifier la sémantique basée sur C pour atteindre ce qui précède.
FL4SOF
C ne prend pas en charge les destructeurs. Vous devez taper quelque chose pour les faire fonctionner. Cela signifie qu'ils ne se nettoient pas. GObject ne change pas la langue.
Tom
0

Regardez http://ldeniau.web.cern.ch/ldeniau/html/oopc/oopc.html . Si rien d'autre, lire la documentation est une expérience enrichissante.

Tim
la source
3
Veuillez fournir un contexte pour le lien que vous partagez. Bien que le lien que vous avez partagé puisse en effet être très utile, il est conseillé de capturer plutôt les aspects clés de l'article partagé qui répondent à la question. De cette façon, même si le lien est supprimé, votre réponse sera toujours pertinente et utile.
ishmaelMakitla
0

Je suis un peu en retard à la fête ici, mais j'aime éviter les deux macro extrêmes - trop ou trop de code obscurcit, mais quelques macros évidentes peuvent rendre le code POO plus facile à développer et à lire:

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

Je pense que cela a un bon équilibre, et les erreurs qu'il génère (au moins avec les options par défaut de gcc 6.3) pour certaines des erreurs les plus probables sont utiles au lieu de dérouter. Le but est d'améliorer la productivité du programmeur non?

duanev
la source
0

Si vous avez besoin d'écrire un petit code, essayez ceci: 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
la source
2
Veuillez ne pas simplement publier un outil ou une bibliothèque comme réponse. Démontrez au moins comment cela résout le problème dans la réponse elle-même.
Baum mit Augen
0

Je travaille également là-dessus sur la base d'une solution macro. Donc c'est pour les plus courageux, je suppose ;-) Mais c'est déjà assez sympa, et je travaille déjà sur quelques projets en plus. Cela fonctionne de sorte que vous définissiez d'abord un fichier d'en-tête séparé pour chaque classe. Comme ça:

#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))               \

Pour implémenter la classe, vous créez un fichier d'en-tête pour elle et un fichier C dans lequel vous implémentez les méthodes:

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

Dans l'en-tête que vous avez créé pour la classe, vous incluez les autres en-têtes dont vous avez besoin et définissez les types, etc. liés à la classe. Dans l'en-tête de classe et dans le fichier C, vous incluez le fichier de spécification de classe (voir le premier exemple de code) et une macro X. Ces X-macros ( 1 , 2 , 3 etc.) étendront le code aux structures de classe réelles et autres déclarations.

Pour hériter d'une classe, #define SUPER supername et ajoutersupername__define \ comme première ligne dans la définition de classe. Les deux doivent être là. Il existe également un support JSON, des signaux, des classes abstraites, etc.

Pour créer un objet, utilisez simplement W_NEW(classname, .x=1, .y=2,...) . L'initialisation est basée sur l'initialisation struct introduite dans C11. Cela fonctionne bien et tout ce qui n'est pas répertorié est mis à zéro.

Pour appeler une méthode, utilisez W_CALL(o,method)(1,2,3) . Cela ressemble à un appel de fonction d'ordre supérieur, mais ce n'est qu'une macro. Il se développe pour devenir ((o)->klass->method(o,1,2,3))un très bon hack.

Voir la documentation et le code lui-même .

Puisque le framework a besoin d'un code standard, j'ai écrit un script Perl (wobject) qui fait le travail. Si vous utilisez cela, vous pouvez simplement écrire

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

et il créera le fichier de spécification de classe, l'en-tête de classe et un fichier C, qui inclut Point_impl.coù vous implémentez la classe. Cela économise beaucoup de travail, si vous avez beaucoup de classes simples mais que tout est toujours en C. wobject est un scanner très simple basé sur des expressions régulières qui est facile à adapter à des besoins spécifiques ou à réécrire à partir de zéro.

J.P.
la source
0

Vous pouvez essayer COOP , un framework convivial pour les programmeurs pour la POO en C, qui comprend les classes, les exceptions, le polymorphisme et la gestion de la mémoire (important pour le code incorporé). C'est une syntaxe relativement légère, voir le tutoriel dans le Wiki .

Shmuel Fine
la source