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

49

Was ist der Vorteil der Rückgabe eines Zeigers auf eine Struktur im Vergleich zur Rückgabe der gesamten Struktur in der returnAnweisung der Funktion?

Ich spreche von Funktionen wie fopenund 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 Entwurfsentscheidung als nur eine Programmierfrage ist, und ich bin neugierig, mehr über die Vor- und Nachteile der beiden Methoden zu erfahren.

Einer der Gründe, warum ich dachte, dass dies ein Vorteil für die Rückgabe eines Zeigers auf eine Struktur wäre, besteht darin, dass ich leichter feststellen kann, ob die Funktion durch die Rückgabe eines NULLZeigers fehlgeschlagen ist .

Eine vollständige Struktur zurückzugeben, NULLdie schwieriger oder weniger effizient wäre, nehme ich an. Ist das ein triftiger Grund?

yoyo_fun
quelle
9
@ JohnR.Strohm Ich habe es ausprobiert und es funktioniert tatsächlich. Eine Funktion kann eine Struktur zurückgeben.
yoyo_fun
27
Vorstandardisierung C erlaubte nicht, dass Strukturen kopiert oder als Wert übergeben wurden. In der C-Standardbibliothek gibt es viele Überbleibsel aus dieser Zeit, die heute nicht so geschrieben wurden, z. B. dauerte es bis C11, bis die völlig falsch gestaltete gets()Funktion entfernt wurde. Einige Programmierer haben immer noch eine Abneigung gegen das Kopieren von Strukturen, alte Gewohnheiten sterben schwer.
amon
26
FILE*ist effektiv ein undurchsichtiger Griff. Der Benutzercode sollte sich nicht darum kümmern, wie seine interne Struktur aussieht.
CodesInChaos
3
Die Rückgabe als Referenz ist nur dann eine vernünftige Standardeinstellung, wenn Sie über eine Speicherbereinigung verfügen.
Idan Arye
6
@ JohnR.Strohm Das "sehr Senior" in Ihrem Profil scheint vor 1989 zu liegen ;-) - als ANSI C erlaubte, was K & R C nicht erlaubte: Strukturen in Zuweisungen kopieren, Parameter übergeben und Werte zurückgeben. In K & Rs ursprünglichem Buch heißt es in der Tat ausdrücklich (ich umschreibe): "Sie können genau zwei Dinge mit einer Struktur tun, ihre Adresse mit aufnehmen & und auf ein Mitglied mit zugreifen .."
Peter - Reinstate Monica

Antworten:

61

Es gibt mehrere praktische Gründe, warum Funktionen wie fopenRückgabezeiger anstelle von Instanzen von structTypen verwendet werden:

  1. Sie möchten die Darstellung des structTyps vor dem Benutzer verbergen .
  2. Sie ordnen ein Objekt dynamisch zu.
  3. Sie verweisen über mehrere Verweise auf eine einzelne Instanz eines Objekts.

Bei Typen wie FILE *folgt vorgehen: Sie möchten dem Benutzer keine Details der Typdarstellung zur Verfügung stellen. Ein FILE *Objekt dient als undurchsichtiges Handle und Sie übergeben dieses Handle einfach an verschiedene E / A-Routinen (und dies FILEist häufig der Fall) als implementiert structTyp, ist es nicht haben zu sein).

Sie können also irgendwo einen unvollständigen struct Typ in einer Kopfzeile anzeigen:

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 erstellen FILE *und zuweisen , um es durch fopen, freopenusw., aber ich kann das Objekt verweist er auf die nicht direkt manipulieren.

Es ist auch wahrscheinlich, dass die fopenFunktion ein FILEObjekt dynamisch, mithilfe von mallocoder ähnlichem zuweist. In diesem Fall ist es sinnvoll, einen Zeiger zurückzugeben.

Schließlich ist es möglich, dass Sie einen Zustand in einem structObjekt speichern und diesen Zustand an mehreren Stellen verfügbar machen müssen. Wenn Sie Instanzen des structTyps zurückgeben, sind diese Instanzen voneinander getrennte Objekte im Speicher und werden möglicherweise nicht synchronisiert. Wenn Sie einen Zeiger auf ein einzelnes Objekt zurückgeben, bezieht sich jeder auf dasselbe Objekt.

John Bode
quelle
31
Ein besonderer Vorteil der Verwendung des Zeigers als undurchsichtigen Typ besteht darin, dass sich die Struktur selbst zwischen Bibliotheksversionen ändern kann und Sie die Aufrufer nicht neu kompilieren müssen.
Barmar
6
@Barmar: In der Tat ist ABI-Stabilität das große Verkaufsargument von C und es wäre ohne undurchsichtige Zeiger nicht so stabil.
Matthieu M.
37

Es gibt zwei Möglichkeiten, "eine Struktur zurückzugeben". Sie können eine Kopie der Daten oder einen Verweis (Zeiger) darauf zurückgeben. Es wird im Allgemeinen aus mehreren Gründen 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 in Ihrem Code häufig vorkommt, kann dies zu einem spürbaren Leistungsunterschied führen.

Zweitens, egal wie oft Sie einen Zeiger kopieren, zeigt er immer noch auf dieselbe Struktur im Speicher. Alle Änderungen daran wirken sich auf dieselbe Struktur aus. Wenn Sie jedoch die Struktur selbst kopieren und dann eine Änderung vornehmen, wird die Änderung nur in 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 die meiste Zeit ist es nicht, und es kann Fehler verursachen, wenn Sie es falsch verstehen.

Mason Wheeler
quelle
54
Der Nachteil der Rückkehr per Mauszeiger: Jetzt müssen Sie das Eigentum an diesem Objekt verfolgen und es möglicherweise freigeben. Außerdem kann die Indirektion von Zeigern teurer sein als eine schnelle Kopie. Hier gibt es viele Variablen, daher ist es nicht allgemein besser, Zeiger zu verwenden.
amon
17
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. Man kann also nicht immer sagen, dass das Kopieren eines Zeigers weniger kostet als das Kopieren einer Struktur.
Solomon Slow
37
Dies ist meistens eine gute Antwort, aber ich bin manchmal anderer Meinung , 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 unangenehme Methoden, 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.
Doc Brown
6
Es ist zu beachten, dass dies wirklich nur für externe APIs gilt. Für interne Funktionen wird jeder auch nur 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 darin zu konstruieren. Die Argumente von unveränderlich gegen veränderlich wurden oft genug vorgebracht, aber ich denke, wir können uns alle einig sein, dass die Behauptung, dass unveränderliche Datenstrukturen so gut wie nie das sind, was Sie wollen, nicht wahr ist.
Voo
6
Sie könnten auch Compilation Fire Walls als Pro für Zeiger erwähnen. 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 eigentlich ein Nebeneffekt der Kapselung, die erreicht wird, wenn Schnittstelle und Implementierung getrennt sind. Für das Zurückgeben (und Übergeben, Zuweisen) nach Wert sind die Implementierungsinformationen erforderlich.
Peter - Reinstate Monica
12

Neben anderen Antworten lohnt es sich manchmal, einen kleinen struct Wert zurückzugeben. Beispielsweise könnte man ein Paar von Daten und einen damit verbundenen Fehlercode (oder Erfolgscode) zurückgeben.

Beispielsweise wird fopennur ein Datenwert (der geöffnete FILE*) zurückgegeben und im Fehlerfall der Fehlercode über die errnopseudo-globale Variable ausgegeben. Aber es wäre vielleicht besser, ein structvon zwei Mitgliedern zurückzugeben: das FILE*Handle und den Fehlercode (der gesetzt würde, wenn das Datei-Handle ist NULL). Aus historischen Gründen ist dies nicht der Fall (und Fehler werden über das errnoglobale Protokoll gemeldet , das heute ein Makro ist).

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

Beachten Sie auch, dass unter Linux / x86-64 die ABI- und Aufrufkonventionen (siehe x86-psABI- Seite) festlegen, dass eines structvon zwei skalaren Elementen (z. B. ein Zeiger und eine Ganzzahl oder zwei Zeiger oder zwei Ganzzahlen) durch zwei Register zurückgegeben wird (und das ist sehr effizient und geht nicht durch den Speicher).

In neuem C-Code kann das Zurückgeben eines kleinen C structlesbarer, threadfreundlicher und effizienter sein.

Basile Starynkevitch
quelle
Tatsächlich sind in kleine Strukturen gepacktrdx:rax . Wird struct foo { int a,b; };also verpackt rax(zB mit shift / oder) zurückgeschickt und muss mit shift / mov ausgepackt werden. Hier ist ein Beispiel für Godbolt . X86 kann jedoch die niedrigen 32-Bit-Werte eines 64-Bit-Registers für 32-Bit-Vorgänge verwenden, ohne sich um die hohen Bit-Werte zu kümmern. Dies ist also immer zu schade, aber definitiv schlimmer als die meiste Zeit zwei Register für Strukturen mit zwei Elementen zu verwenden.
Peter Cordes
Verwandte Themen : bugs.llvm.org/show_bug.cgi?id=34840std::optional<int> gibt den Booleschen Wert in der oberen Hälfte von zurück rax. Sie benötigen daher eine 64-Bit-Maskenkonstante, um ihn zu testen test. Oder Sie könnten verwenden bt. Aber es ist scheiße für den Aufrufer und Angerufenen im Vergleich zur Verwendung dl, was Compiler für "private" Funktionen tun sollten. Auch verwandt: libstdc ++ kann std::optional<T>nicht einfach kopiert werden, auch wenn T ist . Daher wird es immer über den versteckten Zeiger zurückgegeben: stackoverflow.com/questions/46544019/… . (libc ++ 's ist trivial kopierbar)
Peter Cordes
@ PeterCordes: Ihre verwandten Dinge sind C ++, nicht C
Basile Starynkevitch
Ups, richtig. Nun, dasselbe würde genau für struct { int a; _Bool b; };C gelten, wenn der Aufrufer den Booleschen Wert testen wollte, da trivial kopierbare C ++ - Strukturen das gleiche ABI wie C verwenden.
Peter Cordes
1
Klassisches Beispieldiv_t div()
chux
6

Du bist auf dem richtigen Weg

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

Einer der Gründe, warum ich dachte, dass dies ein Vorteil für die Rückgabe eines Zeigers auf eine Struktur wäre, ist, dass ich leichter feststellen kann, ob die Funktion fehlgeschlagen ist, indem ich den NULL-Zeiger zurückgebe.

Die Rückgabe einer FULL-Struktur, die NULL ist, ist vermutlich schwieriger oder weniger effizient. Ist das ein triftiger Grund?

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

Der Hauptgrund ist jedoch die dynamische Speicherzuweisung. Wenn ein Programm kompiliert wird, wissen Sie oft nicht genau, wie viel Speicher Sie für bestimmte Datenstrukturen benötigen. In diesem Fall wird die benötigte Speicherkapazität zur Laufzeit festgelegt. Sie können Speicher mit 'malloc' anfordern und ihn dann freigeben, 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 Ahnung, 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 free geben Zeiger auf Positionen im Speicher zurück. Funktionen, die die dynamische Speicherzuweisung verwenden, geben also Zeiger zurück, auf die sie ihre Strukturen im Speicher erstellt haben.

In den Kommentaren sehe ich auch, dass es eine Frage gibt, ob Sie eine Struktur von einer Funktion zurückgeben können. Sie können das in der Tat 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;
}
Ryan
quelle
Wie kann es sein, dass Sie nicht wissen, wie viel Speicher eine bestimmte Variable benötigt, wenn Sie bereits den Strukturtyp definiert haben?
yoyo_fun
9
@JenniferAnderson C hat ein Konzept für unvollständige Typen: Ein Typname kann deklariert, aber noch nicht definiert werden, daher ist seine Größe nicht verfügbar. Ich kann keine Variablen dieses Typs deklarieren, aber ich kann 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 und so die Kapselung ermöglichen.
amon
@amon So werden also Funktionsheader (Prototypen / Signaturen) deklariert, bevor deklariert wird, wie sie tatsächlich in C funktionieren. Und es ist möglich, dasselbe mit den Strukturen und Gewerkschaften in C
yoyo_fun,
@JenniferAnderson Sie Funktion deklarieren Prototypen (Funktionen ohne Körper) in Header - Dateien und diese Funktionen in anderem Code dann anrufen können, ohne den Körper der Funktionen zu kennen, weil der Compiler muss nur wissen , wie die Argumente zu arrangieren, und wie das akzeptieren Rückgabewert. Bis Sie das Programm verknüpfen, müssen Sie tatsächlich die Funktion kennen Definition (dh mit einem Körper), aber Sie nur verarbeiten müssen , dass einmal. Wenn Sie einen nicht einfachen Typ verwenden, muss er auch die Struktur dieses Typs kennen, aber die Zeiger haben häufig dieselbe Größe und es spielt keine Rolle, ob ein Prototyp verwendet wird.
simpleuser
6

So etwas wie a FILE*ist in Bezug auf den Clientcode nicht wirklich ein Zeiger auf eine Struktur, sondern eine Art undurchsichtiger Bezeichner, der einer anderen Entität wie einer Datei zugeordnet ist. Wenn ein Programm aufruft fopen, kümmert es sich im Allgemeinen nicht um den Inhalt der zurückgegebenen Struktur - alles, was es interessiert, ist, dass andere Funktionen wie freaddas tun, was sie brauchen, um damit zu tun.

Wenn eine Standardbibliothek FILE*Informationen über z. B. die aktuelle Leseposition in dieser Datei enthält, muss ein Aufruf von freadin der Lage sein, diese Informationen zu aktualisieren. Mit freadeinem Zeiger auf den Empfang FILEmacht so einfach. Wenn freadstattdessen ein empfangen FILEwürde, hätte es keine Möglichkeit, FILEdas vom Anrufer gehaltene Objekt zu aktualisieren .

Superkatze
quelle
3

Ausblenden von Informationen

Was ist der Vorteil der Rückgabe eines Zeigers auf eine Struktur im Vergleich zur Rückgabe der gesamten Struktur in der return-Anweisung der Funktion?

Am häufigsten werden Informationen versteckt . C bietet beispielsweise nicht die Möglichkeit, Felder als structprivat zu kennzeichnen, geschweige denn Methoden für den Zugriff darauf bereitzustellen.

Wenn Sie also gewaltsam verhindern möchten, dass Entwickler den Inhalt eines Pointees sehen und manipulieren können, FILEbesteht die einzige Möglichkeit darin, zu verhindern, dass sie seiner Definition ausgesetzt werden, indem Sie den Zeiger als undurchsichtig behandeln, dessen Pointee-Größe und Definition sind der Außenwelt unbekannt. Die Definition von FILEwird dann nur für diejenigen sichtbar sein, die die Operationen implementieren, für die ihre Definition erforderlich ist fopen, während nur die Strukturdeklaration für den öffentlichen Header sichtbar ist.

Binäre Kompatibilität

Das Ausblenden der Strukturdefinition kann auch dazu beitragen, Freiraum für die Wahrung der Binärkompatibilität in dylib-APIs zu schaffen. Es ermöglicht den Implementierern der Bibliothek, die Felder in der undurchsichtigen Struktur zu ändern, ohne die Binärkompatibilität mit denen 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 es hat.

Zum Beispiel kann ich einige alte Programme ausführen, die zur Zeit von Windows 95 erstellt wurden (nicht immer perfekt, aber überraschenderweise funktionieren noch viele). Möglicherweise verwendete ein Teil des Codes für diese alten Binärdateien undurchsichtige Zeiger auf Strukturen, deren Größe und Inhalt sich seit der Windows 95-Ära geändert haben. Die Programme funktionieren jedoch weiterhin in neuen Versionen von Fenstern, da sie nicht dem Inhalt dieser Strukturen ausgesetzt waren. Bei der Arbeit an einer Bibliothek, bei der die Binärkompatibilität wichtig ist, darf sich das, was dem Client nicht zugänglich ist, im Allgemeinen ändern, ohne die Abwärtskompatibilität zu beeinträchtigen.

Effizienz

Die Rückgabe einer vollständigen Struktur, die NULL ist, ist vermutlich schwieriger oder weniger effizient. Ist das ein triftiger Grund?

Es ist in der Regel weniger effizient, vorausgesetzt, der Typ kann praktisch in den Stapel passen und zugewiesen werden, es sei denn, im Hintergrund wird in der Regel ein viel weniger verallgemeinerter Speicherallokator verwendet als mallocein bereits zugewiesener Poolspeicher mit fester Größe und nicht mit variabler Größe. In diesem Fall handelt es sich höchstwahrscheinlich um einen Kompromiss bei der Sicherheit, der es den Bibliotheksentwicklern ermöglicht, Invarianten (konzeptionelle Garantien) im Zusammenhang mit zu pflegen FILE.

Zumindest aus Performance- fopenGründen ist es kein so gültiger Grund, einen Zeiger zurückgeben zu lassen, da der einzige Grund, der zurückgegeben NULLwird, darin besteht, dass eine Datei nicht geöffnet werden konnte. Dies würde ein außergewöhnliches Szenario optimieren, um alle gängigen Ausführungspfade zu verlangsamen. In einigen Fällen kann es einen gültigen Produktivitätsgrund geben, Entwürfe einfacher zu gestalten, damit sie Zeiger zurückgeben, damit NULLsie unter bestimmten Bedingungen nachträglich zurückgegeben werden können.

Bei Dateioperationen ist der Aufwand im Vergleich zu den Dateioperationen relativ gering, und das manuelle Vorgehen fclosemuss ohnehin nicht vermieden werden. Es ist also nicht so, als könnten wir dem Client den Aufwand ersparen, die Ressource freizugeben (zu schließen), indem wir die Definition der Datei offenlegen FILEund sie als Wert zurückgeben, fopenoder wenn wir angesichts der relativen Kosten der Dateioperationen selbst einen erheblichen Leistungsschub erwarten, um eine Heap-Zuweisung zu vermeiden .

Hotspots und Fixes

In anderen Fällen habe ich jedoch eine Menge verschwenderischen C-Codes in Legacy-Codebasen mit Hotspots in mallocund unnötigen obligatorischen Cache-Fehlern profiliert, da diese Vorgehensweise mit undurchsichtigen Zeigern zu häufig angewendet wurde und zu viele Dinge unnötigerweise auf dem Heap zugewiesen wurden, manchmal in große Schleifen.

Eine alternative Methode, die ich stattdessen verwende, besteht darin, Strukturdefinitionen offen zu legen, auch wenn der Client nicht dazu gedacht ist, sie zu manipulieren, indem er einen Namenskonventionsstandard verwendet, um mitzuteilen, 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 Probleme mit der Binärkompatibilität gibt, habe ich es für gut genug befunden, nur überflüssig zusätzlichen Speicherplatz für zukünftige Zwecke zu reservieren:

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 aber lebensrettend sein, wenn wir in Zukunft feststellen, dass wir weitere Daten hinzufügen müssen, Fooohne die Binärdateien zu beschädigen, die unsere Bibliothek verwenden.

Meiner Meinung nach ist das Ausblenden von Informationen und die Binärkompatibilität in der Regel der einzige vernünftige Grund, um neben Strukturen mit variabler Länge nur die Heap-Zuweisung zuzulassen (was immer erforderlich wäre oder zumindest etwas umständlich zu verwenden, wenn der Client eine Zuweisung vornehmen müsste Speicher auf dem Stapel in einer VLA-Art und Weise, um die VLS zuzuweisen). Sogar große Strukturen sind oft günstiger, wenn die Software viel mehr mit dem Hot-Memory auf dem Stack arbeitet. Und selbst wenn sie bei der Wertschöpfung nicht günstiger zu haben wären, 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);
}

... Fooohne die Möglichkeit einer überflüssigen Kopie vom Stapel zu initialisieren . Oder der Kunde hat sogar die Freiheit, Fooauf dem Haufen zuzuteilen, wenn er dies aus irgendeinem Grund möchte.


quelle