Wie würde ich in C entscheiden, ob ich eine Struktur oder einen Zeiger auf eine Struktur zurückgeben möchte?

Lesezeit: 11 Minuten

Benutzeravatar von Dellowar
Dellowar

In letzter Zeit an meinem C-Muskel zu arbeiten und die vielen Bibliotheken durchzusehen, mit denen ich gearbeitet habe, hat mir sicherlich eine gute Vorstellung davon gegeben, was gute Praxis ist. Eine Sache, die ich NICHT gesehen habe, ist eine Funktion, die eine Struktur zurückgibt:

something_t make_something() { ... }

Nach dem, was ich aufgenommen habe, ist dies der “richtige” Weg, dies zu tun:

something_t *make_something() { ... }
void destroy_something(something_t *object) { ... }

Die Architektur in Code-Snippet 2 ist WEITERHIN beliebter als Snippet 1. Also frage ich jetzt, warum sollte ich jemals eine Struktur direkt zurückgeben, wie in Snippet 1? Welche Unterschiede sollte ich berücksichtigen, wenn ich mich zwischen den beiden Optionen entscheide?

Außerdem, wie ist diese Option im Vergleich?

void make_something(something_t *object)

  • Der wichtige Unterschied, den ich sehe, ist das Kopieren vs. nicht und Heap vs. Stack.

    – Iharob Al Asimi

    21. Oktober 2016 um 2:59 Uhr

  • Markieren Sie nicht sowohl C als auch C++. Die Antworten auf dieselbe Frage für die beiden Sprachen sind sehr unterschiedlich. Wähle eins.

    Benutzer2486888

    21. Oktober 2016 um 3:29 Uhr


  • Ich habe einige Änderungen vorgenommen, um zu verhindern, dass diese Frage als meinungsbasiert gekennzeichnet wird. „Best Practices“ kann eine sehr verschwommene Linie sein, und die Vorstellung, dass eine Option besser ist als die andere, kann subjektiv sein. Lassen Sie mich wissen, wenn die bearbeitete Version der Frage zu weit von dem entfernt ist, was Sie wissen möchten.

    – Dietrich Ep

    21. Oktober 2016 um 4:16 Uhr

  • Wenn die Struktur zu groß ist, um wie ein normaler Rückgabewert zurückgegeben zu werden (z. B. in einem Register), verlangt die überwiegende Mehrheit der ABIs von Compilern, dass sie die erste Form in die zweite Form umwandeln, wodurch effektiv ein versteckter Zeiger übergeben wird, der die make_something Funktion wird gefüllt. Daher sind die beiden Formulare aus der Sicht des Objektcodes im Grunde identisch, der einzige Unterschied besteht darin, wie Ihre API für den Client aussehen soll. Und aus diesem Grund würde ich die meiste Zeit Form Nr. 1 wählen, weil es so viel einfacher ist. Lassen Sie den Compiler die Drecksarbeit der Übergabe von Zeigern erledigen.

    – Cody Grey

    21. Oktober 2016 um 10:50 Uhr


  • @CodyGray: Außer … ABI-Kompatibilität kann erreicht werden, indem undurchsichtige Typen verwendet werden (Lundins Antwort), was das Übergeben von Zeigern erfordert. Und wenn es darauf ankommt, dann ist es wirklich wichtig.

    – Matthias M.

    21. Oktober 2016 um 11:56 Uhr

Wann something_t ist klein (sprich: das Kopieren ist ungefähr so ​​billig wie das Kopieren eines Zeigers) und Sie möchten, dass es standardmäßig dem Stapel zugewiesen wird:

something_t make_something(void);

something_t stack_thing = make_something();

something_t *heap_thing = malloc(sizeof *heap_thing);
*heap_thing = make_something();

Wann something_t ist groß oder Sie möchten, dass es Heap-zugewiesen wird:

something_t *make_something(void);

something_t *heap_thing = make_something();

Unabhängig von der Größe something_tund wenn es Ihnen egal ist, wo es zugewiesen ist:

void make_something(something_t *);

something_t stack_thing;
make_something(&stack_thing);

something_t *heap_thing = malloc(sizeof *heap_thing);
make_something(heap_thing);

  • In diesem Thread gibt es viele gute Antworten. Dieser ist effektiv am meisten mit den wenigsten Worten erklärt.

    – Dellowar

    21. Oktober 2016 um 6:14 Uhr

  • Neben den richtigen Überlegungen zur Größe des Objekts ist letzteres Beispiel ein guter Ratschlag für Benutzerfreundlichkeit und Einfachheit. Vielleicht kannst du eine zurückgeben int mit einigen Fehlercodes für die Ausführung.

    – EnzoR

    21. Oktober 2016 um 7:01 Uhr


  • Fast zu wenig Worte.

    – Leichtigkeitsrennen im Orbit

    21. Oktober 2016 um 9:01 Uhr

  • Wenn Sie sich für Option 2 entschieden haben, müssen Sie auch angeben free_something.

    – Orangenhund

    21. Oktober 2016 um 17:01 Uhr

  • Stil 3 ist auch im umgekehrten Szenario nützlich, wo es Ihnen wirklich wichtig ist exakt wo ein Objekt zugeordnet ist, vielleicht weil seine Identität wichtig ist.

    – Leuschenko

    22. Oktober 2016 um 0:15 Uhr

Yakk - Benutzeravatar von Adam Nevraumont
Yakk – Adam Nevraumont

Hier geht es fast immer um ABI-Stabilität. Binäre Stabilität zwischen Versionen der Bibliothek. In den Fällen, in denen dies nicht der Fall ist, geht es manchmal darum, Strukturen mit dynamischer Größe zu haben. Selten geht es um extrem groß structs oder Leistung.


Es ist äußerst selten, dass die Zuweisung von a struct auf dem Heap und die Rückgabe ist fast so schnell wie die Rückgabe als Wert. Das struct müsste riesig sein.

Geschwindigkeit ist wirklich nicht der Grund für Technik 2, Return-by-Pointer, statt Return-by-Value.

Technik 2 existiert für die ABI-Stabilität. Wenn Sie eine haben struct und Ihre nächste Version der Bibliothek fügt weitere 20 Felder hinzu, Verbraucher Ihrer vorherigen Version der Bibliothek sind binärkompatibel wenn ihnen vorkonstruierte Zeiger übergeben werden. Die zusätzlichen Daten nach dem Ende der struct sie wissen, ist etwas, worüber sie nichts wissen müssen.

Wenn Sie es auf dem Stack zurückgeben, weist der Aufrufer den Speicher dafür zu, und er muss mit Ihnen vereinbaren, wie groß es ist. Wenn Ihre Bibliothek seit der letzten Neuerstellung aktualisiert wurde, werden Sie den Stapel löschen.

Technik 2 erlaubt Ihnen auch, zusätzliche Daten sowohl vor als auch nach dem Zeiger, den Sie zurückgeben, zu verbergen (welche Versionen, die Daten an das Ende der Struktur anhängen, sind eine Variante davon). Sie könnten die Struktur mit einem Array variabler Größe beenden oder dem Zeiger einige zusätzliche Daten voranstellen oder beides.

Wenn Sie eine Stapelzuweisung wünschen structs in einem stabilen ABI, fast alle Funktionen, die mit dem sprechen struct müssen Versionsinformationen übergeben werden.

So

something_t make_something(unsigned library_version) { ... }

wo library_version wird von der Bibliothek verwendet, um festzustellen, welche Version von something_t es wird erwartet, dass es zurückkehrt ändert, wie viel des Stapels es manipuliert. Dies ist mit Standard-C nicht möglich, aber

void make_something(something_t* here) { ... }

ist. In diesem Fall, something_t könnte eine haben version Feld als erstes Element (oder ein Größenfeld), und Sie würden verlangen, dass es vor dem Aufruf ausgefüllt wird make_something.

Anderer Bibliothekscode, der a something_t würde das dann abfragen version Feld, um festzustellen, welche Version von something_t sie arbeiten mit.

Als Faustregel gilt, dass Sie niemals bestehen sollten struct Objekte nach Wert. In der Praxis ist dies in Ordnung, solange sie kleiner oder gleich der maximalen Größe sind, die Ihre CPU in einer einzelnen Anweisung verarbeiten kann. Aber stilistisch vermeidet man es typischerweise auch dann noch. Wenn Sie Strukturen niemals als Wert übergeben, können Sie später Mitglieder zur Struktur hinzufügen, ohne dass die Leistung beeinträchtigt wird.

ich denke, dass void make_something(something_t *object) ist die gebräuchlichste Art, Strukturen in C zu verwenden. Die Zuordnung überlassen Sie dem Aufrufer. Es ist effizient, aber nicht schön.

Verwenden Sie jedoch objektorientierte C-Programme something_t *make_something() da sie mit dem Konzept von gebaut werden undurchsichtiger Typ, wodurch Sie gezwungen sind, Zeiger zu verwenden. Ob der zurückgegebene Zeiger auf dynamischen Speicher oder etwas anderes zeigt, hängt von der Implementierung ab. OO mit undurchsichtigem Typ ist oft eine der elegantesten und besten Möglichkeiten, komplexere C-Programme zu entwerfen, aber leider wissen nur wenige C-Programmierer es.

  • Das eines Antwort berührend undurchsichtige Typen, die ein Eckpfeiler der ABI-Stabilität sind. Danke mein Herr.

    – Matthias M.

    21. Oktober 2016 um 11:54 Uhr

  • -1 für “kleiner oder gleich der maximalen Größe, die Ihre CPU in einer einzigen Anweisung verarbeiten kann”. Malloc braucht viel mehr als eine einzelne Anweisung. Es gibt keine genaue Methode, um zu bestimmen, wie groß eine Struktur sein muss, bevor es sinnvoll ist, sie als Referenz zu übergeben (da dies davon abhängt, wie sie verwendet wird), anstatt sie zuzuweisen, aber in der Praxis ist sie um einiges größer als sizeof(void*) für die meisten Anwendungsfälle. Beispielsweise übergeben die meisten Spiele 4×4-Matrizen nach Wert.

    – Robert Fraser

    21. Oktober 2016 um 17:51 Uhr


  • @Lundin Wenn Sie “objektorientiert” sagen, beziehen Sie sich auf das Programmierparadigma, das populär wurde, nachdem sich C etabliert hatte, oder auf etwas anderes? Ich will damit nicht andeuten, dass es unmöglich ist, OOP in C zu verwenden, ich möchte nur sicherstellen, dass ich an das richtige Konzept denke.

    – Gordon Gustafson

    25. Oktober 2016 um 23:52 Uhr

  • @GordonGustafson Die Objektorientierung wird allgemein als gute Methode zum ordnungsgemäßen Entwerfen von Programmen anerkannt. Die Wahl der Sprache spielt keine Rolle. OO besteht aus 3 Dingen: private Kapselung von Implementierung und Daten (ziemlich wichtig), modulare Programmierung, bei der jede Klasse autonom ist und sich nur um ihren eigenen Zweck kümmert (sehr wichtig) und Vererbung mit/ohne Polymorphismus (könnte gelegentlich nützlich sein). Sogar alte C-Programme, die von guten Programmierern geschrieben wurden, verwendeten eine Art Objektorientierung, obwohl die Klassen damals Dinge wie “ADT” hießen.

    – Ludin

    26. Oktober 2016 um 15:29 Uhr

  • Die einzige Möglichkeit, eine rein private Kapselung in C zu erreichen, ist der undurchsichtige Typ (halbe Versionen sind mit der Verwendung von möglich static Daten, aber das erlaubt weder mehrere Instanzen der Klasse, noch ist es Thread-sicher). Undurchsichtiger Typ kann auch verwendet werden, um Vererbung und Polymorphismus zu erreichen.

    – Ludin

    26. Oktober 2016 um 15:31 Uhr

Einige Vorteile des ersten Ansatzes:

  • Weniger Code zu schreiben.
  • Eher idiomatisch für den Anwendungsfall der Rückgabe mehrerer Werte.
  • Funktioniert auf Systemen ohne dynamische Zuordnung.
  • Wahrscheinlich schneller für kleine oder kleinere Objekte.
  • Kein Speicherverlust durch Vergessen free.

Einige Nachteile:

  • Wenn das Objekt groß ist (z. B. ein Megabyte), kann es einen Stapelüberlauf verursachen oder langsam sein, wenn Compiler es nicht gut optimieren.
  • Kann Leute überraschen, die C in den 1970er Jahren gelernt haben, als dies nicht möglich war, und nicht auf dem Laufenden gehalten haben.
  • Funktioniert nicht mit Objekten, die einen Zeiger auf einen Teil von sich selbst enthalten.

Ich bin etwas überrascht.

Der Unterschied besteht darin, dass Beispiel 1 eine Struktur auf dem Stack erstellt, Beispiel 2 sie auf dem Heap. In C oder C++-Code, der effektiv C ist, ist es idiomatisch und praktisch, die meisten Objekte auf dem Heap zu erstellen. In C++ nicht, meistens gehen sie auf den Stack. Der Grund dafür ist, dass wenn Sie ein Objekt auf dem Stack erstellen, der Destruktor automatisch aufgerufen wird, wenn Sie es auf dem Heap erstellen, muss er explizit aufgerufen werden. So ist es viel einfacher sicherzustellen, dass es keine Speicherlecks gibt und Ausnahmen zu behandeln ist alles kommt auf den Stack. In C muss der Destruktor sowieso explizit aufgerufen werden, und es gibt kein Konzept für eine spezielle Destruktorfunktion (Sie haben natürlich Destruktoren, aber es sind nur normale Funktionen mit Namen wie destroy_myobject()).

Jetzt gilt die Ausnahme in C++ für Low-Level-Container-Objekte, zB Vektoren, Bäume, Hash-Maps und so weiter. Diese behalten Heap-Member bei und haben Destruktoren. Jetzt bestehen die meisten speicherintensiven Objekte aus ein paar unmittelbaren Datenmitgliedern, die Größen, IDs, Tags usw. angeben, und dann den Rest der Informationen in STL-Strukturen, vielleicht ein Vektor von Pixeldaten oder eine Karte englischer Wort/Wert-Paare. Die meisten Daten befinden sich also tatsächlich auf dem Heap, sogar in C++.

Und modernes C++ ist so konzipiert, dass dieses Muster

class big
{
    std::vector<double> observations; // thousands of observations
    int station_x;                    // a bit of data associated with them
    int station_y; 
    std::string station_name; 
}  

big retrieveobservations(int a, int b, int c)
{
    big answer;
    //  lots of code to fill in the structure here

    return answer;
}

void high_level()
{
   big myobservations = retriveobservations(1, 2, 3);
}

Wird zu ziemlich effizientem Code kompiliert. Das große Beobachtungselement erzeugt keine unnötigen Makework-Kopien.

  • Zu sagen, dass C++ den Heap weniger verwendet als C, ist einfach albern. Zunächst einmal bedeutet RAII nicht unbedingt, dass die lokale Klasse den Heap nicht verwendet – es bedeutet nur, dass Speicher nicht so leicht verloren geht. Wenn der Heap nicht verwendet wurde, wozu dann der Destruktor? “Regel von 3”. So ziemlich jeder C++-Standardbibliothekscontainer verwendet den Heap, einschließlich std::string. Ein Hauptunterschied zwischen C und C++ besteht darin, dass Sie in C den Heap nur bei Bedarf verwenden, während Sie ihn in C++ häufig verwenden, ohne es zu wissen. Das ist eigentlich einer der Hauptgründe, warum C++ für die Entwicklung eingebetteter Systeme verpönt ist.

    – Ludin

    21. Oktober 2016 um 6:43 Uhr


Benutzeravatar von user3386109
Benutzer3386109

Im Gegensatz zu einigen anderen Sprachen (wie Python) hat C kein Tupelkonzept. Folgendes ist beispielsweise in Python zulässig:

def foo():
    return 1,2

x,y = foo()
print x, y

Die Funktion foo gibt zwei Werte als Tupel zurück, denen zugewiesen wird x und y.

Da C nicht über das Konzept eines Tupels verfügt, ist es unbequem, mehrere Werte von einer Funktion zurückzugeben. Eine Möglichkeit, dies zu umgehen, besteht darin, eine Struktur zu definieren, die die Werte enthält, und dann die Struktur wie folgt zurückzugeben:

typedef struct { int x, y; } stPoint;

stPoint foo( void )
{
    stPoint point = { 1, 2 };
    return point;
}

int main( void )
{
    stPoint point = foo();
    printf( "%d %d\n", point.x, point.y );
}

Dies ist nur ein Beispiel, bei dem Sie möglicherweise sehen, dass eine Funktion eine Struktur zurückgibt.

  • Zu sagen, dass C++ den Heap weniger verwendet als C, ist einfach albern. Zunächst einmal bedeutet RAII nicht unbedingt, dass die lokale Klasse den Heap nicht verwendet – es bedeutet nur, dass Speicher nicht so leicht verloren geht. Wenn der Heap nicht verwendet wurde, wozu dann der Destruktor? “Regel von 3”. So ziemlich jeder C++-Standardbibliothekscontainer verwendet den Heap, einschließlich std::string. Ein Hauptunterschied zwischen C und C++ besteht darin, dass Sie in C den Heap nur bei Bedarf verwenden, während Sie ihn in C++ häufig verwenden, ohne es zu wissen. Das ist eigentlich einer der Hauptgründe, warum C++ für die Entwicklung eingebetteter Systeme verpönt ist.

    – Ludin

    21. Oktober 2016 um 6:43 Uhr


1414040cookie-checkWie würde ich in C entscheiden, ob ich eine Struktur oder einen Zeiger auf eine Struktur zurückgeben möchte?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy