Warum geben viele Funktionen, die Strukturen in C zurückgeben, tatsächlich Zeiger auf Strukturen zurück?

Was ist der Vorteil der Rückgabe eines Zeigers auf eine Struktur gegenüber der Rückgabe der gesamten Struktur in return Anweisung der Funktion?

Ich spreche von Funktionen wie fopen und anderen Funktionen auf niedriger Ebene, aber wahrscheinlich gibt es Funktionen auf höherer Ebene, die auch Zeiger auf Strukturen zurückgeben.

Ich glaube, dass dies eher eine Designentscheidung als nur eine Programmierfrage ist, und ich bin neugierig, mehr über die Vor- und Nachteile der beiden Methoden zu erfahren.

Eine der Methoden Gründe, aus denen ich dachte, dass es von Vorteil wäre, einen Zeiger auf eine Struktur zurückzugeben, besteht darin, leichter erkennen zu können, ob die Funktion fehlgeschlagen ist, indem der NULL Zeiger zurückgegeben wird.

Die Rückgabe einer vollständigen Struktur mit dem Namen NULL wäre vermutlich schwieriger oder weniger effizient. Ist das ein gültiger Grund?

Kommentare

  • @ JohnR.Strohm Ich habe es versucht und es funktioniert tatsächlich. Eine Funktion kann eine Struktur zurückgeben. Was ist der Grund, warum dies nicht getan wird?
  • Vor der Standardisierung C war es nicht möglich, Strukturen zu kopieren oder als Wert zu übergeben. Die C-Standardbibliothek enthält viele Überbleibsel aus dieser Zeit, die heute nicht so geschrieben würden, z. Es dauerte bis C11, bis die völlig falsch gestaltete Funktion gets() entfernt wurde. Einige Programmierer haben immer noch eine Abneigung gegen das Kopieren von Strukturen, alte Gewohnheiten sterben schwer.
  • FILE* ist praktisch ein undurchsichtiger Griff. Benutzercode sollte sich nicht um seine interne Struktur kümmern.
  • Die Rückgabe als Referenz ist nur dann eine vernünftige Standardeinstellung, wenn Sie über eine Garbage Collection verfügen.
  • @ JohnR.Strohm Die “ sehr älter “ in Ihrem Profil scheint vor 1989 zurückzugehen 😉 – als ANSI C erlaubte, was K & RC hat nicht ‚ t: Strukturen in Zuweisungen, Parameterübergabe und Rückgabewerte kopieren. Das ursprüngliche Buch von K & R ‚ wurde tatsächlich explizit angegeben (ich ‚ m paraphrasiere): “ Sie können genau zwei Dinge mit einer Struktur tun, ihre Adresse mit & nehmen und mit . “

Antwort

Dort Es gibt mehrere praktische Gründe, warum Funktionen wie fopen Zeiger auf anstelle von Instanzen von struct -Typen zurückgeben:

  1. Sie möchten die Darstellung des Typs struct vor dem Benutzer verbergen.
  2. Sie weisen ein Objekt dynamisch zu;
  3. Sie sind es Verweisen auf eine einzelne Instanz eines Objekts über mehrere Referenzen:

Bei Typen wie FILE * liegt es daran, dass Sie dies nicht tun Ich möchte dem Benutzer Details der Darstellung des Typs anzeigen – ein FILE * -Obje ct dient als undurchsichtiges Handle, und Sie übergeben dieses Handle einfach an verschiedene E / A-Routinen (und während FILE häufig als struct Typ, es muss nicht sein sein).

Sie können also einen unvollständigen struct Typ irgendwo in einen Header einfügen:

typedef struct __some_internal_stream_implementation FILE; 

Während Sie eine Instanz eines unvollständigen Typs nicht deklarieren können, können Sie einen Zeiger darauf deklarieren. So kann ich ein FILE * erstellen und es über fopen, freopen usw. zuweisen. , aber ich kann das Objekt, auf das es zeigt, nicht direkt manipulieren.

Es ist auch wahrscheinlich, dass die Funktion fopen eine Objekt dynamisch mit malloc oder ähnlichem. In diesem Fall ist es sinnvoll, einen Zeiger zurückzugeben.

Schließlich ist es möglich, dass Sie einen Status in einem struct -Objekt speichern, und Sie müssen diesen Status an verschiedenen Stellen verfügbar machen. Wenn Sie Instanzen vom Typ struct zurückgeben, sind diese Instanzen separate Objekte im Speicher und werden möglicherweise nicht mehr synchronisiert. Durch die Rückgabe eines Zeigers auf ein einzelnes Objekt verweist jeder auf dasselbe Objekt.

Kommentare

  • Ein besonderer Vorteil der Verwendung des Zeigers als Ein undurchsichtiger Typ ist, dass sich die Struktur selbst zwischen Bibliotheksversionen ändern kann und Sie ‚ die Anrufer nicht neu kompilieren müssen.
  • @Barmar: In der Tat ist ABI-Stabilität das große Verkaufsargument von C, und es wäre ohne undurchsichtige Zeiger nicht so stabil.

Antwort

Es gibt zwei Möglichkeiten, eine Struktur zurückzugeben. Sie können eine Kopie der Daten oder einen Verweis (Zeiger) darauf zurückgeben.Aus mehreren Gründen wird es im Allgemeinen bevorzugt, einen Zeiger zurückzugeben (und im Allgemeinen weiterzugeben).

Erstens dauert das Kopieren einer Struktur viel länger als das Kopieren eines Zeigers. Wenn dies etwas ist Ihr Code kommt häufig vor und kann zu spürbaren Leistungsunterschieden führen.

Zweitens zeigt er immer noch auf dieselbe Struktur im Speicher, unabhängig davon, wie oft Sie einen Zeiger kopieren. Alle Änderungen daran werden in derselben Struktur wiedergegeben. Wenn Sie jedoch die Struktur selbst kopieren und dann eine Änderung vornehmen, wird die Änderung nur auf dieser Kopie angezeigt. Jeder Code, der eine andere Kopie enthält, wird die Änderung nicht sehen. Manchmal, sehr selten, ist dies das, was Sie wollen, aber meistens nicht, und es kann Fehler verursachen, wenn Sie es falsch verstehen.

Kommentare

  • Der Nachteil der Rückgabe per Zeiger: Jetzt müssen Sie ‚ den Besitz dieses Objekts verfolgen und dies möglich machen befreie es. Außerdem kann die Zeiger-Indirektion teurer sein als eine schnelle Kopie. Da es hier viele Variablen gibt, ist die Verwendung von Zeigern nicht allgemein besser.
  • Außerdem sind Zeiger heutzutage auf den meisten Desktop- und Serverplattformen 64-Bit. Ich ‚ habe in meiner Karriere mehr als ein paar Strukturen gesehen, die in 64 Bit passen würden. Sie können also ‚ t immer sagen, dass das Kopieren eines Zeigers weniger kostet als das Kopieren einer Struktur.
  • Dies ist meistens eine gute Antwort , aber ich bin nicht einverstanden mit dem Teil manchmal, sehr selten, das ist, was Sie wollen, aber meistens ist es ‚ nicht – ganz im Gegenteil. Das Zurückgeben eines Zeigers ermöglicht verschiedene Arten von unerwünschten Nebenwirkungen und verschiedene Arten von bösen Möglichkeiten, um den Besitz eines Zeigers falsch zu machen. In Fällen, in denen die CPU-Zeit nicht so wichtig ist, bevorzuge ich die Kopiervariante. Wenn dies eine Option ist, ist sie viel weniger fehleranfällig.
  • Es sollte beachtet werden, dass dies wirklich der Fall ist gilt nur für externe APIs. Für interne Funktionen wird jeder selbst geringfügig kompetente Compiler der letzten Jahrzehnte eine Funktion neu schreiben, die eine große Struktur zurückgibt, um einen Zeiger als zusätzliches Argument zu verwenden und das Objekt direkt dort zu konstruieren. Die Argumente von unveränderlich gegen veränderlich wurden oft genug vorgebracht, aber ich denke, wir sind uns alle einig, dass die Behauptung, dass unveränderliche Datenstrukturen fast nie das sind, was Sie wollen, nicht wahr ist.
  • Sie könnten auch die Zusammenstellung von Brandmauern erwähnen als Profi für Zeiger. In großen Programmen mit weit verbreiteten Headern verhindern unvollständige Typen mit Funktionen die Notwendigkeit, jedes Mal neu zu kompilieren, wenn sich ein Implementierungsdetail ändert. Das bessere Kompilierungsverhalten ist tatsächlich ein Nebeneffekt der Kapselung, der erreicht wird, wenn Schnittstelle und Implementierung getrennt werden. Für die Rückgabe (und Übergabe, Zuweisung) nach Wert sind die Implementierungsinformationen erforderlich.

Antwort

Zusätzlich zu anderen Antworten Manchmal lohnt es sich, ein kleines struct nach Wert zurückzugeben. Beispielsweise könnte man ein Paar von Daten und einen damit verbundenen Fehlercode (oder Erfolgscode) zurückgeben.

Zum Beispiel gibt fopen nur zurück Eine Daten (die geöffnete FILE*) und im Fehlerfall geben den Fehlercode über die errno pseudo-globale Variable. Aber es wäre vielleicht besser, ein struct von zwei Mitgliedern zurückzugeben: das FILE* -Handle und den Fehlercode (der festgelegt würde, wenn Das Dateihandle lautet NULL). Aus historischen Gründen ist dies nicht der Fall (und Fehler werden über die errno global gemeldet, die heute ein Makro ist).

Beachten Sie, dass die Go-Sprache hat eine nette Notation, um zwei (oder einige) Werte zurückzugeben.

Beachten Sie auch, dass unter Linux / x86-64 die ABI und Aufrufkonventionen (siehe x86-psABI -Seite) geben an, dass eine struct von zwei skalaren Elementen (z. B. einem Zeiger und einer Ganzzahl oder zwei Zeigern oder zwei Ganzzahlen) wird über zwei Register zurückgegeben (und dies ist sehr effizient und geht nicht durch den Speicher).

In neuem C-Code kann die Rückgabe eines kleinen C struct lesbarer, threadfreundlicher und effizienter sein.

Kommentare

  • Tatsächlich werden kleine Strukturen in rdx:rax gepackt. Daher wird struct foo { int a,b; }; in rax (z. B. mit shift / or) gepackt zurückgegeben und muss mit shift / mov entpackt werden. Hier ist ‚ ein Beispiel für Godbolt . X86 kann jedoch die niedrigen 32 Bits eines 64-Bit-Registers für 32-Bit-Operationen verwenden, ohne sich um die hohen Bits zu kümmern. Daher ist ‚ immer zu schlecht, aber definitiv schlechter als die Verwendung 2 registriert sich die meiste Zeit für Strukturen mit zwei Mitgliedern.
  • Verwandte Themen: bugs.llvm.org/show_bug.cgi? id = 34840 std::optional<int> gibt den Booleschen Wert in der oberen Hälfte von rax zurück, sodass Sie eine 64-Bit-Maske benötigen Konstante zum Testen mit test. Oder Sie können bt verwenden. Aber es ist schade für den Anrufer und den Angerufenen, dl zu verwenden, was Compiler für “ private “ Funktionen. Ebenfalls verwandt: libstdc ++ ‚ s std::optional<T> ist nicht ‚ nicht trivial kopierbar, selbst wenn T ist Daher wird immer über den versteckten Zeiger zurückgegeben: stackoverflow.com/questions/46544019/… . (libc ++ ‚ s ist trivial kopierbar)
  • @PeterCordes: Ihre verwandten Dinge sind C ++, nicht C
  • Hoppla, richtig. Nun, dasselbe würde genau für struct { int a; _Bool b; }; in C gelten, wenn der Aufrufer den Booleschen Wert testen wollte, da trivial kopierbare C ++ – Strukturen dieselbe ABI wie verwenden C.
  • Klassisches Beispiel div_t div()

Antwort

Sie sind auf dem richtigen Weg

Beide von Ihnen genannten Gründe sind gültig:

Einer der Gründe, warum ich Ich dachte, das wäre ein Vorteil, wenn man einen Zeiger auf eine Struktur zurückgibt, um leichter erkennen zu können, ob die Funktion durch die Rückgabe des NULL-Zeigers fehlgeschlagen ist.

Die Rückgabe einer FULL-Struktur, die NULL ist, wäre vermutlich schwieriger oder weniger effizient. Ist dies ein gültiger Grund?

Wenn Sie eine Textur (zum Beispiel) irgendwo im Speicher haben und diese Textur an mehreren Stellen in Ihrem Speicher referenzieren möchten Programm; Es wäre nicht ratsam, jedes Mal eine Kopie zu erstellen, wenn Sie darauf verweisen möchten. Wenn Sie stattdessen einfach einen Zeiger übergeben, um auf die Textur zu verweisen, wird Ihr Programm viel schneller ausgeführt.

Der größte Grund jedoch Dies ist eine dynamische Speicherzuweisung. Wenn Sie ein Programm kompilieren, wissen Sie häufig nicht genau, wie viel Speicher Sie für bestimmte Datenstrukturen benötigen. In diesem Fall wird die benötigte Speichermenge zur Laufzeit festgelegt Fordern Sie Speicher mit malloc an und geben Sie ihn dann frei, wenn Sie mit free fertig sind.

Ein gutes Beispiel hierfür ist das Lesen aus einer vom Benutzer angegebenen Datei. In diesem Fall haben Sie keine Idee, wie groß die Datei sein kann, wenn Sie das Programm kompilieren. Sie können nur herausfinden, wie viel Speicher Sie benötigen, wenn das Programm tatsächlich ausgeführt wird.

Sowohl malloc- als auch freie Rückgabezeiger auf Speicherorte im Speicher funktionieren Wenn Sie die dynamische Speicherzuweisung verwenden, werden Zeiger darauf zurückgegeben, wo sie ihre Strukturen im Speicher erstellt haben.

Außerdem sehe ich in den Kommentaren, dass es eine Frage gibt, ob Sie eine Struktur von einer Funktion zurückgeben können. Sie können dies tatsächlich tun. Folgendes sollte funktionieren:

struct s1 { int integer; }; struct s1 f(struct s1 input){ struct s1 returnValue = xinput return returnValue; } int main(void){ struct s1 a = { 42 }; struct s1 b= f(a); return 0; } 

Kommentare

  • Wie ist es möglich, nicht zu wissen, wie viel Speicher vorhanden ist? Eine bestimmte Variable wird benötigt, wenn Sie den Strukturtyp bereits definiert haben?
  • @JenniferAnderson C hat ein Konzept unvollständiger Typen: Ein Typname kann deklariert, aber noch nicht definiert werden, also ist nicht verfügbar. Ich kann keine Variablen dieses Typs deklarieren, kann aber Zeiger auf diesen Typ deklarieren, z. struct incomplete* foo(void). Auf diese Weise kann ich Funktionen in einem Header deklarieren, aber nur die Strukturen in einer C-Datei definieren, wodurch die Kapselung ermöglicht wird.
  • @amon So deklarieren Sie also Funktionsheader (Prototypen / Signaturen), bevor Sie deklarieren, wie sie sind Arbeit wird tatsächlich in C erledigt? Und es ist möglich, dasselbe mit den Strukturen und Gewerkschaften in C
  • @JenniferAnderson zu tun, wenn Sie Funktions-Prototypen (Funktionen ohne Körper) in Header-Dateien deklarieren und diese Funktionen dann aufrufen können in anderem Code, ohne den Hauptteil der Funktionen zu kennen, da der Compiler nur wissen muss, wie die Argumente angeordnet werden und wie der Rückgabewert akzeptiert wird. Wenn Sie das Programm verknüpfen, müssen Sie die Funktion definition (d. H. Mit einem Body) kennen, aber Sie müssen diese nur einmal verarbeiten. Wenn Sie einen nicht einfachen Typ verwenden, muss dieser auch die Struktur des Typs ‚ kennen. Zeiger haben jedoch häufig die gleiche Größe und ‚ spielt keine Rolle für die Verwendung eines Prototyps ‚.

Antwort

So etwas wie ein FILE* ist in Bezug auf den Clientcode kein Zeiger auf eine Struktur, sondern eine Form einer undurchsichtigen Kennung, die mit einigen verknüpft ist andere Entität wie eine Datei. Wenn ein Programm fopen aufruft, kümmert es sich im Allgemeinen nicht um den Inhalt der zurückgegebenen Struktur – alles, was es interessiert, ist dass andere Funktionen wie fread alles tun, was sie dazu brauchen.

Wenn eine Standardbibliothek innerhalb einer FILE* Informationen über z. An der aktuellen Leseposition in dieser Datei müsste ein Aufruf von fread in der Lage sein, diese Informationen zu aktualisieren. Wenn fread einen Zeiger auf FILE erhält, ist dies einfach. Wenn fread stattdessen ein FILE erhalten würde, hätte es keine Möglichkeit, das FILE -Objekt zu aktualisieren vom Anrufer gehalten.

Antwort

Ausblenden von Informationen

Was ist der Vorteil der Rückgabe eines Zeigers auf eine Struktur gegenüber der Rückgabe der gesamten Struktur in der return-Anweisung von die Funktion?

Die häufigste Funktion ist das Ausblenden von Informationen. C hat beispielsweise nicht die Möglichkeit, Felder eines struct privat zu machen, geschweige denn Methoden bereitzustellen, um auf sie zuzugreifen.

Wenn Sie also zwangsweise darauf zugreifen möchten Verhindern Sie, dass Entwickler den Inhalt eines Pointees wie FILE sehen und manipulieren können. Die einzige Möglichkeit besteht darin, zu verhindern, dass sie seiner Definition ausgesetzt werden, indem Sie den Zeiger behandeln als undurchsichtig, deren Spitzengröße und Definition der Außenwelt unbekannt sind. Die Definition von FILE ist dann nur für diejenigen sichtbar, die die Operationen implementieren, für die eine Definition erforderlich ist, wie z. B. , während nur die Strukturdeklaration für den öffentlichen Header sichtbar ist.

Binärkompatibilität

Das Ausblenden der Strukturdefinition kann auch dazu beitragen, Raum zum Atmen zu schaffen, um die Binärkompatibilität in Dylib-APIs zu gewährleisten. Dadurch können die Bibliotheksimplementierer die Felder in der undurchsichtigen Struktur ändern ohne die Binärkompatibilität mit denjenigen zu beeinträchtigen, die die Bibliothek verwenden, da die Art ihres Codes nur wissen muss, was sie mit der Struktur tun können, nicht wie groß sie ist oder welche Felder sie hat.

Als Zum Beispiel kann ich einige alte Programme ausführen, die während der heutigen Windows 95-Ära erstellt wurden (nicht immer perfekt, aber überraschenderweise funktionieren noch viele). Es besteht die Möglichkeit, dass ein Teil des Codes für diese alten Binärdateien undurchsichtige Zeiger auf Strukturen verwendete, deren Größe und Inhalt sich seit der Windows 95-Ära geändert haben. Die Programme funktionieren jedoch weiterhin in neuen Windows-Versionen, da sie nicht dem Inhalt dieser Strukturen ausgesetzt waren. Wenn Sie an einer Bibliothek arbeiten, in der Binärkompatibilität wichtig ist, kann sich das, was der Client nicht ausgesetzt ist, im Allgemeinen ändern, ohne zu brechen Abwärtskompatibilität.

Effizienz

Eine vollständige Struktur zurückzugeben, die NULL ist, wäre vermutlich schwieriger oder weniger effizient. Ist dies ein gültiger Grund?

Es ist normalerweise weniger effizient, vorausgesetzt, der Typ kann praktisch passen und auf dem Stapel zugewiesen werden, es sei denn, es gibt normalerweise viel weniger Verallgemeinerter Speicherzuweiser, der hinter den Kulissen als malloc verwendet wird, wie ein bereits zugewiesener Allokator-Pooling-Speicher mit fester Größe anstelle von variabler Größe. Dies ist in diesem Fall ein Sicherheits-Kompromiss Dies ermöglicht es den Bibliotheksentwicklern wahrscheinlich, Invarianten (konzeptionelle Garantien) in Bezug auf FILE beizubehalten.

Dies ist zumindest unter Leistungsgesichtspunkten kein so gültiger Grund fopen gibt einen Zeiger zurück, da der einzige Grund, warum „div NULL zurückgegeben wird, darin besteht, dass eine Datei nicht geöffnet werden kann. Dies würde ein Ausnahmeszenario optimieren, um alle gängigen Ausführungspfade zu verlangsamen. In einigen Fällen kann es einen gültigen Produktivitätsgrund geben, Designs einfacher zu gestalten, damit sie Zeiger zurückgeben, damit NULL unter bestimmten Nachbedingungen zurückgegeben werden kann.

Bei Dateivorgängen ist der Overhead im Vergleich zu den Dateivorgängen selbst relativ relativ gering, und die manuelle Notwendigkeit, fclose zu verwenden, kann ohnehin nicht vermieden werden. Es ist also nicht so, dass wir dem Client den Aufwand ersparen können, die Ressource freizugeben (zu schließen), indem wir die Definition von FILE verfügbar machen und sie als Wert in oder erwarten Sie angesichts der relativen Kosten der Dateivorgänge selbst eine erhebliche Leistungssteigerung, um eine Heap-Zuordnung zu vermeiden.

Hotspots und Fixes

In anderen Fällen habe ich jedoch viel verschwenderischen C-Code in älteren Codebasen mit Hotspots in malloc und unnötige obligatorische Cache-Fehler, da diese Vorgehensweise zu häufig mit undurchsichtigen Zeigern verwendet wird und zu viele Dinge unnötig auf dem Heap zugewiesen werden, manchmal in großen Schleifen.

Eine alternative Methode, die ich stattdessen verwende, besteht darin, Strukturdefinitionen verfügbar zu machen, auch wenn der Client sie nicht manipulieren soll, indem ein Namenskonventionsstandard verwendet wird, um zu kommunizieren, dass niemand anderes die Felder berühren sollte:

struct Foo { /* priv_* indicates that you shouldn"t tamper with these fields! */ int priv_internal_field; int priv_other_one; }; struct Foo foo_create(void); void foo_destroy(struct Foo* foo); void foo_something(struct Foo* foo); 

Wenn es in Zukunft Bedenken hinsichtlich der Binärkompatibilität gibt, habe ich es als gut genug empfunden, nur überflüssig zusätzlichen Speicherplatz für zukünftige Zwecke zu reservieren, wie zum Beispiel:

struct Foo { /* priv_* indicates that you shouldn"t tamper with these fields! */ int priv_internal_field; int priv_other_one; /* reserved for possible future uses (emergency backup plan). currently just set to null. */ void* priv_reserved; }; 

Dieser reservierte Speicherplatz ist etwas verschwenderisch, kann jedoch lebensrettend sein, wenn wir in Zukunft feststellen, dass wir ohne die Binärdateien zu beschädigen, die unsere Bibliothek verwenden.

Meiner Meinung nach ist das Ausblenden von Informationen und die Binärkompatibilität normalerweise der einzig gute Grund, nur die Heap-Zuweisung von zuzulassen Strukturen neben Strukturen variabler Länge (die es immer erfordern würden oder zumindest etwas umständlich zu verwenden wären, wenn der Client Speicher auf dem Stapel in einer VLA-Mode zuweisen müsste Ion, um die VLS zuzuweisen). Selbst große Strukturen sind oft billiger als der Wert zurückzugeben, wenn dies bedeutet, dass die Software viel besser mit dem heißen Speicher auf dem Stapel arbeitet. Und selbst wenn es nicht billiger wäre, bei der Erstellung nach Wert zurückzukehren, könnte man dies einfach tun:

int foo_create(struct Foo* foo); ... /* In the client code: */ struct Foo foo; if (foo_create(&foo)) { foo_something(&foo); foo_destroy(&foo); } 

… um vom Stapel ohne die Möglichkeit einer überflüssigen Kopie. Oder der Client hat sogar die Freiheit, Foo auf dem Heap zuzuweisen, wenn er dies aus irgendeinem Grund möchte.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.