home |
10. Abend |
|
Druckversion dieser Seite |
Ziele
|
|
Referenzen |
||
Beim erzeugen einer Variablen belegen wir einen gewissen Teil des Speichers und schreiben einen Wert an diese Speicherstelle indem wir der Variable einen Wert geben. Beim erzeugen einer neuen Variable belegen wir neuen Speicherplatz, auch wenn wir der neuen Variablen den Wert der alten Variablen zuweisen ! | ||
|
||
Mit einer Referenz können wir auf eine bereits existierende Variable zugreifen, dieser aber einen anderen Namen geben ! | ||
Eine Referenz definiert man indem man einen Datentypen wählt und ein & hinzufügt : | ||
Um also in unserem Beispiel von oben b als Referenz von a zu definieren schreiben wir folgendes : | ||
|
||
Mit b "refererenzieren" wir also a. Beachte, dass der Datentyp der Referenz der gleiche Datentyp sein muss wie der Datentyp der referenzierten Variable. Und beachte auch : Referenzen müssen initialisiert werden ! Das heisst folgender Code compiliert nicht und ist darum rot. | ||
|
||
Über eine Referenz kann also eine vorher definierte Variable mit einem anderen Namen angesprochen und auch verändert werden. Soll eine Referenz nur verwendet werden um eine bereits bestehende Variable auszulesen, diese aber nicht zu ändern verwenden wir eine konstante Referenz. | ||
|
||
Funktionsaufrufe mit und ohne Referenzen |
||
Die Bedeutung von Referenzen und nicht-Referenzen zeigt sich bei Funktionsaufrufen. | ||
Betrachten wir eine ganz einfache Funktion test, die keinen Rückgaberwert hat (also void) und einen Parameter vom Datentyp long hat. | ||
|
||
Beim Aufruf einer solchen Funktion geschieht folgendes : Der Compiler erzeugt für die Funktion eine neue Variable mit dem Namen z und gibt dieser den Wert der beim Funktionsaufruf als Parameter übergeben wird. | ||
|
||
Versuchen wir ungefähr niederzuschreiben was der Compiler für Code erzeugt : | ||
|
||
Die ursprüngliche Variable a bleibt also unverändert, denn beim Funktionsaufruf wird eine Kopie der Variable a mit dem Namen z erzeugt. Wollen wir eine Funktion, die unsere Variable a wirklich verändert brauchen wir so etwas : | ||
|
||
Das können wir jetzt einfach wieder als sauberes C++ so niederschreiben : | ||
|
||
Im Debugger und auch im Konsolenfenster solltest du den Unterschied jetzt feststellen. Der Compiler erzeugt jetzt nicht eine neue Variable sondern greift auf die originale Variable zu, auch wenn mit einem anderen Namen. | ||
Referenzen sind nicht nur nützlich um übergeben Werte zu ändern, sondern können helfen Code effizienter zu machen. | ||
Betrachte folgenden Code mit einer zugegebenermassen absurden NamenTest-Funktion : | ||
|
||
Wie wir wissen muss der string name in der Funktion neu erzeugt werden und mit dem gleichen Inhalt gefüllt werden wie der string, der der Funktion als Parameter übergeben wird. Je nach Länge des strings kann das bei häufigen Aufrufen ein Weile dauern ! Durch einfaches ändern des Parameters auf eine string-Referenz können wir unser Programm effizienter gestalten. | ||
|
||
Da wir in unserem Beispiel den Namen in der Funktion nicht ändern wollen sondern nur effizienter übergeben wollen, verwenden wir in solchen Fällen eine konstante Referenz. | ||
|
||
Ihr werdet ab heute von mir häufig solchen Code zu sehen bekommen ! Wenn Euch nicht klar ist wieso ich gewisse Parameter als Referenzen, als konstante Referenzen oder "By Value" definiere fragt sofort danach ! Diesen Code hier oben werden wir in der Lektion noch gemeinsam analysieren und optimieren ! | ||
Übung |
||
Schreibe eine Funktion mit dem Namen "SwapLongs". Diese Funktion soll zwei ihr übergebene Parameter vom Datentyp long tauschen ( = swap) , so dass folgendes Programm die gewünschte Ausgabe erzeugt. Überlege Dir auch was die Funktion für einen Rückgabewert haben muss. | ||
|
||
Zeiger |
||
Gegner von C und von C++ führen häufig als Argument gegen diese wunderbare Programmiersprache ins Feld, dass man in C mit Zeigern herumhantieren kann und dadurch unsichere Programme schreiben kann. Sie haben recht. Ich habe selber schon kriminellen Code gesehen, der besonders durch unachtsame Anwendung von Zeigern zu einer Zeitbombe geworden ist. Trotzdem, man kann Zeiger sorgfältig anwenden und hat mit ihnen ein weiteres Mittel gute Designs in C++ zu verwirklichen (Amen). |
||
Erstes Beispiel für einen Zeiger: | ||
|
||
Das Anlegen einer Variable - hier der Variable test - führt dazu, dass zur Laufzeit Speicher für diese Variable angelegt wird. In unserer Umgebung werden für eine int - Variable 4 Bytes Speicher reserviert. Die Variable zeigerAufTest ist ein Zeiger auf so eine Speicherstelle. Zur Laufzeit können wir den Inhalt der Variablen mit Hilfe des Debuggers
genau ansehen. |
||
Die Variable test hat den Wert 2, klar ! Interessant ist der Ort im Speicher wo die Variable gespeichert ist. Die Adresse ist hier als HEX-Wert angegeben : 0x0012ff7c. Der Wert hat keine tiefere Bedeutung, wir können ihn im Memory - Fenster (rechts) aber betrachten. Die vier Bytes, die unseren Wert ausmachen sind in der ersten Zeile. Das Byte mit dem tiefsten Wert (siehe spezielle Erklärung hierzu ) steht ganz links und enthält wie erwartet den Wert 2, während die anderen Bytes rechts davon, die unseren int ausmachen den Wert 0 haben. | ||
Ergänztes BeispielBeim nächsten Beispiel, das sich kaum vom ersten unterscheidet weisen wir über einen Zeiger der Variablen einen neuen Wert zu. |
||
|
||
Der Variableninhalt sieht kurz vor dem Ende des Programms folgendermassen aus : | ||
Wichtig ist, dass wir durch die Zuweisung auf der zweitletzten Zeile direkt die Variable test ändern, nicht aber die Variable test2. Die Variable test2 wird neu erzeugt Und erhält auch einen eigenen Speicherplatz. Sie wird jedoch mit dem Wert initialisiert der im Speicher steht, wo zeigerAufTest hin zeigt. | ||
Funktionsaufrufe mit Zeigern |
||
Auch mit dem Zeiger haben wir die Möglichkeit die Funktion Test von oben so umzuschreiben, dass sie sich ähnlich wie mit der Referenz verhält. Wir können nämlich anstatt einen long als Parameter einen Zeiger auf long (long*) als Parameter verwenden. | ||
|
||
Im Gegensatz zum Aufruf mit Referenzen muss aber diesmal der aufrufende Code so umgeschrieben werden, dass der Funktion wirklich eine Adresse übergeben wird ! | ||
Zeiger müssen initialisiert werden. Sie zeigen meistens auf Speicher, der uns nicht gehört !Im folgenden Beispiel sehen wir einen Fehler, den "Anfänger" (und auch andere) häufig machen. Das Problem ist, dass ein Zeiger irgendwohin in den Speicher zeigen kann
und häufig nicht auf Speicher, der unserem Programm gehört.
Im Programm unten legen wir eine Variable ZeigerAufEinZeichen
als Zeiger auf eine char - Variable an, ohne aber eine char - Variable
wirklich anzulegen ! Das Schreiben an die Speicherstelle, auf die ZeigerAufEinZeichen
zeigt ist eine "Speicherschutzverletzung". |
||
|
||
Korrigiert sieht das Beispiel so aus : | ||
|
||
Übungsaufgabe |
||
Schreibe nun eine zweite Funktion "SwapLongs". Diesmal soll die Funktion aber mit Zeigern funktionieren ! Schreibe also die Funktion mit long* als Übergabeparameter. Schreibe auch gleich das main aus der Übung von vorhin so um, dass das Beispiel kompiliert und das richtige ausgibt ! |