Kapitel 4. Daten aus C-Funktionen zurückgeben
Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com
Die Rückgabe von Daten aus einem Funktionsaufruf ist eine Aufgabe, mit der du konfrontiert wirst, wenn du irgendeinen Code schreibst, der länger als 10 Zeilen ist und der wartbar sein soll. Die Rückgabe von Daten ist eine einfache Aufgabe - du musst einfach die Daten übergeben, die du zwischen zwei Funktionen austauschen möchtest - und in C hast du nur die Möglichkeit, direkt einen Wert zurückzugeben oder Daten über emulierte "by-reference" Parameter zurückzugeben. Es gibt nicht viele Möglichkeiten und es gibt nicht viel Anleitung - richtig? Falsch! Selbst die einfache Aufgabe, Daten aus C-Funktionen zurückzugeben, ist schon knifflig, und es gibt viele Wege, die du einschlagen kannst, um dein Programm und deine Funktionsparameter zu strukturieren.
Besonders in C, wo du die Speicherzuweisung und -freigabe selbst verwalten musst, wird die Übergabe komplexer Daten zwischen Funktionen schwierig, weil es keinen Destruktor oder Garbage Collector gibt, der dir beim Aufräumen der Daten hilft. Du musst dich fragen: Sollen die Daten auf den Stack gelegt werden oder sollen sie alloziert werden? Wer sollte sie zuweisen - der Aufrufer oder der Empfänger?
In diesem Kapitel findest du bewährte Methoden, wie du Daten zwischen Funktionen austauschen kannst. Diese Muster helfen Anfängern in der C-Programmierung, Techniken für die Rückgabe von Daten in C zu verstehen, und sie helfen fortgeschrittenen C-Programmierern, besser zu verstehen, warum diese verschiedenen Techniken angewendet werden.
Abbildung 4-1 zeigt einen Überblick über die in diesem Kapitel besprochenen Muster und ihre Beziehungen, und Tabelle 4-1 enthält eine Zusammenfassung der Muster.
Laufendes Beispiel
Du möchtest die Funktionalität implementieren, um dem Benutzer Diagnoseinformationen für einen Ethernet-Treiber anzuzeigen. Zunächst fügst du diese Funktionalität einfach direkt in die Datei mit der Ethernet-Treiber-Implementierung ein und greifst direkt auf die Variablen zu, die die benötigten Informationen enthalten:
void
ethShow
()
{
printf
(
"%i packets received
\n
"
,
driver
.
internal_data
.
rec
);
printf
(
"%i packets sent
\n
"
,
driver
.
internal_data
.
snd
);
}
Später stellst du fest, dass die Funktionalität zur Anzeige von Diagnoseinformationen für deinen Ethernet-Treiber höchstwahrscheinlich wachsen wird, also beschließt du, sie in eine separate Implementierungsdatei zu packen, um deinen Code sauber zu halten. Jetzt brauchst du einen einfachen Weg, um die Informationen von deiner Ethernet-Treiberkomponente zu deiner Diagnosekomponente zu transportieren.
Eine Lösung wäre, globale Variablen zu verwenden, um diese Informationen zu transportieren. Wenn du aber globale Variablen verwendest, war die Aufteilung der Implementierungsdatei umsonst. Du teilst die Dateien auf, weil du zeigen willst, dass diese Codeteile nicht eng miteinander verbunden sind - mit globalen Variablen würdest du diese enge Verbindung wieder herstellen.
Eine viel bessere und sehr einfache Lösung ist die folgende: Lass deine Ethernet-Komponente Getter-Funktionen haben, die die gewünschten Informationen als Rückgabewert liefern.
Rückgabewert
Problem
Die Funktionsteile, die du aufteilen willst, sind nicht unabhängig voneinander. Wie in der prozeduralen Programmierung üblich, liefert ein Teil ein Ergebnis, das dann von einem anderen Teil benötigt wird. Die Funktionsteile, die du aufteilen willst, müssen sich einige Daten teilen.
Du möchtest einen Mechanismus für die gemeinsame Nutzung von Daten haben, der deinen Code leicht verständlich macht. Du willst in deinem Code deutlich machen, dass Daten zwischen Funktionen ausgetauscht werden, und du willst sicherstellen, dass Funktionen nicht über Nebenkanäle kommunizieren, die im Code nicht klar erkennbar sind. Daher ist die Verwendung von globalen Variablen zur Rückgabe von Informationen an einen Aufrufer keine gute Lösung für dich, da auf globale Variablen von jedem anderen Teil des Codes aus zugegriffen und sie verändert werden können. Außerdem ist aus der Funktionssignatur nicht ersichtlich, welche globale Variable genau für die Rückgabe von Daten verwendet wird.
Globale Variablen haben auch den Nachteil, dass sie zum Speichern von Zustandsinformationen verwendet werden können, was bei identischen Funktionsaufrufen zu unterschiedlichen Ergebnissen führen kann. Das macht den Code schwieriger zu verstehen. Abgesehen davon wäre Code, der globale Variablen für die Rückgabe von Informationen verwendet, nicht reentrant und könnte in einer Multithreading-Umgebung nicht sicher verwendet werden.
Lösung
Verwende einfach den einen C-Mechanismus, der dafür gedacht ist, Informationen über das Ergebnis eines Funktionsaufrufs zu erhalten: den Rückgabewert. Der Mechanismus zur Rückgabe von Daten in C kopiert das Funktionsergebnis und gibt dem Aufrufer Zugriff auf diese Kopie.
Abbildung 4-2 und der folgende Code zeigen, wie du den Rückgabewert implementierst.
Code des Anrufers
int
my_data
=
getData
();
/* use my_data */
Callee's Code
int
getData
()
{
int
requested_data
;
/* .... */
return
requested_data
;
}
Konsequenzen
Ein Rückgabewert ermöglicht es dem Aufrufer, eine Kopie des Funktionsergebnisses zu erhalten. Kein anderer Code außer der Funktionsimplementierung kann diesen Wert verändern, und da es sich um eine Kopie handelt, wird dieser Wert ausschließlich von der aufrufenden Funktion verwendet. Im Vergleich zur Verwendung globaler Variablen ist klarer definiert, welcher Code die vom Funktionsaufruf abgerufenen Daten beeinflusst.
Da keine globalen Variablen verwendet werden und stattdessen die Kopie des Funktionsergebnisses genutzt wird, kann die Funktion reentrant sein und sicher in einerMultithreading-Umgebung verwendet werden.
Bei eingebauten C-Typen kann eine Funktion jedoch nur ein einziges Objekt des in der Funktionssignatur angegebenen Typs zurückgeben. Es ist nicht möglich, eine Funktion mit mehreren Rückgabetypen zu definieren. Du kannst z. B. keine Funktion haben, die drei verschiedene int
Objekte zurückgibt. Wenn du mehr Informationen zurückgeben willst, als in einem einfachen, skalaren C-Typ enthalten sind, musst du eine Aggregat-Instanz oder Out-Parameter verwenden.
Auch wenn du Daten aus einem Array zurückgeben willst, ist der Rückgabewert nicht das, was du willst, denn er kopiert nicht den Inhalt des Arrays, sondern nur den Zeiger auf das Array. Der Aufrufer könnte dann mit einem Zeiger auf Daten enden, die aus dem Geltungsbereich herausgelaufen sind. Für die Rückgabe von Arrays musst du andere Mechanismen verwenden, z. B. einen Caller-Owned Buffer oder wenn der Callee Allocates.
Erinnere dich daran, dass du immer dann, wenn der einfache Rückgabewert-Mechanismus ausreicht, diese einfachste Option zur Datenrückgabe wählen solltest. Du solltest dich nicht für mächtigere, aber auch komplexere Muster wie Out-Parameter, Aggregate Instance, Caller-Owned Buffer oder Callee Allocates entscheiden.
Bekannte Verwendungszwecke
Die folgenden Beispiele zeigen Anwendungen dieses Musters:
-
Du kannst dieses Muster überall finden. Jede Funktion, die nicht von
void
stammt, gibt Daten auf diese Weise zurück. -
Jedes C-Programm hat eine
main
Funktion, die bereits einen Rückgabewert an ihren Aufrufer (z. B. das Betriebssystem) liefert.
Angewandt auf das laufende Beispiel
Die Anwendung von Return Value war einfach. Jetzt hast du eine neue Diagnosekomponente in einer vom Ethernet-Treiber getrennten Implementierungsdatei, und diese Komponente erhält die Diagnoseinformationen vom Ethernet-Treiber, wie im folgenden Code gezeigt:
Ethernet-Treiber-API
/* Returns the number of total received packets*/
int
ethernetDriverGetTotalReceivedPackets
();
/* Returns the number of total sent packets*/
int
ethernetDriverGetTotalSentPackets
();
Code des Anrufers
void
ethShow
()
{
int
received_packets
=
ethernetDriverGetTotalReceivedPackets
();
int
sent_packets
=
ethernetDriverGetTotalSentPackets
();
printf
(
"%i packets received
\n
"
,
received_packets
);
printf
(
"%i packets sent
\n
"
,
sent_packets
);
}
Dieser Code ist leicht zu lesen, und wenn du zusätzliche Informationen hinzufügen möchtest, kannst du einfach zusätzliche Funktionen hinzufügen, um diese Informationen zu erhalten. Und das ist genau das, was du als Nächstes tun willst. Du möchtest mehr Informationen über die gesendeten Pakete anzeigen. Du möchtest dem Benutzer zeigen, wie viele Pakete erfolgreich gesendet wurden und wie viele fehlgeschlagen sind. Dein erster Versuch ist, den folgenden Code zu schreiben:
void
ethShow
()
{
int
received_packets
=
ethernetDriverGetTotalReceivedPackets
();
int
total_sent_packets
=
ethernetDriverGetTotalSentPackets
();
int
successfully_sent_packets
=
ethernetDriverGetSuccesscullySentPackets
();
int
failed_sent_packets
=
ethernetDriverGetFailedPackets
();
printf
(
"%i packets received
\n
"
,
received_packets
);
printf
(
"%i packets sent
\n
"
,
total_sent_packets
);
printf
(
"%i packets successfully sent
\n
"
,
successfully_sent_packets
);
printf
(
"%i packets failed to send
\n
"
,
failed_sent_packets
);
}
Mit diesem Code stellst du schließlich fest, dass successfully_sent_packets
plus failed_sent_packets
manchmal anders als erwartet eine höhere Zahl ergibt als total_sent_packets
. Das liegt daran, dass dein Ethernet-Treiber in einem separaten Thread läuft und zwischen deinen Funktionsaufrufen zum Abrufen der Informationen weiterarbeitet und seine Paketinformationen aktualisiert. Wenn der Ethernet-Treiber also zum Beispiel zwischen deinem Aufruf ethernetDriverGetTotalSentPackets
und ethernetDriverGetSuccesscullySentPackets
erfolgreich ein Paket sendet, sind die Informationen, die du dem Benutzer zeigst, nicht konsistent.
Eine mögliche Lösung wäre, dafür zu sorgen, dass der Ethernet-Treiber nicht arbeitet, während du die Funktionen zum Abrufen der Paketinformationen aufrufst. Du könntest z. B. eine Mutex oder eine Semaphore verwenden, um dies sicherzustellen, aber bei einer so einfachen Aufgabe wie dem Abrufen von Paketstatistiken solltest du nicht derjenige sein, der sich mit diesem Problem befassen muss.
Eine viel einfachere Alternative ist es, mehrere Informationen aus einem Funktionsaufruf zurückzugeben, indem du Out-Parameter verwendest.
Out-Parameter
Problem
C unterstützt nur die Rückgabe eines einzigen Typs bei einem Funktionsaufruf, was es kompliziert macht, mehrere Informationen zurückzugeben.
Die Verwendung von globalen Variablen für den Transport der Daten, die deine Informationen repräsentieren, ist keine gute Lösung, da Code, der globale Variablen für die Rückgabe von Informationen verwendet, nicht reentrant ist und in einer Multithreading-Umgebung nicht sicher verwendet werden kann. Außerdem kann auf globale Variablen von jedem anderen Teil des Codes aus zugegriffen werden, und wenn du globale Variablen verwendest, ist aus der Funktionssignatur nicht ersichtlich, welche globalen Variablen genau für die Rückgabe der Daten verwendet werden. Globale Variablen machen deinen Code also schwer zu verstehen und zu pflegen. Auch die Verwendung der Rückgabewerte mehrerer Funktionen ist keine gute Option, denn die Daten, die du zurückgeben willst, hängen zusammen, so dass die Aufteilung auf mehrere Funktionsaufrufe den Codeweniger lesbar macht.
Da die Daten miteinander verbunden sind, möchte der Aufrufer einen konsistenten Schnappschuss all dieser Daten erhalten. Das wird zu einem Problem, wenn mehrere Rückgabewerte in einer Multithreading-Umgebung verwendet werden, weil sich die Daten zur Laufzeit ändern können. In diesem Fall müsstest du sicherstellen, dass sich die Daten zwischen den verschiedenen Funktionsaufrufen des Aufrufers nicht ändern. Du kannst aber nicht wissen, ob der Aufrufer bereits alle Daten gelesen hat oder ob es noch weitere Informationen gibt, die der Aufrufer mit einem weiteren Funktionsaufruf abrufen möchte. Deshalb kannst du nicht sicherstellen, dass die Daten zwischen den Funktionsaufrufen des Aufrufers nicht verändert werden. Wenn du mehrere Funktionen verwendest, um zusammenhängende Informationen bereitzustellen, kennst du die Zeitspanne nicht, in der sich die Daten nicht ändern dürfen. Bei diesem Ansatz kannst du also nicht garantieren, dass der Aufrufer einen konsistenten Schnappschuss der Informationen erhält.
Mehrere Funktionen mit Rückgabewerten sind auch keine gute Lösung, wenn für die Berechnung der zugehörigen Daten viel Vorbereitungsarbeit erforderlich ist. Wenn du z. B. die Privat- und Mobiltelefonnummer einer bestimmten Person aus einem Adressbuch zurückgeben möchtest und du separate Funktionen hast, um dieNummern abzurufen, musst du den Adressbucheintrag dieser Person für jeden Funktionsaufruf separat durchsuchen. Das erfordert unnötige Rechenzeit und Ressourcen.
Lösung
Gib alle Daten mit einem Funktionsaufruf zurück, indem du By-Referenz-Argumente mit Zeigern emulierst.
C unterstützt weder die Rückgabe mehrerer Typen über den Rückgabewert noch By-Referenz-Argumente, aber By-Referenz-Argumente können emuliert werden, wie in Abbildung 4-3 und dem folgenden Code gezeigt.
Code des Anrufers
int
x
,
y
;
getData
(
&
x
,
&
y
);
/* use x,y */
Callee's Code
void
getData
(
int
*
x
,
int
*
y
)
{
*
x
=
42
;
*
y
=
78
;
}
Du hast eine einzelne Funktion mit vielen Zeigerargumenten. Dereferenziere in der Funktionsimplementierung die Zeiger und kopiere die Daten, die du an den Aufrufer zurückgeben willst, in die Instanz, auf die gezeigt wird. Achte bei der Funktionsimplementierung darauf, dass sich die Daten während des Kopierens nicht ändern. Dies kann durch gegenseitigen Ausschluss erreicht werden.
Konsequenzen
Jetzt werden alle Daten, die zusammenhängende Informationen darstellen, in einem einzigen Funktionsaufruf zurückgegeben und können konsistent gehalten werden (zum Beispiel durch das Kopieren von Daten, die durch Mutex oder Semaphoren geschützt sind). Die Funktion ist reentrant und kann sicher in einer Multithreading-Umgebung verwendet werden.
Für jedes zusätzliche Datenelement wird ein zusätzlicher Zeiger an die Funktion übergeben. Das hat den Nachteil, dass die Parameterliste der Funktion immer länger wird, wenn du viele Daten zurückgeben willst. Viele Parameter für eine Funktion zu haben, ist ein Codegeruch, weil es den Code unlesbar macht. Deshalb werden selten mehrere Out-Parameter für eine Funktion verwendet. Um den Code aufzuräumen, werden stattdessen zusammengehörige Informationen mit einer Aggregat-Instanz zurückgegeben.
Außerdem muss der Aufrufer für jedes Datenelement einen Zeiger an die Funktion übergeben. Das bedeutet, dass für jedes Datenelement ein zusätzlicher Zeiger auf dem Stack abgelegt werden muss. Wenn der Stack-Speicher des Aufrufers sehr begrenzt ist, kann das zu einem Problem werden.
Out-Parameter haben den Nachteil, dass sie, wenn man nur die Funktionssignatur betrachtet, nicht eindeutig als Out-Parameter identifiziert werden können. Anhand derFunktionssignatur kann der Aufrufer nur vermuten, dass es sich bei einem Zeiger um einen Out-Parameter handeln könnte. Ein solcher Zeigerparameter könnte aber auch eine Eingabe für die Funktion sein. Daher muss in der API-Dokumentation klar beschrieben werden, welche Parameter für die Eingabe und welche für die Ausgabe bestimmt sind.
Bei einfachen, skalaren C-Typen kann der Aufrufer einfach den Zeiger auf eine Variable als Funktionsargument übergeben. Für die Funktionsimplementierung sind alle Informationen zur Interpretation des Zeigers aufgrund des angegebenen Zeigertyps vorgegeben. Für die Rückgabe von Daten mit komplexen Typen, wie z. B. Arrays, muss entweder ein Caller-Owned Buffer bereitgestellt werden oder der Callee Allocates und zusätzliche Informationen über die Daten, wie z. B. ihre Größe, müssenmitgeteilt werden.
Bekannte Verwendungszwecke
Die folgenden Beispiele zeigen Anwendungen dieses Musters:
-
Die Windows-Funktion
RegQueryInfoKey
gibt über die Out-Parameter der Funktion Informationen über einen Registrierungsschlüssel zurück. Der Aufrufer liefertunsigned long
Zeiger, und die Funktion schreibt neben anderen Informationen die Anzahl der Unterschlüssel und die Größe des Schlüsselwerts in dieunsigned long
Variablen, auf die gezeigt wird. -
Apples Cocoa API für C-Programme verwendet einen zusätzlichen
NSError
Parameter, um Fehler zu speichern, die während der Funktionsaufrufe auftreten. -
Die Funktion
userAuthenticate
des Echtzeitbetriebssystems VxWorks verwendet Rückgabewerte, um Informationen zurückzugeben, in diesem Fall, ob ein angegebenes Passwort für einen angegebenen Anmeldenamen korrekt ist. Außerdem benötigt die Funktion einen Out-Parameter, um die mit dem angegebenen Anmeldenamen verknüpfte Benutzer-ID zurückzugeben.
Angewandt auf das laufende Beispiel
Wenn du anwendest, erhältst du den folgenden Code: Out-Parameter:
Ethernet-Treiber-API
/* Returns driver status information via out-parameters. total_sent_packets --> number of packets tried to send (success and fail) successfully_sent_packets --> number of packets successfully sent failed_sent_packets --> number of packets failed to send */
void
ethernetDriverGetStatistics
(
int
*
total_sent_packets
,
int
*
successfully_sent_packets
,
int
*
failed_sent_packets
)
;
Um Informationen über gesendete Pakete abzurufen, musst du nur einen Funktionsaufruf an den Ethernet-Treiber tätigen, und der Ethernet-Treiber kann sicherstellen, dass die in diesem Aufruf gelieferten Daten konsistent sind.
Code des Anrufers
void
ethShow
()
{
int
total_sent_packets
,
successfully_sent_packets
,
failed_sent_packets
;
ethernetDriverGetStatistics
(
&
total_sent_packets
,
&
successfully_sent_packets
,
&
failed_sent_packets
);
printf
(
"%i packets sent
\n
"
,
total_sent_packets
);
printf
(
"%i packets successfully sent
\n
"
,
successfully_sent_packets
);
printf
(
"%i packets failed to send
\n
"
,
failed_sent_packets
);
int
received_packets
=
ethernetDriverGetTotalReceivedPackets
();
printf
(
"%i packets received
\n
"
,
received_packets
);
}
Du ziehst in Erwägung, die received_packets
im selben Funktionsaufruf mit den gesendeten Paketen abzurufen, aber du merkst, dass der eine Funktionsaufruf immer komplizierter wird. Ein Funktionsaufruf mit drei Out-Parametern ist bereits kompliziert zu schreiben und zu lesen. Beim Aufruf der Funktionen kann die Reihenfolge der Parameter leicht durcheinander geraten. Das Hinzufügen eines vierten Parameters würde den Code nicht besser machen.
Um den Code lesbarer zu machen, kann eine Aggregate-Instanz verwendet werden.
Aggregat Instanz
Problem
C unterstützt nur die Rückgabe eines einzigen Typs bei einem Funktionsaufruf, was es kompliziert macht, mehrere Informationen zurückzugeben.
Die Verwendung von globalen Variablen für den Transport der Daten, die deine Informationen repräsentieren, ist keine gute Lösung, da Code, der globale Variablen für die Rückgabe von Informationen verwendet, nicht reentrant ist und in einer Multithreading-Umgebung nicht sicher verwendet werden kann. Außerdem kann auf globale Variablen von jedem anderen Teil des Codes aus zugegriffen werden, und wenn du globale Variablen verwendest, ist aus der Funktionssignatur nicht ersichtlich, welche globalen Variablen genau für die Rückgabe der Daten verwendet werden. Globale Variablen machen deinen Code also schwer zu verstehen und zu pflegen. Auch die Verwendung der Rückgabewerte mehrerer Funktionen ist keine gute Option, denn die Daten, die du zurückgeben willst, hängen zusammen, so dass die Aufteilung auf mehrere Funktionsaufrufe den Code weniger lesbar macht.
Eine einzige Funktion mit vielen Ausgangsparametern ist auch keine gute Idee, denn wenn du viele solcher Ausgangsparameter hast, kannst du sie leicht verwechseln und dein Code wird unlesbar. Außerdem willst du zeigen, dass die Parameter eng miteinander verbunden sind, und es kann sogar sein, dass du denselben Satz von Parametern an andere Funktionen weitergeben oder von ihnen zurückgeben musst. Wenn du das explizit mit Funktionsparametern machst, musst du jede dieser Funktionen ändern, falls später weitere Parameter hinzukommen.
Da die Daten miteinander verbunden sind, möchte der Aufrufer einen konsistenten Schnappschuss all dieser Daten erhalten. Das wird zu einem Problem, wenn mehrere Rückgabewerte in einer Multithreading-Umgebung verwendet werden, weil sich die Daten zur Laufzeit ändern können. In diesem Fall müsstest du sicherstellen, dass sich die Daten zwischen den verschiedenen Funktionsaufrufen des Aufrufers nicht ändern. Du kannst aber nicht wissen, ob der Aufrufer bereits alle Daten gelesen hat oder ob es noch weitere Informationen gibt, die der Aufrufer mit einem weiteren Funktionsaufruf abrufen möchte. Deshalb kannst du nicht sicherstellen, dass die Daten zwischen den Funktionsaufrufen des Aufrufers nicht verändert werden. Wenn du mehrere Funktionen verwendest, um zusammenhängende Informationen bereitzustellen, kennst du die Zeitspanne nicht, in der sich die Daten nicht ändern dürfen. Bei diesem Ansatz kannst du also nicht garantieren, dass der Aufrufer einen konsistenten Schnappschuss der Informationen erhält.
Mehrere Funktionen mit Rückgabewerten sind auch keine gute Lösung, wenn für die Berechnung der zugehörigen Daten viel Vorbereitungsarbeit erforderlich ist. Wenn du z. B. die Privat- und Mobiltelefonnummer einer bestimmten Person aus einem Adressbuch zurückgeben möchtest und du separate Funktionen hast, um die Nummern abzurufen, musst du den Adressbucheintrag dieser Person für jeden Funktionsaufruf separat durchsuchen. Das erfordert unnötige Rechenzeit und Ressourcen.
Lösung
Lege alle Daten, die miteinander verbunden sind, in einem neu definierten Typ ab. Definiere diese Aggregat-Instanz so, dass sie alle zusammenhängenden Daten enthält, die du gemeinsam nutzen möchtest. Definiere sie in der Schnittstelle deiner Komponente, damit der Aufrufer direkt auf alle Daten zugreifen kann, die in der Instanz gespeichert sind.
Um dies zu implementieren, definierst du eine struct
in deiner Header-Datei und definierst alle Typen, die von der aufgerufenen Funktion zurückgegeben werden sollen, als Mitglieder dieser struct
. In der Funktionsimplementierung kopierst du die Daten, die zurückgegeben werden sollen, in die Mitglieder von struct
, wie in Abbildung 4-4 gezeigt. Achte bei der Funktionsimplementierung darauf, dass sich die Daten während des Kopierens nicht ändern. Dies kann durch gegenseitigen Ausschluss über Mutex oder Semaphoren erreicht werden.
Um die struct
tatsächlich an den Anrufer zurückzugeben, gibt es zwei Möglichkeiten:
-
Übergib die gesamte
struct
als Rückgabewert. In C können nicht nur eingebaute Typen als Rückgabewert von Funktionen übergeben werden, sondern auch benutzerdefinierte Typen wie z. B.struct
. -
Übergib einen Zeiger an die
struct
mit einem Out-Parameter. Wenn jedoch nur Zeiger übergeben werden, stellt sich die Frage, wer den Speicher, auf den gezeigt wird, bereitstellt und besitzt. Dieses Problem wird in Caller-Owned Buffer und Callee Allocates behandelt. Anstatt einen Zeiger zu übergeben und den Aufrufer direkt auf die Aggregat-Instanz zugreifen zu lassen, könntest du diestruct
vor dem Aufrufer verstecken, indem du einHandle verwendest.
Der folgende Code zeigt die Variante mit der Übergabe der gesamten struct
:
Code des Anrufers
struct
AggregateInstance
my_instance
;
my_instance
=
getData
();
/* use my_instance.x
use my_instance.y, ... */
Callee's Code
struct
AggregateInstance
{
int
x
;
int
y
;
}
;
struct
AggregateInstance
getData
(
)
{
struct
AggregateInstance
inst
;
/* fill inst.x and inst.y */
return
inst
;
}
Konsequenzen
Jetzt kann der Aufrufer mehrere Daten, die zusammenhängende Informationen darstellen, über die Aggregat-Instanz mit einem einzigen Funktionsaufruf abrufen. Die Funktion ist reentrant und kann sicher in einer Multithreading-Umgebung verwendet werden.
Dadurch erhält der Aufrufer eine konsistente Momentaufnahme der zugehörigen Informationen. Außerdem wird der Code des Aufrufers sauberer, weil er nicht mehrere Funktionen oder eine Funktion mit vielen Out-Parametern aufrufen muss.
Wenn du Daten zwischen Funktionen ohne Zeiger mit Hilfe von Rückgabewerten weitergibst, werden alle diese Daten auf den Stack gelegt. Wenn du eine struct
an 10 verschachtelte Funktionen weitergibst, liegt diese struct
10 Mal auf dem Stack. In manchen Fällen ist das kein Problem, aber in anderen schon - vor allem, wenn struct
zu groß ist und du nicht jedes Mal den gesamten struct
auf den Stack kopieren willst, um Speicher zu verschwenden. Aus diesem Grund wird häufig statt der direkten Übergabe oder Rückgabe einer struct
ein Zeiger auf diese struct
übergeben oder zurückgegeben.
Wenn du Zeiger an die struct
übergibst oder die struct
Zeiger enthält, musst du bedenken, dass C keine Deep Copy für dich durchführt. C kopiert nur die Zeigerwerte und nicht die Instanzen, auf die sie zeigen. Das ist vielleicht nicht das, was du willst. Sobald Zeiger ins Spiel kommen, musst du dich um die Bereitstellung und das Aufräumen des Speichers kümmern, auf den sie zeigen. Dieses Problem wird in Caller-Owned Buffer und Callee Allocates behandelt.
Bekannte Verwendungszwecke
Die folgenden Beispiele zeigen Anwendungen dieses Musters:
-
Der Artikel "Patterns of Argument Passing" von Uwe Zdun beschreibt dieses Muster, einschließlich C++-Beispielen, als Context Object, und das Buch Refactoring: Improving the Design of Existing Code von Martin Fowler (Addison-Wesley, 1999) wird es als Parameter Object bezeichnet.
-
Der Code des Spiels NetHack speichert Monster-Attribute in Aggregate Instances und bietet eine Funktion, um diese Informationen abzurufen.
-
Die Implementierung des Texteditors sam kopiert
structs
bei der Übergabe an Funktionen und bei der Rückgabe aus Funktionen, um den Code einfacher zu halten.
Angewandt auf das laufende Beispiel
Mit der Aggregate Instance erhältst du den folgenden Code:
Ethernet-Treiber-API
struct
EthernetDriverStat
{
int
received_packets
;
/* Number of received packets */
int
total_sent_packets
;
/* Number of sent packets (success and fail)*/
int
successfully_sent_packets
;
/* Number of successfully sent packets */
int
failed_sent_packets
;
/* Number of packets failed to send */
};
/* Returns statistics information of the Ethernet driver */
struct
EthernetDriverStat
ethernetDriverGetStatistics
();
Code des Anrufers
void
ethShow
()
{
struct
EthernetDriverStat
eth_stat
=
ethernetDriverGetStatistics
();
printf
(
"%i packets received
\n
"
,
eth_stat
.
received_packets
);
printf
(
"%i packets sent
\n
"
,
eth_stat
.
total_sent_packets
);
printf
(
"%i packets successfully sent
\n
"
,
eth_stat
.
successfully_sent_packets
);
printf
(
"%i packets failed to send
\n
"
,
eth_stat
.
failed_sent_packets
);
}
Jetzt hast du einen einzigen Aufruf an den Ethernet-Treiber, und der Ethernet-Treiber kann sicherstellen, dass die in diesem Aufruf gelieferten Daten konsistent sind. Außerdem sieht dein Code aufgeräumter aus, weil die Daten, die zusammengehören, jetzt in einem einzigen struct
gesammelt werden.
Als Nächstes möchtest du deinem Benutzer weitere Informationen über den Ethernet-Treiber anzeigen. Du möchtest dem Benutzer zeigen, zu welcher Ethernet-Schnittstelle die Paketstatistikinformationen gehören, und deshalb möchtest du den Treibernamen einschließlich einer textlichen Beschreibung des Treibers anzeigen. Beides ist in einem String enthalten, der in der Ethernet-Treiberkomponente gespeichert ist. Der String ist ziemlich lang und du weißt nicht genau, wie lang er ist. Glücklicherweise ändert sich der String unter während der Laufzeit nicht, sodass du auf eine unveränderliche Instanz zugreifen kannst.
Unveränderliche Instanz
Problem
Du möchtest Informationen, die in großen, unveränderlichen Datenmengen enthalten sind, einem Aufrufer zur Verfügung stellen.
Das Kopieren der Daten für jeden einzelnen Aufrufer wäre eine Speicherverschwendung. Daher ist die Bereitstellung aller Daten durch die Rückgabe einer Aggregat-Instanz oder durch das Kopieren aller Daten in Out-Parameter aufgrund der Stack-Speicherbeschränkungen keine Option.
Normalerweise ist es schwierig, einfach einen Zeiger auf solche Daten zurückzugeben. Du hast das Problem, dass ein Zeiger solche Daten verändern kann, und sobald mehrere Aufrufer dieselben Daten lesen und schreiben, musst du dir Mechanismen einfallen lassen, um sicherzustellen, dass die Daten, auf die du zugreifen willst, konsistent und aktuell sind. Glücklicherweise sind die Daten, die du dem Aufrufer zur Verfügung stellen willst, bei der Kompilierung oder beim Booten festgelegt und ändern sich nicht während der Laufzeit.
Lösung
Verfüge über eine Instanz (z.B. struct
), die die freizugebenden Daten im statischen Speicher enthält. Stelle diese Daten den Benutzern zur Verfügung, die darauf zugreifen wollen, und sorge dafür, dass sie sie nicht verändern können.
Schreibe die Daten, die in der Instanz enthalten sein sollen, zur Kompilierzeit oder zur Bootzeit und ändere sie zur Laufzeit nicht mehr. Du kannst die Daten entweder direkt hartkodiert in dein Programm schreiben oder sie beim Programmstart initialisieren (siehe "Software-Modul mit globalem Zustand" für Initialisierungsvarianten und "Ewiger Speicher" für Varianten der Speicherung). Wie in Abbildung 4-5 zu sehen ist, müssen sich mehrere Aufrufer (und mehrere Threads) auch dann nicht umeinander kümmern, wenn sie gleichzeitig auf die Instanz zugreifen, denn die Instanz ändert sich nicht und befindet sich daher immer in einem konsistenten Zustand und enthält die erforderlichen Informationen.
Implementiere eine Funktion, die einen Zeiger auf die Daten zurückgibt. Alternativ könntest du die Variable, die die Daten enthält, auch direkt global machen und in deine API einbauen, weil sich die Daten zur Laufzeit sowieso nicht ändern. Trotzdem ist die Getter-Funktion besser, weil sie im Vergleich zu globalen Variablen das Schreiben von Unit-Tests einfacher macht und du bei zukünftigen Verhaltensänderungen deines Codes (wenn deine Daten nicht mehr unveränderlich sind) deine Schnittstelle nicht ändern musst.
Um sicherzustellen, dass der Aufrufer die Daten nicht verändert, musst du bei der Rückgabe eines Zeigers auf die Daten die Daten, auf die gezeigt wird, wie im folgenden Code gezeigt, zu const
machen:
Code des Anrufers
const
struct
ImmutableInstance
*
my_instance
;
my_instance
=
getData
(
)
;
/* use my_instance->x, use my_instance->y, ... */
Callee API
struct
ImmutableInstance
{
int
x
;
int
y
;
};
Callee Implementierung
static
struct
ImmutableInstance
inst
=
{
12
,
42
};
const
struct
ImmutableInstance
*
getData
()
{
return
&
inst
;
}
Konsequenzen
Der Aufrufer kann eine einfache Funktion aufrufen, um auf komplexe oder große Daten zuzugreifen und muss sich nicht darum kümmern, wo diese Daten gespeichert sind. Der Aufrufer muss keine Puffer bereitstellen, in denen diese Daten gespeichert werden können, er muss den Speicher nicht aufräumen und er muss sich nicht um die Lebensdauer der Daten kümmern - sie sind einfach immer vorhanden.
Der Aufrufer kann alle Daten über den abgerufenen Zeiger lesen. Die einfache Funktion zum Abrufen des Zeigers ist reentrant und kann sicher in Multithreading-Umgebungen verwendet werden. Auch auf die Daten kann in Multithreading-Umgebungen sicher zugegriffen werden, da sie sich zur Laufzeit nicht ändern und mehrere Threads, die die Daten nur lesen, kein Problem darstellen.
Allerdings können die Daten zur Laufzeit nicht geändert werden, ohne dass weitere Maßnahmen ergriffen werden. Wenn es notwendig ist, dass der Aufrufer die Daten ändern kann, dann kann etwas wie Copy-on-Write implementiert werden. Wenn sich die Daten generell zur Laufzeit ändern können, ist eine Immutable Instance keine Option. Stattdessen muss für das Teilen komplexer und großer Daten ein Caller-Owned Buffer verwendet werden oder der Callee Allocates.
Bekannte Verwendungszwecke
Die folgenden Beispiele zeigen Anwendungen dieses Musters:
-
In seinem Artikel "Patterns in Java: Patterns of Value" beschreibt Kevlin Henney das ähnliche Immutable Object Pattern im Detail und liefert C++ Codebeispiele.
-
Der Code des Spiels NetHack speichert unveränderliche Monster-Attribute in einer unveränderlichen Instanz und bietet eine Funktion zum Abrufen dieser Informationen.
Angewandt auf das laufende Beispiel
Normalerweise ist die Rückgabe eines Zeigers für den Zugriff auf Daten, die in einer Komponente gespeichert sind, knifflig. Denn wenn mehrere Aufrufer auf diese Daten zugreifen (und sie vielleicht auch schreiben), ist ein einfacher Zeiger nicht die richtige Lösung, weil du nie weißt, ob der Zeiger noch gültig ist und ob die in diesem Zeiger enthaltenen Daten konsistent sind. Aber in diesem Fall haben wir Glück, denn wir haben eine unveränderliche Instanz. Sowohl der Name des Treibers als auch die Beschreibung sind Informationen, die zur Kompilierzeit festgelegt werden und sich danach nicht mehr ändern. Wir können also einfach einen konstanten Zeiger auf diese Daten abrufen:
Ethernet-Treiber-API
struct
EthernetDriverInfo
{
char
name
[
64
];
char
description
[
1024
];
};
/* Returns the driver name and description */
const
struct
EthernetDriverInfo
*
ethernetDriverGetInfo
();
Code des Anrufers
void
ethShow
()
{
struct
EthernetDriverStat
eth_stat
=
ethernetDriverGetStatistics
();
printf
(
"%i packets received
\n
"
,
eth_stat
.
received_packets
);
printf
(
"%i packets sent
\n
"
,
eth_stat
.
total_sent_packets
);
printf
(
"%i packets successfully sent
\n
"
,
eth_stat
.
successfully_sent_packets
);
printf
(
"%i packets failed to send
\n
"
,
eth_stat
.
failed_sent_packets
);
const
struct
EthernetDriverInfo
*
eth_info
=
ethernetDriverGetInfo
();
printf
(
"Driver name: %s
\n
"
,
eth_info
->
name
);
printf
(
"Driver description: %s
\n
"
,
eth_info
->
description
);
}
Als nächsten Schritt möchtest du dem Benutzer neben dem Namen und der Beschreibung der Ethernet-Schnittstelle auch die aktuell konfigurierte IP-Adresse und Subnetzmaske anzeigen. Die Adressen werden als String im Ethernet-Treiber gespeichert. Beide Adressen sind Informationen, die sich während der Laufzeit ändern können, daher kannst du nicht einfach einen Zeiger auf eine unveränderliche Instanz zurückgeben.
Es wäre zwar möglich, dass der Ethernet-Treiber diese Strings in eine Aggregate-Instanz packt und diese Instanz einfach zurückgibt (Arrays in einer struct
werden bei der Rückgabe der struct
kopiert), aber eine solche Lösung ist für große Datenmengen eher unüblich, da sie viel Stack-Speicher verbraucht. Normalerweise werden stattdessen Zeiger verwendet.
Die Verwendung von Zeigern ist genau die Lösung, die du suchst: Verwende einen Caller-Owned Buffer.
Anrufer-eigener Puffer
Problem
Du willst dem Aufrufer komplexe oder große Daten mit bekannter Größe zur Verfügung stellen, die nicht unveränderlich sind (sie ändern sich zur Laufzeit).
Da sich die Daten zur Laufzeit ändern (z. B. weil du den Aufrufern Funktionen zum Schreiben der Daten zur Verfügung stellst), kannst du dem Aufrufer nicht einfach einen Zeiger auf statische Daten zur Verfügung stellen (wie es bei einer Immutable Instance der Fall ist). Wenn du den Aufrufern einfach einen solchen Zeiger zur Verfügung stellst, hast du das Problem, dass die Daten, die ein Aufrufer liest, inkonsistent (teilweise überschrieben) sein könnten, weil in einer Multithreading-Umgebung ein anderer Aufrufer diese Daten gleichzeitig schreiben könnte.
Einfach alle Daten in eine Aggregate-Instanz zu kopieren und sie über den Rückgabewert an den Aufrufer zu übergeben, ist keine Option, denn da die Daten groß sind, können sie nicht über den Stack übergeben werden, der nur über sehr begrenzten Speicher verfügt.
Wenn du stattdessen nur einen Zeiger auf die Aggregat-Instanz zurückgibst, gibt es kein Problem mehr mit der Stack-Speicherbegrenzung, aber du musst bedenken, dass C die Arbeit des Deep-Copy nicht für dich übernimmt. C gibt nur den Zeiger zurück. Du musst sicherstellen, dass die Daten (die in einer Aggregate-Instanz oder in einem Array gespeichert sind), auf die verwiesen wird, auch nach dem Funktionsaufruf noch gültig sind. Du kannst die Daten zum Beispiel nicht in Auto-Variablen innerhalb deiner Funktion speichern und einen Zeiger auf diese Variablen bereitstellen, weil die Variablen nach dem Funktionsaufruf nicht mehr gültig sind.
Nun stellt sich die Frage, wo die Daten gespeichert werden sollen. Es muss geklärt werden, ob der Aufrufer oder der Angerufene den benötigten Speicher bereitstellen soll und wer dann für die Verwaltung und das Aufräumen des Speichers verantwortlich ist.
Lösung
Require muss der Aufrufer der Funktion, die die großen, komplexen Daten zurückgibt, einen Puffer und dessen Größe zur Verfügung stellen. Kopiere in der Funktionsimplementierung die benötigten Daten in den Puffer, wenn die Puffergröße groß genug ist.
Stelle sicher, dass sich die Daten während des Kopierens nicht ändern. Dies kann durch gegenseitigen Ausschluss über Mutex oder Semaphoren erreicht werden. Der Aufrufer hat dann einen Schnappschuss der Daten im Puffer, ist der alleinige Eigentümer dieses Schnappschusses und kann somit immer auf diesen Schnappschuss zugreifen, auch wenn sich die ursprünglichen Daten in der Zwischenzeit ändern.
Der Aufrufer kann den Puffer und seine Größe jeweils als separaten Funktionsparameter bereitstellen oder er kann den Puffer und seine Größe in eine Aggregat-Instanz packen und der Funktion einen Zeiger auf die Aggregat-Instanz übergeben.
Da der Aufrufer der Funktion den Puffer und dessen Größe zur Verfügung stellen muss, muss er die Größe im Voraus kennen. Damit der Aufrufer weiß, wie groß der Puffer sein muss, muss die Größenanforderung in der API enthalten sein. Dies kann durch die Definition der Größe als Makro oder durch die Definition einer struct
, die einen Puffer mit der erforderlichen Größe enthält, in der API erfolgen.
Abbildung 4-6 und der folgende Code zeigen das Konzept eines Caller-Owned Buffer.
Code des Anrufers
struct
Buffer
buffer
;
getData
(
&
buffer
);
/* use buffer.data */
Callee's API
#define BUFFER_SIZE 256
struct
Buffer
{
char
data
[
BUFFER_SIZE
];
};
void
getData
(
struct
Buffer
*
buffer
);
Die Umsetzung von Callee
void
getData
(
struct
Buffer
*
buffer
)
{
memcpy
(
buffer
->
data
,
some_data
,
BUFFER_SIZE
);
}
Konsequenzen
Die großen, komplexen Daten können dem Aufrufer mit einem einzigen Funktionsaufruf konsistent zur Verfügung gestellt werden. Die Funktion ist reentrant und kann sicher in einer Multithreading-Umgebung verwendet werden. Außerdem kann der Aufrufer in Multithreading-Umgebungen sicher auf die Daten zugreifen, da er der alleinige Eigentümer des Puffers ist.
Der Aufrufer stellt einen Puffer mit der erwarteten Größe zur Verfügung und kann sogar die Art des Speichers für diesen Puffer bestimmen. Der Aufrufer kann den Puffer auf dem Stack ablegen (siehe "Stack First") und von dem Vorteil profitieren, dass der Stack-Speicher aufgeräumt wird, nachdem die Variable den Gültigkeitsbereich verlassen hat. Alternativ kann der Aufrufer den Speicher auf den Heap legen, um die Lebensdauer der Variablen zu bestimmen oder um keinen Stapelspeicher zu verschwenden. Es kann auch sein, dass die aufrufende Funktion nur einen Verweis auf einen Puffer hat, den sie von ihrer aufrufenden Funktion erhalten hat. In diesem Fall kann dieser Puffer einfach weitergegeben werden und es besteht keine Notwendigkeit, mehrere Puffer zu haben.
Der zeitintensive Vorgang des Zuweisens und Freigebens von Speicher wird während des Funktionsaufrufs nicht durchgeführt. Der Aufrufer kann bestimmen, wann diese Vorgänge stattfinden, und so wird der Funktionsaufruf schneller und deterministischer.
Aus der API geht eindeutig hervor, dass der Aufrufer Dedicated Ownership über den Puffer hat. Der Aufrufer muss den Puffer bereitstellen und ihn anschließend aufräumen. Wenn der Aufrufer den Puffer zugewiesen hat, ist er auch dafür verantwortlich, ihn anschließend wieder freizugeben.
Der Aufrufer muss die Größe des Puffers vorher kennen. Da diese Größe bekannt ist, kann die Funktion sicher im Puffer arbeiten. Aber in manchen Fällen kennt der Aufrufer vielleicht nicht die genaue Größe und es wäre besser, wenn stattdessen der Callee Allocates.
Bekannte Verwendungszwecke
Die folgenden Beispiele zeigen Anwendungen dieses Musters:
-
Der NetHack-Code nutzt dieses Muster, um die Informationen über ein Savegame an die Komponente zu übermitteln, die dann den Spielfortschritt auf der Festplatte speichert.
-
Das Betriebssystem B&R Automation Runtime verwendet dieses Muster für eine Funktion zum Abrufen der IP-Adresse.
-
Die C stdlib-Funktion
fgets
liest Eingaben aus einem Stream und speichert sie in einem bereitgestellten Puffer.
Angewandt auf das laufende Beispiel
Du jetzt der Ethernet-Treiberfunktion einen Caller-Owned Buffer zur Verfügung und die Funktion kopiert ihre Daten in diesen Puffer. Du musst im Voraus wissen, wie groß der Puffer sein muss. Im Fall der Abfrage der IP-Adresse ist das kein Problem, denn die Zeichenkette hat eine feste Größe. Du kannst also einfach den Puffer für die IP-Adresse auf den Stack legen und diese Stack-Variable dem Ethernet-Treiber zur Verfügung stellen. Alternativ wäre es möglich gewesen, den Puffer auf dem Heap zuzuweisen, aber in diesem Fall ist das nicht nötig, weil die Größe der IP-Adresse bekannt ist und die Größe der Daten klein genug ist, um auf den Stack zu passen:
Ethernet-Treiber-API
struct
IpAddress
{
char
address
[
16
];
char
subnet
[
16
];
};
/* Stores the IP information into 'ip', which has to be provided
by the caller*/
void
ethernetDriverGetIp
(
struct
IpAddress
*
ip
);
Code des Anrufers
void
ethShow
()
{
struct
EthernetDriverStat
eth_stat
=
ethernetDriverGetStatistics
();
printf
(
"%i packets received
\n
"
,
eth_stat
.
received_packets
);
printf
(
"%i packets sent
\n
"
,
eth_stat
.
total_sent_packets
);
printf
(
"%i packets successfully sent
\n
"
,
eth_stat
.
successfully_sent_packets
);
printf
(
"%i packets failed to send
\n
"
,
eth_stat
.
failed_sent_packets
);
const
struct
EthernetDriverInfo
*
eth_info
=
ethernetDriverGetInfo
();
printf
(
"Driver name: %s
\n
"
,
eth_info
->
name
);
printf
(
"Driver description: %s
\n
"
,
eth_info
->
description
);
struct
IpAddress
ip
;
ethernetDriverGetIp
(
&
ip
);
printf
(
"IP address: %s
\n
"
,
ip
.
address
);
}
Als Nächstes möchtest du deine Diagnosekomponente so erweitern, dass sie auch einen Speicherauszug des letzten empfangenen Pakets ausgibt. Das ist nun eine Information, die zu groß ist, um sie auf dem Stack abzulegen. Und da Ethernet-Pakete eine variable Größe haben, kannst du nicht im Voraus wissen, wie groß der Puffer für das Paket sein muss. Deshalb ist der Caller-Owned Buffer keine Option für dich.
Du könntest natürlich auch einfach die Funktionen EthernetDriverGetPacketSize()
und EthernetDriverGetPacket(buffer)
verwenden, aber auch hier hättest du das Problem, dass du zwei Funktionen aufrufen müsstest. Zwischen den beiden Funktionsaufrufen könnte der Ethernet-Treiber ein weiteres Paket empfangen, was deine Daten inkonsistent machen würde. Außerdem ist diese Lösung nicht sehr elegant, weil du zwei verschiedene Funktionen aufrufen müsstest, um einen Zweck zu erfüllen. Stattdessen ist es viel einfacher, wenn der Callee Allocates.
Callee Zuteilungen
Problem
Du willst dem Aufrufer komplexe oder große Daten unbekannter Größe zur Verfügung stellen, und diese Daten sind nicht unveränderlich (sie ändern sich zur Laufzeit).
Die Daten ändern sich zur Laufzeit (vielleicht, weil du den Aufrufern Funktionen zum Schreiben der Daten zur Verfügung stellst), daher kannst du dem Aufrufer nicht einfach einen Zeiger auf statische Daten zur Verfügung stellen (wie es bei einer Immutable Instance der Fall ist). Wenn du den Aufrufern einfach einen solchen Zeiger zur Verfügung stellst, hast du das Problem, dass die Daten, die ein Aufrufer liest, inkonsistent (teilweise überschrieben) sein könnten, weil in einer Multithreading-Umgebung ein anderer Aufrufer diese Daten gleichzeitig schreiben könnte.
Einfach alle Daten in eine Aggregate-Instanz zu kopieren und sie über den Rückgabewert an den Aufrufer zu übergeben, ist keine Option. Mit dem Rückgabewert kannst du nur Daten bekannter Größe weitergeben, und da die Daten groß sind, können sie nicht über den Stack weitergegeben werden, der nur über sehr begrenzten Speicher verfügt.
Wenn du stattdessen nur einen Zeiger auf die Aggregat-Instanz zurückgibst, gibt es kein Problem mehr mit der Stack-Speicherbegrenzung, aber du musst bedenken, dass C die Arbeit des Deep-Copy nicht für dich übernimmt. C gibt nur den Zeiger zurück. Du musst sicherstellen, dass die Daten (die in einer Aggregate-Instanz oder in einem Array gespeichert sind), auf die verwiesen wird, auch nach dem Funktionsaufruf noch gültig sind. Du kannst die Daten zum Beispiel nicht in Auto-Variablen innerhalb deiner Funktion speichern und einen Zeiger auf diese Variablen bereitstellen, weil die Variablen nach dem Funktionsaufruf aus dem Geltungsbereich herauslaufen und aufgeräumt werden.
Nun stellt sich das Problem, wo die Daten gespeichert werden sollen. Es muss geklärt werden, ob der Aufrufer oder der Angerufene den benötigten Speicher bereitstellen soll und wer dann für die Verwaltung und das Aufräumen des Speichers zuständig ist.
Die Menge der Daten, die du bereitstellen willst, steht zur Kompilierzeit nicht fest. Du willst zum Beispiel eine Zeichenkette mit unbekannter Größe zurückgeben. Das macht die Verwendung eines Caller-Owned Buffer unpraktisch, weil der Aufrufer die Größe des Puffers vorher nicht kennt. Der Aufrufer könnte die benötigte Puffergröße vorher abfragen (z. B. mit einer getRequiredBufferSize()
Funktion), aber auch das ist unpraktisch, denn um ein Stück Daten abzurufen, müsste der Aufrufer mehrere Funktionsaufrufe tätigen. Außerdem könnten sich die Daten, die du bereitstellen willst, zwischen diesen Funktionsaufrufen ändern, und dann würde der Aufrufer wieder einen Puffer mit der falschen Größe bereitstellen.
Lösung
Weisen Sie innerhalb der Funktion, die die großen, komplexen Daten liefert, einen Puffer mit der erforderlichen Größe zu. Kopiere die benötigten Daten in den Puffer und gib einen Zeiger auf diesen Puffer zurück.
Gib den Zeiger auf den Puffer und seine Größe als Out-Parameter an den Aufrufer weiter. Nach dem Funktionsaufruf kann der Aufrufer mit dem Puffer arbeiten, kennt seine Größe und hat das alleinige Eigentum an dem Puffer. Der Aufrufer bestimmt die Lebensdauer des Puffers und ist daher dafür verantwortlich, ihn aufzuräumen, wie in Abbildung 4-7 und dem folgenden Code gezeigt.
Code des Anrufers
char
*
buffer
;
int
size
;
getData
(
&
buffer
,
&
size
);
/* use buffer */
free
(
buffer
);
Callee's Code
void
getData
(
char
*
*
buffer
,
int
*
size
)
{
*
size
=
data_size
;
*
buffer
=
malloc
(
data_size
)
;
/* write data to buffer */
}
Wenn du die Daten in diesen Puffer kopierst, musst du sicherstellen, dass sie sich in der Zwischenzeit nicht ändern. Dies kann durch gegenseitigen Ausschluss über Mutex oder Semaphoren erreicht werden.
Alternativ können der Zeiger auf den Puffer und die Größe in einer Aggregat-Instanz abgelegt werden, die als Rückgabewert bereitgestellt wird. Um dem Aufrufer zu verdeutlichen, dass in der Aggregate-Instanz ein Zeiger vorhanden ist, der freigegeben werden muss, kann die API eine zusätzliche Funktion zum Aufräumen bereitstellen. Wenn auch eine Funktion zum Aufräumen bereitgestellt wird, sieht die API bereits sehr ähnlich aus wie eine API mit einem Handle, was den zusätzlichen Vorteil der Flexibilität mit sich bringt, während die API-Kompatibilität erhalten bleibt.
Unabhängig davon, ob die aufgerufene Funktion den Puffer über eine Aggregate-Instanz oder über Out-Parameter bereitstellt, muss dem Aufrufer klar gemacht werden, dass er Eigentümer des Puffers ist und für dessen Freigabe verantwortlich ist. Diese dedizierte Eigentümerschaft muss in der API gut dokumentiert sein.
Konsequenzen
Der Aufrufer kann mit einem einzigen Funktionsaufruf den Puffer mit bisher unbekannter Größe abrufen. Die Funktion ist reentrant, kann sicher in Multithreading-Umgebungen verwendet werden und liefert dem Aufrufer konsistente Informationen über den Puffer und seine Größe. Wenn der Aufrufer die Größe kennt, kann er sicher mit den Daten arbeiten. So kann der Aufrufer zum Beispiel auch mit nicht beendeten Zeichenketten umgehen, die über solche Puffer transportiert werden.
Der Aufrufer ist Eigentümer des Puffers, bestimmt seine Lebensdauer und ist dafür verantwortlich, ihn wieder freizugeben (genau wie bei einem Handle). Wenn man sich die Schnittstelle ansieht, muss klar sein, dass der Aufrufer dies tun muss. Eine Möglichkeit, dies klarzustellen, besteht darin, es in der API zu dokumentieren. Eine andere Möglichkeit ist eine explizite Aufräumfunktion, um zu verdeutlichen, dass etwas aufgeräumt werden muss. Eine solche Aufräumfunktion hat den zusätzlichen Vorteil, dass die gleiche Komponente, die den Speicher zuweist, ihn auch wieder freigibt. Das ist wichtig, wenn die beiden beteiligten Komponenten mit unterschiedlichen Compilern kompiliert wurden oder auf unterschiedlichen Plattformen laufen - in solchen Fällen könnten sich die Funktionen zum Zuweisen und Freigeben von Speicher zwischen den Komponenten unterscheiden, so dass es zwingend erforderlich ist, dass dieselbe Komponente, die den Speicher zuweist, ihn auch wieder freigibt.
Der Aufrufer kann nicht bestimmen, welche Art von Speicher für den Puffer verwendet werden soll - das wäre mit einem Caller-Owned Buffer möglich gewesen. Jetzt muss der Aufrufer die Art des Speichers verwenden, die innerhalb des Funktionsaufrufs zugewiesen wird.
Das Allokieren braucht Zeit, was bedeutet, dass der Funktionsaufruf im Vergleich zum Caller-Owned Buffer langsamer und weniger deterministisch ist.
Bekannte Verwendungszwecke
Die folgenden Beispiele zeigen Anwendungen dieses Musters:
-
Die Funktion
malloc
tut genau das. Sie weist Speicherplatz zu und stellt ihn dem Aufrufer zur Verfügung. -
Die Funktion
strdup
nimmt eine Zeichenkette als Eingabe, ordnet die duplizierte Zeichenkette zu und gibt sie zurück. -
Die Funktion
getifaddrs
Linux liefert Informationen über konfigurierte IP-Adressen. Die Daten mit diesen Informationen werden in einem Puffer gespeichert, der von der Funktion zugewiesen wird. -
Der NetHack-Code verwendet dieses Muster, um Puffer abzurufen.
Angewandt auf das laufende Beispiel
Der folgende finale Code deiner Diagnosekomponente ruft die Paketdaten in einem Puffer ab, den der Callee Allocates:
Ethernet-Treiber-API
struct
Packet
{
char
data
[
1500
];
/* maximum 1500 byte per packet */
int
size
;
/* actual size of data in the packet */
};
/* Returns a pointer to a packet that has to be freed by the caller */
struct
Packet
*
ethernetDriverGetPacket
();
Code des Anrufers
void
ethShow
()
{
struct
EthernetDriverStat
eth_stat
=
ethernetDriverGetStatistics
();
printf
(
"%i packets received
\n
"
,
eth_stat
.
received_packets
);
printf
(
"%i packets sent
\n
"
,
eth_stat
.
total_sent_packets
);
printf
(
"%i packets successfully sent
\n
"
,
eth_stat
.
successfully_sent_packets
);
printf
(
"%i packets failed to send
\n
"
,
eth_stat
.
failed_sent_packets
);
const
struct
EthernetDriverInfo
*
eth_info
=
ethernetDriverGetInfo
();
printf
(
"Driver name: %s
\n
"
,
eth_info
->
name
);
printf
(
"Driver description: %s
\n
"
,
eth_info
->
description
);
struct
IpAddress
ip
;
ethernetDriverGetIp
(
&
ip
);
printf
(
"IP address: %s
\n
"
,
ip
.
address
);
struct
Packet
*
packet
=
ethernetDriverGetPacket
();
printf
(
"Packet Dump:"
);
fwrite
(
packet
->
data
,
1
,
packet
->
size
,
stdout
);
free
(
packet
);
}
In dieser letzten Version der Diagnosekomponente sehen wir alle vorgestellten Möglichkeiten, wie man Informationen von einer anderen Funktion abrufen kann. All diese Möglichkeiten in einem Stück Code zu mischen, ist vielleicht nicht das, was du wirklich willst, denn es wird ein bisschen verwirrend, wenn ein Teil der Daten auf dem Stack und ein anderer Teil der Daten auf dem Heap liegt. Sobald du Puffer zuweist, willst du nicht verschiedene Ansätze vermischen, also ist die Verwendung von Caller-Owned Buffer und Callee Allocates in einer einzigen Funktion vielleicht nicht das, was du tun willst. Wähle stattdessen einen Ansatz, der deinen Bedürfnissen entspricht, und bleibe innerhalb einer Funktion oder Komponente bei diesem. Das macht deinen Code einheitlicher und leichter verständlich.
Wenn du jedoch nur ein einziges Stück Daten von einer anderen Komponente abrufen musst und du die Wahl hast, die einfacheren Alternativen zum Abrufen von Daten zu verwenden (die Muster, die weiter oben in diesem Kapitel behandelt wurden), dann solltest du das immer tun, um deinen Code einfach zu halten. Wenn du zum Beispiel die Möglichkeit hast, Puffer auf den Stack zu legen, dann tu das, denn so sparst du dir die Mühe, den Puffer wieder freizugeben.
Zusammenfassung
In diesem Kapitel wurden verschiedene Möglichkeiten aufgezeigt, wie man Daten aus Funktionen zurückgibt und wie man mit Puffern in C umgeht. Die einfachste Möglichkeit ist es, Return Value zu verwenden, um ein einzelnes Datenelement zurückzugeben, aber wenn mehrere zusammenhängende Daten zurückgegeben werden müssen, dann verwende stattdessen Out-Parameters oder noch besser Aggregate Instance. Wenn sich die zurückzugebenden Daten zur Laufzeit nicht ändern, kann Immutable Instance verwendet werden. Bei der Rückgabe von Daten in einem Puffer kann Caller-Owned Buffer verwendet werden, wenn die Größe des Puffers vorher bekannt ist, und Callee Allocates, wenn die Größe vorher unbekannt ist.
Mit den Mustern aus diesem Kapitel hat ein C-Programmierer einige grundlegende Werkzeuge und Anleitungen, wie man Daten zwischen Funktionen transportiert und wie man mit der Rückgabe, Zuweisung und Freigabe von Puffern umgeht.
Ausblick
Im nächsten Kapitel geht es darum, wie größere Programme in Software-Module organisiert sind und wie die Lebensdauer und der Besitz von Daten in diesen Software-Modulen gehandhabt wird. Diese Muster geben einen Überblick über die Bausteine, die zum Aufbau größerer Teile von C-Code verwendet werden.
Get Fließend C now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.