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?
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 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 NULL
Zeigers fehlgeschlagen ist .
Eine vollständige Struktur zurückzugeben, NULL
die schwieriger oder weniger effizient wäre, nehme ich an. Ist das ein triftiger Grund?
quelle
gets()
Funktion entfernt wurde. Einige Programmierer haben immer noch eine Abneigung gegen das Kopieren von Strukturen, alte Gewohnheiten sterben schwer.FILE*
ist effektiv ein undurchsichtiger Griff. Der Benutzercode sollte sich nicht darum kümmern, wie seine interne Struktur aussieht.&
und auf ein Mitglied mit zugreifen.
."Antworten:
Es gibt mehrere praktische Gründe, warum Funktionen wie
fopen
Rückgabezeiger anstelle von Instanzen vonstruct
Typen verwendet werden:struct
Typs vor dem Benutzer verbergen .Bei Typen wie
FILE *
folgt vorgehen: Sie möchten dem Benutzer keine Details der Typdarstellung zur Verfügung stellen. EinFILE *
Objekt dient als undurchsichtiges Handle und Sie übergeben dieses Handle einfach an verschiedene E / A-Routinen (und diesFILE
ist häufig der Fall) als implementiertstruct
Typ, ist es nicht haben zu sein).Sie können also irgendwo einen unvollständigen
struct
Typ in einer Kopfzeile anzeigen: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 durchfopen
,freopen
usw., aber ich kann das Objekt verweist er auf die nicht direkt manipulieren.Es ist auch wahrscheinlich, dass die
fopen
Funktion einFILE
Objekt dynamisch, mithilfe vonmalloc
oder ähnlichem zuweist. In diesem Fall ist es sinnvoll, einen Zeiger zurückzugeben.Schließlich ist es möglich, dass Sie einen Zustand in einem
struct
Objekt speichern und diesen Zustand an mehreren Stellen verfügbar machen müssen. Wenn Sie Instanzen desstruct
Typs 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.quelle
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.
quelle
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
fopen
nur ein Datenwert (der geöffneteFILE*
) zurückgegeben und im Fehlerfall der Fehlercode über dieerrno
pseudo-globale Variable ausgegeben. Aber es wäre vielleicht besser, einstruct
von zwei Mitgliedern zurückzugeben: dasFILE*
Handle und den Fehlercode (der gesetzt würde, wenn das Datei-Handle istNULL
). Aus historischen Gründen ist dies nicht der Fall (und Fehler werden über daserrno
globale 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
struct
von 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
struct
lesbarer, threadfreundlicher und effizienter sein.quelle
rdx:rax
. Wirdstruct foo { int a,b; };
also verpacktrax
(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.std::optional<int>
gibt den Booleschen Wert in der oberen Hälfte von zurückrax
. Sie benötigen daher eine 64-Bit-Maskenkonstante, um ihn zu testentest
. Oder Sie könnten verwendenbt
. Aber es ist scheiße für den Aufrufer und Angerufenen im Vergleich zur Verwendungdl
, was Compiler für "private" Funktionen tun sollten. Auch verwandt: libstdc ++ kannstd::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)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.div_t div()
Du bist auf dem richtigen Weg
Beide von Ihnen genannten Gründe sind gültig:
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:
quelle
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.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 aufruftfopen
, kümmert es sich im Allgemeinen nicht um den Inhalt der zurückgegebenen Struktur - alles, was es interessiert, ist, dass andere Funktionen wiefread
das 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 vonfread
in der Lage sein, diese Informationen zu aktualisieren. Mitfread
einem Zeiger auf den EmpfangFILE
macht so einfach. Wennfread
stattdessen ein empfangenFILE
würde, hätte es keine Möglichkeit,FILE
das vom Anrufer gehaltene Objekt zu aktualisieren .quelle
Ausblenden von Informationen
Am häufigsten werden Informationen versteckt . C bietet beispielsweise nicht die Möglichkeit, Felder als
struct
privat 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,
FILE
besteht 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 vonFILE
wird dann nur für diejenigen sichtbar sein, die die Operationen implementieren, für die ihre Definition erforderlich istfopen
, 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
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
malloc
ein 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 pflegenFILE
.Zumindest aus Performance-
fopen
Gründen ist es kein so gültiger Grund, einen Zeiger zurückgeben zu lassen, da der einzige Grund, der zurückgegebenNULL
wird, 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, damitNULL
sie 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
fclose
muss 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 offenlegenFILE
und sie als Wert zurückgeben,fopen
oder 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
malloc
und 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:
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:
Dieser reservierte Speicherplatz ist etwas verschwenderisch, kann aber lebensrettend sein, wenn wir in Zukunft feststellen, dass wir weitere Daten hinzufügen müssen,
Foo
ohne 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:
...
Foo
ohne die Möglichkeit einer überflüssigen Kopie vom Stapel zu initialisieren . Oder der Kunde hat sogar die Freiheit,Foo
auf dem Haufen zuzuteilen, wenn er dies aus irgendeinem Grund möchte.quelle