Objektorientierung in C.


157

Was wäre eine Reihe von raffinierten Präprozessor-Hacks (ANSI C89 / ISO C90-kompatibel), die eine hässliche (aber verwendbare) Objektorientierung in C ermöglichen?

Ich bin mit einigen verschiedenen objektorientierten Sprachen vertraut. Antworten Sie daher bitte nicht mit Antworten wie "Learn C ++!". Ich habe " Objektorientierte Programmierung mit ANSI C " (Vorsicht: PDF-Format ) und einige andere interessante Lösungen gelesen , aber ich interessiere mich hauptsächlich für Ihre :-)!


Siehe auch Können Sie objektorientierten Code in C schreiben?


1
Kann ich antworten, um D zu lernen und das c-kompatible Abi zu verwenden, wenn Sie C. digitalmars.com/d
Tim Matthews am

2
@ Dinah: Danke für das "Siehe auch". Dieser Beitrag war interessant.

1
Die interessante Frage scheint zu sein, warum Sie einen Pre-Prozessor-Hack von OOP auf C.
Calyth

3
@Calyth: Ich finde, dass OOP nützlich ist und "ich arbeite mit einigen eingebetteten Systemen, auf denen wirklich nur ein C-Compiler verfügbar ist" (von oben). Finden Sie raffinierte Präprozessor-Hacks nicht interessant anzusehen?

Antworten:


31

C Object System (COS) klingt vielversprechend (es ist immer noch in der Alpha-Version). Aus Gründen der Einfachheit und Flexibilität wird versucht, die verfügbaren Konzepte auf ein Minimum zu beschränken: einheitliche objektorientierte Programmierung, einschließlich offener Klassen, Metaklassen, Eigenschaftsmetaklassen, Generika, Multimethoden, Delegierung, Eigentum, Ausnahmen, Verträge und Schließungen. Es gibt einen Entwurf (PDF), der dies beschreibt.

Ausnahme in C ist eine C89-Implementierung des TRY-CATCH-ENDLICH in anderen OO-Sprachen. Es kommt mit einer Testsuite und einigen Beispielen.

Beide von Laurent Deniau, an dem viel gearbeitet wird OOP in C arbeitet .


@vonbrand COS ist nach Github migriert, wo das letzte Commit im letzten Sommer stattfindet. Die Reife kann einen Mangel an Engagement erklären.
Philant

185

Ich würde von der Verwendung von Präprozessoren (ab) abraten, um zu versuchen, die C-Syntax der einer anderen objektorientierteren Sprache ähnlicher zu machen. Auf der einfachsten Ebene verwenden Sie einfach einfache Strukturen als Objekte und geben sie durch Zeiger weiter:

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

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

Um Dinge wie Vererbung und Polymorphismus zu bekommen, muss man etwas härter arbeiten. Sie können die manuelle Vererbung durchführen, indem Sie das erste Mitglied einer Struktur als Instanz der Oberklasse festlegen und dann Zeiger auf Basisklassen und abgeleitete Klassen frei umwandeln:

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

Um Polymorphismus (dh virtuelle Funktionen) zu erhalten, verwenden Sie Funktionszeiger und optional Funktionszeigertabellen, die auch als virtuelle Tabellen oder vtables bezeichnet werden:

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

Und so macht man Polymorphismus in C. Es ist nicht schön, aber es macht den Job. Es gibt einige Probleme mit Zeigerumwandlungen zwischen Basis- und abgeleiteten Klassen, die sicher sind, solange die Basisklasse das erste Mitglied der abgeleiteten Klasse ist. Die Mehrfachvererbung ist viel schwieriger. In diesem Fall müssen Sie Ihre Zeiger manuell anpassen, um zwischen anderen Basisklassen als der ersten zu wechseln. Dies ist sehr schwierig und fehleranfällig.

Eine andere (knifflige) Sache, die Sie tun können, ist, den dynamischen Typ eines Objekts zur Laufzeit zu ändern! Sie weisen ihm einfach einen neuen vtable-Zeiger zu. Sie können sogar einige der virtuellen Funktionen selektiv ändern, während andere beibehalten werden, wodurch neue Hybridtypen erstellt werden. Achten Sie nur darauf, eine neue vtable zu erstellen, anstatt die globale vtable zu ändern. Andernfalls wirken Sie sich versehentlich auf alle Objekte eines bestimmten Typs aus.


6
Adam, der Spaß am Ändern der globalen vtable eines Typs besteht darin, die
Ententypisierung

Jetzt habe ich Mitleid mit C ++ ... Natürlich ist die C ++ - Syntax klarer, aber da es sich nicht um eine triviale Syntax handelt, bin ich entschärft. Ich frage mich, ob etwas Hybrides zwischen C ++ und C erreicht werden könnte, also wäre void * immer noch ein gültiger Castable-Typ. Der Teil mit struct derived {struct base super;};ist offensichtlich zu erraten, wie es funktioniert, da es durch die Bytereihenfolge korrekt ist.
Jokoon

2
+1 für eleganten Code, gut geschrieben. Genau das habe ich gesucht!
Homunculus Reticulli

3
Gut gemacht. Genau so habe ich es gemacht und es ist auch der richtige Weg. Anstatt einen Zeiger auf die Struktur / das Objekt zu benötigen, sollten Sie einfach einen Zeiger auf eine Ganzzahl (Adresse) übergeben. Auf diese Weise können Sie jede Art von Objekt für unbegrenzte polymorphe Methodenaufrufe übergeben. Außerdem fehlt nur eine Funktion zum Initialisieren Ihrer Strukturen (Objekte / Klassen). Dies würde eine Malloc-Funktion beinhalten und einen Zeiger zurückgeben. Vielleicht füge ich ein Stück hinzu, wie man Nachrichten

1
Dies ist der Strohhalm, der mich von C ++ gebrochen hat, und um C mehr zu verwenden (bevor ich C ++ nur für die Vererbung verwendet habe) Danke
Anne Quinn

31

Ich habe einmal mit einer C-Bibliothek gearbeitet, die auf eine Weise implementiert wurde, die mir sehr elegant erschien. Sie hatten in C eine Möglichkeit geschrieben, Objekte zu definieren und dann von ihnen zu erben, sodass sie so erweiterbar waren wie ein C ++ - Objekt. Die Grundidee war folgende:

  • Jedes Objekt hatte eine eigene Datei
  • Öffentliche Funktionen und Variablen werden in der .h-Datei für ein Objekt definiert
  • Private Variablen und Funktionen befanden sich nur in der C-Datei
  • Um zu "erben", wird eine neue Struktur erstellt, wobei das erste Mitglied der Struktur das Objekt ist, von dem geerbt werden soll

Das Erben ist schwer zu beschreiben, aber im Grunde war es das Folgende:

struct vehicle {
   int power;
   int weight;
}

Dann in einer anderen Datei:

struct van {
   struct vehicle base;
   int cubic_size;
}

Dann könnten Sie einen Van im Speicher erstellen lassen, der von Code verwendet wird, der nur über Fahrzeuge Bescheid weiß:

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

Es hat wunderbar funktioniert und die .h-Dateien haben genau definiert, was Sie mit jedem Objekt tun können sollen.


Ich mag diese Lösung wirklich, außer dass alle Interna des "Objekts" öffentlich sind.
Lawrence Dol

6
@ Software Monkey: C hat keine Zugriffskontrolle. Die einzige Möglichkeit, Implementierungsdetails auszublenden, besteht darin, über undurchsichtige Zeiger zu interagieren, was ziemlich schmerzhaft sein kann, da auf alle Felder über Zugriffsmethoden zugegriffen werden muss, die wahrscheinlich nicht inline sind.
Adam Rosenfield

1
@Adam: Compiler, die Link-Time-Optimierungen unterstützen, werden sie inline
Christoph

9
Wenn Sie dies tun, sollten Sie auch sicherstellen, dass alle Funktionen in der .c-Datei, die nicht als öffentlich definiert sind, als statisch definiert sind, damit sie nicht als benannte Funktionen in Ihren Objektdateien enden. Das stellt sicher, dass niemand seine Namen in der Linkphase finden kann.
jmucchiello

2
@Marcel: C wurde verwendet, weil der Code auf Low-Level-Boards bereitgestellt wurde, auf denen verschiedene Prozessoren für autonome Systeme ausgeführt werden. Sie alle unterstützten das Kompilieren von C in ihre jeweiligen nativen Binärdateien. Der Ansatz machte das Lesen des Codes sehr einfach, sobald Sie realisierten, was sie versuchten zu tun.
Kieveli

18

Der GNOME-Desktop für Linux ist in objektorientiertem C geschrieben und verfügt über ein Objektmodell namens " GObject ", das Eigenschaften, Vererbung, Polymorphismus sowie einige andere Extras wie Referenzen, Ereignisbehandlung (als "Signale" bezeichnet) und Laufzeit unterstützt Eingabe, private Daten usw.

Es enthält Präprozessor-Hacks, mit denen Sie beispielsweise in der Klassenhierarchie typisieren usw. Hier ist eine Beispielklasse, die ich für GNOME geschrieben habe (Dinge wie gchar sind typedefs):

Klassenquelle

Klassenkopf

Innerhalb der GObject-Struktur gibt es eine GType-Ganzzahl, die als magische Zahl für das dynamische Typisierungssystem von GLib verwendet wird (Sie können die gesamte Struktur in einen "GType" umwandeln, um den Typ zu finden).


Leider funktioniert die Readme / Tutorial-Datei (Wiki-Link) nicht und es gibt nur ein Referenzhandbuch dafür (ich spreche von GObject und nicht von GTK). Bitte geben Sie einige Tutorial-Dateien für das gleiche ...
FL4SOF

Links wurden behoben.
James Cape

4
Links sind wieder kaputt.
SeanRamey

6

Ich habe so etwas in C gemacht, bevor ich wusste, was OOP ist.

Es folgt ein Beispiel, das einen Datenpuffer implementiert, der bei Bedarf bei einer minimalen Größe, einem Inkrement und einer maximalen Größe wächst. Diese spezielle Implementierung basierte auf "Elementen", dh sie wurde entwickelt, um eine listenartige Sammlung eines beliebigen C-Typs zu ermöglichen, nicht nur einen Byte-Puffer variabler Länge.

Die Idee ist, dass das Objekt mit xxx_crt () instanziiert und mit xxx_dlt () gelöscht wird. Jede der "Mitglied" -Methoden benötigt einen speziell eingegebenen Zeiger, um damit zu arbeiten.

Auf diese Weise habe ich eine verknüpfte Liste, einen zyklischen Puffer und eine Reihe anderer Dinge implementiert.

Ich muss gestehen, ich habe nie darüber nachgedacht, wie Vererbung mit diesem Ansatz implementiert werden kann. Ich stelle mir vor, dass eine Mischung aus dem von Kieveli angebotenen ein guter Weg sein könnte.

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 war einfach ein typedef von int - ich habe es verwendet, um mich daran zu erinnern, dass seine Länge von Plattform zu Plattform (für die Portierung) variabel war.


7
Holy Moly, dies könnte einen verschleierten C-Wettbewerb gewinnen! ich mag das! :)
horseyguy

@horseyguy Nein, das konnte es nicht. Es wurde veröffentlicht. Sie erwägen auch die Aufnahme von Missbrauch von Header-Dateien gegen das iocccsize-Tool. Es ist auch kein vollständiges Programm. 2009 gab es keinen Wettbewerb, daher kann ich die iocccsize nicht vergleichen. Das CPP wurde auch viele Male missbraucht, so dass es ziemlich alt ist. Tut mir leid. Ich versuche nicht negativ zu sein, aber ich bin realistisch. Ich verstehe aber irgendwie deine Bedeutung und es ist eine gute Lektüre und ich habe sie hochgestimmt. (Und ja, ich
nehme

6

Etwas abseits des Themas, aber der ursprüngliche C ++ - Compiler Cfront kompilierte C ++ zu C und dann zu Assembler.

Konservierte hier .


Ich habe es tatsächlich schon einmal gesehen. Ich glaube, es war eine schöne Arbeit.

@Anthony Cuozzo: Stan Lippman hat ein großartiges Buch mit dem Titel "C ++ - Inside the Object Model" geschrieben, in dem er viele seiner Erfahrungen und Designentscheidungen beim Schreiben und Verwalten von C-Front erzählte. Es ist immer noch eine gute Lektüre und hat mir beim Übergang von C zu C ++ vor vielen Jahren sehr
geholfen

5

Wenn Sie Methoden, die für Objekte aufgerufen werden, als statische Methoden betrachten, die ein implizites ' this' an die Funktion übergeben, kann dies das Denken von OO in C erleichtern.

Beispielsweise:

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

wird:

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

Oder etwas ähnliches.


6
@Artelius: Sicher, aber manchmal ist das Offensichtliche nicht, bis es angegeben ist. +1 dafür.
Lawrence Dol

1
Besser noch wärestring->length(s);
OozeMeister

4

ffmpeg (ein Toolkit für die Videoverarbeitung) ist in reinem C (und Assemblersprache) geschrieben, verwendet jedoch einen objektorientierten Stil. Es ist voll von Strukturen mit Funktionszeigern. Es gibt eine Reihe von Factory-Funktionen, die die Strukturen mit den entsprechenden "Methoden" -Zeigern initialisieren.


Ich sehe keine Factory-Funktionen darin (ffmpeg), sondern es scheint keinen Polymorphismus / Vererbung zu verwenden (trivialer Weg, wie oben vorgeschlagen).
FL4SOF

avcodec_open ist eine Factory-Funktion. Es stopft Funktionszeiger in eine AVCodecContext-Struktur (wie draw_horiz_band). Wenn Sie sich die Verwendung des Makros FF_COMMON_FRAME in avcodec.h ansehen, sehen Sie etwas, das der Vererbung von Datenelementen ähnelt. IMHO, ffmpeg beweist mir, dass OOP am besten in C ++ gemacht wird, nicht in C.
Mr Fooz

3

Wenn Sie wirklich catefully, auch Standard - C - Bibliothek verwendet OOP denken - prüfen , FILE *als Beispiel: fopen()initialisiert ein FILE *Objekt, und verwenden Sie es verwendet , Mitglied Methoden fscanf(), fprintf(), fread(), fwrite()und andere, und schließlich zum Abschluss bringt es mit fclose().

Sie können auch den Pseudo-Objective-C-Weg wählen, der ebenfalls nicht schwierig ist:

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

Benutzen:

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

Dies kann sich aus einem solchen Objective-C-Code ergeben, wenn ein ziemlich alter Objective-C-zu-C-Übersetzer verwendet wird:

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

Was macht __attribute__((constructor))man in void __meta_Foo_init(void) __attribute__((constructor))?
AE Drew

1
Dies ist eine GCC-Erweiterung, die sicherstellt, dass die markierte Funktion aufgerufen wird, wenn die Binärdatei in den Speicher geladen wird. @AEDrew
Maxthon Chan

popen(3)gibt auch ein FILE *für ein anderes Beispiel zurück.
Pryftan

3

Ich denke, was Adam Rosenfield gepostet hat, ist die richtige Art, OOP in C zu machen. Ich möchte hinzufügen, dass das, was er zeigt, die Implementierung des Objekts ist. Mit anderen Worten, die eigentliche Implementierung würde in die .cDatei eingefügt, während die Schnittstelle in die Header- .hDatei eingefügt würde . Verwenden Sie zum Beispiel das obige Affenbeispiel:

Die Schnittstelle würde folgendermaßen aussehen:

//monkey.h

    struct _monkey;

    typedef struct _monkey monkey;

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

Sie können in der Schnittstellendatei sehen .h, dass Sie nur Prototypen definieren. Anschließend können Sie den Implementierungsteil " .cDatei" in eine statische oder dynamische Bibliothek kompilieren . Dadurch wird eine Kapselung erstellt, und Sie können die Implementierung nach Belieben ändern. Der Benutzer Ihres Objekts muss fast nichts über die Implementierung wissen. Dies konzentriert sich auch auf das Gesamtdesign des Objekts.

Ich bin der persönlichen Überzeugung, dass oop eine Möglichkeit ist, Ihre Codestruktur und Wiederverwendbarkeit zu konzipieren, und wirklich nichts mit den anderen Dingen zu tun hat, die zu C ++ hinzugefügt werden, wie Überladen oder Vorlagen. Ja, das sind sehr nette nützliche Funktionen, aber sie sind nicht repräsentativ dafür, was objektorientierte Programmierung wirklich ist.


Sie können eine Struktur mit deklarieren. typedef struct Monkey {} Monkey; Was bringt es, sie nach ihrer Erstellung zu tippen?
MarcusJ

1
@MarcusJ Das struct _monkeyist einfach ein Prototyp. Die tatsächliche Typdefinition wird in der Implementierungsdatei (der C-Datei) definiert. Dies erzeugt den Kapselungseffekt und ermöglicht es dem API-Entwickler, die Affenstruktur in Zukunft neu zu definieren, ohne die API zu ändern. Benutzer der API müssen sich nur mit den tatsächlichen Methoden befassen. Der API-Designer kümmert sich um die Implementierung, einschließlich der Anordnung des Objekts / der Struktur. Daher sind die Details des Objekts / der Struktur vor dem Benutzer verborgen (ein undurchsichtiger Typ).

Ich definiere meine Strukturen in den Headern. Ist das nicht Standard? Nun, ich mache das so, weil ich gelegentlich auf Mitglieder der Struktur außerhalb dieser Bibliothek zugreifen muss.
MarcusJ

1
@MarcusJ Sie können Ihre Strukturen in den Headern definieren, wenn Sie möchten (es gibt keinen Standard). Wenn Sie jedoch die interne Struktur später ändern möchten, können Sie Ihren Code beschädigen. Die Kapselung ist lediglich ein Codierungsstil, der es einfacher macht, eine Implementierung zu ändern, ohne den Code zu beschädigen. Sie können jederzeit über Zugriffsmethoden wie int getCount(ObjectType obj)usw. auf Ihre Mitglieder zugreifen, wenn Sie die Struktur in der Implementierungsdatei definieren.

2

Meine Empfehlung: Halte es einfach. Eines der größten Probleme, das ich habe, ist die Wartung älterer Software (manchmal über 10 Jahre alt). Wenn der Code nicht einfach ist, kann es schwierig sein. Ja, man kann sehr nützliches OOP mit Polymorphismus in C schreiben, aber es kann schwierig zu lesen sein.

Ich bevorzuge einfache Objekte, die eine genau definierte Funktionalität enthalten. Ein gutes Beispiel hierfür ist GLIB2 , zum Beispiel eine Hash-Tabelle:

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

Die Schlüssel sind:

  1. Einfache Architektur und Designmuster
  2. Erreicht eine grundlegende OOP-Kapselung.
  3. Einfach zu implementieren, zu lesen, zu verstehen und zu warten

1

Wenn ich OOP in CI schreiben würde, würde ich wahrscheinlich ein Pseudo- Pimpl- Design verwenden. Anstatt Zeiger an Strukturen zu übergeben, übergeben Sie am Ende Zeiger an Zeiger an Strukturen. Dies macht den Inhalt undurchsichtig und erleichtert Polymorphismus und Vererbung.

Das eigentliche Problem mit OOP in C ist, was passiert, wenn Variablen den Gültigkeitsbereich verlassen. Es gibt keine vom Compiler generierten Destruktoren, was zu Problemen führen kann. Makros können möglicherweise helfen, aber es wird immer hässlich anzusehen sein.


1
Beim Programmieren in C beschäftige ich mich mit dem Umfang, indem ich ifAnweisungen verwende und sie am Ende freigebe. Zum Beispielif ( (obj = new_myObject()) ) { /* code using myObject */ free_myObject(obj); }

1

Eine andere Möglichkeit, mit C in einem objektorientierten Stil zu programmieren, besteht darin, einen Codegenerator zu verwenden, der eine domänenspezifische Sprache in C umwandelt. Wie bei TypeScript und JavaScript, um OOP auf js zu bringen.


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

Ausgabe:

6.56
13.12

Hier ist eine Show, was OO-Programmierung mit C ist.

Dies ist echtes, reines C, keine Präprozessormakros. Wir haben Vererbung, Polymorphismus und Datenkapselung (einschließlich Daten, die für Klassen oder Objekte privat sind). Es gibt keine Chance für ein geschütztes Qualifikationsäquivalent, dh private Daten sind auch in der Vererbungskette privat. Dies ist jedoch keine Unannehmlichkeit, da ich dies nicht für notwendig halte.

CPolygon wird nicht instanziiert, weil wir es nur verwenden, um Objekte entlang der Vererbungskette zu manipulieren, die gemeinsame Aspekte haben, diese aber unterschiedlich implementieren (Polymorphismus).


0

@Adam Rosenfield hat eine sehr gute Erklärung, wie man mit C OOP erreicht

Außerdem würde ich Ihnen empfehlen, zu lesen

1) pjsip

Eine sehr gute C-Bibliothek für VoIP. Sie können lernen, wie OOP durch Strukturen und Funktionszeigertabellen erreicht wird

2) iOS-Laufzeit

Erfahren Sie, wie iOS Runtime Ziel C unterstützt. Es erreicht OOP durch einen Zeiger, eine Metaklasse


0

Für mich sollte die Objektorientierung in C folgende Merkmale haben:

  1. Kapselung und Ausblenden von Daten (kann mithilfe von Strukturen / undurchsichtigen Zeigern erreicht werden)

  2. Vererbung und Unterstützung des Polymorphismus (Einzelvererbung kann mithilfe von Strukturen erreicht werden - stellen Sie sicher, dass die abstrakte Basis nicht instanziierbar ist)

  3. Konstruktor- und Destruktorfunktionalität (nicht leicht zu erreichen)

  4. Typprüfung (zumindest für benutzerdefinierte Typen, da C keine erzwingt)

  5. Referenzzählung (oder etwas, um RAII zu implementieren )

  6. Eingeschränkte Unterstützung für die Ausnahmebehandlung (setjmp und longjmp)

Darüber hinaus sollte es sich auf ANSI / ISO-Spezifikationen und nicht auf compilerspezifische Funktionen stützen.


Für Nummer (5) - Sie können RAII nicht in einer Sprache ohne Destruktoren implementieren (was bedeutet, dass RAII keine vom Compiler unterstützte Technik in C oder Java ist).
Tom

Konstruktoren und Destruktoren können für c-basierte Objekte geschrieben werden - ich denke, GObject macht das. und natürlich RAAI (es ist nicht einfach, kann hässlich sein und muss überhaupt nicht pragmatisch sein) - alles, was ich gesucht habe, ist, C-basierte Semantik zu identifizieren, um das Obige zu erreichen.
FL4SOF

C unterstützt keine Destruktoren. Sie müssen etwas eingeben, damit sie funktionieren. Das heißt, sie räumen sich nicht auf. GObject ändert die Sprache nicht.
Tom

0

Schauen Sie sich http://ldeniau.web.cern.ch/ldeniau/html/oopc/oopc.html an . Wenn nichts anderes die Dokumentation durchliest, ist dies eine aufschlussreiche Erfahrung.


3
Bitte geben Sie den Kontext für den Link an, den Sie teilen. Obwohl der von Ihnen freigegebene Link in der Tat sehr hilfreich sein kann, ist es ratsam, die wichtigsten Aspekte des freigegebenen Artikels zu erfassen, die auf die Frage antworten. Auf diese Weise bleibt Ihre Antwort auch dann relevant und hilfreich, wenn der Link entfernt wird.
ishmaelMakitla

0

Ich bin ein bisschen zu spät zur Party hier, aber ich vermeide gerne beide Makro-Extreme - zu viele oder zu viele verschleierte Codes, aber ein paar offensichtliche Makros können die Entwicklung und das Lesen des OOP-Codes erleichtern:

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

Ich denke, dies hat eine gute Balance, und die Fehler, die es (zumindest mit den Standardoptionen von gcc 6.3) für einige der wahrscheinlicheren Fehler erzeugt, sind hilfreich, anstatt zu verwirren. Der springende Punkt ist, die Produktivität der Programmierer zu verbessern, nicht wahr?



0

Ich arbeite auch daran basierend auf einer Makrolösung. Es ist also nur für die Mutigsten, denke ich ;-) Aber es ist schon ganz nett und ich arbeite bereits an ein paar Projekten darüber. Es funktioniert so, dass Sie zuerst eine separate Header-Datei für jede Klasse definieren. So was:

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

Um die Klasse zu implementieren, erstellen Sie eine Header-Datei und eine C-Datei, in der Sie die folgenden Methoden implementieren:

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

In den Header, den Sie für die Klasse erstellt haben, fügen Sie andere benötigte Header hinzu und definieren Typen usw., die sich auf die Klasse beziehen. Sowohl im Klassenkopf als auch in der C-Datei enthalten Sie die Klassenspezifikationsdatei (siehe erstes Codebeispiel) und ein X-Makro. Diese X-Makros ( 1 , 2 , 3 usw.) erweitern den Code auf die tatsächlichen Klassenstrukturen und andere Deklarationen.

Um eine Klasse zu erben, #define SUPER supername und hinzufügensupername__define \ als erste Zeile in die Klassendefinition ein. Beide müssen da sein. Es gibt auch JSON-Unterstützung, Signale, abstrakte Klassen usw.

Um ein Objekt zu erstellen, verwenden Sie einfach W_NEW(classname, .x=1, .y=2,...) . Die Initialisierung basiert auf der in C11 eingeführten Strukturinitialisierung. Es funktioniert gut und alles, was nicht aufgeführt ist, wird auf Null gesetzt.

Verwenden Sie zum Aufrufen einer Methode W_CALL(o,method)(1,2,3) . Es sieht aus wie ein Funktionsaufruf höherer Ordnung, ist aber nur ein Makro. Es erweitert sich, ((o)->klass->method(o,1,2,3))was ein wirklich schöner Hack ist.

Siehe Dokumentation und den Code selbst .

Da das Framework Boilerplate-Code benötigt, habe ich ein Perl-Skript (Wobject) geschrieben, das den Job erledigt. Wenn Sie das verwenden, können Sie einfach schreiben

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

und es werden die Klassenspezifikationsdatei, der Klassenkopf und eine C-Datei erstellt, die enthält, Point_impl.cwo Sie die Klasse implementieren. Es spart ziemlich viel Arbeit, wenn Sie viele einfache Klassen haben, aber immer noch alles in C. Wobject ist ist ein sehr einfacher Scanner auf der Basis regulärer Ausdrücke, der sich leicht an bestimmte Anforderungen anpassen oder von Grund auf neu schreiben lässt.



0

Sie können COOP ausprobieren , ein programmiererfreundliches Framework für OOP in C, das Klassen, Ausnahmen, Polymorphismus und Speicherverwaltung (wichtig für eingebetteten Code) enthält. Es ist eine relativ leichte Syntax, siehe das Tutorial im Wiki .

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.