 Ja, herzlich willkommen zurück zu Programmiert-Paradigmen. Wir sind hier im Modul Data Abstraction und Object Orientation und das ist der vierte von fünf Teilen, in dem es um Dynamic Method Binding geht, also der Frage, wie überhaupt entschieden wird, welche Methode denn genau aufgerufen wird, wenn ich eine Methode eines Objektes aufrufe und es da vielleicht mehrere Klassen gibt und vielleicht auch mehrere Definition dieser Methode oder einer Methode mit diesem Namen. Um zu verstehen, was Dynamic Method Binding ist, schauen wir uns einfach mal die zwei möglichen Arten, wie Method Binding in Programmiersprachen üblicherweise funktionieren kann an, nämlich Static und Dynamic Method Binding. In beiden Fällen haben wir Vererbung und eine Unterklasse, in der eine Methode definiert wird oder überschrieben wird, die es auch schon in der Oberklasse gibt. Und die Frage ist jetzt, wenn ich ein Objekt habe, was ein Objekt der Unterklasse sein kann, wie wird dann entschieden, welche Methode jetzt eigentlich aufgerufen wird, nämlich entweder die der Unterklasse oder die der Oberklasse. Und da gibt es im Prinzip zwei mögliche Antworten, nämlich wir können einmal den Typ der Variable anschauen, indem dieses Objekt gespeichert ist und die Methode dann entsprechend danach auswählen oder wir können uns den Typ des Objekts angucken, was in dieser Variable gespeichert ist und die Methode dann entsprechend dieses Types auswählen. Schauen wir uns das Ganze am besten mal anhand eines Beispiels ein bisschen genauer an und zwar ist das etwas Kot in C++, indem wir drei Klassen definieren, nämlich Person, Student und Professor. Diese drei Klassen bieten jeweils eine Methode PrintMailingLabel an, die für diese jeweilige Person irgendeine Art von Label aus druckt und diese Methode wird eben nicht nur von der Person-Klasse angeboten, sondern eben auch von der Student oder von der Professor-Klasse dann entsprechend überschrieben. Was wir hier unten dann haben, sind ein paar Variablen, und zwar zum einen zwei Variablen von den jeweiligen Untertypen, also Student und Professor, die dann auch gleich implizit Objekte initialisieren. Und hier unten haben wir dann zwei weitere Variablen, X und Y, die den Typ Pointer of Person haben, also sozusagen nicht festlegen, ob da jetzt ein Student oder ein Professor dahinter steckt, sondern einfach nur sagen, das ist eine Person. Und wir schreiben dann eine Referenz auf den Studenten, den wir erstellt haben in die Variable X und eine Referenz auf den Professor, den wir erstellt haben in die Variable Y. So, dann rufen wir diese PrintMailingLabel-Methoden auf, und zwar auf zwei Arten und Weisen, einmal hier unten, indem wir sie direkt auf den Variablen der speziellen Unterklassen, also Student und Professor, aufrufen. Und was hier aufgerufen wird, ist in eigentlich einen gängigen Sprachen klar, nämlich die Methode der Unterklassen, also für den ersten Aufruf hier Student PrintMailingLabel und für den zweiten Aufruf dann die Implementierung, die wir hier haben, also Professor PrintMailingLabel. Die Frage ist jetzt, was passiert hier unten, also wenn wir jetzt diese Variable nehmen, die den Typ der Oberklasse, also Person haben, aber eigentlich auf Objekte vom Typ Student und Professor zeigen, wenn wir jetzt diese PrintMailingLabel-Methode aufrufen, ist die Frage, welcher dieser drei Methoden, die wir hier oben definiert haben, wird denn jetzt eigentlich aufgerufen. Die erste mögliche Antwort auf die Frage ist das, was wir bei Static Method Binding bekommen, und zwar ist dieser Antwort, dass die Methode gebunden wird, also ausgewählt wird, mithilfe des Types der Variable. Der große Vorteil davon ist, dass wir das ganze statisch machen können, also wenn das Programm kompiliert wird, wissen wir, zumindest in der statisch typisierten Sprache, den Typen jeder Variable. Das heißt, wir wissen auch zur Compile Time schon, welche Methode da schlussendlich aufgerufen wird. Für unser Beispiel bedeutet das, dass wir für die Aufrufe hier hinten, ich gehe nochmal zurück, die jeweiligen Methoden von der Klasse Person aufrufen, also diese Methode hier oben. Weil diese Variable in x und y die Appointer vom Typ Person sind, das heißt, statisch wissen wir nur, dass sich hinter diesen Variablen eine Instanz von Person versteckt, oder zumindest wissen wir es nicht besser, sagen wir es mal so, und deswegen wird dann auch nur die Methode von Person aufgerufen. Die zweite mögliche Antwort auf die Frage, welche Methode da eigentlich aufgerufen werden, ist Dynamic Method Binding, und hier erfolgt das Binding nicht basierend auf dem Typ der Variable, sondern basierend auf dem Typ des Objekts, was in diese Variable gespeichert ist. Im Allgemeinen kann man den Typen des Objektes nicht zur Compile Time rausfinden, in manchen Fällen schon, aber in vielen Fällen eben auch leider nicht, sondern man weiß erst zur Laufzeit, welcher Typ denn da tatsächlich dahinter steckt. Warum ist eine interessante Frage? Man kann sich zum Beispiel einfach vorstellen, dass irgendwo ein if ist, was bei einer Bedingung in diese Variable vielleicht eine Instanz vom Typ Professor steckt, und wenn die Bedingung nicht war, ist dann eine Instanz vom Typ Student steckt, und anschließend weiß man nicht mehr, was in dieser Variable drin ist, oder zumindest weiß das der Compile dann nicht, wenn er den Code nicht ausführt, sondern den Code einfach nur statisch kompiliert. Weil man das nicht weiß, muss das Method Binding dann zur Laufzeit aufgelöst werden, und deswegen auch der Name Dynamic, und das bedeutet, dass zur Laufzeit geschaut wird, was für ein Objekt steckt in diese Variable, welchen Typ hat dieses Objekt, und damit entsprechend die Methode dieses Types aufgerufen wird. Das heißt für unser Beispiel, gehe ich auch nochmal zurück, wird beim ersten Aufruf bei dem hier geschaut, was ist denn eigentlich der Typ des Objektes, was sich hinter X verbirgt. Da sehen wir, aha, das ist ja eigentlich der Student, und dementsprechend wird dann diese Methode hier oben aufgerufen, denn die passt ja zum Typ Student. Und für den zweiten Aufruf wird geschaut, was ist der Typ, das sich hinter Y verbirgt, also die Variable Y selbst ist ja einfach nur vom Typ Pointe auf Pürsen, aber hinter diesem Y ist dann zur Laufzeit ein Objekt vom Typ Professor, und deswegen wird dann auch diese entsprechende Methode, die in der Professorklasse deklariert ist, aufgerufen. Jetzt ist die Frage, wenn man eine Programmiersprache entwickelt, sollte man Dynamic oder Static Method Binding benutzen und wie so oft gibt es da gewisse Trade-offs und Dinge, die dafür und dagegen sprechen. Static Method Binding hat den großen Vorteil, dass man keine Performance Penalty dadurch zahlen muss, dass zur Laufzeit erst aufgelöst werden muss, welche Methode jetzt aufgerufen wird. Denn der Compiler weiß jetzt zur Zeit des Kompilierens schon, was der Typ jeder Variable ist und weiß deswegen genau, dass an dieser Code Stelle diese eine Methode aufgerufen werden muss. Und der große Nachteil dementsprechend beim Dynamic Method Binding ist, dass wir das eben zur Laufzeit bezahlen, denn da wird einfach Code ausgeführt, der eben dann nachschaut, hey, was ist eigentlich der Typ, der sich hinter dieser Variable verbirgt und dann erst wird entschieden, welche Methode aufgerufen wird. Der Nachteil vom Static Method Binding ist, dass eine Unterklasse ihren eigenen Zustand nicht vollständig kontrollieren kann. Die Unterklasse hat vielleicht eine Methode, die, wenn, ja, so überschreibende Methode der Oberklasse, die bestimmte Updates des Zustandes, also der Felder vornehmen möchte. Und jetzt kann es aber passieren, dass diese Unterklassensinstanz durch über eine Variable der Oberklasse verwendet wird. Und dann wird aber nicht die überschriebene Methode der Unterklasse aufgerufen, sondern die Original Methode der Oberklasse und die Updates, die ich auf dem aktuellen Zustand machen möchte, werden dann einfach nicht gemacht. Das heißt, die Unterklasse kann nicht vollständig kontrollieren, wie ihr eigener Zustand aussieht und entsprechend aktualisiert wird. Wohin gegen wir das mit Dynamic Method Binding natürlich machen können, weil da immer die Methode, die in der Unterklasse überschrieben wurde, aufgerufen wird. Zur Verteutlichung des Problems, was wir, wenn wir Static Method Binding haben, dann manchmal bekommen können, schauen wir uns mal diese zwei C++-Klassen an, die wir haben. Also die Oberklasse beschreibt ein Textfile, das hat einen Namen und während ich jetzt dieses Textfile lese, habe ich eine Position, die mir einfach angeht, wie weit ich im Lesen schon gekommen bin. Und dann habe ich hier diese Methode Seq, die den File-Pointer, also sozusagen die Position, an der ich mich aktuell befinde, auf einen bestimmten Offset setzt. Also quasi sagt Lies als nächstes dort weiter. Dann habe ich eine Unterklasse dieser Klasse, die heißt Read-Ahead Textfile und wie der Name schon suggeriert, liest die bestimmte Zeichen eben schon im Voraus, um dann, wenn der Klein dieser Klasse die Zeichen tatsächlich lesen möchte, schnell antworten zu können und nicht dann erst auf das Datei-System zugreifen muss. Und um das tun zu können, fügt die Klasse hier so ein Feld-Upcoming-Characters hinzu, in dem einfach eine gewisse Anzahl von Zeichen schon im Voraus gelesen wird, so dass man, wenn man dann tatsächlich die nächsten Zeichen liest, das schneller machen kann, als wenn wir das wie in der Oberklasse in dem Moment erst vom Datei-System lesen würden. Und um diese Upcoming-Characters natürlich auch immer aktuell zu halten, müssen wir in der Unterklasse diese Methode Seq überschreiben. Und zwar dadurch, dass wir dann, wenn wir jetzt an ein bestimmtes Offset springen, dann die entsprechenden Zeichen, die ab diesem Offset kommen, schon voreines lesen, so dass man die dann gleich parat hat. Also was die Unterklasse hier machen muss, ist eben diese Upcoming-Characters zu überschreiben. Das Problem ist jetzt aber, dass wenn wir Static Method Binding haben, wir nicht wissen, ob Seq tatsächlich aufgerufen wird. Also wenn wir eine Instanz von Read-Ahead-Text-Fall irgendwo haben, die uns aber in eine Variable vom Typ TextFall gespeichert ist und dann jemand Seq auf dieser Variable aufruft, dann wird mit Static Method Binding eben das Seq von TextFall aufgerufen und nicht das Seq von Read-Ahead-Text-Fall. Das heißt, unsere Upcoming-Characters werden dann einfach nicht aktualisiert und die Klasse kann sozusagen nicht wirklich das tun, was sie an der Stelle machen möchte. So, jetzt kennen Sie diese zwei Arten, wie die Method Binding Frage beantwortet werden kann, nämlich Static Method Binding und Dynamic Method Binding. Schauen wir uns jetzt mal inwiefern, dass tatsächlich in populären Programmiersprachen auftritt. Und was wir hier sehen, ist, dass es im Prinzip eine ganze Bandbreite gibt, die tatsächlich in verschiedenen Sprachen auch umgesetzt werden kann. Also auf dem einen Ende des Spektrums haben wir die Sprachen, in denen Dynamic Method Binding für alle Methoden einfach immer gilt. Das ist zum Beispiel der Fall in Smalltalk, Python oder Ruby. Das heißt, wenn ich in einer Unterklasse eine Methode überschreibe, dann wird die immer dynamisch aufgelöst. Und es ist garantiert, dass wenn ich eine Instanz der Unterklasse habe, dann tatsächlich auch deren Methode aufgerufen wird. In manchen Sprachen, zum Beispiel Java oder Eiffel, ist Dynamic Method Binding das, was bei Default passiert. Allerdings kann ich Methoden oder auch Klassen markieren, so dass sie nicht überschrieben werden können. Also in Java kann ich das zum Beispiel mit dem Final Keyword machen. Und das führt dazu, dass diese Methoden dann nicht in der Unterklasse überschrieben werden können. Und ich dementsprechend den Methodenaufruf dann statisch auflösen kann, denn ich weiß, es wird keine überschreibende Methode in einer der Unterklassen geben und muss deswegen auch nicht bis zur Laufzeit warten, um herauszufinden, zu können, welche Methode aufgerufen werden soll, sondern kann das zur Compile Time machen, was das Programm oder Umständen effizienter macht. Die dritte Variante, die wir auch in manchen Sprachen haben, ist, dass Static Binding bei Default gemacht wird und der Programmierer explizit spezifizieren muss, wenn er Dynamic Method Binding möchte. Schauen wir uns erstmal noch zwei Beispiele für diesen mittleren Fall an. Das ist da, wo bestimmte Methoden als nicht überschreibbar oder auch manche Klassen als nicht erweiterbar deklariert werden können. Das ist der Fall zum Beispiel in Java und Eiffel und zwar mit Hilfe dieser zwei Keywords Final in Java, was sowohl für Klassen als auch für Methoden gelten kann und mit dem ich angeben kann, dass diese Klasse nicht erweitert werden kann oder dass diese bestimmte Methode nicht überschrieben werden kann. Und wie gesagt, wenn das der Fall ist, kann ich dann für Aufrufe dieser einer finalen Methode zum Beispiel statisch rausfinden, welche Implementierung der Methode benutzt wird, denn ich weiß, es wird keine Untertypen geben, Unterklassen geben, die diese Methode dann überschreiben. In Eiffel geht es so ähnlich und zwar mit dem Frozen Keyword, was für bestimmte Methoden erlaubt zu sagen, dass die nicht überschrieben werden können und dann analog zu Java da einfach klar ist, dass es keine Unterklasse gibt, die diese eine Methode überschreiben kann. In C++ und Csharp befinden wir uns ein bisschen mehr auf der linken Seite dieses Spektrums, was ich vorhin gezeigt habe und zwar für bestimmte Fälle, denn es gibt hier im Prinzip zwei Arten, die auf die eine Unterklasse die Methode, die Methode hinzufügen kann bzw. Methode überschreiben kann, nämlich zum einen das Overriding und zum anderen das Redefining. Also Overriding ist das, was der Name sagt, also die Unterklasse definiert eine Methode und überschreibt damit die Definition dieser Methode, die in der Oberklasse existiert. Und wenn wir Overriding in C++ oder Csharp tatsächlich verwenden, dann haben wir auch Dynamic Binding, dann wird dynamisch aufgelöst, welche Methode aufgerufen wird und es ist garantiert, dass dann auch die Methode der entsprechenden Unterklasse aufgerufen wird. Wenn wir allerdings Redefining benutzen, das heißt, wenn wir zwar eine Methode definieren, die denselben Namen hat, die aber nicht wirklich die Methode der Oberklasse überschreibt, dann wird Static Binding verwendet. Das heißt, in dem Fall wird einfach nur nach dem Typen der Variable geschaut und dementsprechend dann die Methode aufgerufen, was dann auch die Methode der Oberklasse sein kann, die vielleicht ein Objekt der Unterklasse vor uns haben. Wie genau wird unterscheiden, unterschieden jetzt zwischen Overriding und Redefining. Also in C++ hat man das ja schon ein bisschen gesehen, im letzten Teil, nämlich in der Form, dass eine Methode in der Superklasse mit dem Virtual Keyword markiert werden muss und nur dann kann sie in der Unterklasse tatsächlich überschrieben werden. In C++ ist es ähnlich aber irgendwie auch wieder anders, denn hier muss die Unterklasse explizit angeben, dass sie tatsächlich die Methode der Oberklasse überschreibt und nicht nur eine andere Methode mit dem selben Namen auch definiert. Und zwar wird hierfür das Override Keyword verwendet, was explizit macht, dass die Unterklasse tatsächlich die Methode der Oberklasse überschreibt. Man muss ein bisschen aufpassen, in Java gibt es ja auch Override als Anotation, was allerdings optional ist und quasi nur als Hilfe gedacht ist, um zu sagen, ich möchte jetzt die Methode der Oberklasse überschreiben, aber wenn ich es nicht hinschreibe, bekomme ich trotzdem noch Dynamic Method Binding, wohin ging ich in C++ Override tatsächlich hinschreiben muss, denn ansonsten überschreibe ich eben nicht die Methode der Oberklasse, sondern definiert daneben einfach nur eine andere Methode, die zufällig den selben Namen hat. Zur Verdeutigung schauen wir uns einfach nochmal das Beispiel an, was wir in einem früheren Teil ja schon gesehen haben, indem wir diese zwei C++-Klassen A und B hatten, wo die Klasse A eine Methode V anbietet, die dann in Methode B auch angeboten wird, und zwar so, wie es jetzt hier gerade steht, eben nicht überschrieben wird, sondern nur redefiniert wird. Und hier unten in der Main Methode haben wir dann im Prinzip genau dieses Problem, wo die Frage ist, ob wir jetzt Dynamic oder Static Method Binding verwenden, denn wir erstellen ein Pointer vom Typ A, eine Variable, die Pointer von Typ A ist und haben dann hier eine Instanz unserer Klasse B, schreiben diese Instanz dann in die Variable A und rufen dann FU auf. Das heißt, wenn wir an der Stelle Static Method Binding verwenden, wird das FU von A aufgerufen, das hier oben, denn der statische Typ der Variable A ist ja die Klasse A. Wenn wir allerdings Dynamic Method Binding verwenden, dann wird hier unten das FU von B aufgerufen, denn hinter dieser Variable, die statisch gesehen den Typ A hat, steckt hier eigentlich eine Instanz von B und deswegen würde mit Dynamic Method Binding tatsächlich dieses FU von B aufgerufen. Wenn wir das Ganze jetzt so haben, wie es hier steht, kompilieren wir es mal, dann wird A.FU aufgerufen, denn diese Methode ist hier oben ja nicht als virtual deklariert, das heißt, hier unten wird nicht überschrieben, sondern einfach nur eine weitere Methode mit demselben Namen definiert. Das heißt, wir benutzen Static Method Binding und rufen deswegen auch nur die Methode von A auf. Wenn ich jetzt hingegen hier oben virtual davor schreibe, dann wird diese Methode hier unten tatsächlich überschrieben und das heißt, dieser Aufruf wird dann mit Dynamic Method Binding aufgelöst und wir würden in dem Fall dann das FU von B aufgerufen. Schauen wir mal an, ob das wirklich so ist. Siehe da, in dem Fall kommt dann B.FU raus. Das heißt also in C++ muss man wirklich unterscheiden zwischen Overriding und Redefining, denn das entscheidet darüber, welche Art von Method Binding tatsächlich benutzt wird. So, diese ganze Frage, ob wir jetzt Dynamic Method Binding oder Static Method Binding verwenden, spielt natürlich immer nur dann eine Rolle, wenn die Methode sowohl in der Superklasse als auch in der Superklasse definiert ist. Jetzt kann man in vielen Sprachen das Ganze auch vermeiden, indem man abstrakte Methoden oder geabstrakte Klassen definiert, in denen die eine bestimmte Methode sozusagen nicht implementiert ist und wir aber explizit angeben, dass eine Subklasse diese Methode dann anbieten muss und implementieren muss. Wenn ich eine abstrakte Methode habe, wenn ich eine abstrakte Klasse habe, dann sage ich, dass die ganze Klasse abstrakt ist, was bedeutet, dass mindestens eine abstrakte Methode darin zu finden ist und man diese Klasse so noch nicht instantiieren kann, denn diese abstrakte Methode muss erst implementiert werden, also das heißt, man muss erst eine Unterklasse der Klasse erstellen und kann diese dann instantiieren, sofern in ihr dann auch die abstrakte Methode definiert wurde. Schauen wir uns dazu am Westen mal wieder zwei Beispiele an, und zwar in Java und dann auch noch in C++, wo wir jeweils eine Klasse A haben, in der es eine abstrakte Methode gibt, in dem Fall FU, und deswegen ist dann auch die Klasse abstrakt, und dann haben wir eine Klasse B, die die Klasse A erweitert, oder von ihr erbt und diese Methode dann aber implementiert. Da passiert zwar hier nichts in dem Method Body, aber da ist zumindest eine Implementierung da. Und was wir hier jetzt versuchen können zu machen, ist, dass wir eine Instanz von A erstellen und wir werden aber sehen, dass das nicht funktioniert, denn A ist ja eine abstrakte Klasse. Wenn ich jetzt aber stattdessen eine Instanz von B erstellen würde, dann sollte das funktionieren, denn B hat alle Methoden konkretisiert und alle Methoden können dann tatsächlich aufgerufen werden, inklusive dieser Methode FU. In C++ sieht das Ganze so ähnlich aus, also da haben wir auch wieder eine Klasse A, die eine Methode FU hat und um anzuzeigen, dass diese Methode FU hier eben nicht implementiert ist, also sozusagen abstrakt ist, eizialisieren wir diese Methode mit ist gleich null, ein bisschen ungewöhnliche Syntax, aber das bedeutet im Prinzip einfach, diese Methode gibt es jetzt so noch nicht und sie muss stattdessen in einer Unterklasse dann konkretisiert werden und das passiert dann hier, wo wir A erweitern in Klasse B und dann tatsächlich auch die Implementierung von FU zur Verfügung stellen und wenn wir dann ein B-Objekt erstellen, könnten wir diese Methode FU dann auch aufrufen. So, um zu schauen, ob diese Idee von Dynamic Method Binding und Static Method Binding so halbwegs klar ist, machen wir mal ein kleines Quiz und zwar wie üblich über Ilias. Also was Sie hier sehen, ist ein Stück Code in irgendeiner Pseudosprache, also das ist jetzt weder Java noch Python noch C++, die Syntax sieht so ein bisschen nach Python aus, aber ja eigentlich auch ein bisschen nach Java, ist einfach eine Pseudosprache und die Frage ist, welche Methoden werden denn hier unten aufgerufen, wo wir diese zwei Aufrufe von bar haben, wenn die Sprache eben entweder nur Dynamic Method Binding benutzt oder immer Static Method Binding benutzt oder Static Method Binding benutzt, außer die überschreibende Methode ist mit override markiert. Ja, wird Sie bitten wie immer, das Video hier kurz anzuhalten, kurz darüber nachzudenken, dann Ilias abzustimmen und anschließend dann erst die Lösung anzuschauen. So, schauen wir die Lösung mal an, also in Variante A, also einer Programmiersprache, wo immer Dynamic Method Binding verwendet wird, würden beide diese Aufrufe die bar Methode von B aufrufen, denn hinter Variabler X und Y verbirgt sich in beiden Fällen dieses eine Objekt vom Typ B, und wenn also die Methodenauflösung basierend auf dem Typ das Objekt gemacht wird, kommen wir dann bei der bar Methode von B raus. Wenn wir hingegen die Sprache haben, die grundsätzlich immer Static Method Binding verwendet, dann würde der erste Aufruf in der Klasse A rauskommen und A As Methode bar aufrufen, denn die Variable X, die wir hier unten haben, hat hier als statisch deklarierten Typ A, das heißt aus statischer Sicht ist das ein A und somit würde dieser Aufruf dann an die bar Methode von A gehen. Der zweite Aufruf hingegen ist auf einer Variable vom Typ B und deswegen geht der zweite Aufruf dann zu der bar Variante von B. Und dann schlussendlich die dritte Programmiersprache, die wir hier haben, nämlich eine, die bei default Static Method Binding verwendet, außer die überschreibende Methode ist mit override markiert, da kämen wir selber raus wie in Variante B. Denn die überschreibende Methode, also dieses bar hier unten, ist ja nicht mit override markiert und deswegen passiert das Gleiche. Wenn jetzt hier davor noch override stehen würde, kämen wir dann derselbe raus wie in Variante A. So jetzt wissen Sie, was Dynamic and Static Method Binding eigentlich ist und wie die Sprachen, die man so kennt, das Ganze umsetzen. Eine Frage, die jetzt vielleicht noch offen ist, ist, wie das Ganze eigentlich implementiert wird. Also woher weiß das Programm, wenn es dann einmal kompiliert ist, denn eigentlich welche Methode es dann tatsächlich aufrufen muss und wie funktioniert das Ganze, wenn wir Verärbung haben. Also wie wird da sichergestellt, dass in dem kompilierten Code dann tatsächlich die richtigen Methode jeweils aufgerufen wird. Um das ein bisschen zu verstehen, schauen wir uns das jetzt mal an und die am häufigsten verwendete Methode, um das Ganze zu machen, sind sogenannte Virtual Method Tables oder auch kurz V-Tables. Was die im Prinzip machen ist, dass die alle Methoden, die eine Klasse anbietet, auflistet. Also eine Tabelle, in der einfach diese ganzen Methoden drinstehen und zwar so, dass jeder Eintrag eine Methode ist und jeder Eintrag ist dann wiederum ein Pointer auf ein Stück Code, dann die Implementierung der Methode zu finden ist. Und was die einzelnen Objekte, die es jetzt von einer Klasse gibt, dann jeweils machen müssen, ist einfach so ein Pointer auf die entsprechende V-Table zu haben, wo sozusagen alle Methoden, die diese Klasse anbietet, dann aufgelistet sind. Schauen wir uns das Ganze mal im konkreten Beispiel an und zwar mit Hilfe einer C++-Klasse namens FU, die wie folgt definiert ist. Ein paar Felder, ein Integer-Feld vom Typ A, ein Double-Feld namens D und dann ein Feld namens C vom Typ Character. Und dann haben wir auch noch eine Reihe von Methoden, die Public sind. Das gebe ich in C++ durch diese Public-Section an und wir gehen davon aus, dass all diese Methoden virtual sind. Das heißt, sie können von Unterklassen überschrieben werden, wenn die Unterklassen das wollen. Zwei gibt es hier eine Methode namens K, eine weitere, ups, die nimmt einen, gibt einen Int zurück namens L, dann noch eine dritte Methode, die ist wieder void und heißt M. Und all diese Methoden sind natürlich hier schon in der Klasse FU definiert. Schreibt die jeweiligen Bodies der Methode dann nicht hin, aber die gibt es hier natürlich, also hier in diesem Punkt, und das war auch schon die Klasse und dann erstellen wir gleich von der Klasse auch noch eine Instanz, indem wir hinter dieser Deklaration der Klasse auch gleich noch einen Variablenamen hinsetzen. Das können wir auch in zwei Schritten machen, aber hier ist es jetzt in einem. So, die Frage ist jetzt, wie sieht eigentlich der Zustand des Speichers aus, den wir bekommen, wenn wir jetzt anschauen, wie dieses Objekt F der Klasse FU im Speicher repräsentiert ist. Also F sieht dem Speicher wie folgt aus, wir haben irgendwo ein Stück Speicher, wo dieses F dann drin gespeichert ist, und das sieht so aus, dass wir zum einen die Felder von F da haben, also das Feld A, das Feld B und das Feld C. C ist, weil es ja nur vom Typ Charakter ist, kürzer, also wir haben hier sozusagen noch ein bisschen Speicher übrig, woin gegen Int und Double hier zum Beispiel 32-Bit groß wären. Und dann haben wir hier am Anfang von F noch was anderes, und das ist einfach ein Pointer auf diese V-Table, also diese Virtual Method Table, in der dann alle Methoden von FU, die dann also auch dem Objekt F zur Verfügung stehen, aufgelistet sind. Und das ist auch einfach wieder nur ein Stück Speicher, in dem Pointer auf die jeweiligen Implementierung der Methoden drin stecken. Und ich schreibe hier Fus V-Table und nicht Fs V-Table, weil eben diese V-Table geteilt wird von allen Instanzen der Klasse FU, also wenn wir noch eine zweite Variable F2 hätten, dann würde die auch ein Pointer auf dieselbe V-Table haben. Und hier drin sind dann die Pointer auf die jeweiligen Implementierung, also zum Beispiel für Fus Methode K, hätten wir hier dann den entsprechenden Code Pointer, der dann auf ein anderes Stück Speicher zeigt, in dem dann tatsächlich die Instruktionen drin sind, die diese Methode implementieren und genauso für die anderen Methoden, die es hier noch gibt, die dann auch entsprechende Code Pointer auf die jeweiligen Code Stellen hätten. So, jetzt wissen wir, wie das Ganze im Speicher aussieht. Jetzt müssen wir uns noch anschauen, was passiert, wenn wir jetzt eine Methode auf unsere Variable F aufrufen. Und genau wollen wir da sehen, welcher Code denn hier vom Compiler dann tatsächlich dafür generiert wird. Und zwar gehen wir mal davon aus, dass wir einen Aufruf haben, in dem Dynamic Method Binding verwendet wird. Und das ist hier der Fall, die Methoden ja virtual sind. Und zwar rufen wir in dem Fall zum Beispiel f.m auf. So, was passiert da? Ich schreibe das Ganze jetzt in so Pseudo-Instruktionscode hin. Tatsächlich generiert der Compiler natürlich Code für die entsprechende Maschine, also zum Beispiel x86 Assembler, aber ich benutze jetzt mal so ein Pseudo-Low-Level-Assembly-Code. Also wir würden zunächst erstmal die Adresse von f lesen und die in ein Register zum Beispiel r1 schreiben. Anschließend brauchen wir die V-Table, die sich am Anfang von diesem Stück Speicher befindet. Und diese Adresse der V-Table lesen wir, indem wir dieses r1 dann dereferenzieren. Also was wir hier bekommen, ist dann schlussendlich die Adresse der V-Table. Und jetzt müssen wir die richtige Methode rauspicken und die Adresse dieser Methode schreiben wir dann auch wieder nach r2. So, und es funktioniert jetzt so, wir schauen an, wo geht die V-Table los und addieren dann den richtigen Offset, um bei Methode m rauszukommen dazu. Methode m ist die dritte Methode, weil die Indizierung bei 0 anfängt, machen wir minus 1 und dann multiplizieren wir das Ganze noch mal 4 unter der Annahme, dass die Adressen jeweils 4 bytes groß sind. So, und was wir dann rausbekommen, ist die Byte Adresse, an der tatsächlich fm implementiert ist und wenn wir das dann dereferenzieren, haben wir also die entsprechende Adresse von fm und das können wir dann aufrufen, indem wir jetzt diesen Code Point dann nochmal dereferenzieren und das Ganze dann an die Call Instruktion übergeben. So, jetzt wissen Sie also, wie die Instanz dieser FU-Klasse dargestellt wird und wie dann der Methodenaufruf einer Methode dieser Klasse geschieht. Spannend wird es jetzt, wenn wir auch noch Vererbung mit dabei haben, denn was wir nun wollen, ist, dass die Art und Weise, wie die Instanzen von Unterklassen repräsentiert werden, kompatibel sind mit der Art und Weise, wie die Klassen repräsentiert werden. Und der Grund ist ganz einfach, dass wir nicht jedes Mal, wenn wir eine neue Unterklasse, irgendwo erstellen, jeden Kleincode neu kompilieren müssen, was wir müssten, wenn diese beiden Repräsentationen nicht miteinander kompatibel werden. In C++ oder in vielen anderen Sprachen ist es ja so, dass ich den Kleincode vielleicht einmal kompilieren kann und später dann Unterklassen noch hinzufüge und trotzdem muss ich den Kleincode dann nicht nochmal kompilieren und wie das Ganze funktioniert, schauen wir uns jetzt gleich noch am Beispiel wieder an. So, in dem erweiterten Beispiel erstellen wir jetzt also noch eine zweite Klasse, nennen wir sie mal BA, die unsere Klasse von gerade eben, nämlich die Klasse FU, erweitert und zwar über Public Inheritance. Und was BA jetzt macht, ist Folgendes. Also zunächst fügen wir ein weiteres Feld hinzu, nämlich das Integerfeld W und dann fügen wir auch noch weitere Methoden hinzu, also wieder in dieser Public Section, nämlich zum einen überschreiben wir eine der Methoden, die in Fugchen angeboten wurden, diese Methode M, also das ist Overriding, und dann definieren wir auch noch zwei neue Methoden, die es in der Oberklasse nicht gab, die machen wir auch wieder Virtual, sodass sie von möglichen Unterklassen von BA dann auch wieder erweitert werden könnten und die nennen wir S und auch noch T. Nachdem wir die Klasse definiert haben, erstellen wir auch gleich wieder eine Instanz davon und die heißt dann BA. Schauen wir uns an, wie das BA jetzt im Speicher aussieht, also so ähnlich wie gerade eben schon beim F, gibt es da irgendwo ein Stück Speicher, in dem jetzt dieses Objekt drin steht und der große Unterschied wird jetzt sein, dass wir eben zusätzlich zu den Feldern von FU, hier auch noch die entsprechenden Felder von BA hinzufügen müssen und zwar machen wir das so, dass die Reihenfolge der Felder, die von FU geerbt wurden, genau dieselbe ist wie in FU selbst, also wir haben wieder A und B und dann C, was wieder ein bisschen kürzer ist, weil es ja nur ein Character ist und dann zusätzlich haben wir noch dieses Extrafeld W, was die Unterklasse BA hinzufügt und was wichtig ist jetzt hier, ist dass die Reihenfolge der Felder zumindest der Anfang, also der Prefix der Felder ist derselbe wie in der Implementierung oder in der Speicherepräsentation von dem Objekt F, was wir gerade eben gesehen haben. Gehe hier nochmal zurück, also das ging auch los mit A, B, C und hier ist es auch so, das heißt also, wenn jemand eine Instanz von FU oder von BA hat, kann der Klein immer auf A, B und C zugreifen und egal, ob das jetzt eine Instanz von FU oder BA ist, wird das entsprechende Feld dann am selben Offset sein. Das heißt, der Klein muss nicht wissen, ob das jetzt eine Instanz von FU oder von BA ist. Neben den Feldern haben wir natürlich auch die Methoden und auch da gibt es wieder eine V-Table und zwar ist die genauso wie gerade eben schon wieder auf Klassenebene definiert, also wir haben eine V-Table für alle Instanzen von BA und hier verfolgen wir eigentlich wieder genau dieselbe Idee, wie gerade eben schon, also es wird so gemacht, dass das Layout dieser V-Table ist mit dem Layout, was wir in der Oberklasse FU haben. Das heißt also, schau nochmal zurück, in FU hatten wir K, L, M und N und brauchen also jetzt genau diese selben oder am selben Offset dann wieder die jeweilige Methode, die aufgerufen werden soll, wenn bei einer Instanz von BA die Methode K, L, M und N aufgerufen wird. Schauen wir mal an mit K, K wurde nicht überschrieben, das heißt, wir rufen weiterhin den Kot von FU auf, also da ist eine Adresse, die dann sozusagen auf die alte Implementierung genau dieselbe Implementierung, die wir hier auch schon hatten, wieder zeigt. Das gleiche für L, denn auch L wird nicht überschrieben, spannender wird es jetzt bei BA, M, denn hier hat M diese Methode ja überschrieben und deswegen zeigt der Kot Punkte dann eben nicht auf die Implementierung die wir hier drüben haben, sondern zeigt auf eine andere Implementierung, nämlich die der überschriebenen Methode und für die letzte geerbte Methode nämlich N bleibt alles beim Alten, also das kommt auch wieder hier an vörter Stelle und der Pointer wird auf genau dasselbe zeigen, wie in der V-Table von FU. Also jetzt haben wir die Methoden, die geerbt sind, alle drin inklusive der einen, die wir überschreiben und was wir jetzt noch hinzufügen müssen sind die neuen Methoden, nämlich S was dann hinten dran gefügt wird und genauso T die dann jeweils auch wieder Kot-Pointer haben auf die entsprechenden Implementierung dieser neuen Methode. Was jetzt wichtig ist an dieser V-Table ist, dass die der Prefix aller Methoden, die auch schon in FU vorhanden sind, genau derselbe ist wie in der V-Table die wir hier gesehen haben, die für die klasse FU war. Das heißt also ein Kleint kann jetzt unabhängig davon ob das eine Instanz von FU oder von BAS an einen bestimmten Offset zum Beispiel die Methode M finden, aber weil wir den Kot-Pointer, der schlussendlich in der V-Table drin steht, jetzt auf BAS Implementierung von M gesetzt haben hier, wird dann, obwohl wir den selben Kleinkot haben, also obwohl wir am selben Offset einfach die Methode aufrufen, dann die entsprechende Methode der Unterklasse, also BAM, aufgerufen. Zum Abschluss des kleinen Beispiels schauen wir uns mal noch ein bisschen Kleinkot an. Also was wir hier haben ist noch mal ganz kurz gezeigt die 2 Implementierung von FU und BAS, die wir gerade schon gesehen haben. Dann erstellen wir 2 Variablen von die auch gleich diese Klassen instantiieren und haben außerdem dann noch 2 Variablen, die jeweils ein Pointer auf den Typ FU oder ein Pointer auf den Typen BAS darstellen. Und jetzt versuchen wir hier 2 Assignments, nämlich zum 1 dieses, was die Adresse unseres Bar-Objektes nimmt und in Q reinschreibt. Q ist vom Typ Pointer auf FU, also dem Obertypen und das ist okay, das können wir machen, denn dieser Pointer vom Obertyp kann natürlich auch auf ein Objekt im Speicher zeigen, was vom Untertyp ist, denn die Repräsentation von B hat genau dieselben den selben Prefix, sowohl für die Felder als auch für die V-Table wie eine Repräsentation von der Klasse Bar und also wie eine Repräsentation von der Klasse FU und damit können wir quasi, obwohl wir hier diesen Pointer vom Typ FU verwenden auch die Instanz von Bar korrekt verwenden. Was jetzt nicht geht, ist dieses 2. Assignment, also das würde uns vom Compiler auch gleich einen Typ Fehler geben, wo wir versuchen, diese Adresse von unserem FU-Objekt, also dem Oberklassenobjekt an diesem Pointer S zu schreiben, der aber sagt, er zeigt auf ein Bar-Objekt, denn wenn wir es jetzt hier verwenden würden und würden versuchen auf Felder, die nur Bar hat, aber die FU nicht hat zuzugreifen, dann wäre eine Speicherrepräsentation des Objekts beziehungsweise wenn wir mit einem Tode aufrufen, dann auch in der V-Table eben dieses Feld oder diese Methode nicht vorhanden und um diesen Fehler dann gleich zu verhindern, bekommen wir hier einfach einen statischen Typfehler, der uns sagt, wir können diese Zuordnung so nicht machen. Ja, und damit sind wir auch schon wieder am Ende dieses 4. von 5 Teilen zum Thema Data Abstraction and Object Orientation. Es ging hier um Dynamic Method Binding, also der Frage aufgelöst wird, welche Methode aufgerufen wird, wenn es nicht nur eine Klasse gibt, sondern auch noch eine Unterklasse, die diese Methode vielleicht überschreibt und wir haben gesehen, dass verschiedene Sprachen da verschiedene Antworten geben und Sie haben jetzt auch gerade ein bisschen gesehen, wie das denn überhaupt implementiert wird, also was für Code der Compiler dann dafür generieren würde. Ich hoffe, Sie haben es dazu gelernt. Vielen Dank für's Zuhören und bis zum nächsten Mal.