webentwicklung-frage-antwort-db.com.de

Wie implementiert man eine Klasse in C?

Angenommen, ich muss C verwenden (keine C++ - oder objektorientierten Compiler) und ich habe keine dynamische Speicherzuordnung. Mit welchen Techniken kann ich eine Klasse implementieren oder eine gute Annäherung an eine Klasse? Ist es immer eine gute Idee, die "Klasse" in einer separaten Datei zu isolieren? Angenommen, wir können den Speicher vorbelegen, indem wir eine feste Anzahl von Instanzen annehmen oder sogar den Verweis auf jedes Objekt vor der Kompilierung als Konstante definieren. Fühlen Sie sich frei, Annahmen darüber zu machen, welches OOP= Konzept ich implementieren muss (es wird variieren) und für jedes die beste Methode vorzuschlagen.

Beschränkungen:

  • Ich muss C und kein OOP) verwenden, da ich Code für ein eingebettetes System schreibe und der Compiler und die bereits vorhandene Codebasis in C sind.
  • Es gibt keine dynamische Speicherzuweisung, da wir nicht genügend Speicher haben, um davon auszugehen, dass uns der Speicher nicht ausgeht, wenn wir ihn dynamisch zuweisen.
  • Die Compiler, mit denen wir arbeiten, haben keine Probleme mit Funktionszeigern
130
Ben Gartner

Das hängt von dem genauen "objektorientierten" Funktionsumfang ab, den Sie haben möchten. Wenn Sie Dinge wie Überladen und/oder virtuelle Methoden benötigen, müssen Sie wahrscheinlich Funktionszeiger in Strukturen einfügen:

typedef struct {
  float (*computeArea)(const ShapeClass *shape);
} ShapeClass;

float shape_computeArea(const ShapeClass *shape)
{
  return shape->computeArea(shape);
}

Auf diese Weise können Sie eine Klasse implementieren, indem Sie die Basisklasse "erben" und eine geeignete Funktion implementieren:

typedef struct {
  ShapeClass shape;
  float width, height;
} RectangleClass;

static float rectangle_computeArea(const ShapeClass *shape)
{
  const RectangleClass *rect = (const RectangleClass *) shape;
  return rect->width * rect->height;
}

Dazu müssen Sie natürlich auch einen Konstruktor implementieren, der sicherstellt, dass der Funktionszeiger ordnungsgemäß eingerichtet ist. Normalerweise würden Sie der Instanz dynamisch Speicher zuweisen, aber Sie können dies auch dem Aufrufer überlassen:

void rectangle_new(RectangleClass *rect)
{
  rect->width = rect->height = 0.f;
  rect->shape.computeArea = rectangle_computeArea;
}

Wenn Sie mehrere verschiedene Konstruktoren wollen, müssen Sie die Funktionsnamen "dekorieren", Sie können nicht mehr als eine rectangle_new() -Funktion haben:

void rectangle_new_with_lengths(RectangleClass *rect, float width, float height)
{
  rectangle_new(rect);
  rect->width = width;
  rect->height = height;
}

Hier ist ein einfaches Beispiel, das die Verwendung zeigt:

int main(void)
{
  RectangleClass r1;

  rectangle_new_with_lengths(&r1, 4.f, 5.f);
  printf("rectangle r1's area is %f units square\n", shape_computeArea(&r1));
  return 0;
}

Ich hoffe, das gibt Ihnen zumindest einige Ideen. Für ein erfolgreiches und umfangreiches objektorientiertes Framework in C schauen Sie in die Bibliothek GObject von glib.

Beachten Sie auch, dass oben keine explizite "Klasse" modelliert wird. Jedes Objekt verfügt über eigene Methodenzeiger, die etwas flexibler sind als in C++ üblich. Auch kostet es Speicher. Sie können dem entgehen, indem Sie die Methodenzeiger in eine class -Struktur einfügen und für jede Objektinstanz eine Möglichkeit erfinden, auf eine Klasse zu verweisen.

81
unwind

Ich musste es auch einmal für eine Hausaufgabe machen. Ich folgte diesem Ansatz:

  1. Definieren Sie Ihre Datenelemente in einer Struktur.
  2. Definieren Sie Ihre Funktionsmember, die als erstes Argument einen Zeiger auf Ihre Struktur verwenden.
  3. Tun Sie dies in einem Header & einem c. Header für Strukturdefinition & Funktionsdeklarationen, c für Implementierungen.

Ein einfaches Beispiel wäre dies:

/// Queue.h
struct Queue
{
    /// members
}
typedef struct Queue Queue;

void Push(Queue* q, int element);
void pop(Queue* q);
// etc.
/// 
23
erelender

Wenn Sie nur eine Klasse möchten, verwenden Sie ein Array von structs als "Objekt" -Daten und übergeben Sie Zeiger an die "Element" -Funktionen. Sie können typedef struct _whatever Whatever Verwenden, bevor Sie struct _whatever Deklarieren, um die Implementierung vor dem Clientcode zu verbergen. Es gibt keinen Unterschied zwischen einem solchen "Objekt" und dem Objekt der C-Standardbibliothek FILE.

Wenn Sie mehr als eine Klasse mit Vererbung und virtuellen Funktionen wünschen, ist es üblich, Zeiger auf die Funktionen als Mitglieder der Struktur oder einen gemeinsamen Zeiger auf eine Tabelle mit virtuellen Funktionen zu haben. Die Bibliothek GObject verwendet sowohl diesen als auch den typedef-Trick und ist weit verbreitet.

Es gibt auch ein Buch über Techniken dafür, das online verfügbar ist - Objektorientierte Programmierung mit ANSI C .

12
Pete Kirkham

C Schnittstellen und Implementierungen: Techniken zum Erstellen wiederverwendbarer Software, David R. Hanson

http://www.informit.com/store/product.aspx?isbn=020149841

Dieses Buch deckt Ihre Frage hervorragend ab. Es ist in der Addison Wesley Professional Computing-Serie.

Das grundlegende Paradigma ist ungefähr so:

/* for data structure foo */

FOO *myfoo;
myfoo = foo_create(...);
foo_something(myfoo, ...);
myfoo = foo_append(myfoo, ...);
foo_delete(myfoo);
7
Mark Harrison

sie können einen Blick auf GOBject werfen. Es ist eine Betriebssystembibliothek, mit der Sie ein Objekt ausführlich bearbeiten können.

http://library.gnome.org/devel/gobject/stable/

7
Alex

Verwenden Sie ein struct, um die Datenelemente einer Klasse zu simulieren. In Bezug auf den Methodenumfang können Sie private Methoden simulieren, indem Sie die private Funktionsprototypen in die .c-Datei und die public Funktionen in die .h-Datei einfügen.

4
Taylor Leese

Ich werde ein einfaches Beispiel geben, wie OOP in C gemacht werden sollte. Ich erkenne, dass dieser Thead aus dem Jahr 2009 ist, möchte dies aber trotzdem hinzufügen.

/// Object.h
typedef struct Object {
    uuid_t uuid;
} Object;

int Object_init(Object *self);
uuid_t Object_get_uuid(Object *self);
int Object_clean(Object *self);

/// Person.h
typedef struct Person {
    Object obj;
    char *name;
} Person;

int Person_init(Person *self, char *name);
int Person_greet(Person *self);
int Person_clean(Person *self);

/// Object.c
#include "object.h"

int Object_init(Object *self)
{
    self->uuid = uuid_new();

    return 0;
}
uuid_t Object_get_uuid(Object *self)
{ // Don't actually create getters in C...
    return self->uuid;
}
int Object_clean(Object *self)
{
    uuid_free(self->uuid);

    return 0;
}

/// Person.c
#include "person.h"

int Person_init(Person *self, char *name)
{
    Object_init(&self->obj); // Or just Object_init(&self);
    self->name = strdup(name);

    return 0;
}
int Person_greet(Person *self)
{
    printf("Hello, %s", self->name);

    return 0;
}
int Person_clean(Person *self)
{
    free(self->name);
    Object_clean(self);

    return 0;
}

/// main.c
int main(void)
{
    Person p;

    Person_init(&p, "John");
    Person_greet(&p);
    Object_get_uuid(&p); // Inherited function
    Person_clean(&p);

    return 0;
}

Das Grundkonzept beinhaltet das Platzieren der 'geerbten Klasse' an der Spitze der Struktur. Auf diese Weise wird beim Zugriff auf die ersten 4 Bytes in der Struktur auch auf die ersten 4 Bytes in der 'geerbten Klasse' zugegriffen (unter Berücksichtigung nicht verrückter Optimierungen). Wenn der Zeiger der Struktur auf die 'geerbte Klasse' gesetzt wird, kann die 'geerbte Klasse' auf die 'geerbten Werte' genauso zugreifen wie auf ihre Mitglieder.

Diese und einige Benennungskonventionen für Konstruktoren, Destruktoren, Zuordnungs- und Freigabefunktionen (ich empfehle init, clean, new, free) werden Ihnen einen langen Weg bringen.

Verwenden Sie für virtuelle Funktionen Funktionszeiger in der Struktur, möglicherweise mit Class_func (...); Wrapper auch. Fügen Sie für (einfache) Vorlagen einen size_t-Parameter hinzu, um die Größe zu bestimmen, benötigen Sie einen void * -Zeiger oder benötigen Sie einen Klassentyp mit genau der Funktionalität, die Sie interessieren. (z. B. int GetUUID (Object * self); GetUUID (& p);)

4
YoYoYonnY
#include <stdio.h>
#include <math.h>
#include <string.h>
#include <uchar.h>

/**
 * Define Shape class
 */
typedef struct Shape Shape;
struct Shape {
    /**
     * Variables header...
     */
    double width, height;

    /**
     * Functions header...
     */
    double (*area)(Shape *shape);
};

/**
 * Functions
 */
double calc(Shape *shape) {
        return shape->width * shape->height;
}

/**
 * Constructor
 */
Shape _Shape() {
    Shape s;

    s.width = 1;
    s.height = 1;

    s.area = calc;

    return s;
}

/********************************************/

int main() {
    Shape s1 = _Shape();
    s1.width = 5.35;
    s1.height = 12.5462;

    printf("Hello World\n\n");

    printf("User.width = %f\n", s1.width);
    printf("User.height = %f\n", s1.height);
    printf("User.area = %f\n\n", s1.area(&s1));

    printf("Made with \xe2\x99\xa5 \n");

    return 0;
};
4
Pierozi

In Ihrem Fall könnte die gute Annäherung der Klasse das an ADT sein. Aber es wird immer noch nicht dasselbe sein.

3
Artem Barger

Möglicherweise dieses Online-Buch könnte Ihnen helfen, Ihr Problem zu lösen.

Meine Strategie ist:

  • Definieren Sie den gesamten Code für die Klasse in einer separaten Datei
  • Definieren Sie alle Schnittstellen für die Klasse in einer separaten Header-Datei
  • Alle Member-Funktionen nehmen ein "ClassHandle", das für den Instanznamen steht (anstelle von o.foo (), rufen Sie foo (oHandle) auf
  • Der Konstruktor wird durch eine Funktion ersetzt, die ClassInit (ClassHandle h, int x, int y, ...) OR ClassHandle ClassInit (int x, int y, ...) abhängig vom Speicher ungültig macht Allokationsstrategie
  • Alle Mitgliedsvariablen werden als Mitglied einer statischen Struktur in der Klassendatei gespeichert, um sie in der Datei zu kapseln und zu verhindern, dass externe Dateien darauf zugreifen
  • Die Objekte werden in einem Array der obigen statischen Struktur mit vordefinierten Handles (auf der Oberfläche sichtbar) oder einer festgelegten Anzahl von Objekten gespeichert, die instanziiert werden können
  • Falls nützlich, kann die Klasse öffentliche Funktionen enthalten, die das Array durchlaufen und die Funktionen aller instanziierten Objekte aufrufen (RunAll () ruft jeden Run (oHandle) auf
  • Eine Deinit-Funktion (ClassHandle h) gibt den zugewiesenen Speicher (Array-Index) in der dynamischen Zuweisungsstrategie frei

Hat jemand Probleme, Lücken, potenzielle Fallstricke oder versteckte Vor-/Nachteile für eine der Varianten dieses Ansatzes? Wenn ich eine Entwurfsmethode neu erfinde (und ich nehme an, ich muss es sein), können Sie mich auf den Namen der Methode verweisen?

3
Ben Gartner

Siehe auch diese Antwort und diese

Es ist möglich. Es scheint immer eine gute Idee zu sein, aber danach wird es zu einem Alptraum für die Instandhaltung. Ihr Code wird mit Codeteilen übersät, die alles zusammenbinden. Ein neuer Programmierer wird viele Probleme haben, den Code zu lesen und zu verstehen, wenn Sie Funktionszeiger verwenden, da nicht klar ist, welche Funktionen aufgerufen werden.

Daten, die sich mit get/set-Funktionen verstecken, lassen sich leicht in C implementieren, bleiben aber dort stehen. Ich habe dies in der eingebetteten Umgebung mehrfach versucht und am Ende ist es immer ein Wartungsproblem.

Da Sie alle bereit sind, Wartungsprobleme zu haben, würde ich klar steuern.

3
Gerhard

Es gibt ein sehr umfangreiches Buch zu diesem Thema, das einen Blick wert sein könnte:

Objektorientierte Programmierung in ANSI-C

3
Ruben Steins

Mein Ansatz wäre, das struct und alle primär zugeordneten Funktionen in eine separate Quelldatei (en) zu verschieben, damit es sein kann "portabel" verwendet.

Abhängig von Ihrem Compiler können Sie möglicherweise Funktionen in struct aufnehmen, aber das ist ein sehr compilerspezifische Erweiterung und hat nichts mit der letzten Version des Standards zu tun, die ich routinemäßig verwendet habe :)

2
warren

Der erste C++ - Compiler war eigentlich ein Präprozessor, der den C++ - Code in C übersetzte.

Es ist also sehr gut möglich, Klassen in C zu haben. Sie könnten versuchen, einen alten C++ - Präprozessor zu finden und herauszufinden, welche Lösungen er erstellt.

2
Toad

GTK baut vollständig auf C auf und verwendet viele OOP Konzepte. Ich habe den Quellcode von GTK durchgelesen und es ist ziemlich beeindruckend und definitiv einfacher zu lesen. Das Grundkonzept ist, dass jedes " class "ist einfach eine Struktur und die dazugehörigen statischen Funktionen. Alle statischen Funktionen akzeptieren die Struktur" instance "als Parameter, führen alle erforderlichen Aktionen aus und geben bei Bedarf Ergebnisse zurück. Beispielsweise haben Sie möglicherweise die Funktion" GetPosition (CircleStruct obj ) ". Die Funktion würde einfach durch die Struktur graben, die Positionsnummern extrahieren, wahrscheinlich ein neues PositionStruct-Objekt erstellen, das x und das y in die neue PositionStruct einfügen und es zurückgeben. GTK implementiert sogar die Vererbung auf diese Weise, indem Strukturen in Strukturen eingebettet werden. ziemlich clever.

2
rocketsarefast

Möchten Sie virtuelle Methoden?

Wenn nicht, definieren Sie einfach eine Reihe von Funktionszeigern in der Struktur selbst. Wenn Sie alle Funktionszeiger Standard-C-Funktionen zuweisen, können Sie Funktionen von C in einer sehr ähnlichen Syntax aufrufen wie unter C++.

Wenn Sie virtuelle Methoden haben möchten, wird es komplizierter. Grundsätzlich müssen Sie für jede Struktur eine eigene VTable implementieren und der VTable Funktionszeiger zuweisen, je nachdem, welche Funktion aufgerufen wird. Sie benötigen dann eine Reihe von Funktionszeigern in der Struktur selbst, die wiederum den Funktionszeiger in der VTable aufrufen. Dies ist im Wesentlichen das, was C++ tut.

TBH obwohl ... wenn Sie letzteres wollen, dann sind Sie wahrscheinlich besser dran, nur einen C++ - Compiler zu finden, den Sie verwenden und das Projekt neu kompilieren können. Ich habe nie die Besessenheit verstanden, dass C++ in Embedded nicht verwendbar ist. Ich habe es oft benutzt und es funktioniert schnell und hat keine Speicherprobleme. Sicher, Sie müssen etwas vorsichtiger sein, was Sie tun, aber es ist wirklich nicht so kompliziert.

1
Goz

C ist keine OOP - Sprache, wie Sie zu Recht betonen, es gibt also keinen eingebauten Weg, eine wahre Klasse zu schreiben. Am besten ist es, sich structs) anzuschauen und Funktionszeiger , mit denen Sie eine Näherung für eine Klasse erstellen können. Da C jedoch prozedural ist, möchten Sie möglicherweise mehr C-ähnlichen Code schreiben (dh ohne zu versuchen, Klassen zu verwenden) ).

Wenn Sie C verwenden können, können Sie wahrscheinlich auch C++ verwenden und Klassen abrufen.

0
user130076