wake-up-neo.com

Erstellen von "Klassen" in C, auf dem Stapel gegen den Haufen?

Immer wenn ich eine "Klasse" von C sehe (jede Struktur, die für den Zugriff auf Funktionen verwendet werden soll, die einen Zeiger als erstes Argument verwenden), sehe ich sie wie folgt implementiert:

typedef struct
{
    int member_a;
    float member_b;
} CClass;

CClass* CClass_create();
void CClass_destroy(CClass *self);
void CClass_someFunction(CClass *self, ...);
...

In diesem Fall ist CClass_create immer mallocs der Speicher und gibt einen Zeiger darauf zurück.

Wenn ich sehe, dass new unnötig in C++ auftaucht, scheint dies normalerweise C++ - Programmierer in den Wahnsinn zu treiben, aber diese Vorgehensweise erscheint in C akzeptabel. Gibt es einen Grund dafür, warum Heap-zugewiesene struct "Klassen" so häufig sind?

47
Therhang

Dafür gibt es mehrere Gründe.

  1. "Undurchsichtige" Zeiger verwenden
  2. Mangel an Destruktoren
  3. Eingebettete Systeme (Stapelüberlaufproblem)
  4. Behälter
  5. Trägheit
  6. "Faulheit"

Lass uns sie kurz besprechen.

Für undurchsichtige Zeiger können Sie so etwas tun:

struct CClass_;
typedef struct CClass_ CClass;
// the rest as in your example

Daher sieht der Benutzer die Definition von struct CClass_ nicht, isoliert sie von den Änderungen und aktiviert andere interessante Dinge, wie z. B. das Implementieren der Klasse für verschiedene Plattformen.

Dies verbietet natürlich die Verwendung von Stack-Variablen von CClass. Aber, OTOH, kann man sehen, dass dies die statische Zuordnung von CClass-Objekten (aus einem Pool) nicht verbietet - zurückgegeben von CClass_create oder möglicherweise einer anderen Funktion wie CClass_create_static.

Fehlende Destruktoren - Da der C-Compiler Ihre CClass-Stack-Objekte nicht automatisch zerstört, müssen Sie dies selbst tun (manuelles Aufrufen der Destruktorfunktion). Der einzige Vorteil, der übrig bleibt, ist die Tatsache, dass die Stapelzuweisung im Allgemeinen schneller als die Heapzuordnung ist. OTOH, Sie müssen den Heap nicht verwenden - Sie können aus einem Pool oder einer Arena oder etwas Ähnlichem zuweisen, und das ist fast so schnell wie die Stapelzuweisung, ohne dass die unten beschriebenen Probleme bei der Stapelzuordnung auftreten.

Eingebettete Systeme - Stack ist keine "unendliche" Ressource, wissen Sie. Sicher, für die meisten Anwendungen auf heutigen "regulären" Betriebssystemen (POSIX, Windows ...) ist es fast so. Auf eingebetteten Systemen kann der Stack jedoch nur wenige KB betragen. Das ist extrem, aber selbst "große" eingebettete Systeme haben einen Stapel, der sich in MBs befindet. Es wird also ausgehen, wenn es zu stark beansprucht wird. Wenn dies der Fall ist, gibt es meistens keine Garantie, was passieren wird - AFAIK, sowohl in C als auch in C++ ist dies "undefiniertes Verhalten". OTOH, CClass_create() kann einen NULL-Zeiger zurückgeben, wenn Sie keinen Speicher mehr haben, und Sie können damit umgehen.

Container - C++ - Benutzer mögen Stapelzuordnung, aber wenn Sie einen std::vector auf Stapel erstellen, wird sein Inhalt heap zugewiesen. Sie können das natürlich ändern, aber das ist das Standardverhalten. Es macht das Leben viel einfacher, "alle Mitglieder eines Containers sind Heap-Allocation" zu sagen, als zu versuchen, herauszufinden, wie Sie damit umgehen sollen, falls dies nicht der Fall ist.

Trägheit - na ja, der OO kam von Smalltalk. Alles ist dynamisch dort, also ist die "natürliche" Übersetzung in C die "Alles auf den Haufen setzen". So waren die ersten Beispiele so und sie haben andere über viele Jahre inspiriert.

" Laziness " - Wenn Sie wissen, dass Sie nur Stapelobjekte möchten, benötigen Sie Folgendes:

CClass CClass_make();
void CClass_deinit(CClass *me);

Wenn Sie jedoch sowohl Stack als auch Heap zulassen möchten, müssen Sie Folgendes hinzufügen:

CClass *CClass_create();
void CClass_destroy(CClass *me);

Dies ist mehr Arbeit für den Implementierer, aber auch für den Benutzer verwirrend. Man kann leicht unterschiedliche Schnittstellen erstellen, aber es ändert nichts daran, dass Sie zwei Funktionsgruppen benötigen. 

Natürlich ist der Grund der "Behälter" auch teilweise ein Grund der "Faulheit".

50

Angenommen, CClass_create und CClass_destroy verwenden Sie malloc/free in Ihrer Frage, dann ist folgende Vorgehensweise für mich eine schlechte Praxis:

void Myfunc()
{
  CClass* myinstance = CClass_create();
  ...

  CClass_destroy(myinstance);
}

weil wir malloc und free leicht vermeiden könnten:

void Myfunc()
{
  CClass myinstance;        // no malloc needed here, myinstance is on the stack
  CClass_Initialize(&myinstance);
  ...

  CClass_Uninitialize(&myinstance);
                            // no free needed here because myinstance is on the stack
}

mit

CClass* CClass_create()
{
   CClass *self= malloc(sizeof(CClass));
   CClass_Initialize(self);
   return self;
}

void CClass_destroy(CClass *self);
{
   CClass_Uninitialize(self);
   free(self);
}

void CClass_Initialize(CClass *self)
{
   // initialize stuff
   ...
}

void CClass_Uninitialize(CClass *self);
{
   // uninitialize stuff
   ...
}

In C++ würden wir auch Folgendes tun:

void Myfunc()
{
  CClass myinstance;
  ...

}

als das:

void Myfunc()
{
  CClass* myinstance = new CCLass;
  ...

  delete myinstance;
}

Um unnötige new/delete zu vermeiden.

14
Jabberwocky

Wenn in C eine Komponente eine "create" -Funktion bereitstellt, hat der Komponentenimplementierer auch die Kontrolle darüber, wie die Komponente initialisiert wird. Also nicht nur emuliert C++ 'operator new, sondern auch den Klassenkonstruktor.

Wenn Sie die Kontrolle über die Initialisierung aufgeben, bedeutet dies viel mehr Fehlerprüfung der Eingänge. Wenn Sie die Kontrolle behalten, ist es einfacher, konsistentes und vorhersagbares Verhalten bereitzustellen.

Ich nehme auch die Ausnahme von malloc always an, das zur Speicherzuordnung verwendet wird. Dies kann oft der Fall sein, aber nicht immer. In einigen eingebetteten Systemen werden Sie beispielsweise feststellen, dass malloc/free überhaupt nicht verwendet wird. Die X_create-Funktionen können auf andere Weise zuordnen, z. von einem Array, dessen Größe zur Kompilierzeit festgelegt ist.

9

Dies bringt viele Antworten hervor, weil es etwas meinungsbasiertes ist. Trotzdem möchte ich erklären, warum ich persönlich lieber meine "C-Objekte" auf dem Haufen zugewiesen habe. Der Grund ist, dass meine Felder alle verborgen sind (sprich: privat), um Code zu verbrauchen. Dies wird als undurchsichtiger Zeiger bezeichnet. In der Praxis bedeutet dies, dass Ihre Header-Datei das verwendete struct nicht definiert, sondern nur deklariert. Als direkte Konsequenz kann der verbrauchende Code die Größe der struct-Datei nicht kennen und die Stapelzuweisung wird daher unmöglich.

Der Vorteil ist: verbrauchender Code kann niemals von der Definition des struct abhängen, das heißt, es ist unmöglich, dass Sie den Inhalt des struct von außen als inkonsistent darstellen und Sie unnötige Rekompilierungen vermeiden verbraucht Code, wenn sich struct ändert.

Das erste Problem wird in c ++ behandelt, indem Felder als private deklariert werden. Die Definition Ihres class wird jedoch immer noch in allen Compilierungseinheiten importiert, in denen er verwendet wird. Daher müssen Sie sie erneut kompilieren, auch wenn sich nur Ihre private-Mitglieder ändern. Die häufig in c ++ verwendete Lösung ist das pimpl-Muster: Alle privaten Mitglieder in einem zweiten struct (oder: class), das nur in der Implementierungsdatei definiert ist. Dies setzt natürlich voraus, dass Ihre pimpl dem Heap zugeordnet ist.

Hinzu kommt: modern OOP Sprachen (wie z. B. Java oder c # ) verfügen über Mittel zum Zuordnen von Objekten (und entscheiden normalerweise, ob es sich um einen Stack oder einen Heap handelt) ohne Aufrufcode, der über ihre Definition Bescheid weiß.

8
user2371524

Ich würde den "Konstruktor" in eine void CClass_create(CClass*); ändern.

Es wird keine Instanz/Referenz der Struktur zurückgeben, sondern bei einer Instanz aufgerufen werden. 

Ob es auf dem "Stack" oder dynamisch zugewiesen wird, hängt vollständig von den Anforderungen Ihres Nutzungsszenarios ab. Unabhängig davon, wie Sie es zuweisen, rufen Sie einfach CClass_create() auf, indem Sie die zugewiesene Struktur als Parameter übergeben.

{
    CClass stk;
    CClass_create(&stk);

    CClass *dyn = malloc(sizeof(CClass));
    CClass_create(dyn);

    CClass_destroy(&stk); // the local object lifetime ends here, dyn lives on
}

// and later, assuming you kept track of dyn
CClass_destroy(dyn); // destructed
free(dyn); // deleted

Achten Sie nur darauf, keine Referenz auf ein lokales Objekt (auf dem Stack zugewiesen) zurückzugeben, da dies UB ist.

Unabhängig davon, wie Sie es zuweisen, müssen Sie void CClass_destroy(CClass*); an der richtigen Stelle aufrufen (das Ende der Lebensdauer dieses Objekts). Bei dynamischer Zuweisung geben Sie auch diesen Speicher frei.

Unterscheidung zwischen Zuteilung/Freigabe und Konstruktion/Zerstörung, die sind nicht gleich (selbst wenn sie in C++ automatisch miteinander gekoppelt sind).

3
dtech

Im Allgemeinen bedeutet die Tatsache, dass Sie einen * sehen, nicht, dass es malloc 'd war. Sie könnten beispielsweise einen Zeiger auf die globale Variable static haben. In Ihrem Fall nimmt CClass_destroy() tatsächlich keinen Parameter an, vorausgesetzt es kennt bereits einige Informationen über das Objekt, das zerstört wird.

Darüber hinaus sind Zeiger, ob malloc 'd, die einzige Möglichkeit, das Objekt zu ändern.

Ich sehe keine besonderen Gründe für die Verwendung des Heapspeichers anstelle des Stapels: Sie benötigen nicht weniger Speicherplatz. Für die Initialisierung solcher "Klassen" sind jedoch Init/destroy-Funktionen erforderlich, da die zugrunde liegende Datenstruktur tatsächlich dynamische Daten enthalten muss, also die Verwendung von Zeigern.

3
edmz

C fehlen bestimmte Dinge, die C++ - Programmierer für selbstverständlich halten.

  1. öffentliche und private Planer
  2. konstrukteure und Destruktoren

Der große Vorteil dieses Ansatzes besteht darin, dass Sie die Struktur in Ihrer C-Datei ausblenden können und mit den Funktionen zum Erstellen und Zerstören die korrekte Konstruktion und Zerstörung erzwingen können.

Wenn Sie die Struktur in Ihrer .h-Datei verfügbar machen, können Benutzer direkt auf die Member zugreifen, wodurch die Kapselung unterbrochen wird. Auch wenn Sie das Erstellen nicht erzwingen, kann Ihr Objekt falsch erstellt werden.

2
doron

Denn eine Funktion kann eine Stack-zugeordnete Struktur nur zurückgeben, wenn sie keine Zeiger auf andere zugewiesene Strukturen enthält. Wenn sie nur einfache Objekte (int, bool, floats, Zeichen und Arrays, aber kein Zeiger ) enthält kann es auf Stapel zuweisen. Aber Sie müssen wissen, dass es kopiert wird, wenn Sie es zurückschicken. Wenn Sie Zeiger auf andere Strukturen zulassen oder die Kopie vermeiden möchten, verwenden Sie Heap.

Wenn Sie die Struktur jedoch in einer Top-Level-Einheit erstellen und nur in aufgerufenen Funktionen verwenden und niemals zurückgeben können, ist Stack geeignet

2
Serge Ballesta

Wenn die maximale Anzahl von Objekten eines Typs, die gleichzeitig vorhanden sein müssen, festgelegt ist, muss das System in der Lage sein, mit jeder "Live" -Instanz etwas zu tun, und die betreffenden Objekte verbrauchen nicht zu viel Geld, am besten Ein Ansatz ist im Allgemeinen weder eine Heap-Zuordnung noch eine Stack-Zuordnung, sondern ein statisch zugewiesenes Array zusammen mit den Methoden "create" und "destroy". Die Verwendung eines Arrays vermeidet die Notwendigkeit, eine verknüpfte Liste von Objekten zu führen, und macht es möglich, den Fall zu handhaben, in dem ein Objekt nicht sofort zerstört werden kann, weil es "beschäftigt" ist [z. Wenn Daten auf einem Kanal über Interrupt oder DMA ankommen, wenn der Benutzercode entscheidet, dass er nicht mehr an dem Kanal interessiert ist und über diesen Kanal verfügt, kann der Benutzercode das Kennzeichen "Dispose after done" setzen und zurückkehren, ohne sich darum kümmern zu müssen einen anstehenden Interrupt haben oder DMA Speicher überschreiben, der ihm nicht mehr zugewiesen ist].

Die Verwendung eines Pools fester Größe von Objekten fester Größe macht die Zuweisung und die Aufhebung der Zuweisung viel vorhersehbarer als das Entnehmen von Speicher aus einem Heap unterschiedlicher Größe. In den Fällen, in denen der Bedarf variabel ist und die Objekte viel Platz beanspruchen (einzeln oder zusammen), ist der Ansatz nicht besonders gut geeignet, wenn der Bedarf jedoch weitgehend konstant ist (z. B. benötigt eine Anwendung immer 12 Objekte und manchmal bis zu 3 Objekte) mehr) kann es viel besser als alternative Ansätze. Die einzige Schwachstelle besteht darin, dass alle Einstellungen entweder an der Stelle ausgeführt werden müssen, an der der statische Puffer deklariert ist, oder sie müssen durch ausführbaren Code in den Clients ausgeführt werden. Es ist nicht möglich, die Syntax der Variableninitialisierung an einem Clientstandort zu verwenden.

Bei diesem Ansatz ist es übrigens nicht erforderlich, dass der Clientcode Zeiger auf irgendetwas erhält. Stattdessen kann man die Ressourcen anhand der gewünschten Integer-Größe identifizieren. Wenn die Anzahl der Ressourcen niemals die Anzahl der Bits in einer int überschreiten muss, kann es außerdem hilfreich sein, dass einige Statusvariablen ein Bit pro Ressource verwenden. Beispielsweise könnte man die Variablen timer_notifications (nur über Interrupt-Handler geschrieben) und timer_acks (nur über Hauptleitungscode geschrieben) haben und angeben, dass das Bit N von (timer_notifications ^ timer_acks) gesetzt wird, wenn der Timer N einen Dienst wünscht. Bei Verwendung eines solchen Ansatzes muss der Code nur zwei Variablen lesen, um zu bestimmen, ob ein Zeitgeber gewartet werden muss, anstatt für jeden Zeitgeber eine Variable lesen zu müssen.

2
supercat

Ist Ihre Frage "warum in C normalerweise Speicher dynamisch zugewiesen wird und in C++ nicht?"

In C++ gibt es eine Menge Konstrukte, die neue überflüssige Kopier-, Verschiebungs- und normale Konstruktoren, Destruktoren, die Standardbibliothek und Zuordnungslisten machen.

Aber in C kommt man nicht herum.

1

Es ist eigentlich ein Rückschlag auf C++, der "Neues" zu einfach macht.

In der Theorie ist die Verwendung dieses Klassenaufbaumusters in C identisch mit der Verwendung von "new" in C++, daher sollte es keinen Unterschied geben. Die Menschen neigen jedoch dazu, über die Sprachen nachzudenken, und die Reaktionen auf den Code sind unterschiedlich.

In C ist es sehr üblich, über die genauen Vorgänge nachzudenken, die der Computer ausführen muss, um Ihre Ziele zu erreichen. Es ist nicht universell, aber es ist eine sehr gewöhnliche Denkweise. Es wird davon ausgegangen, dass Sie sich die Zeit genommen haben, um die Kosten/Nutzen-Analyse des Malloc/free durchzuführen.

In C++ ist es viel einfacher geworden, Codezeilen zu schreiben, die sehr viel für Sie tun, ohne dass Sie es überhaupt merken. Es ist durchaus üblich, dass jemand eine Zeile Code schreibt und nicht einmal merkt, dass es 100 oder 200 neue/Löschungen erforderlich gemacht hat! Dies hat zu einem Rückschlag geführt, bei dem der C++ - Entwickler bei Nachrichten und Löschvorgängen fanatisch nitpick wird, aus Angst, dass sie versehentlich überall aufgerufen werden.

Dies sind natürlich Verallgemeinerungen. Auf keinen Fall passen die gesamten C- und C++ - Communities in diese Form. Wenn Sie jedoch Probleme haben, neue Dinge zu verwenden, anstatt Dinge auf den Haufen zu legen, kann dies die Hauptursache sein.

1
Cort Ammon

Es ist ziemlich seltsam, dass man es so oft sieht. Sie müssen wie ein "fauler" Code gesucht haben.

In C ist die von Ihnen beschriebene Technik normalerweise für "undurchsichtige" Bibliothekstypen reserviert, d. H. Strukturtypen, deren Definitionen absichtlich für den Code des Clients unsichtbar gemacht werden. Da der Client solche Objekte nicht deklarieren kann, muss das Idiom wirklich dynamische Zuordnung im "versteckten" Bibliothekscode haben.

Wenn das Ausblenden der Definition der Struktur nicht erforderlich ist, sieht ein typisches C-Idiom normalerweise wie folgt aus

typedef struct CClass
{
    int member_a;
    float member_b;
} CClass;

CClass* CClass_init(CClass* cclass);
void CClass_release(CClass* cclass);

Die Funktion CClass_init initialisiert das *cclass-Objekt und gibt denselben Zeiger als Ergebnis zurück. Das heißt Die Last des Zuweisens von Speicher für das Objekt wird dem Anrufer auferlegt, und der Anrufer kann ihn auf jede beliebige Weise zuordnen

CClass cclass;
CClass_init(&cclass);
...
CClass_release(&cclass);

Ein klassisches Beispiel für diesen Begriff wäre pthread_mutex_t mit pthread_mutex_init und pthread_mutex_destroy.

Die Verwendung der früheren Technik für nicht deckende Typen (wie in Ihrem ursprünglichen Code) ist in der Regel eine fragwürdige Praxis. Es ist genau fragwürdig, da der dynamische Speicher in C++ unentgeltlich verwendet wird. Es funktioniert zwar, aber die Verwendung von dynamischem Speicher, wenn es nicht erforderlich ist, ist in C genauso verpönt wie in C++.

0
AnT