 Ja, herzlich willkommen zurück zu Programmierparadigmen. In diesem Modul der Veranstaltung geht es um composite Types, also komplexen Typen, die aus anderen Typen zusammengesetzt werden, also so Dinge wie Records oder Arrays oder auch komplexe Datenstrukturen, die man mithilfe von Pointern bauen kann. Und wie all diese komplexen Typen in Programmiersprachen repräsentiert werden und wie das Ganze dann auch innerhalb der Sprache implementiert wird, das schauen wir uns in diesem Modul genauer an. Bevor wir mit dem eigentlichen Inhalt anfangen, machen wir eine kleine Aufwärmübung, und zwar in Form eines Quizzes. Und zwar geht es in diesem Quiz um ein Stück Java-Code, nämlich das, was Sie hier sehen. Und die Frage ist, welche der Zeilen in diesem Code denn zu einem Compile Time Fehler führen, weil da irgendwas mit den Typen nicht stimmt. Also ich würde Sie bitten, das Stück Code einfach kurz anzuschauen, ein bisschen drüber nachzudenken und dann das Video zu stoppen, in Ilias abzustimmen, welche Zeilen denn hierzu Komplierfehlern führen und anschließend dann erst weiterzuschauen. Schauen wir uns mal die Lösung an. Also was hier oben schied ist, dass zwei Variablen A und B deklariert werden. Die erste davon ist ein Integer-Array. Das zweite ist einfach nur eine Integer-Variable. Und hier in der zweiten Zeile haben wir dann zwei weitere Variablen C und D, die jeweils beide wieder Integer-Array sind. Was man beachten muss, ist, dass es also zwei mögliche Varianten syntaktisch gibt, wie man ein Array deklarieren kann. Beide führen schlussendlich dazu, dass wir eine Integer-Array-Variable haben. Wenn wir jetzt den Code hier unten anschauen, dann ist in der Zeile hier oben alles soweit in Ordnung. Den ersten Fehler gibt es in der Zeile dahinter, denn hier wird versucht in diese Variable B, die ihr einfach nur als Int deklariert wurde, ein Integer-Array zu schreiben. Und das funktioniert nicht, weil das eben ja nicht kompatible Typen sind. Genauso in der nächsten Zeile, wo wir versuchen, ein Character-Array in diese Variable C zu schreiben. Aber C wurde ja deklariert als Integer-Array-Variable. Und deswegen geht das auch nicht. Und in der letzten Zeile ist dann wieder alles in Ordnung, denn hier schreiben wir einfach nur ein Integer-Array in die Variable D. Und das ist ja der richtige Typ. Was ich hier in dem Beispiel gesehen habe, ist eine Form von sogenannten Composite Types, also komplexen oder zusammengesetzten Typen. Das Besondere an diesen Typen ist, dass sie eben nicht nur die einfachen primitiven Typen sind, die in der Programmiersprache ja eh schon drin sind, sowas wie Int oder Character oder manchmal auch String je nach Sprache, sondern dass es neue Typen sind, die man zusammensetzen kann, indem man die einfacheren Typen, die ich gerade genannt habe oder vielleicht auch andere Composite Types nimmt und miteinander kombiniert. Und zwar mit Hilfe eines sogenannten Type-Constructors. Also was genau diese Type-Constructors sind, werden wir noch sehen, aber die Grundidee ist, dass sie im Prinzip einen neuen Typ mehr stellen. Je nach Sprache gibt es dann eine ganze Reihe von Arten, wie das gemacht werden kann. Hier sind mal so ein paar aufgeführt. Also Records sind eine Art von Composite-Type, die wir uns noch genauer anschauen werden, genau so Arrays, die werden wir auch genauer anschauen. Dann sind in manchen Sprachen String-Composite Types, die ja erst explizit zusammengebaut werden müssen, und zwar aus einzelnen Charakters. In anderen Sprachen sind die auch direkt als primitiver Typ schon Teil der Sprache. Und dann haben wir natürlich so eine ganze Reihe von komplexen Datenstrukturen, da sie ja sicherlich alle schon mal irgendwo benutzt haben, nämlich zum Beispiel Mengen oder auch alles, was man aus Pointen zusammensetzen kann oder natürlich auch Listen. Und auch das werden wir uns in diesem Modul genauer anschauen. Hier ist mal ein kleiner Überblick, worum es insgesamt in diesem Modul gehen soll. Also es sind eigentlich drei große Teile, nämlich Records, Arrays und dann alles, was man so mit Pointen machen kann, wobei der Pointer-Teil dann so groß ist, dass ich den nochmal in zwei Einheiten aufteile. Fangen wir gleich mal direkt mit dem ersten Teil an und zwar mit den Records. Und die erste Frage ist natürlich, was ist das überhaupt? Also die Grundidee von einem Record oder manchmal heißen die auch Structures oder Structs, ist, dass ich verschiedene Daten, die verschiedene Typen haben, miteinander verbinden möchte und das quasi als eine Datenstruktur betrachten möchte. Es ist also sozusagen eine heterogene Mischung von Typen, weil eben jeder dieser einzelnen Bestandteile nicht vom selben Typ sein muss, sondern ich kann zum Beispiel ein Integer und ein Character und vielleicht auch ein Bull zusammenstecken. Diese Zusammenstecken funktionieren so, dass ich eine komplexe Datenstruktur erstelle, also ein Composite-Type erstelle, indem jeder dieser Komponenten ein sogenanntes Feld ist und ich dann über dieses Feld auf diese einzelnen Komponenten zugreifen kann. Manchmal ganz interessant, wo das eigentlich historisch herkommt. Also die erste Sprache, die dieses Konzept von Records überhaupt eingeführt hat, war Cobol und die vielleicht heute häufigste Form, davon nämlich die Structs, die man heutzutage in C sieht und die wir auch gleich in Beispielen genauer anschauen, werden die sind eigentlich zurückzuführen auf Algol 68, was vor langer, langer Zeit mal dieses Struct Keyboard eingeführt hat, was ungefähr so ist wie das, was man heute auch noch in C sieht. Schauen wir uns im besten Mal ein konkretes Beispiel an und zwar in C. Was wir hier sehen, ist ein Struct namens Element und das repräsentiert ein chemisches Element. Dieses Struct besteht jetzt aus verschiedenen Feldern, genau gesagt 4 und diese Felder haben jeweils einen Typ, zum Beispiel, dass hier oben ist einfach ein Feld, was den Namen des chemischen Elements beinhaltet und in dem Fall ein Character Array der Größe 2 ist. Das heißt also eines dieser Felder ist selbst auch wieder ein zusammengesetzter Typ, nämlich ein Array aus Characters. Das heißt, man kann diese Composite Types natürlich in der Form auch ineinander verschachteln. Die anderen Felder sind dann ein Integer, ein Double und ein Bool. Das heißt, ich kann also diese verschiedenen Daten, Teile einfach hintereinander schreiben und den jeweils ein Typ geben und die dann sozusagen zu einem großen Typen nämlich in dem Fall dieses chemische Element zusammenfügen. Wenn ich das Ganze jetzt benutzen will, muss ich eine Variable definieren, in dem Fall zum Beispiel Copper, wie die dann den Typ dieses Structs hat und das mache ich in C so, dass ich dieses Struct Keyword hier auch nochmal davor schreiben muss und dann den Namen des Structs selbst. Also der Typ besteht sozusagen nicht nur aus dem Namen Elements, sondern außerdem noch aus dem Keyword Struct. Wenn ich diese Variable dann habe, dann kann ich die Felder des Structs ganz einfach benutzen, indem ich da was reinschreibe oder was auslese. Also hier zum Beispiel wird was reingeschrieben an das erste Element dieses Name Arrays hier oben oder hier wird dann zum Beispiel ausgelesen, ob dieses Bool Gefield Metallic denn wahr oder falsch ist. Und die Notation, die dafür verwendet wird, ist im Prinzip einfach immer diese Dot Notation, wo ich einfach die Variable nehme, die das auf das Struct zeigt und dann Punkt und dann den Namen des entsprechenden Feldes. So, das war jetzt zum Beispiel aus C. Schauen wir uns mal an, wie das Ganze in anderen Sprachen vielleicht auch noch zur Verfügung steht. Also in den meisten Programmiersprachen gibt es irgendeine Form von Structures oder Records, die man jeweils mit dem entsprechenden Typ Konstrukte dann erstellen kann. Also in C haben wir gerade gesehen, gibt es eben dieses Structs. In C++ gibt es eine spezielle Form von Klassen, die im Prinzip genau dasselbe sind. In Fortran gibt es auch Records und zwar heißen die da ganz einfach nur Types, weil das sozusagen die Standard zusammengesetzt Typen sind, die es in der Sprache nun mal gibt. In anderen Sprachen, z.B. C++ oder Swift, wird unterschieden zwischen Struct Types und Class Types, wobei die Struct Types eben genau die Records oder Structures sind, um die es hier gerade geht. Dann gibt es Sprachen, die sowas ähnliches anbieten, nämlich in der Form von Tupeln. Ein Beispiel davon ist O'Cammel. Und das Besondere hier ist, dass die Reihenfolge der einzelnen Felder in dem Record dann nicht mehr wichtig sind, sondern einfach nur gesagt wird, es gibt da einen Tupel, wo diese Endfelder drin sind, aber in welche Reihenfolge, die jetzt gespeichert werden, ist nicht festgelegt. Und dann gibt es auch noch Sprachen, wie z.B. Java, die dieses Konzept von Records oder Structs in der Form gar nicht anbieten, sondern im Prinzip nur Klassen haben. Das heißt, man kann mit Klassen vielleicht sowas ähnliches modellieren, aber Klassen bieten dann außerdem natürlich auch noch andere Dinge, wie z.B. Methoden an. Also das heißt, diese Structures oder Records in der Reihenform gibt es in Java so nicht. Eine interessante Frage bezüglich der Implementierung von Structs oder Records in Programmiersprachen ist, wie die eigentlich im Speicher repräsentiert werden. Der Grund, warum man das gerne verstehen möchte als Programmierer ist, weil das natürlich einen Einfluss darauf hat, wie viel Speicher das Programm schlussendlich braucht und auch darauf, wie effizient es vielleicht ist, auf dieses Struct oder Record, auf die eine oder andere Art zuzugreifen. In den meisten Sprachen wird es so gemacht, dass ein Record im Speicher repräsentiert wird, indem die einzelnen Felder einfach nacheinander, so wie sie in der Sprache oder im Programmcode auch definiert sind, dann im Speicher repräsentiert werden. Das heißt, jedes Feld besteht im Prinzip aus einer Adresse, nämlich der Stadtadresse, wo der ganze Record beginnt, plus einem Offset, also quasi wie viel von dieser Stadtadresse weiter dann das eigentliche bestimmte Feld beginnt. Oft gibt es dann außerdem noch sogenannte Alignment Constraints, die dafür sorgen, dass es eventuell in dieser Speicher repräsentation gewisse Löcher gibt. Und zwar hängen diese Alignment Constraints mit der darunter liegenden Hardware-Architektur ab, die manchmal eben bestimmte Bedingungen stellt, an welchen Stellen denn überhaupt auf Speicher zugreifen werden kann. Zum Beispiel muss bei ich86 die Adressen, an denen ein 4-byte int gespeichert werden kann, immer durch vier Teilen sein. Das heißt, wenn ich meine einzelnen Felder in dem Record jetzt vielleicht eigentlich so hätte, dass das nicht der Fall ist und das vielleicht mein int bei einer Adresse losgehen würde, die eben nicht durch vier Teilen ist, dann muss ich das anpassen bzw. das macht dann die Sprache im Limitierung für mich, indem sie das Record so abspeichert, dass dann ein bisschen Platz gelassen wird, sodass das Integer-Feld dann tatsächlich erst bei einer Adresse losgeht, die durch vier geteilt werden kann. Diese Alignment Constraints hängen wie gesagt von der jeweiligen Architektur, also das ist nicht überall so, wie das, was ich gerade für x86 beschrieben habe, sondern je nach Architektur gibt es bestimmte Constraints für bestimmte Typen und die Sprache im Limitierung muss dann einfach drauf achten, die Records entsprechend im Speicher darzustellen. Schauen wir uns diese Alignment Constraints und die Repräsentation im Speicher mal einem Beispiel an. Und zwar das Beispiel, was wir gerade eben schon gesehen haben, nämlich dieses Struct in C, was ein chemisches Element repräsentiert. Und die Frage ist jetzt, wie das Ganze, wenn wir so ein Struct dann im Speicher haben, tatsächlich dargestellt wird. Und das schauen wir uns hier mal an. Also wir schauen uns das Memory Layout von diesem Struct an. Ich werde das so machen, dass ich einfach so Kästchen zeichne und quasi diese Breite hier eines Kästchens entspricht vier bytes, also 32 bits. So, jetzt haben wir gesagt, die Felder sind in der Reihenfolge im Speicher repräsentiert, wie sie auch tatsächlich im Programmcode aufgeschrieben sind. Das heißt, wir fangen an mit diesem Name Feld, was aus zwei Charakters besteht. Und ein Charakter ist ein byte groß, also ist das quasi die Hälfte von meiner Breite, die ich hier habe, sprich ungefähr das wäre dann sozusagen dieses erste Feld namens Name. Das nächste Feld ist jetzt ein Integer Atomic Number. Und wie ich gerade gesagt hatte, muss bei zum Beispiel x86 so ein Integer immer bei einer Dresse anfangen, die durch vier Teil bei ist. Das heißt, wir können die nicht direkt hinter dem Name Feld anfangen lassen, sondern müssen dann hier erst mal ein Loch lassen. Das repräsentiert einfach mal durch dieses schrafierte Feld. Und dahinter käme dann das, ja, dieser Integer-Wert Atomic Number und da ins vier byte groß sind, würde das quasi einmal hier so quer rübergehen und dann hier so repräsentiert werden. Anschließend kommt dieses Feld Atomic Weight, was vom Typ Double ist. Double, wie der Name schon sagt, ist doppelt so groß wie Integers. Das heißt, wir hätten quasi nicht nur einmal, sondern zweimal diese Größe. Und ja, da das mit den Alignment Constraints so weit alles okay ist, kommt das direkt hinter der Atomic Number. Und jetzt haben wir noch das Einfeld übrig, nämlich unseren Boolean-Wert Metallic. Und obwohl Boolean ja eigentlich nur ein Bit groß ist, wird der in C als ein byte repräsentiert. Das heißt, der wäre dann sozusagen hier zu finden. Und damit das ganze Schluss endlich auch wieder durch, ja, an einer durch vier Teilbarn Adresse beginnen kann, wenn ich mehrere solche Structs hintereinander habe, ist dahinter noch mal so ein kleines Loch, was dann in der Regel unbenutzt bleibt. Wie man jetzt an den Beispiel gesehen hat, kann diese Speicherrepräsentation ja unter Umständen ziemlich ineffizient sein. Kann es einfach, weil relativ große Löcher bleiben. Und im Prinzip für jede Instanz dieses Structs, was ich irgendwo im Speicher habe, doch relativ viel Speicher verschwendet wird. Und die Frage ist, was kann man dagegen machen beziehungsweise, was kann vielleicht der Compiler dagegen machen und wie kann der Compiler erreichen oder der Prokommierer auch erreichen, dass mehr Platz benutzt wird und wir sozusagen den Platz optimal ausnutzen. Da gibt es im Prinzip zwei Optionen, nämlich Packing und die Felder einfach in der anderen Reihenfolge anzuordnen. Diese erste Option Packing bedeutet, dass der Compiler die Lücken soweit es geht, einfach weglässt und die Alignment Constraints, die die Architektur vielleicht hat, ja, vernetzt, was im Speicher natürlich viel effizienter ist, dann aber dazu führt, dass bestimmte Instruktionen, die auf dieser Architektur normalerweise benutzt werden können, dann eben nicht benutzt werden können, weil diese Instruktionen davon ausgehen, dass die Alignment Constraints eingehalten werden. Und stattdessen muss der Compiler, wenn dann Berechnungen auf diesem Struct oder auf diesen bestimmten Feldern des Structs ausgeführt werden sollen, zusätzliche Instruktion generieren, die dann zum Beispiel die Werte einzeln im Speicher betrachten, erst zusammensetzen, dafür vielleicht ein Register verwenden und dann wieder an die Stellen schreiben, wo sie entsprechend unseres zusammengepackten Layouts im Speicher zu finden sind. Das heißt, das kann man machen, aber das nutzt eigentlich diesen Trade-off zwischen Speicher und Zeit, den man sehr oft im Programm hat, in der Form aus, dass der Speicher minimiert wird oder der Bedarf an Speicher minimiert wird, aber dafür dann zur Laufzeit mehr Zeit benutzt werden muss, um die eigentlichen Berechnungen auszuführen. In manchen Sprachen kann man dem Compiler sagen, dass er Packing durchführen soll. Zum Beispiel beim GCC Compiler kann man das mit Hilfe von sogenannten Pragmas machen, was im Prinzip so kleine Anotationen sind, die dem Compiler einen Hinweis geben, dass an der Stelle noch bitte Packing verwendet werden soll. Die zweite Option ist, die Felder in der anderen Reihenfolge anzuordnen. Das könnte einerseits der Programmierer selbst machen, wenn der Programmierer weiß, in welche Reihenfolge oder welche Alignment-Constraints existieren und sich vielleicht ein bisschen Gedanken drüber macht, wie das Struct im Speicher repräsentiert wird, kann man das einfach selbst machen. Oder das könnte theoretisch auch der Compiler machen, indem er einfach die Reihenfolge so optimiert, dass die Lücken, die durch die Alignment-Constraints entstehen, minimiert werden. In C und C++ wird es standardmäßig nicht gemacht. Und zwar aus dem einfachen Grund, dass Programmierer manchmal sich einfach darauf verlassen, dass das Memory Layout ebenso ist, wie die Felder im Code hingeschrieben werden. Das heißt, die üblichen C und C++-Compiler machen das nicht. Aber man könnte sich vorstellen, dass Compiler das machen könnten. Und wenn nicht, kann man das als Programmierer natürlich ganz einfach auch selbst machen. Ja, und zu schauen, wie viel davon jetzt hängen geblieben ist, machen wir mal ein kleines Quiz. Und zwar haben wir hier ein anderes Struct als das, was wir gerade eben schon als Beispiel hatten. Und die Frage ist, wie viel Bytes braucht man dann im Speicher, wenn man einen Array mit fünf Elementen, die jeweils ein Struct von diesem Typ Quiz sind, im Speicher repräsentieren will. Und zwar haben wir eine Reihe von Annahmen. Die eine Annahme ist, dass wir kein Packing verwenden und die anderen Annahmen sind da alle hier links auf der Rechts, auf der Folie aufgeführt, nämlich einfach Annahmen bezüglich der Größe der einzelnen Datentypen, also wie groß ist zum Beispiel ein Character oder ein Int und so weiter. Und auch ein Alignment Constraints, nämlich das Pointers Align sein müssen. Das heißt also, dass Pointers immer an Adressen abgespeichert werden müssen, die durch vier Teil war sind. Und die Frage ist jetzt, wie viel Bytes braucht man? Am besten lösen Sie das, indem wir das wirklich mal aufmalen, so wie wir das gerade eben auch schon gemacht haben und sich Gedanken darüber machen, wie groß denn eigentlich ein Struct ist und das Ganze dann mal fünf rechnen und dann haben Sie auch schon die Lösung. Ich lasse Sie das mal machen und bitte stimmen Sie dann in Ilias ab, um zu überprüfen, dass sie tatsächlich da auch selber sich Gedanken gemacht haben. Und anschließend erkläre ich dann die Lösung. Schauen wir jetzt mal die Lösung an. Und zwar machen wir das so, dass wir einfach hier wieder aufmalen, wie dann dieses Struct oder eins von diesen Structs im Speicher repräsentiert wird. Und wie gerade eben auch schon werde ich einfach diese Breite hier als vier Bytes beziehungsweise 32-Bits annehmen. Also dann geht es los mit dem ersten Element in diesem Struct, nämlich dem Int k. Wir hatten gesagt, Ints sind 32-Bit groß. Das heißt, das steht einfach hier und nimmt einmal diese Breite in der Anspruch. Anschließend kommt Rates. Rates ist ein Array aus drei Floats. Jedes Float ist 32-Bit beziehungsweise vier Bytes groß. Das heißt, wir haben dreimal diese ganze Breite hier. Also das sind die drei Elemente dieses Arrays namens Rates. Anschließend kommt ein weiteres Array, nämlich Name. Das besteht aus sechs Charakters und jedes Charakter ist zwei Bytes groß. Sorry, ein Byte groß. Also haben wir sechs davon. Das heißt, wir brauchen einmal die komplette Breite. Da haben wir schon mal vier und dann noch einmal die Hälfte. Das heißt, wir kommen dann irgendwo hier so raus und benutzen quasi diesen Platz für Name. Und als nächstes kommt jetzt noch und als nächstes und letztes Feld kommt dieser Pointer auf, was wahrscheinlich eine Funktion sein wird, zumindest vom Namen her. Und wir hatten gesagt, dass Pointer acht Bytes groß sind und außerdem allein sein müssen. Das heißt, der Pointer muss bei einer Dresse losgehen, die durch vier Teilbar ist. Das heißt, wir können ihn nicht direkt hier losgehen lassen, denn das wäre eine Dresse, die eben nicht durch vier Teilbar ist, sondern wir verschwenden sozusagen diesen Platz hier hinten. Und der Pointer geht dann hier los und hat wie gesagt acht Bytes Größe. Also Funktionen sozusagen diesen Platz hier in Anspruch. So, wenn wir das Ganze jetzt zusammen zählen, dann sind das 1, 2, 3, 4, 5, 6, 7, 8 Bytes. Das heißt, wir haben acht mal, sorry, acht Zeilen und jede Zeile sind vier Bytes. Das heißt, wir haben acht mal vier Bytes. Also insgesamt 32 Bytes für ein Element in unserem Array. Wir hatten aber gesagt, wir wollen ein Array aus fünf solchen Structs. Das heißt, wir haben also acht mal fünf und sorry, 32 mal fünf und das sind dann insgesamt 160 Bytes. Vielleicht noch so ein kleiner Tipp, wenn man ein bisschen mehr damit noch rumspielen möchte. Man kann in C ganz einfach die Größe eines Structs rausfinden, indem man diesen Size of Operator oder diese Funktionen benutzt, wo ich ganz einfach diesen Typen übergebe, also zum Beispiel Size of Struct von Quiz, diese eine Klammer hier, die sollte da nicht sein. Und was einem das dann zurückgibt, ist einfach die Größe in Bytes, die dieses Struct im Speicher verbrauchen wird. Das heißt, man kann einfach mal mit diesem Beispiel oder anderen Beispielen das Ganze ausprobieren, vielleicht auch mit verschiedenen Compilern, um einfach mal zu sehen, wie groß diese Structs denn tatsächlich im Speicher sind. So was wir bis jetzt gesehen haben, ist, wie wir diese Records einfach definieren. Jetzt kann man natürlich innerhalb eines Records auch noch auf einen anderen Record wieder verweisen. Das heißt, man kann diese Records auch ineinander verschachteln. Und zwar kann man das in der Regel in zwei verschiedenen Art und Weisen machen. Nämlich einmal, indem man es einfach lexikalisch schachtelt und anders der zweite Option ist, dass man dem Record einen Namen gibt und diesen Namen dann innerhalb desselben Records oder eines anderen Records wieder verwendet. Schauen wir uns mal die Option 1 an, also diese lexikalische Verschachtelung. Was wir hier machen, ist, dass wir ein Struct oder Record definieren, nämlich diesen Outer Record. Und in drin definieren wir dann noch ein weiteres Struct und geben dem dann auch wieder ein dem Feld, was durch dieses inneres Struct repräsentiert wird, einen Namen, nämlich in dem Fall Nested Field. Und was hier auffällt, ist, dass das inneres Struct eigentlich keinen Namen ein sich hat, weil wir das auch gar nicht von außerhalb verwenden können, sondern das definiert einfach nur diese Struktur und gibt dem Feld, wo diese Struktur dann auftaucht, einen Namen, aber der Struktur selbst geben wir keinen Namen. Die zweite Option ist, dass wir alle Structs benennen, also sowohl das Innere als auch das Äußere hier, das äußere so wie gerade eben gehabt. Und außerdem haben wir irgendwo anders einen zweiten Struct definiert, was wir Inner Record nennen und verwenden das dann hier, indem wir sagen, es gibt hier einen Feld namens Nested Field vom Typ Struct Inner Record. Im Speicher sehen diese beiden Varianten genau gleich aus, ist einfach nur eine andere Art und Weise, das ganze aufzuschreiben. Eine interessante Frage, die im Zusammenhang ist, was eigentlich die genaue Bedeutung ist von diesen verschachtelten Records. Schauen wir uns mal ein Beispiel an, um das Ganze zu illustrieren. Was wir in dem Beispiel haben, sind zwei Structs, nämlich beide vom Typ Struct S und das eine heißt S1, das andere heißt S2. Anschließend schreiben wir in das eine Struct rein. Ich habe die Structs jetzt hier nicht gegeben, aber man kann sich von der Benutzung im Prinzip ableiten, wie die Struktur in drin aussieht. Und zwar hat dieses Struct ein Feld N und das N ist dann selbst wieder ein Struct, was ein Feld J hat und wir schreiben an S1.n.j diesen Wert 0. So, und was wir anschließend machen ist dieses Struct S1, der Variable S2 zuweisen und dann greifen wir wieder auf ein Feld zu, was wir mit 0.n.j finden und schreiben da 7 rein, aber diesmal eben mit S2 und anschließend geben wir den Wert von S1.n.j aus. Und jetzt ist die große Frage, was gibt das eigentlich aus? Nämlich gibt das jetzt 0 oder 7 aus. Um diese Frage beantworten zu können, müssen wir uns ein bisschen genauer mit damit beschäftigen, was eigentlich bedeutet, wenn eine Variable irgendwo auftaucht. Und zwar gibt es da in Programmiersprachen eigentlich zwei mögliche Antworten, nämlich einmal das sogenannte Reference Model und auf der anderen Seite das sogenannte Value Model. Reference Model bedeutet, dass eine Variable, eine Referenz auf die Speicherstelle meint, an der das entsprechende Speicherobjekt dann abgelegt ist, wo hingegen im Value Model man mit der Variable tatsächlich den Wert meint, der dort gespeichert ist. Und je nachdem, ob die Sprache jetzt das Reference Model oder das Value Model verwendet, wird diese Frage, die wir gerade eben auf der vorigen Folie gesehen haben, eben mit 0 oder mit 7 beantwortet. In C ist es so, dass für alle Variablen, die auf der linken Seite eines Assignments auftreten, also das, wo man reinschreibt, da ist das Reference Model gemeint. Also da meinen wir eine bestimmte Stelle im Speicher, wo hingegen alles, was auf der rechten Seite auftaucht, das Value Model verwendet. Das heißt, da wird tatsächlich der Wert gemeint und eben nicht nur die Adresse, an der dieser Wert gespeichert ist. In Java ist es zum Beispiel anders, denn hier wird das nicht danach unterschieden, ob wir auf der linken oder rechten Seite von einem Assignment sind, sondern es geht danach, was für ein Typ die Variable hat. Und zwar ist es da so, dass für die Built-in Types, also Dinge wie Int oder Boole, das Value Model verwendet wird und für alle anderen wird grundsätzlich immer das Reference Model verwendet. Noch mal zurück zu dem Beispiel, also die Stelle, wo das tatsächlich eine Rolle spielt, ist im Prinzip hier dieses Assignment, wo die Frage eben ist, ob das jetzt, ob das hier sozusagen die Adresse von S1 ist oder der Wert von S1, dann wenn es die Adresse ist, dann würde hier schlussendlich 7 rauskommen. Wenn es aber der Wert ist, dann würden wir sozusagen den kompletten Wert von S1 nach S2 reinkopieren und anschließend diesen Wert hier nur überschreiben, aber der Originalwert von S1 bleibt derselbe und dann käme schlussendlich 0 raus. Bevor wir uns anschauen, ob es 0 oder 7 rauskommt, will ich diese Unterscheidung zwischen Value Model und Reference Model nochmal am Beispiel versuchen, ein bisschen deutlicher zu machen, und zwar indem wir uns einfach zwei Sprachen anschauen, die da unterschiedliche Antworten geben, nämlich zum einen C und zum anderen Java. Und für jede dieser Sprachen werden wir einfach sehr ähnlich Datenstruktur definieren und dann mal schauen, wie die im Speicher genau abgebildet sind. Und zwar haben wir für C hier ein Struct, nennen wir es mal T, indem wir ein Integerfeld namens J und ein anderes Integerfeld namens K haben und dann haben wir ein zweites Struct namens S, wo wir ein weiteres Integerfeld haben namens I und dann eben ein verschachtelter Referenz auf ein verschachtelter Struct und zwar indem ich hier unser Struct S, sorry, T benutze und zwar für das Feld N. Und wenn wir uns das anschauen, wie das Ganze im Speicher repräsentiert wird, indem wir uns mal eine Instanz von diesem Struct S anschauen, dann sieht das so aus, dass wir da als erstes natürlich das I haben und direkt dahinter kommen dann die Felder, die in diesem Struct T drin sind, nämlich das J und anschließend das K. Das heißt, wir haben im Prinzip diese drei Felder hintereinander und das J und das K, die werden jeweils mit noch einem Endpunkt davor natürlich referenziert, ganz einfach, weil das der Name des Feldes ist, an dem dieses Struct T gespeichert ist. Wenn wir das Ganze jetzt in Java ausdrücken wollten, da haben wir ja keine Structs, dann würden wir schon dessen Klassen nehmen und hätten eine Klasse namens T, in der wir dann zum Beispiel ein PublicIntfeld hätten namens J, ein weiteres PublicIntfeld namens K und anschließend noch eine zweite Klasse für S. Hier hätten wir auch wieder das Intfeld namens I und jetzt analog zu diesem verschachtelten Struct, was wir in C hätten, hätten wir ein Feld vom Typ T namens N und interessanterweise wird das Ganze jetzt im Speicher aber anders repräsentiert als in C, denn was wir hier hätten ist zum einen die Repräsentation dieses eines Objekts vom Typ S, in der wir natürlich den Platz für das Feld I hätten und dann auch noch das Feld N, aber N beinhaltet jetzt nicht direkt die Werte I und K, die in der Klasse T gespeichert sind, sondern das ist nur eine Referenz, also ein Pointer auf eine andere Datenstruktur, die irgendwo anders im Speicher liegt, vielleicht gleich dahinter, aber vielleicht auch gleich, vielleicht auch ganz woanders und da sind dann J und K gespeichert und eben weil das in diesen zwei Sprachen unterschiedlich im Speicher repräsentiert wird, wird dieser Code, den wir da gerade eben gesehen haben, auch eine andere Semantik haben, je nachdem ob wir den in C oder in Java ausführen. So hier sehen wir jetzt nochmal den C und Java Code, mit dem wir jetzt diese beiden Structs beziehungsweise Klassen benutzen würden und die Frage ist jetzt, was gibt dieser Code aus? Also den Code auf der linken Seite ist der, den sie gerade eben schon gesehen hatten und der Code auf der rechten Seite ist im Prinzip genau das gleiche, bloß eben jetzt in Java und interessanter Weise kommt eben weil die Sprachen unterschiedliche Regeln haben, ob jetzt ja wann das Value Model und wann das Referenz Model verwendet wird, weil da unterschiedliche Regeln existieren, kommen unterschiedliche Sachen raus, nämlich wird in C Null ausgegeben und in Java 7 und der große Unterschied ist im Prinzip dieses Assignment, was hier beziehungsweise hier stattfindet, weil in C für den Wert auf der rechten Seite des Assignments das Value Model gilt, ist das der komplette Wert all der Dinge, die in S1 gespeichert sind, also im Prinzip dieses komplette Struct inklusive der verschachtelten Felder nj und nk, das wird komplett nach S2, also eine andere Speicherstelle kopiert und deswegen können wir das dann hier auch überschreiben, wenn wir hier einfach was anderes bei S2nj reinschreiben, aber das originale S1nj ist noch derselbe wie vorher und deswegen kommt schlussendlich Null raus. In Java ist das anders, denn wenn wir hier dieses Assignment machen, dann wird für die rechte Seite für das S1 das Referenz Model verwendet, das heißt das ist eine Referenz auf dieses S1 und das führt dazu, dass sowohl S1 als auch S2 auf dieselbe Speicherstelle zeigen und wenn wir dann an dieser Speicherstelle was reinschreiben, also hier in S2.n.j den Wert 7 schreiben, dann überschreiben wir natürlich auch die Speicherstelle auf die S1.n.j zeigt und deswegen bekommen wir dann schlussendlich hier auch die 7 ausgegeben, die wir da neu reingeschrieben haben. So, neben den üblichen Records bzw. Structs, die wir bis jetzt angeschaut haben, gibt es auch noch eine andere Art von Records, nämlich sogenannte Variant Records oder manchmal auch besser bekannt unter dem Namen Union. Die sind im Prinzip eine spezielle Form von den Records, die wir bis jetzt gesehen haben mit einer ganz wichtigen Besonderheit, nämlich dass die gleiche Stelle im Speicher verwendet wird für die verschiedenen Variable bzw. verschiedenen Felder, die in dieser Union oder diesen Variant Record drin sind. Das heißt, ich kann da vielleicht drei Felder haben, aber die werden trotzdem alle an derselben Speicherstelle geschrieben und zwar unter der Ahnung, dass diese Variable bzw. Felder nie gleichzeitig benutzt werden müssen. Das heißt, ich habe da ein Datentyp, bei dem ich im Prinzip sage, der kann das repräsentieren oder das, oder das. Und je nachdem, wie ich den gerade benutze, ist halt mal das eine oder mal das andere drin. Und die Größe dieser Variant Records muss dann jeweils die Größe des größten Feldes sein, die da drin sind, denn sonst würde das natürlich nicht reinpassen, wenn ich diesen Union oder diesen Variant Record mal für dieses größte Feld verwende. Schauen wir uns für das ganz am besten mal ein Beispiel an, weil das Konzept dieser Unions manchmal doch ein bisschen verwirrend ist. Und zwar haben wir hier ein Beispiel in C, eine Sprache, die diese Unions tatsächlich öfters mal verwendet. Und ich definiere hier eine solche Union oder so ein Variant Record namens My Data und sag, dass diese Union drei Felder hat, nämlich ein Integerfeld B, ein Doublefeld D und ein Boolean Feld namens B. So, und dann hier unten verwende ich diese Union, indem ich eine Instanz von erstellen, nämlich Data, und schreibe dann an dieses Feld namens I, was ja ein Integer sein soll, den Wert 23 und gebe anschließend das, was in Datapunkt I drin steht, auch aus. Wenn ich das Ganze jetzt kompiliere und ausführe, bekomme ich 23, also soweit irgendwie alles so, wie man das sicherlich erwarten würde. Jetzt machen wir noch ein bisschen mehr und schreiben, nachdem wir dieses Datapunkt I beschrieben haben, jetzt mal Datapunkt D. Was hier passiert ist, dass wir ja dieses Doublefeld haben, was das Double ist, zweimal so groß ist wie ein Integer und schreiben den Wert 2,5 an die Stelle und anschließend geben wir das aus, was in Datapunkt I drin steht. Wenn das Ganze jetzt ein normaler Record wäre, müsste das jetzt auch wieder 23 ausgeben, weil wir das Feld I ja nicht überschrieben haben, aber weil das Ganze eine Union ist und all diese drei Felder, also sowohl I als auch D als auch B, an derselben Stelle gespeichert werden, überschreiben wir hier in dieser Zeile den Wert, den wir vorher an I geschrieben haben und zwar wird jetzt da einfach das rein, ja im Prinzip die bittweise Repräsentation von dem Floating Point Wert 2,5 reingeschrieben und wenn wir das jetzt auslesen, als wäre es eigentlich ein Integer, der da losgeht. Schauen wir mal, was dann passiert. Dann bekommen wir nämlich 0 ausgegeben und es liegt ganz einfach daran, dass die ersten vier bytes der Repräsentation von diesem Double Wert 2,5 so aussehen wie der Integer Wert 0, wenn man diese ersten vier bytes einfach als Integer interpretiert und genau das ist es, was wir hier machen, denn wir sagen, ließ das, was am Anfang dieser Union steht und geht davon aus, dass das ein Wert unseres Integerfeldes I ist. Um das Ganze nochmal zu zeigen, benutzen wir mal noch das dritte Feld und schreiben jetzt anschließend noch an unser Feld B den Wert 1, also sozusagen der Boolean True und geben dann wieder aus, was in Data.i steht und was jetzt rauskommt ist interessanterweise 1, denn die bittweise Repräsentation dieses Booleans sieht eben aus wie der Integer 1 und weil wir hier ja den Wert lesen, der nun einfach an dieser Speicherstelle steht und das Ganze als Int interpretieren bekommen wir dann 1. Also wichtig, wenn man diese Unions verwendet, immer darauf achten, dass das tatsächlich alles an derselben Stelle im Speicher geschrieben ist und normalerweise muss man das dann natürlich so machen, dass man sich nicht selbst verwirrt, indem man vielleicht das eine Feld reinschreibt und anschließend das andere Feld rausliest, sondern immer nur eins dieser möglichen Felder hier oben tatsächlich pro Zeit benutzt, weil man eben sonst genau dieses etwas verwirrende Verhalten bekommt, was ich hier im Beispiel gerade gezeigt habe. Jetzt mag man sich fragen, wann man diese Unions dann tatsächlich mal verwenden will und es gibt tatsächlich Szenarien, wo es durchaus Sinn macht, auf Unions zurückzugreifen. Also ein Beispiel, was ich hier mal aufgeschrieben habe, ist, wenn bestimmte Bytes einfach je nachdem, in welchem Teil meines Programms ich bin, verschieden interpretiert werden sollen. Also zum Beispiel, wenn ich ein Memory Manager implementiere, also das Stück Software, was dafür sorgt, dass der Speicher verwaltet wird und dann zum Beispiel, wenn ich Melok aufruf, in CME ein Stück Speicher zurückgibt, dann wird so ein Memory Manager üblicherweise dieselben Speicherblöcke sowohl für die eigentlichen Daten, die da der Benutzer oder der Programmierer reinschreiben will, verwenden, als auch für gewisse Ja-Meter-Daten oder so Bookkeeping-Information, also zum Beispiel, wo denn jetzt die nächsten freien Speicherblöcke sind. Das heißt dieselben Bytes werden einfach verschieden benutzt zu verschiedenen Zeiten und da werden Unions Einweg das Ganze dann durch eine Datenstruktur auch abzubilden. Ein anderes Beispiel ist, wenn ich einen bestimmten Datentyp verwenden will, der aber alternativ verschiedene Mengen von Feldern haben kann. Also wenn ich zum Beispiel einen Rackout habe, mit dem ich Angestellte in dem Unternehmen repräsentieren will, dann hängen die Eigenschaften, die diese Angestellten haben, vielleicht davon ab, in welchem Department und in welchem Teil meines Unternehmens die Angestellten tatsächlich sind. Und je nachdem, ob die jetzt in dem einen Department oder in dem anderen sind, kann ich denen dann verschiedene Felder geben. Ich weiß aber genau, dass ich nie beide Felder oder alle Felder benutzen werde, weil jeder Employee eben immer nur in einem Department ist und nie in mehreren gleichzeitig und das ist auch was, was man mit Unions ganz gut abbilden kann. So und damit sind wir auch schon wieder am Ende dieses ersten Teils hier im Modul Composite Types. Also was wir jetzt angeschaut haben, sind Records und wie die in Programmiersprachen angeboten werden und dann schlussendlich auch implementiert werden. Und weiter geht es dann im zweiten Teil mit anderen Composite Types, nämlich Errays. Bis dahin erst mal danke für's Zuhören und bis zum nächsten Mal.