 Willkommen zurück zu Programmierparadigmen. Wir sind hier in Teil 4 des Moduls composite types und in diesem 4. Teil geht es ebenso wie im 3. Teil weiter um pointer und wie die im Programmiersprachen repräsentiert werden und was man mit denen so alles anstellen kann. Ja, am letzten Teil hat man ja schon gesehen, was man alles so mit Pointer machen kann, welche Operation da üblicherweise drauf ausgeführt werden und was wir jetzt hier in dem 4. Teil noch machen wollen ist uns A anschauen, wie Pointer und Arrays denn zusammenhängen und zwar in der Sprache C, wo diese beiden Konzepte sehr, sehr nah beieinander sind. Dann schauen wir, was alles passieren kann, wenn ich Referenzen habe, die vielleicht auch was zeigen, was gar nicht mehr da ist, sogenannte Dengling References und schlussendlich schauen wir uns kurz an, was Gabic Collection denn macht und inwiefern das hilft mit Pointer umzugehen. Schauen wir uns erstmal an, was Pointer und Arrays denn in der Sprache C denn alles so gemeinsam haben bzw. wie die miteinander zusammenhängen. Also diese beiden Konstrukte sind sehr, sehr ähnlich und man kann mehr oder weniger frei zwischen Pointers und Arrays hin und her wechseln und quasi ein Array mit Hilfe von Pointer bearbeiten und ein Pointer dann auch wieder als Array betrachten. Und das ganze mal zu illustrieren ist hier so ein kleines Beispiel, indem wir zunächst erstmal drei Variablen haben, eine Intvariable, ein Pointer auf ein Int in A und dann ein Array namens B und dann benutzen wir diese drei wie folgt. Also initial wird erstmal der Pointer A auf dieses Array in B zeigen, das heißt ich kann sozusagen den Pointer einfach auf den Beginn des Arrays zeigen lassen und das ist eine legale Operation ist auch typkompatibel, weil der Pointer ganz einfach auf den Beginn des Arrays zeigt und ich über diesen Pointer oder auch über das Array B auf dieselben Speicherstellen zugreifen kann. Was dann diese vier Assignments hier unten machen ist, dass sie im Prinzip alle dasselbe in die Variable n reinschreiben, nämlich den Wert 4, der hier an der vierten Stelle in unserem Array gespeichert ist, aber eben mit Hilfe von verschiedenen Syntax. Im ersten Beispiel benutze ich die ganz normale Array Syntax, wo ich einfach auf das Element beim Index 3, also das vierte Element zugreifen. Hier benutze ich eine Pointer Arithmetic Syntax, indem ich den Pointer A nehme, der ja auf den Anfang des Arrays zeigt, plus drei Rechner, weil das ein Pointer auf ein Int ist, heißt das also quasi dreimal Größe von Int weiterrücken. Und anschließend mit diesem Sternchen den Wert, dess ich an dieser Speicherstelle dann befindet, D-Referenziere, also Auslese. Die dritte Variante ist hier unten. Hier benutze ich das Array B, so wie man Arrays üblicherweise verwendet. Es ist wichtig zu sehen, dass das anders ist als das, was ich hier oben gemacht habe, denn hier habe ich einfach den Pointer A so verwendet, als wäre er ein Array. Das kann ich machen und dadurch, dass das ein Int Pointer ist, weiß der Kompiler dann auch, was ich meine und behandelt es einfach so, als wäre A auch ein Int Array. Und schlussendlich ist dann hier nochmal das Inverse wieder. Ich benutze nämlich jetzt das Array B so, als wäre es ein Pointer, indem ich auf das Array bzw. das, was in B gespeichert ist, drei drauf addiere und den Wert, der sich dann daraus ergibt, die Adresse, die sich daraus ergibt, die Referenziere und das funktioniert im Prinzip genauso wie das, was ich hier oben gesehen habe. Was man sieht an diesem Beispiel ist, dass Pointer und Arrays eigentlich mehr oder weniger derselbe sind in C und ich die mehr oder weniger gleich verwenden kann. Um vielleicht noch ein bisschen genauer zu erklären, warum dieser Pointer Arithmetic, die wir gerade gesehen haben, eigentlich genau das Gleiche ist, wie dieser direkte Zugriff auf das Array mithilfe der eckigen Klammern, schauen wir uns mal an, wie das eigentlich alles definiert ist in C. Und zwar funktioniert das so, dass der Subscript Operator, also diese eckigen Klammern, die man üblicherweise benutzt, um auf ein Element eines Arrays zuzugreifen, genau in mithilfe von Pointer Arithmetic definiert ist. Also wenn ich zwei Expressions, zwei Ausdrücke e1 und e2 habe und die so verwendet, dass ich sage, ich greife auf das e2 Element von e1 zurück, dann heißt das im Prinzip folgendes, nämlich, dass ich die Adresse, die auf die e1 zeigt, nehme, dazu den Wert, zu dem e2 evaluiert, drauf addiere und das Ganze dann die Referenziere, also den Wert, der sich an dieser daraus ergebenden Adresse befindet, dann auslesen. Und interessanterweise ist deshalb, sind diese zwei Varianten, die ich jetzt hier gerade unterstreiche, es ist semantisch equivalent. Also wenn ich ein Array habe und auf das dritte Element zugreife, dann ist das equivalent zu der Zahl 3 und da das Arrayte Element zu benutzen. Die zweite Variante hier schreibt man üblicherweise nicht hin, weil es ein bisschen verwirrend ist, aber wenn man verwirrenden Code schreiben will, das ist ein guter Anfang, denn dadurch, dass das eben so definiert ist, wie es da oben steht, bedeuten die zwei eben genau das Gleiche. Diese Pointe Arithmetic, die wir jetzt gerade gesehen haben, kann ich neben der Adition natürlich auch für andere arithmetische Operationen verwenden. Also insbesondere macht es manchmal Sinn, Pointes zu subtrahieren, zum Beispiel, wenn ich den Abstand zwischen zwei Elementen in einem Array wissen will. Also wenn ich zwei Pointe P1 und P2 habe, die beide jeweils auf irgendein Element im selben Array zeige, dann kann ich mit P1 minus P2 herausfinden, wie weit diese Elemente dann voneinander entfernt sind. So ähnlich funktioniert es auch mit Vergleichsoperatoren. Also wenn ich zum Beispiel wieder zwei Pointe P1 und P2 habe, die jeweils auf irgendein Element im selben Array zeigen, dann kann ich mit dem Vergleichsoperator größer gleich oder kleiner gleich und so weiter schauen, ob das eine Element an einem höheren oder niedrigeren Index ist als das andere. Also wenn mein Array zum Beispiel sortiert ist und ich jetzt zwei Pointe auf irgendwelche Elemente in diesem Array habe, kann ich schauen, ob diese Elemente dann größer oder kleiner sind, ohne dass ich mir wirklich den Wert der Elemente anschaube, aber einfach nur, weil ich weiß, dass das Array ja schon sortiert ist. All diese Pointe Arithmetik-Operationen sind immer skaliert entsprechend des Types, denen der Pointe hat. Also wenn das ein Pointe auf int ist und ich dann zum Beispiel minus 2 rechne, dann heißt das minus 2 mal Größe eines ints und das macht eben genau Sinn, weil dadurch sichergestellt wird, dass die Pointe Arithmetik, sofern ich den richtigen Pointetype habe, genau dasselbe macht, wie wenn ich den Array auf irgendein Array zeige. Das Array jetzt auf die traditionellere Art und Weise zugreifen will. Jetzt haben wir gesehen, dass Arrays und Pointe das eigentlich mehr oder weniger das gleiche sind, aber es gibt einen einzigen doch sehr wichtigen Unterschied und das ist in der Art und Weise wie der Speicher, der sich hinter diesem Pointe beziehungsweise dem Array befindet, denn allzuiert wird. Bei Arrays ist es so, dass der Speicher implizit allzuiert wird. Also ich muss nicht explizit sagen, hey, gib mir doch bitte mal Platz für zum Beispiel 10 Integer-Werte, sondern ich deklariere einfach so eine Variable, die sagt hier ist eine Variable, die ein Array auf Integers der Größe 10 enthält und dann wird implizit Platz für genau 10 Integer-Variablen allzuiert. Bei Pointern funktioniert das anders, also wenn ich jetzt statt diese Array Notation, die hier unten verwende würde und einfach sage, hey, ich habe jetzt ein Pointer auf ein Int, dann allzuiert das erstmal noch gar nichts, sondern ich muss anschließend noch Melloc aufrufen zum Beispiel, um dann eben tatsächlich Platz für diese 10 Ints zu allzuieren. So, um folgen wollen wir uns ein bestimmtes Problem anschauen, was auftreten kann, wenn man mit Pointern arbeitet, nämlich die sogenannten Dengling References. Das Problem hier ist, dass ich einen Pointe habe, der sozusagen live ist, das heißt, ja, da ist die Adresse tatsächlich noch in irgendeiner Variable gespeichert, die ich noch verwende, allerdings zeigt dieser Adresse oder dieser Pointe nicht mehr auf ein valides Speicherobjekt. Das ist im Prinzip so ein bisschen das Gegenstück zum Memory Leak, was wir vorher ja schon gesehen haben, also beim Memory Leak war das Problem, dass ich irgendwo ein Stück Speicher allzuiert habe, aber gar kein Pointer mehr drauf zeigt. Und bei Dengling References habe ich einen Pointer, der irgendwo hin zeigt, aber der Speicher, der da vielleicht mal war, der ist da jetzt nicht mehr für mich allzuiert. Das kann in verschiedenen Situationen geschehen, also zwei Beispiele sind, dass ich zum Beispiel ein Pointer auf ein Objekt habe, was auf dem Stack gespeichert ist und das dann aber in irgendeiner Form aus der aktuellen Funktion rausgegeben wird, zum Beispiel über den Return Value oder indem ich die Adresse dieses auf dem Stack gespeicherten Objektes irgendwo hinschreibe. In dem Moment, wo die Funktion aber zurückkehrt, wird natürlich auch der Stack Frame dieses Funktionsaufrufs freigegeben und damit ist der Speicher nicht mehr valide und trotzdem existiert dieser Pointer noch. Eine andere Situation, wo das Ganze aufdrehten kann, ist, wenn ich ein Objekt auf dem heap gespeichert habe und das Expecid dealoziere, zum Beispiel mit free und der Pointer darauf aber trotzdem noch existiert und nicht nallgesetzt wurde oder vielleicht einfach aus dem Scope verschwunden ist, sondern einfach noch weiter da ist, weil dann habe ich eben diesen Pointer auf das Stück Speicher, was ich jetzt eigentlich schon freigegeben habe. Das Problem an der ganzen Sache mit diesen denkigen References ist, ist, dass das Verhalten, was sich daraus ergibt, nicht definiert ist. Das heißt, der Compiler kann im Prinzip damit machen, was er möchte und insbesondere kann es zum Beispiel passieren, dass mein Programm dann einfach abstürzt oder dass ich vielleicht ein bestimmtes Stück Speicher schreibe, wo das jetzt eigentlich schon wieder anders verwendet wurde, weil Meloq natürlich den Speicher der freigegeben wurde dann auch wieder in anderen Stellen das Programms weitergibt und das kann zu sehr unerwarteten Verhalten führen. So, das Beispiel macht mal gleich in Form eines Quizzes und zwar in Form von etwas C-Code, der hier aufgeschrieben ist, der Meloq verwendet und free verwendet und vielleicht auch einen denkigen Reference oder mehrere beinhaltet. Die Frage für Sie ist jetzt, an welchen Zeilen oder an welcher Zeile vielleicht auch macht denn dieser C-Code Gebrauch von einer denkigen Reference und wie immer bitte im Ilias abstimmen und dann anschließend erst das Video weiterlaufen lassen. Schauen wir uns mal die Lösung an. Also was hier passiert ist folgendes. Wir haben diesen Pointer vom Typ Character Pointer und alle Zieren Speicher, der dann dahinter steht und zwar so viel, dass genau drei Charakter reinpassen. Anschließend fangen wir an, da was reinzuschreiben an, der erste Character wird A sein, der dritte Character wird C sein und für den zweiten Character rufen wir diese Foo Funktion hier auf. Foo ist hier oben definiert. Foo hat als Rückgärbewert ein Character Pointer und was Foo jetzt macht ist, einen weiteren Character Pointer erstellen, nämlich dieses CP und dahinter auch gleich Speicher zu alle Zieren, nämlich für einen Character. Da schreiben wir dann B rein und geben anschließend diesen Character Pointer auch zurück. Was jetzt hier passiert ist, ist, dass wir diesen Pointer dereferenzieren durch dieses Sternchen vor dem Foo und das bedeutet, dass wir den Wert, der an diesem Speicher steht, auslesen, also das B und anschließend in unseren bereits allizierten Speicher an die zweite Stelle reinschreiben, sodass dann CSP von 1 auf den Wert B hat. Anschließend rufen wir Free auf und geben quasi all den Speicher, auf den dieser CSP Pointer zeigt, nämlich all das wieder frei, das heißt, wir geben dreimal die Größe eines Characters frei. Und schlussendlich geben wir dann hier unten so ein paar Werte aus, indem wir auf diese Erray-Elemente bzw. Pointer wieder zugreifen, nämlich auf das erste, zweite und dritte. Und jetzt um die Frage zu beantworten, diese drei Zugriffe sind alle drei, denkling References, weil wir haben die Stelle im Speicher, wo diese drei Werte CSP von 0, 1 und 2 stehen, ja hier in Zeile 11 schon frei gegeben. Das heißt, all diese drei Zugriffe greifen auf Speicher zu, der schon wieder frei gegeben wurde und im Prinzip vom Betriebssystem auch für ganz andere Sachen vielleicht schon wieder verwendet werden könnte. Neben diesen denkling References gibt es hier auch noch ein Memory Leak, denn dieses Melok hier oben hat kein dazu passendes Free, denn dieses Free hier unten in Zeile 11 gibt nur diesen Speicher, der hier erzielt wurde, wieder frei, aber eben nicht den von da oben. Das heißt, in dem Beispiel haben wir sowohl drei denkling References als auch ein Memory Leak. Jetzt haben wir so viel uns angeschaut, was man alles mit Pointer machen kann und vor allem, was man auch damit alles falsch machen kann, dass vielleicht die Frage naheliegt, naja, muss ich das denn alles machen? Könnte man denn nicht innerhalb der Sprachimplementierung zumindest Teile dieser Fehler unterbinden? Und die Antwort ist natürlich ja, das geht und zwar mithilfe von Garbage Collection. Also die Idee von Garbage Collection ist, dass die Sprachimplementierung sich um das Dealluziieren des Speichers kümmert, indem sie einfach den Speicher, der nicht mehr gebraucht wird, automatisch wieder frei gibt. Und insbesondere führt das eben dazu, dass wir keine denkling References haben können, denn in dem Moment, wo wir den Speicher nicht explizit frei geben müssen, können wir den auch nicht zu früh frei geben. Und die Programmierer sich damit ja mehr mit anderen Aspekten des Codes befassen können und eben nicht über das so sehr über die Speicherverwaltung nachdenken müssen. Garbage Collection gibt es in vielen Sprachen, insbesondere in den sogenannten Managed Languages, also Sprachen wie zum Beispiel Java, Python oder JavaScript, die in der Regel dann auch eine Virtual Machine haben, auf der der Code ausgeführt wird und die nicht einfach direkt zu Machinecode kompiliert werden. Es ist die Frage, wie kann man so einen Garbage Collector dann überhaupt implementieren? Sie werden das wahrscheinlich nie selbst implementieren, zumindest die meisten, die hier zuhören, werden das vermutlich nie machen, aber es ist trotzdem sehr wichtig zu verstehen, wie das eigentlich implementiert, weil man den natürlich, wenn man so eine Managed Language wie Java zum Beispiel verwendet, natürlich immer mitverwendet. Also wie wird so ein Garbage Collector implementiert? Ein häufiger Weg ist mit Hilfe von sogenannten Reference Counts. Die Idee ist, dass wir für jedes Objekt im Speicher einen Counter haben, ein Zähler, der angibt, wie viele Referenzen auf dieses Objekt in aktuell gerade existieren. Dieser Counter wird inkrementiert, immer wenn ein neuer Pointer auf dieses Objekt erstellt wird. Also wenn ich zum Beispiel den Konstruktor aufrufe, dann ist der Counter erstmal auf 1 gesetzt und wenn ich dann die Referenz vielleicht noch in eine andere Variable schreibe, dann wird das Ganze auf 2 inkrementiert. Und immer wenn ein Pointer zerstört wird oder nicht mehr benutzt werden kann, dann wird der Counter wieder dekrementiert. Also wenn ich zum Beispiel von einer Funktion zurückkehre und jetzt die lokale Variable, die vielleicht auf dieses Objekt gezeigt hat, dann nicht mehr existiert, gibt es also diesen Pointer nicht mehr und dementsprechend wird der Counter dann wieder dekrementiert. Und mit Hilfe dieser Reference Counts kann der Garbage Collector jetzt regelmäßig die nicht mehr benötigten Objekte, also so useless Objects, deallozieren. Und das sind genau die, die eben dann ein Reference Count von 0 haben, weil ich weiß, dass es auf die keine Referenz mehr gibt. Die können also nicht mehr verwendet werden. Und somit können die dann einfach dealloziert werden und zwar automatisch und implizit, ohne dass der Programmierer da explizit free aufrufen müsste oder sowas ähnliches explizit machen muss. Wenn man das Ganze naiv, so wie es gerade erklärt habe, implementieren würde, hätte man ein großes Problem und das sind zyklische Referenzen. Also wenn ich Datenstrukturen habe, die schlussendlich auch wieder auf sie selbst zeigen, zum Beispiel einen Grafen oder sowas, in dem es Kanten und Knoten gibt und die Kanten vielleicht auch mal wieder auf den Ausgangsknoten zurück zeigen, dann führt es dazu, dass ich so eine zirkuläre Datenstruktur habe, wo im Prinzip jedes Speicherobjekt mindestens eine Referenz auf sich hat. Und trotzdem dieses Gesamtkonstrukt, diese Gesamtdatenstruktur vielleicht gar nicht mehr gebraucht wird, eben weil zum Beispiel dieser gesamte Graf gar nicht mehr verwendet wird. Schauen wir uns mal ein Beispiel für so eine zyklische Datenstruktur an und wie die entstehen könnte. Also die Idee wäre, dass wir irgendwo unseren Stack im Programm haben, auf dem dann zum Beispiel ein paar lokale Variablen gespeichert werden. Und irgendwo anders haben wir den Heap auf dem die Datenstruktur, um die es hier schlussendlich gehen soll, gespeichert wird. Und auf dem Stack haben wir einen Pointer, nennen wir ihn mal List Pointer, der auf die Datenstruktur zeigt, um die es hier geht und diese Datenstruktur ist dann auf dem Heap und ist eine Liste, in der wir zum Beispiel jeweils einen Character haben und einen Pointer auf das nächste Element. In der Liste, zum Beispiel hätten wir hier die Liste, die A, B und C enthält und die Liste ist zufällig auch noch zyklisch. Das heißt, das letzte Element zeigt wieder auf das erste und unser List Pointer zeigt jetzt irgendwo in diese Liste rein, zum Beispiel auf dieses erste Element namens A. Soweit so gut, solange der List Pointer existiert, brauchen wir auch diese Datenstruktur auf dem Heap und der Gabelschulektor soll die natürlich noch nicht aufräumen. Aber was passiert jetzt, wenn ich zum Beispiel mein List Pointer irgendwann auf Null setze? Also sozusagen explizit diese Referenz auf diese Liste lösche. Das heißt, mein List Pointer sieht im Speicher dann quasi so aus, wobei ich wieder diesen Querstrich verwende, um eben diesen Null Pointer anzugeben. Und gleichzeitig habe ich natürlich immer noch diese Datenstruktur hier hinten, die genauso aussieht wie das, was ich gerade eben schon oben aufgemalt habe. Deswegen male ich es jetzt nur noch mal so schnell. Und das Problem ist nun, dass jedes dieser Objekte auf dem Heap, also jedes dieser drei Objekte, die A, B und C enthalten, jeweils ein Pointer haben, der auf dieses Objekt zeigt. Das heißt, der Referenz Count ist jeweils eins, das heißt, aus Sicht des Gabelschulektors sieht keines dieser Objekte so aus, als könnte man es aufräumen. Aber de facto gibt es eigentlich gar keinen Pointer mehr, der irgendwie diese Datenstruktur rein zeigt. Das heißt, eigentlich müsste die und kann die weggeräumt werden. Also das Problem, was wir jetzt an dem Beispiel gesehen haben, ist, dass es zirkuläre Datenstrukturen geben kann, in denen eben wirklich jedes einzelne Speicherobjekt nicht mehr gebraucht wird und nutzlos ist, obwohl jedes dieser Objekte mindestens eine Referenz hat, die auf das Objekt zeigt. Und um eben genau mit dieser Situation umzugehen, gibt es natürlich einen besseren Ansatz als den naiven, den ich am Anfang erklärt habe. Und der funktioniert so, dass sich davon ausgeht, dass jedes Objekt, wo was ich irgendwo beim Speicher habe, nutzlos ist. Außer ich finde eine Kette von noch validen Pointers, die irgendwo anfangen, die bei irgendetwas anfangen, dass ein Namen hat, also eine Variable zum Beispiel und schlussendlich bei u rauskommen. Und wenn ich das als Definition nehme für Speicher, den ich noch brauche, dann wird der Gabelschulekte eben auch genau solche zirkulären Datenstrukturen, die eigentlich gar nicht mehr gebraucht werden, aufräumen können. Einen konkreten Algorithmus, mit dem man dann tatsächlich diese Idee umsetzen könnte, ist der sogenannte Markensweep Algorithmus, der so einer der Klassiker unter den Gabelsch-Collection Algorithmen und Varianten davon sind auch tatsächlich in Implementierung von zum Beispiel Java zu finden. Die Grundidee ist, dass der Algorithmus versucht, all diese nicht mehr benötigten Speicherobjekte oder Speicherblöcke zu finden, indem er Folgendes macht. Also er läuft einmal komplett über den Hieb und markiert jeden Block, den er findet, erstmal als nutzlos. Also wir gehen davon aus, brauchen wir erstmal nicht mehr, kann weg. Anschließend fangen wir an, von allen externen Referenzen, also sozusagen alles, was einen Namen hat in dem Programm, also Dinge wie Felder, lokale Variablen oder vielleicht auch globale Variablen, von all diesen Namen beginnen wir loszulaufen und markieren alles, was wir von diesen externen Referenzen aus erreichen können, jeden Memory Block als nützlich. Das heißt, wir hangeln uns sozusagen einmal quer durch den Hieb und alles, was wir erreichen, markieren wir jetzt als nützlich. Und schlussendlich wird dann alles, was nicht als nützlich markiert wurde, als nutzlos betrachtet, weil wir haben am Anfang ja alles als nutzlos angesehen und genau diese Blöcke werden dann freigegeben. Das heißt, die werden in garbage collection terminology auf die sogenannte Freelist gesetzt. Was im Prinzip einfach eine Datenstruktur ist, die all den freien Speicher repräsentiert und wenn dann wieder neuer Speicher benötigt wird, weil zum Beispiel irgendwo ein neues Objekt erstellt wird, dann wird das direkt von dieser Freelist verwendet. Neben dieser einfachen Variante des marken sweep Algorithmus, den ich gerade kurz angerissen habe, gibt es eine ganze Reihe von Optimierung und anderen Algorithmen. Wir werden hier eine Veranstaltung nicht im Detail behandeln können. Ich will das einfach nur mal kurz erwähnen, um zu zeigen, dass diese garbage collection Idee so einfach wie sie vielleicht aus programmierender Sicht ist, unter der Haube doch ein bisschen komplexer ist. Also eine Erweiterung dieses marken sweep Algorithmus, den wir gerade gesehen haben, ist das sogenannte pointer Reversal. Also wenn ich den heap einmal komplett traversiere und dabei jeden pointer, den ich irgendwo finde, folgen möchte, muss ich ja im Prinzip irgendwo ein Stack haben, der mir sagt, wo ich jetzt alles schon war und wo ich vielleicht noch weitere Pointer weiterverfolgen möchte. Dieser Stack kann unter Umständen sehr groß sein und um das zu verhindern, kann man sogenanntes pointer Reversal verwenden, wo ich quasi ein Pointer speichere, der mir sagt, wo ich das auch alles hin zurück muss. Eine andere Erweiterung des einfachen Algorithmus ist das sogenannte Stop and Copy, wo es darum geht, dass ich die Fragmentierung des Speichers verhindern möchte oder zumindest reduzieren möchte. Also wenn ich viele kleine Speicherobjekte habe und die dann immer wieder frei gegeben werden, dann komme ich irgendwann in die Situation, dass ich viele kleine Objekte und viele kleine Lücken dazwischen habe. Und wenn jetzt viel Speicher verwendet, irgendwo gebraucht wird, dann habe ich dafür gar keinen freien Platz und das kann man verhindern, indem man sagt, okay, jetzt halten wir mal komplett an und speichern alles, was noch gebraucht wird oder kopieren alles, was noch gebraucht wird an die eine Ecke des Speichers, so dass die andere Ecke komplett frei ist. Dann hat man nämlich plötzlich wieder Platz für neue große Objekte und das nennt sich Stop and Copy. Eine Variante von diesem Stop and Copy sind auch noch generationsbasierte garbage collectors, wo die Idee ist, dass es in den meisten Programmen so eine Reihe von Speicherobjekten gibt, die relativ langlebig sind und deswegen vielleicht an einem bestimmten Teil des Heaps gespeichert werden sollten, wo ich gar nicht so oft aufräumen muss. Und andere Speicherobjekte doch eher kurzlebig sind und die dann an einer anderen Stelle des Heaps gespeichert werden, wo ständig oder nicht ständig, aber zumindest relativ häufig, der garbage collector aufräumen muss und wo durchaus auch Fragmentierung gestattet ist. Also alles, was ich damit sagen will ist, es gibt eine ganze Reihe von anderen Algorithmen, die garbage collection tatsächlich effizient und praktisch machen. Wie genau die funktionieren, können wir jetzt im Detail hier nicht besprechen, aber es ist wichtig zu wissen, dass das nicht ganz so einfach ist wie, na ja, dann räumen wir einfach mal den Speicher auf, der nicht mehr verwendet wird. Ja, und damit sind wir ja schon am Ende dieses vierten und letzten Teils des Modules composite types. Das heißt, wir haben jetzt ein bisschen mehr darüber kennengelernt, wie in Programmiersprachen denn so komplexe Typen, die zum Beispiel mit Pointer miteinander verknüpft sind, dargestellt werden und wie die dann tatsächlich auch in der Implementierung der Programmiersprache im Speicher repräsentiert werden und welchen Einfluss das Ganze auch auf die Effizienz von Programmen hat. Damit vielen Dank fürs Zuhören und bis zum nächsten Mal.