 Willkommen zurück zu Programmierparadigmen. Wir sind hier im Teil 3 des Moduls zum Thema Composite Types und in diesem dritten und dann auch im vierten Teil geht es um Pointer und was man mit diesen Pointer so alles machen kann und insbesondere welche Composite Types man aus denen dann bauen kann. Bevor ich erkläre was Pointer eigentlich sind und was wir damit alles machen können, haben wir ein kleines Quiz, wo im Prinzip schon mal das abgefragt wird, was sie dann im Ende hoffentlich sehr gut verstehen. Der eine oder andere weiß vielleicht jetzt schon die Lösung, aber das ist einfach um so ein bisschen zu zeigen, was sich hier eigentlich erwartet. Also die Frage ist, was macht denn eigentlich dieser C-Code, den wir hier sehen und was gibt der Schluss endlich hier unten aus? Vielleicht das Einzige, was ich schon mal jetzt dazu sage, ist was diese Notation hier bedeutet. Die bedeutet einfach nur, dass die Adressen, also die Werte der beiden Pointer, die in A und B gespeichert sind, ausgegeben werden. Also das gibt wirklich die Adressen einfach nur als Zahl aus. Ich würde Sie bitten jetzt kurz darüber nachzudenken und dann im Ilias abzustimmen, was dieser Code eigentlich ausgibt, so dass wir dann schauen können, ob Sie das eigentlich richtig schon wissen oder eben noch nicht. So, schauen wir uns mal die Lösung an. Also was hier rauskommt, sind diese zwei hier in Hex aufgeschriebenen Werte 1004 und 1010 und zwar aus folgendem Grund. Also was wir hier machen ist, wir initialisieren erst mal zwei Pointer mit hexadezimalen Zahlen, die jeweils 10000 in Hex beinhalten. Und diese Pointer haben aber verschiedene Typen, denn das eine ist ein Pointer auf Characters und das andere ist ein Pointer auf Intages. Und was wir dann hier machen ist, dass wir auf jeden dieser Pointer den Wert 4 drauf addieren. Aber weil das eben Pointer sind, ist es nicht einfach nur die Zahl 4, die drauf addiert wird, sondern vier mal die Größe dessen, was sich hinter diesem Pointer verbirgt. Das heißt, also im ersten Fall ist es vier mal die Größe von einem Character und da ein Character jeweils ein Weit groß ist, in C erhöhen wir das Ganze hier um vier. Und im zweiten Fall ist es vier mal die Größe eines Ints, das heißt vier mal vier mal vier, also 16 und in Hex kommen wir dann hier deshalb eben auf die 1010 und das wird dann auch entsprechend so rausgegeben. Wir haben das jetzt noch nicht so 100% klar, es ist kein Problem, denn das war erst der Anfang und wir werden uns jetzt Pointer und die rekkusiven Typen, die man dann damit bauen kann, genauer anschauen. Und zwar wird das vier Teile, also das erste ist, wir schauen uns ein bisschen an, welche Operations denn auf Pointer gibt. Das ist das, was hier in dem Video, in dem sie gerade sind, geschieht und dann in dem nächsten Video schauen wir uns die letzten drei Punkte an, nämlich wie Pointer und Arrays dann eigentlich in C zusammenhängen und warum die eigentlich so mehr oder weniger derselbe sind, was dangling References sind und schlussendlich, wie man sich die Arbeit mit Pointer ein bisschen ersparen kann, indem man eine Sprache verwendet, die Garbage Collection anbietet. Fangen wir vielleicht erst mal an mit ein bisschen Motivation, also warum wollen wir überhaupt Pointer haben und warum bieten so viele Programmiersprachen denn Pointer in der einen oder anderen Form an. Der Grund ist ganz einfach, dass die meisten Programme eben nicht nur auf diesen primitiven Typen wie Ind oder Character arbeiten und eben auch nicht nur die eingebauten Composite Types wie zum Beispiel Arrays immer nur verwenden möchten, weil doch recht komplexe Daten behandelt werden und die in irgendeiner Form repräsentiert werden müssen. Und eine gute Art und Weise, solche Daten zu repräsentieren, ist, indem man miteinander verlinkte Datenstrukturen verwendet, also einzelne Datenstrukturen, die aufeinander zeigen, mithilfe von eben solchen Pointern. Hier sind mal drei Beispiele von solchen verlinkten Datenstrukturen, z.B. Listen, wo ein Element immer aufs Nächste zeigt oder Bäume, wo jedes jeder Knoten im Baum auf jeweiligen Kinder zeigt oder Grafen, wo ich beliebige Kanten zwischen Knoten haben kann und die dann z.B. durch Pointer repräsentiert werden können. Oft, aber nicht immer, ist es so, dass eine Datenstruktur eigentlich eine Referenz oder ein Pointer auf weitere Objekte desselben Types haben möchte. Also wenn ich z.B. die Knoten in einem Graph habe, dann zeigen die vielleicht auf andere Knoten, d.h. ich habe im Prinzip nicht nur Pointer, sondern ich habe auch gleich eine reklusive Datenstruktur, die in irgendeiner Form auf Elemente desselben Types zeigen möchte. Ja, und um eben genau das zu machen, kann ich jetzt Pointers und die damit möglichen rekosiven Typen verwenden. Also was so ein Pointer im Prinzip einfach nur ist, dass eine Referenz auf ein Stück speicher, also im Prinzip eine Adresse, an der dann weitere Daten stehen und ich im Prinzip dahin zeige. Ein rekosiver Typ ist jetzt ein zusammengesetzter, also so ein Composite-Type, in dem es Referenzen auf andere Objekte gibt und diese anderen Objekte dann wieder genau denselben Typen haben, wie das Objekt, in dem ich gerade drin bin und deswegen heißt das Ganze dann rekosiv. Ob man die Pointer jetzt in so einer Sprache wirklich explizit machen muss oder ob das vielleicht eher so unter der Decke geschieht, hängt davon ab, ob die Sprache das Referenz oder das Value-Model verwendet bzw. ob sie beides verwendet oder eben nur das Referenz-Model verwendet. In Sprachen, die immer nur das Referenz-Model verwenden für Variablen wie z.B. Java, wo alle Typen, die nicht primitiv sind, eben immer mit dem Referenz-Model verwendet werden, brauche ich eigentlich keine expliziten Pointer. Denn in solchen Sprachen ist eben jede Variable oder jedes Feld, das ich irgendwo verwende, meint immer eine Referenz auf das Objekt, was ich dahinter verbirgt. Das heißt, wenn ich so ein Feld verwende oder wenn ich eine Variable verwende, dann zeigt das automatisch schon auf dieses Objekt, was dahinter ist und zwar zeigt es automatisch auf das Stück Speicher, was dahinter steht. Das heißt, ich habe eigentlich ein Pointer, aber die Sprache macht das implizit, indem das einfach Teil dieses Referenz-Models schon ist. In Programmiersprachen, wo es eben manchmal auch das Value-Model für Variablen gibt, wie z.B. C, wo, wie wir gesehen haben, wenn eine Variable auf der rechten Seite eines Assignments auftritt, eben der direkte Wert gemeint wird, brauche ich irgendeine Art und Weise, wie ich manchmal sagen kann, dass ich eben nicht den Wert meine und alles, was ich hinter diesem Wert verbirg, sondern einfach nur die Adresse dieses Wertes und das kann ich dann eben mit Hilfe von expliziten Pointer machen, die ganz explizit sagen, ich meine nicht den Wert, ich meine die Adresse, wo der Wert gespeichert ist. Wenn ich das nicht hätte, würde ich quasi jedes Mal, wenn ich eine Variable oder ein Feld benutze, damit eben nicht die Speicheradresse meinen, sondern das komplette, den kompletten Wert, der sich dahinter verbirgt, mein und das dann auch entsprechend jeweils wieder kopieren, sodass ich eben nicht diese schön verdienten Datenstrukturen, die wir ja hier haben wollen, aufbauen könnte. Schauen wir uns das Ganze mal mit Hilfe von zwei Beispielen an und zwar mit Hilfe von zwei Sprachen, die eben einmal das Reference-Model grundsätzlich verwenden und einmal eben eine Sprache, die manchmal auch das Value-Model verwendet und zwar O'Cammell und Celion. Was wir jetzt machen werden ist, wir werden uns in beiden Sprachen anschauen, wie man zum Beispiel so ein Baum als Datenstruktur darstellen könnte und Sinn der Übung ist, dass ich zeigen möchte, dass das vielleicht ganz anders aussieht und in der Einsprache die Pointer explizit sind, in der anderen aber nicht, dass das, wenn man sich aber anschaut, was tatsächlich im Speicher passiert, dass eigentlich mehr oder weniger das Gleiche ist. So fangen wir mal mit dem O'Cammell Beispiel an. Also was wir hier machen ist, wir definieren eben einen Typen für ein Element in diesem Baum und wir sagen, dass dieses Element entweder dieser spezielle Wert empty sein kann, was quasi sagt, da ist nix oder es ist ein Knoten, ein Note und dieser besteht dann aus einem Tuple-Typen, der drei Felder hat. Also dieses, was aussieht wie dieses Multiplikationssymbol, ist nicht Multiplikation, hat auch nichts mit Pointer zu tun, sondern konkateniert einfach nur drei Felder, nämlich Einfeld vom Typ Character und dann jeweils Einfeld vom selben Typen, den wir ja hier gerade definiert haben, was sozusagen dann ein Rekosiven-Typen erstellt, indem ich zum einen halt den Wert, der in den bestimmten Knoten des Baums gespeichert ist, habe und das ist in dem Fall ein Character oder anschließend dann noch den linken und den rechten Unterbaum, der eben hinter diesem Knoten dann noch folgt, im Speicher darstellen kann. So, jetzt schauen wir uns mal an, wie das Ganze dann im Speicher aussieht. Also konzeptionell sieht ein Baum, den ich mithilfe dieser Datenstruktur erstellen kann, zum Beispiel so aus, dass ich irgendwo ein Knoten, ein Wurzelknoten mit dem Wert R habe und der hat dann vielleicht ein Kind links namens X und ein Kind rechts namens Y, X hat keine weiteren Kinder und Y hat zwei weitere Kinder, nämlich Z und W. Sondern im Speicher sieht das dann wie folgt aus. Ich habe erst mal den Wurzelknoten, den ich irgendwie darstellen muss. Jeder Knoten besteht sehr entweder ein Note oder eben dieser Spezialwert Empty, in dem Fall hier haben wir ein Note und der hat dann diese drei Elemente unseres Tuples, nämlich einmal den Character, in dem Fall R und dann zwei Punkte und das sind jetzt Punkte, die nicht expliziten, der Sprache sind, aber schlussendlich sind das Referenzen auf weitere Speicherstellen, um eben den linken und den rechten Unterbaum bzw. die Kinder dann zu repräsentieren. Die sehen dann so aus, dass ich einmal den Wert X habe. Das ist auch wieder ein Note, der Character ist eben dieses X und X hat auch wieder zwei Felder für die Kinder und das erste Kind von unserem Wurzelknoten zeigt eben genau hier auf dieses andere Speicherobjekt. Da X keine weiteren Kinder hat, zeigen die beiden Pointer, die wir dann in diesem Wert X drin haben auf diesen Spezialwert Empty und das ist einfach ein weiteres Objekt, was irgendwo im Speicher ist und das und auch das hier zeigen dann einfach beide darauf. Auf der rechten Seite habe ich jetzt hier noch diesen Knoten der Y darstellt, der dann auch wieder zwei weitere Referenzen noch enthält auf weitere Knoten, nämlich einmal den der Z darstellt, der dann auch wieder seine zwei Referenzen haben muss und den hier drüben noch der das W darstellt. Die beiden Referenzen vom Knoten Y gehen dann auf die Objekte für Z und W und da Z und W keine weiteren Kinder haben, zeigt das dann hier alles weiter auf dieses spezielle Empty Objekt, was eben einfach nur anzeigt, dass da nichts weiter ist. Und was wir jetzt hier sehen ist, dass im Speicher natürlich verschiedene Objekte existieren, die miteinander verlinkt sind durch solche Referenzen oder Pointer, aber in OCaml werden diese Pointer nicht explizit gemacht, sondern ich sage einfach nur, es gibt da eben eine weitere Instanz von meinem selben Typen von meinem selben Tree Typen und indem ich einfach nur sage, da ist so eine Instanz, meine ich damit die Speicheradresse, wo diese Instanz tatsächlich geschrieben ist, ohne dass ich das explizit als Pointer repräsentiere. Also dann schauen wir uns mal an, wie das Ganze in C aussehen würde, also ein Weg das Ganze in C zu repräsentieren wäre, indem ich so ein Struct definiere für mein Character Tree und da würde ich zum einen natürlich ein Feld für den Character Wert an sich haben, also für den Wert, den ich in jedem Knoten speichern möchte und zum anderen hätte ich jetzt hier zwei weitere Felder vom Typ Struct Character Tree Pointer, also dieses Sternchen hier vorne sagt, dass das ein Pointer auf so ein Struct Character Tree ist und die heißen dann left und right und sind im Prinzip Pointer auf eben genau die ja auf den linken und den rechten Subbaum, den der dann unter diesen Knoten jeweils noch zu finden ist. Schauen wir uns mal an, wie das jetzt im Speicher aussehen würde und wie gesagt, die der Sinderübung ist, dass es eigentlich relativ ähnlich aussieht, obwohl die Sprachen das ja doch ein bisschen anders darstellen und zwar hätte ich jetzt hier zum einen so ein Struct wo das R drin gespeichert ist und ich anschließend dann noch diese zwei Felder für den Pointer links und rechts habe und für die jeweiligen Kinder, also ich mal den selben Baum auf wie gerade eben schon, hätte ich hier so irgendwo eine Repräsentation für dieses, für den Knoten der das X enthält und der erste Pointer meines R Structs, also der left Pointer würde eben genau auf die Stelle zeigen, wo dann dieses zweite Struct, was X repräsentiert, anfängt und irgendwo anders im Speicher habe ich noch einen so einen Struct und da zeigt dann der zweite Pointer von R hin, also der right Pointer und das repräsentiert den Knoten Y. Die Frage ist, was mache ich jetzt wieder mit dem Fall, dass ich eigentlich keine weiteren Kinder habe und zwar wird das erzählt dann so gemacht, dass da ein spezieller Pointer, nämlich der sogenannte Null Pointer verwendet wird und der bedeutet einfach nur, dass das eigentlich kein Wert dahinter steckt, sondern es ist eine invalidere Adresse, sprich, da geht es nicht weiter und das bedeutet für unseren Character Tree, dass eben keine weiteren Kinder existieren. So, auf der rechten Seite muss ich jetzt unter dem Y natürlich auch noch die zwei anderen Knoten repräsentieren, nämlich den für Z, der dann auch wieder zwei weitere Pointer noch enthält und den für W, die left und right Pointer von Y zeigen da jeweils auf diese beiden drauf und weil die keine weiteren Kinder haben, sind die left und right Pointer von dem Z und W Struct dann jeweils Null Pointer. Was man jetzt sieht, ist, dass das eigentlich sehr ähnlich aussieht zu dem, was wir gerade eben schon für O'Cammel hatten, also die Datenstrukturen sind mehr oder weniger gleich im Speicher mit dem kleinen Unterschied, dass wir Null Pointer in C verwenden statt diesem speziellen MT-Objekt, aber die Grundidee ist eigentlich dieselbe, es gibt da verlinkte Datenstrukturen, die entweder in der Sprache explizit sind, wie in C oder eben etwas impliziter wie in O'Cammel oder zum Beispiel Java. So, eigentlich unabhängig davon wie explizit oder implizit die Pointer in so eine Sprache sind, gibt es eigentlich vier große Familien von Operationen, die man üblicherweise auf Pointer ausführt, die wir uns jetzt gleich noch ein bisschen genauer anschauen werden, nämlich zum einen die Erstellung von einem Pointer, dann die Allokation, also die Frage, wie kann ich eigentlich den Speicher erstellen oder den Speicher bekommen, auf den der Pointer dann zeigt, D-Referencing, also das D-Referenzieren von einem Pointer, also das Zugreifen auf dem, was tatsächlich hinter dem Pointer dann steckt und schlussendlich D-Allokation, wo ich den Speicher auf dem der Pointer gezeigt habe, wieder freingebe, so dass er dann anders verwendet werden kann. In verschiedenen Programmiersprachen werden diese Operationen verschieden behandelt oder verschieden implementiert und wir werden uns ein paar Beispiele im folgenden dazu ansiedeln. Fangen wir mal an mit dem Erstellen von Pointer. Also im Prinzip gibt es da drei Arten und Weisen, wie das üblicherweise gemacht wird. Das erste ist ein implizites Erstellen eines Pointers, nämlich indem ich ein Konstruktor aufrufe. Das kann ich zum Beispiel in C++ machen, sagen wir mal ich hätte in C++ eine Klasse, die solche Character Trees oder Knoten in diesen Character Trees, die wir ja gerade eben schon gesehen haben, darstellt, dann würde ich implizit ein Pointer auf so einen Knoten erstellen, indem ich New Character Tree hinschreibe und also den Konstruktor aufrufe und weil in C++ das eben so definiert ist, bekomme ich dann ein Pointer zurück. Die zweite Variante, wie man ein Pointer erstellen kann, ist indem ich eine eingebaute Funktion nehme, die auf dem Hieb Speicher allezuiert und mir dann eine Referenz auf diesen Speicher zurückgibt. Also in C macht man das üblicherweise mit Melloc. Das ist eine Funktion, der übergebigt eine Zahl, die ansagt, wie viele Bytes speicher ich denn eigentlich möchte. Zum Beispiel kann ich da sagen, ich möchte gerne so viel Speicher, dass da einmal dieses Character Trees struct reinpasst. Das kann ich also mit diesem Size Off machen und bekomme dann von Melloc zurück. Ein Pointer, der eben genau auf ein frisch anoziertes Stück Speicherzeit das genau diese Größe hat. Die dritte Art und Weise, wie ich ein Pointer erstellen kann oder bekommen kann, ist, indem ich den address of Operator verwende, also ein Operator, der mit die Adresse eines bereits irgendwo abgedeckten Stückes Speicher gibt. Also wenn ich zum Beispiel so ein in C ein Integer in eine Variable n schreibe, dann habe ich da erst mal kein Pointer auf diese Variable, sondern ich weiß nur, dass die irgendwo im Speicher abgelegt ist. Und ich kann jetzt aber diesen address of Operator, der in C dieses und, verwenden, um dann eine Adresse auf diesen Speicher zu bekommen. Und also dieses myPointer enthält anschließend genau die Adresse, wo der Wert 3 aktuell schon drinsteht. Das zweite, was man üblicherweise mit Pointer macht, ist, dass man Speicher anoziert. Also der Pointer als sich ist ja erst mal nur eine Adresse und da steckt erst mal noch kein Speicher dahinter, weil die Adresse, also die ist irgendwo im Speicher, aber ob da jetzt wirklich ein Stück Speicher für mich reserviert ist, das steht erst mal noch nicht fest. Und um tatsächlich ein Objekt, was sich hinter dieser Adresse verbirgt, dann benutzen zu können, muss dann natürlich entsprechend Speicher anoziert werden. Das funktioniert in manchen Sprachen implizit, zum Beispiel in OCaml oder Java, wo ich dann zum Beispiel ein Objekt erstelle und in dem Moment, wo ich das Objekt erstelle, für mich Speicher anoziert wird und ich das gar nicht explizit machen muss, sondern die Sprache, das im Prinzip für mich erledigt. Also was jetzt hier in diesem kleinen Codebeispiel hier unten passiert ist, dass ich zum Beispiel einen dieser Knoten, die wir vorher schon gesehen haben, erstelle. Die Werte auch gleich initialisiere und sage, der Character, der gespeichert wird, ist er und der linke und rechte Subbaum sind erst mal leer. Und ich rufe eben diesen Konstruktor auf und bekomme automatisch dann nicht nur den Pointer hier zurück oder eine Referenz auf das Objekt, sondern es wird implizit auch gleich der Speicher anoziert, wo diese Werte, R und so weiter reingeschrieben werden. In anderen Sprachen muss das explizit gemacht werden, also wie wir gerade schon gesehen haben, zum Beispiel in C, wo ich explizit so was wie Meloq aufrufe, um den Speicher dann überhaupt erst mal zu erhalten und anschließend dann diesen Pointer auf dieses Stück Speicher verwenden könnte, um dieses Objekt tatsächlich mit Inhalt zu füllen und zum Beispiel den Character-Wert, den ich in diesen Knoten abspeichern möchte, reinzuschreiben. So, wenn ich jetzt so einen Pointer habe und auch Speicher anoziert wurde, der hinter diesem Pointer steht, dann möchte ich den Pointer natürlich in irgendeine Form verwenden bzw. dereferenzieren. Das heißt, ich möchte auf den Speicher zugreifen, auf den dieser Pointer zeigt und da gibt es im Prinzip zwei Möglichkeiten das zu machen. Die erste Möglichkeit ist, dass ich das komplette Objekt, das ich im Speicher befindet, benutzen möchte und das mache ich mithilfe des dereferenzierungs-Operators der jeweiligen Sprache. Hier sind mal zwei Beispiele aus Pascal und C, fangen wir vielleicht mal mit dem Pascal-Beispiel an, also da habe ich einen Pointer, My Pointer. Wenn ich einfach nur auf My Pointer zugreife, dann bekomme ich die Adresse. Wenn ich jetzt aber den Wert hinter dieser Adresse möchte, dann muss ich hinter My Pointer noch dieses Carat-Symbol schreiben und das gibt mir dann den Wert, der an dieser Adresse steht und wenn das vielleicht ein komplexeres Objekt mit einem Feld namens Well ist, dann kann ich mithilfe dieses Assignments dann da z.B. X reinschreiben. In C sieht das so ähnlich aus, also da hätte ich jetzt z.B. hier einen Pointer namens My Pointer, das ist auch wieder erstmal nur die Adresse. Wenn ich aber dann diesen Dereferenzierungs-Operator, nämlich das Sternchen in C, davor schreibe, dann bekomme ich den Wert, der sich an dieser Adresse befindet, zurück und kann dann z.B. auf dieses Feld Well zugreifen und da was reinschreiben. Wenn ich jetzt nicht das komplette Objekt brauche, sondern nur ein bestimmtes Feld eines Records oder eines komplexen Types referenzieren möchte, dann gibt es in vielen Sprachen da noch ein bisschen bequemere Söntags als das, was wir gerade eben da oben gesehen haben. Zum Beispiel gibt es in C die Notation mit diesem Fall nach rechts, wo ich den Pointer benutze, also My Pointer enthält wieder die Adresse, wo ein Komplex Objekt abgelegt ist, dann den Fall nach rechts und dann den Namen des Feldes und da kann ich dann z.B. X reinschreiben. Das macht genau das Gleiche, was wir gerade gesehen haben, aber ohne dass ich dieses komplette Objekt ist, dereferenzieren muss, sondern indem ich direkt auf dieses eine Feld zugreife und impliziert, dann natürlich das Objekt dereferenziert wird. In anderen Sprachen gibt es die Notation, dass ich einen Punkt verwenden kann, z.B. in Ada oder vielleicht auch in Java, wo ich ein Objekt von einem bestimmten Typen habe, also hier habe ich z.B. eine Variable T, die so ein Character Tree enthält und eine Variable P, die ein Pointer auf so ein Character Tree enthält und in beiden Fällen kann ich dieselbe Notation verwenden, um auf Felder dieses Character Tree Knotens zuzugreifen, nämlich einfach Punkt und dann der Name des Feldes und die Sprache weiß, was dahinter steckt, also ob das jetzt ein Pointer auf so ein Character Tree Knoten ist oder der Character Tree Knoten selbst und fügt dann sozusagen die implizierte Dereferenzierung ein, wenn es den Pointer ist, ohne dass ich mich darum kümmern muss. Wenn ich meinen Pointer jetzt genug verwendet habe und irgendwann nicht mehr brauche, beziehungsweise das Speicherobjekt, was dahinter steht, nicht mehr gebraucht wird, dann muss ich den Pointer wieder deallizierend oder den Speicher wieder freigeben und der Grund ist ganz einfach, dass Speicher natürlich nicht unendlich ist, sondern ich, dass was ich nicht mehr brauche, freigeben muss, denn sonst haben wir irgendwann ja kein Speicher mehr übrig, denn wenn wir ein sogenanntes Memory Leak haben, ja, füllen wir mehr und mehr Speicher, obwohl wir den gar nicht brauchen und schlussendlich ist der Speicher irgendwann voll. In manchen Sprachen muss diese Deallokation explizit gemacht werden, z.B. in C, C++ oder auch Rust, wohingegen in anderen Sprachen das ganze implizit gemacht wird, mithilfe von Garbage Collection, was wir uns dann später hier in dem Module noch ein bisschen genauer anschauen werden, z.B. geschieht das in Java oder Csharp oder Python. Schauen wir uns mal ein Beispiel aus C an, wo das explizit gemacht werden muss und zwar ist das ein Beispiel, wo wir aktuell ein Memory Leak in diesem Stück Code drin haben. Also was hier passiert ist folgendes, wir haben so eine Schleife, die mehrmals die GetLine-Funktion aufruft und was GetLine macht, ist dies eine Zeile, die eingegeben wird von dem Benutzer und schreibt das ganze, was eingegeben wurde, an eine Adresse und diese Adresse erstellen wir hier oben, indem wir eine Variable, die auf dieser Adresse dann zeigen kann, erstellen wir hier oben, indem wir sagen, wir haben da diesen Pointer auf einen Character und außerdem übergeben wir hier noch die Größe, also quasi wie viele Zeichen überhaupt eingelesen werden sollen, das ist hier gar nicht so wichtig, aber wichtig ist, dass bei jeder Iteration dieser Schleife wir ein Stück Speicher adduziiert bekommen, denn GetLine wird implizit Speicher adduziieren und dieses Speicher, dieses Stück Speicher oder beziehungsweise ist dann an der Adresse auf die Line zeigt, wir machen dann hier irgendwas damit und anschließend setzen wir Line wieder auf Null, das heißt wir haben den Pointer auf dieses Stück Speicher nicht mehr, sondern werfen den sozusagen weg, aber dieses Stück Speicher ist trotzdem adduziiert, denn innerhalb dieser GetLine Funktion wird irgendwo Mallock aufgerufen und adduziiert eben Speicher für uns. Um das ganze jetzt zu beheben, also um hier kein Memory League zu haben, denn wenn diese Schleife oft genug ausgeführt würde irgendwann der Speicher voll, sollten wir an der Stelle Free aufrufen, das ist so, dass das Gegenstück zu Mallock, was nämlich nicht Speicher adduziiert, sondern de-adduziiert, also wieder frei gibt und was Free mit Line als Argument hier macht ist zu sagen, schau an die Adresse, wo Line gerade hin zeigt und das Stück Speicher, was da ist, das kannst du jetzt bitte wieder zurückbekommen und der MemoryManager in C wird dann sozusagen dieses Stück Speicher wieder freigegeben. So, zum Abschluss dieses Teils noch ein kleines Quiz, wo es eben genau um das de-adduziieren von Speicher und den daraus vielleicht resultierenden Memory Leaks geht und zwar haben wir hier ein Stück C Code, das Mallock verwendet und Free verwendet und die Frage ist unter der Annahme, dass ein Integer jeweils vier Bytes verwendet, wie viele Bytes werden denn gelegt, also wie viel Speicher wird quasi nicht wieder freigegeben, wenn wir diesen Code so ausführen. Bitte kurz drüber nachdenken, dann in Ilias abstimmen und anschließend erst die Lösung anschauen. Schauen wir mal, was hier passiert. Also wir rufen, diese Schleife wird dreimal ausgeführt, einmal mit I gleich 0, dann mit I gleich 2 und dann mit I gleich 4. In jeder dieser Ausführung alleziieren wir so viel Speicher, wie man für ein Int braucht, also jeweils 4 Bytes und wenn I durch 4 teilbar ist, dann geben wir dieses Stück Speicher auch wieder frei. Das heißt, in der I gleich 0 tun wir das, in der I gleich 2 tun wir das nicht und in der letzten I gleich 4 wird Free dann wieder aufgerufen. Das heißt, es gibt quasi diese eine I in der Mitte, wo wir die 4 Bytes, die wir alleziieren, nicht wieder freigeben und somit ist die korrekte Antwort 4 Bytes, die hier gelegt werden, weil in Free nicht immer aufgerufen wird. Ja, und damit sind wir schon wieder am Ende von Teil 3 von 4. Hier ging es um Pointer, im 4. Teil geht es dann noch mehr um Pointer und damit erstmal Danke fürs Zuhören und bis zum nächsten Mal.