Wie kann ich einen OO-Polymorphismus in C simulieren?

73

Gibt es eine Möglichkeit, OO-ähnlichen Code in der CProgrammiersprache zu schreiben ?


Siehe auch:

Gefunden durch Suchen auf "[c] oo".

prinzdezibel
quelle
danke für den hinweis mit den eckigen klammern. Wusste das nicht.
prinzdezibel
3
Ja, das muss besser erkennbar gemacht werden oder so. Ich habe versucht, einen guten Vorschlag für uservoice zu formulieren, aber er passt nicht zusammen.
dmckee --- Ex-Moderator Kätzchen

Antworten:

60

Der erste C ++ - Compiler ("C mit Klassen") würde tatsächlich C-Code generieren, das ist also definitiv machbar.

Grundsätzlich ist Ihre Basisklasse eine Struktur; Abgeleitete Strukturen müssen die Basisstruktur an der ersten Position enthalten, damit ein Zeiger auf die "abgeleitete" Struktur auch ein gültiger Zeiger auf die Basisstruktur ist.

typedef struct {
   data member_x;
} base;

typedef struct {
   struct base;
   data member_y;
} derived;

void function_on_base(struct base * a); // here I can pass both pointers to derived and to base

void function_on_derived(struct derived * b); // here I must pass a pointer to the derived class

Die Funktionen können als Funktionszeiger Teil der Struktur sein, so dass eine Syntax wie p-> call (p) möglich wird, Sie aber dennoch explizit einen Zeiger auf die Struktur an die Funktion selbst übergeben müssen.

OnkelZeiv
quelle
8
Dies erklärt nicht, wie das Überschreiben von Methoden in C funktionieren würde. Wie können Sie function_on_base überschreiben, um wie in polymorphen C ++ - Aufrufen auf memeber_y von derivative zuzugreifen?
John K
Überschreiben ist in C.
Patrick Collins
10
Diese Antwort ist falsch. Das Übergeben von a struct derived*an function_on_basewird nicht kompiliert. struct derived*ist ein anderer Typ als struct base*selbst wenn die Adresse korrekt ist; Wenn Sie jedoch den Zeiger von derived*auf base*umwandeln, funktioniert dies (Sie verpassen jedoch die Typprüfung zur Kompilierungszeit und erhalten stattdessen zur Laufzeit einen Absturz). @PatrickCollins Zwingende ist möglich in C: pastebin.com/W5xEytbv
weberc2
@ JohnK Siehe obigen Kommentar.
weberc2
@ weberc2 Richtig, ich bin mir nicht sicher, was ich gedacht habe, als ich das geschrieben habe. Ich könnte an "Überladung" gedacht haben, was Ihre Paste auch nahe legt.
Patrick Collins
52

Ein üblicher Ansatz besteht darin, eine Struktur mit Zeigern auf Funktionen zu definieren. Dies definiert 'Methoden', die für jeden Typ aufgerufen werden können. Subtypen setzen dann ihre eigenen Funktionen in dieser gemeinsamen Struktur und geben sie zurück.

Zum Beispiel gibt es im Linux-Kernel struct:

struct inode_operations {
    int (*create) (struct inode *,struct dentry *,int, struct nameidata *);
    struct dentry * (*lookup) (struct inode *,struct dentry *, 
                               struct nameidata *);
    ...
};

Jeder registrierte Art von Dateisystem registriert dann seine eigenen Funktionen create, lookupund die verbleibenden Funktionen. Der Rest des Codes kann dann generische inode_operations verwenden:

struct inode_operations   *i_op;
i_op -> create(...);
Peter Štibraný
quelle
1
Auf diese Weise hat cfront (der ursprüngliche C ++ - Compiler) C ++ in C konvertiert, das dann mit pcc kompiliert wurde. Ich habe viel darüber gelernt, wie dies im Umgang mit Kerndateien aus diesem Durcheinander funktioniert.
Paul Tomblin
Dies ist kein Polymorphismus im C ++ - Stil. Ein großer Vorteil des Polymorphismus gegenüber Funktionszeigern besteht darin, dass der Polymorphismus die Datenbindung ermöglicht, während Funktionszeiger dies nicht tun. In diesem Fall gibt es keine Möglichkeit zur Datenbindung, sondern nur eine Struktur von Funktionszeigern. Außerdem halte ich die Struktur in diesem Kommentar für überflüssig.
user2445507
30

C ++ ist nicht weit von C.

Klassen sind Strukturen mit einem versteckten Zeiger auf eine Tabelle von Funktionszeigern namens VTable. Die Vtable selbst ist statisch. Wenn Typen auf Vtables mit derselben Struktur verweisen, Zeiger jedoch auf eine andere Implementierung verweisen, erhalten Sie Polymorphismus.

Es wird empfohlen, die Aufruflogik in Funktionen zu kapseln, die die Struktur als Parameter verwenden, um Code-Unordnung zu vermeiden.

Sie sollten auch die Instanziierung und Initialisierung von Strukturen in Funktionen (dies entspricht einem C ++ - Konstruktor) und Löschen (Destruktor in C ++) einkapseln. Dies sind sowieso gute Praktiken.

typedef struct
{
   int (*SomeFunction)(TheClass* this, int i);
   void (*OtherFunction)(TheClass* this, char* c);
} VTable;

typedef struct
{
   VTable* pVTable;
   int member;

} TheClass;

So rufen Sie die Methode auf:

int CallSomeFunction(TheClass* this, int i)
{
  (this->pVTable->SomeFunction)(this, i);
}
Denken Sie vor der Codierung
quelle
Das Beispiel ist unvollständig. Es gibt keine Erklärung dafür, wie eine neue Klasse mit verschiedenen Mitgliedern implementiert wird (außer nur dem int). In diesem Fall scheinen alle "polymorphen" Objekte dasselbe int-Element zu haben. Was ist, wenn Sie Klassen mit verschiedenen Mitgliedern definieren möchten? Ich denke in diesem Fall sollte Ihre Struktur einen ungültigen Zeiger auf Datenelemente anstelle eines konkreten int haben.
user2445507
@ user2445507 Das Beispiel implementiert eine vtable, die Funktionen zur Laufzeit aufruft, genau das, was c ++ hinter den Kulissen tun würde, wenn Sie eine Klasse erstellen.
Niclas Larsson
@NiclasLarsson In C ++ ist es möglich, eine andere Klasse zu erstellen, die die reinen virtuellen Funktionen erbt, jedoch mit unterschiedlichen Elementvariablen. Dann kann die in dieser Klasse implementierte virtuelle Funktion diese Mitgliedsvariablen verwenden. Dies ist ein Schlüsselaspekt des Polymorphismus. Das Beispiel zeigt nicht, wie das geht. In diesem Beispiel können Sie auch einfach einen Funktionszeiger verwenden.
user2445507
17

Ich habe mir die Antworten aller anderen angesehen und mir Folgendes ausgedacht:

#include <stdio.h>

typedef struct
{
    int (*get)(void* this);
    void (*set)(void* this, int i);
    int member;

} TheClass;

int Get(void* this)
{
    TheClass* This = (TheClass*)this;
    return This->member;
}

void Set(void* this, int i)
{
    TheClass* This = (TheClass*)this;
    This->member = i;
}

void init(TheClass* this)
{
    this->get = &Get;
    this->set = &Set;
}

int main(int argc, char **argv)
{
    TheClass name;
    init(&name);
    (name.set)(&name, 10);
    printf("%d\n", (name.get)(&name));
    return 0;
}

Ich hoffe das beantwortet einige Fragen.

red_hax0r
quelle
3
Gutes Beispiel. Wäre noch besser, wenn Sie 2 "abgeleitete" Klassen mit unterschiedlichen init / get / set hätten. "private" Mitglieder / Funktionen können mit undurchsichtigen Strukturen ausgeführt werden . Die Namenskonvention ist ebenfalls wichtig: mylib_someClass_aMethod(this)ist eine gute Möglichkeit.
Ciro Santilli 法轮功 冠状 病 六四 事件 29
Das sieht ordentlich aus. Ich habe dies in eine AC-Datei eingefügt und für einen ARM-Mikrocontroller in Ordnung kompiliert. Ich komme noch nicht weit, möchte aber nur eine Notiz als Bestätigung für den Beitrag von red_hax0r hinterlassen.
Coarist
1
Wo ist Polymorphismus?
Ebru Yener
12

Anhang B des Artikels Open Reusable Object Models von Ian Piumarta und Alessandro Warth von VPRI ist eine Implementierung eines Objektmodells in GNU C mit ca. 140 Codezeilen. Es ist eine faszinierende Lektüre!

Hier ist die nicht zwischengespeicherte Version des Makros, das Nachrichten an Objekte mit einer GNU-Erweiterung an C ( Anweisungsausdruck ) sendet :

struct object;

typedef struct object *oop; 
typedef oop *(*method_t)(oop receiver, ...);

//...

#define send(RCV, MSG, ARGS...) ({ \ 
    oop r = (oop)(RCV); \ 
    method_t method = _bind(r, (MSG)); \ 
    method(r, ##ARGS); \ 
}) 

Im gleichen doc, haben einen Blick auf die object, vtable, vtable_delegatedund symbolstructs und die _bindund vtable_lookupFunktionen.

Prost!

Sébastien RoccaSerra
quelle
1

Die Dateifunktionen fopen, fclose, fread sind Beispiele für OO-Code in C. Anstelle der privaten Daten in der Klasse arbeiten sie mit der FILE-Struktur, die zum Einkapseln der Daten verwendet wird, und die C-Funktionen fungieren als Elementklassenfunktionen. http://www.amazon.com/File-Structures-Object-Oriented-Approach-C/dp/0201874016

Priyank Bolia
quelle
3
Das Buch, auf das verwiesen wird, ist nur C ++. Hinweis: Ich habe eine Kopie und würde sie heute nicht empfehlen. Wenn ich heute eine OO-Empfehlung in C geben würde, wäre dies C-Schnittstellen und -Implementierungen : Techniken zum Erstellen wiederverwendbarer Software von David Hanson ( amzn.com/0201498413 ). Absolut brillantes Buch und die meisten Programmierer würden es gut verstehen, die meisten Beispiele stammen aus Compiler-Backends, so dass der Code beispielhaft ist.
Harry
1
#include <stdio.h>

typedef struct {
    int  x;
    int z;
} base;

typedef struct {
    base;
    int y;
    int x;
} derived;

void function_on_base( base * a) // here I can pass both pointers to derived and to base
{
    printf("Class base [%d]\n",a->x);
    printf("Class base [%d]\n",a->z);
}
void function_on_derived( derived * b) // here I must pass a pointer to the derived class
{
    printf("Class derived [%d]\n",b->y);
    printf("Class derived [%d]\n",b->x);
}

int main()
{
    derived d;
    base b;
    printf("Teste de poliformismo\n");

    b.x = 2;
    d.y = 1;
    b.z = 3;
    d.x = 4;
    function_on_base(&b);
    function_on_base(&d);
    function_on_derived(&b);
    function_on_derived(&d);
    return 0;
}

Die Ausgabe war:

Class base [3]
Class base [1]
Class base [4]
Class derived [2]
Class derived [3]
Class derived [1]
Class derived [4]

so funktioniert es, es ist ein polymorpher Code.

OnkelZeiv hat es am Anfang erklärt.

Paulo Silva
quelle
0

Aus Wikipedia: In Programmiersprachen und Typentheorie ist Polymorphismus (aus dem Griechischen πολύς, polys, "many, much" und μορφή, morphē, "form, shape") die Bereitstellung einer einzigen Schnittstelle zu Entitäten verschiedener Typen.

Daher würde ich sagen, dass die einzige Möglichkeit, es in C zu implementieren, darin besteht, verschiedene Argumente zusammen mit einer (halb) automatischen Verwaltung von Typinformationen zu verwenden. Zum Beispiel können Sie in C ++ schreiben (Entschuldigung für die Trivialität):

void add( int& result, int a1, int a2 );
void add( float& result, float a1, float a2 );
void add( double& result, double a1, double a2 );

In C ist unter anderem das Beste, was Sie tun können:

int int_add( int a1, int a2 );
float float_add( float a1, fload a2 );
double double_add( double a1, double a2 );

void add( int typeinfo, void* result, ... );

Dann brauchen Sie:

  1. um das "typeinfo" mit enums / makros zu implementieren
  2. um die letztere Funktion mit stdarg.h Zeug zu implementieren
  3. Abschied von der statischen C-Typprüfung

Ich bin mir fast sicher, dass jede andere Implementierung des Polymorphismus genau so aussehen sollte. Die obigen Antworten scheinen stattdessen zu versuchen, die Vererbung mehr als den Polymorphismus anzusprechen!

EnzoR
quelle
In C11 vereinfacht das neue Schlüsselwort _Generic dieses Entwurfsmuster erheblich. Ich kann es nur jedem empfehlen, der diesen Ansatz wählt.
Caleb Gray
0

Um auch die OO-Funktionalität in C zu erstellen, können Sie sich frühere Antworten ansehen.

Aber (wie es in anderen Fragen gestellt wurde, die auf diese umgeleitet wurden), wenn Sie verstehen möchten, was Polymorphismus ist, anhand von Beispielen in C-Sprache. Vielleicht irre ich mich, aber mir fällt nichts ein, das so einfach zu verstehen ist wie die C-Zeiger-Arithmetik. Meiner Meinung nach ist die Zeigerarithmetik in C von Natur aus polymorph . Im folgenden Beispiel erzeugt dieselbe Funktion (Methode in OO), nämlich die Addition (+), abhängig von den Eigenschaften der Eingabestrukturen ein unterschiedliches Verhalten.

Beispiel:

double a*;
char str*;

a=(double*)malloc(2*sizeof(double));
str=(char*)malloc(2*sizeof(char)); 

a=a+2; // make the pointer a, point 2*8 bytes ahead.

str=str+2; // make the pointer str, point 2*1 bytes ahead.

Haftungsausschluss: Ich bin sehr neu bei C und freue mich sehr darauf, korrigiert zu werden und aus den Kommentaren anderer Benutzer zu lernen oder diese Antwort sogar vollständig zu löschen, falls sie falsch sein sollte. Danke vielmals,

Johnnybegood
quelle
0

Was ich normalerweise gerne mache, ist, die Strukturen in eine andere zu verpacken, die Metainformationen über die verpackte Klasse enthält, und dann besucherähnliche Funktionslisten zu erstellen, die auf die generische Struktur wirken. Der Vorteil dieses Ansatzes besteht darin, dass Sie die vorhandenen Strukturen nicht ändern müssen und Besucher für jede Teilmenge von Strukturen erstellen können.

Nehmen Sie das übliche Beispiel:

typedef struct {
    char call[7] = "MIAO!\n";
} Cat;
    
typedef struct {
    char call[6] = "BAU!\n";
} Dog;

Wir können die 2 Streben in diese neue Struktur einwickeln:

typedef struct {
    const void * animal;
    AnimalType type;
} Animal;

Der Typ kann ein einfaches int sein, aber lassen Sie uns nicht faul sein:

typedef enum  {
    ANIMAL_CAT = 0,
    ANIMAL_DOG,
    ANIMAL_COUNT
} AnimalType;

Es wäre schön, einige Verpackungsfunktionen zu haben:

Animal catAsAnimal(const Cat * c) {
    return (Animal){(void *)c, ANIMAL_CAT};
}

Animal dogAsAnimal(const Dog * d) {
    return (Animal){(void *)d, ANIMAL_DOG};
}

Jetzt können wir unseren "Besucher" definieren:

void catCall ( Animal a ) {
    Cat * c = (Cat *)a.animal;
    printf(c->call);
}

void dogCall ( Animal a ) {
    Dog * d = (Dog *)a.animal;
    printf(d->call);
}

void (*animalCalls[ANIMAL_COUNT])(Animal)={&catCall, &dogCall};

Dann ist die tatsächliche Verwendung:

Cat cat;
Dog dog;

Animal animals[2];
animals[0] = catAsAnimal(&cat);
animals[1] = dogAsAnimal(&dog);

for (int i = 0; i < 2; i++) {
    Animal a = animals[i];
    animalCalls[a.type](a);
}

Der Nachteil dieses Ansatzes besteht darin, dass Sie die Strukturen jedes Mal umbrechen müssen, wenn Sie sie als generischen Typ verwenden möchten.

Federico Devigili
quelle