Strukturierte Datentypen - Überblick Modula-2-Typen und benutzerdefinierte Typen elementare Typen Aufzählungstypen (enumeration types) INTEGER Ampel = (rot,gelb,gruen); REAL CARDINAL Tag = (Mo,Di,Mi,Do,Fr,Sa,So); CHAR ... BOOLEAN = (FALSE,TRUE); Strukturierte Typen Bereichstypen (range types) VektorTyp=[1..10] OF INTEGER; Wochenende = [Sa..So]; Matrix=ARRAY[1..4],[1..10] OF INTEGER; KleineZahlen = [0..100]; Datum = RECORD Tag :INTEGER Monat:INTEGER; Jahr :INTEGER; END; Stop =[rot..gelb]; All diese Typen können bei der Definition strukturierter Datentypen verwendet werden. Praktische Informatik 1 R.Zicari / JWG-Universität Frankfurt Kap.3 - Modula-2 S. 141 Dynamische Datenstrukturen Zeiger (Pointer) und dynamische Datenstrukturen Mit Feldern (Array) und Verbunden (Record) ist es möglich komplexe Datenstrukturen zu definieren. Allerdings sind diese Strukturen statisch (d.h. von fester Größe). Zur Definition von Datenstrukturen, bei denen zur Laufzeit nicht nur die zugeordneten Werte, sondern auch der Aufbau und die Größe variabel sein sollen, braucht man dynamische Strukturen. Beispiele: Listen, Bäume oder Graphen Modula-2 bietet ein einfaches Werkzeug zur Definition beliebiger dynamischer Datenstrukturen: den Zeiger (Pointer) Praktische Informatik 1 R.Zicari / JWG-Universität Frankfurt Kap.3 - Modula-2 S. 142 Zeiger (Pointer) Was sind Zeiger ? Der Wert (Inhalt) einer Zeigervariablen dient als Referenz auf eine Variable seines Bezugstyps (referenced type). Ein Zeiger (Pointer) "zeigt" auf eine andere Variable. Man kann den Wert einer Zeigervariable auch als Anfangsadresse eines bestimmten Speicherbereiches auffassen. Abstraktes Modell Speichermodell 0 my_pointer Blabla Zeigervariable enthält Referenz (Adresse) Bezugsvariable enthält Wert (des Bezugstyps) Praktische Informatik 1 my_pointer 4711 R.Zicari / JWG-Universität Frankfurt 4711 Blabla Kap.3 - Modula-2 S. 143 Zeiger (Pointer) Formale Definition für einen Zeigertyp in Modula-2: TYPE PointerType = POINTER TO Type; Beispiel: TYPE string_pointer = POINTER TO string; • “Type“ stellt den Bezugstyp, also den Typ der referenzierten Variable (referenced variable), dar. • Ein Zeiger (Pointer) kann in Modula-2 nicht auf Variablen beliebigen (unterschiedlichen) Typs zeigen. Dies soll die Programmsicherheit erhöhen, da nichttypisierte Zeigervariablen schwer lokalisierbare Fehler zur Folge haben können. Praktische Informatik 1 R.Zicari / JWG-Universität Frankfurt Kap.3 - Modula-2 S. 144 Zeiger (Pointer) Wie werden Zeiger verwaltet ? Beispiel: Mit dem Befehl VAR my_pointer : string_pointer; wird eine Zeigervariable vom Typ string_pointer deklariert. Die zugehörige Bezugsvariable (referenced variable) muss allerdings zur Laufzeit (also innerhalb des Programms) explizit erzeugt werden. Hierfür steht die Standardprozedur NEW(...) zur Verfügung: NEW(my_pointer); Dieser Befehl hat zur Folge, dass der entsprechende Speicherplatz für die benötigte Bezugsvariable zur Laufzeit allokiert wird. Zusätzlich wird in my_pointer die Adresse auf diesen Speicherplatz eingetragen. DISPOSE(my_pointer); gibt den allokierten Speicherbereich wieder frei, er wird deallokiert. Praktische Informatik 1 R.Zicari / JWG-Universität Frankfurt Kap.3 - Modula-2 S. 145 Zeiger (Pointer) Die Operationen NEW( ) bzw. DISPOSE( ) werden in Aufrufe der Funktionen ALLOCATE( ) bzw. DEALLOCATE( ) übersetzt: NEW (p) ALLOCATE(p,SIZE(XT)) Mit ALLOCATE(p,SIZE(XT)) wird für eine Variable des Typs XT ein Speicherbereich der Größe SIZE(XT) angefordert, und dessen Anfangsadresse wird in p hinterlegt. DISPOSE(p) DEALLOCATE(p,SIZE(XT)) DEALLOCATE(p,SIZE(XT)) bewirkt, dass der Speicherbereich mit der Länge SIZE(XT) ab der Adresse p freigegeben wird. Da Pointer immer einem bestimmten Typ XT zugeordnet sind, kann man direkt aus dem Pointertyp die Länge SIZE(XT) bestimmen. Praktische Informatik 1 R.Zicari / JWG-Universität Frankfurt Kap.3 - Modula-2 S. 146 Zeiger (Pointer) Hierzu muss man allerdings das Modul Storage importieren, da die Funktionen ALLOCATE( ) und DEALLOCATE( ) dort definiert sind. FROM Storage IMPORT ALLOCATE,DEALLOCATE; Praktische Informatik 1 R.Zicari / JWG-Universität Frankfurt Kap.3 - Modula-2 S. 147 Zeiger (Pointer) Die Variablen, auf die my_pointer zeigt, ist anonym (hat also keinen Namen) und kann nur indirekt über den Zeiger my_pointer angesprochen werden! Es ist nicht möglich, die Bezugsvariable ohne den zugehörigen Zeiger anzusprechen. Der Wert (Inhalt) der Bezugsvariablen kann über den sog. Dereferenzierungs-Operator (dereferencing operator) ^ angesprochen werden. Praktische Informatik 1 R.Zicari / JWG-Universität Frankfurt Kap.3 - Modula-2 S. 148 Zeiger (Pointer) Beispiel: Die Bezugsvariable von my_pointer wird also mit my_pointer^ angesprochen. MODULE Pointer_example; ... TYPE string_pointer = Pointer TO string; VAR my_pointer:string_pointer BEGIN Zeigervariable NEW(my_pointer); my_pointer^:='geschafft!'; my_pointer END Pointer_example. Bezugsvariable geschafft! my_pointer^ Praktische Informatik 1 R.Zicari / JWG-Universität Frankfurt Kap.3 - Modula-2 S. 149 Zeiger (Pointer) Frage: Warum sind Zeiger so wichtig ? • Man kann mit Zeigern auf Variablen zeigen, die wiederum auch Zeiger enthalten. Das ermöglicht den Aufbau dynamischer Strukturen! • Analog zu den rekursiven Prozeduren, kann man mit Zeigern rekursive Datenstrukturen konstruieren. • Beispiele hierfür sind Listen oder Bäume. Praktische Informatik 1 R.Zicari / JWG-Universität Frankfurt Kap.3 - Modula-2 S. 150 Lineare Liste Beispiel: Listenerzeugung mit Zeigern Anfang der Erste der Zweite der Dritte Deklaration einer linearen Liste TYPE ListPointer = POINTER TO ListNode; TYPE ListNode = RECORD next:ListPointer; Data:string; END VAR Anfang,p,Listend : ListPointer; Praktische Informatik 1 R.Zicari / JWG-Universität Frankfurt Kap.3 - Modula-2 S. 151 Lineare Liste Frage: Wie beende ich eine Liste oder eine andere dynamische rekursive Datenstruktur? • Wir benötigen ein Mittel, um einen Pointer auf "nichts" zeigen lassen zu können, damit die rekursive Definition der Datenstruktur terminiert. (termination of data structure) • Für Zeigervariablen existiert ein spezieller Wert: NIL (Not In List) • NIL zeigt auf kein Objekt. Damit ist es offensichtlich, dass der Ausdruck p^ nicht ausgewertet werden darf, falls p=NIL ist. Praktische Informatik 1 R.Zicari / JWG-Universität Frankfurt Kap.3 - Modula-2 S. 152 Lineare Liste Wie muss ein Programm zur Erzeugung einer Liste aussehen? • Zunächst müssen wir die Liste initialisieren. Das bedeutet, dass wir die Variable Anfang auf NIL setzen müssen, damit sie nicht auf eine undefinierte Speicherposition zeigt: Anfang := NIL; Damit haben wir die leere Liste erzeugt. NIL Anfang Praktische Informatik 1 R.Zicari / JWG-Universität Frankfurt Kap.3 - Modula-2 S. 153 Lineare Liste Jetzt erzeugen wir das erste Element: • new(p); • Da der Zeiger p^.next auf eine undefinierte Speicherposition zeigen könnte, wird er auf NIL gesetzt: p^.next := NIL; • Wir setzen die gewünschten Daten in unser neues Element: p^.Data := "der Erste"; Anfang NIL p Praktische Informatik 1 der Erste p^.next p^.Data R.Zicari / JWG-Universität Frankfurt NIL Kap.3 - Modula-2 S. 154 Lineare Liste Jetzt müssen wir das neue Element in die leere Liste einfügen: • Dazu setzen wir den Zeiger Anfang so, dass er auf das Element zeigt, auf welches auch p zeigt: Anfang := p; Anfang NIL p der Erste Praktische Informatik 1 R.Zicari / JWG-Universität Frankfurt Kap.3 - Modula-2 S. 155 Lineare Liste Wir “merken“ uns das aktuelle Ende der Liste mit einem Hilfszeiger, den wir z.B. Listend nennen. Listend := p; Anfang NIL p der Erste Listend Praktische Informatik 1 R.Zicari / JWG-Universität Frankfurt Kap.3 - Modula-2 S. 156 Lineare Liste Wir erzeugen einen weiteren Knoten: new(p); p^.next := NIL; p^.DATA := "der Zweite"; Anfang Listend NIL der Erste NIL p der Zweite Praktische Informatik 1 R.Zicari / JWG-Universität Frankfurt Kap.3 - Modula-2 S. 157 Lineare Liste Wir fügen auch diesen Knoten am Ende der Liste ein und aktualisieren unseren Zeiger auf das Listenende: (i) (ii) Listend^.next := p; Listend := Listend^.next; Anfang Listend (ii) (i) der Erste NIL der Zweite p Praktische Informatik 1 R.Zicari / JWG-Universität Frankfurt Kap.3 - Modula-2 S. 158 Lineare Liste Frage: Wie durchsucht man eine lineare Liste? • Um eine lineare Liste durchsuchen zu können, brauchen wir eine Laufvariable (hier current), die angibt, an welcher Listenposition wir uns aktuell befinden. Anfang ... der Erste der Zweite der Dritte current Praktische Informatik 1 R.Zicari / JWG-Universität Frankfurt Kap.3 - Modula-2 S. 159 Lineare Liste Diese Laufvariable wird als Zeiger des Listentyps deklariert, VAR current : ListPointer; und auf den Listenanfang gesetzt (Initialisierung). current := Anfang; Praktische Informatik 1 R.Zicari / JWG-Universität Frankfurt Kap.3 - Modula-2 S. 160 Lineare Liste NIL Anfang ich nicht ich nicht Such mich! current Danach muss man in einer Schleife die Liste durchlaufen, bei jedem Schritt die Suchbedingung überprüfen und die Laufvariable current gegebenenfalls ein Listenelement weiterbewegen. WHILE (current^.Data<>"Such mich!") AND (current^.Data<>NIL) (* Auf Listenende achten *) DO current:=current^.next END; Praktische Informatik 1 R.Zicari / JWG-Universität Frankfurt Kap.3 - Modula-2 S. 161 Lineare Liste • Man kann also eine Funktion schreiben, die eine Liste durchsucht. • Weiterhin ist es möglich, die Elemente an jedem Punkt der Liste einfügen oder löschen zu können. Dazu kann man die Such-Funktion verwenden. • Bei der Implementierung von Einfüge- bzw. Löschoperationen muss man aufpassen, dass man die Liste wieder korrekt zusammenfügt. Praktische Informatik 1 R.Zicari / JWG-Universität Frankfurt Kap.3 - Modula-2 S. 162 Lineare Liste Beispiel: Löschen des Listenelements mit dem Inhalt "der Zweite" Zunächst werden zwei Hilfszeiger lauf1 und lauf2 deklariert. VAR lauf1, lauf2 : Listpointer; Ziel ist es, lauf2 auf das zu löschende Element und lauf1 auf dessen Vorgänger zeigen zu lassen. Praktische Informatik 1 R.Zicari / JWG-Universität Frankfurt Kap.3 - Modula-2 S. 163 Lineare Liste Beide Zeiger werden auf den Listenanfang gesetzt. lauf1:=Anfang; lauf2:=Anfang; ..... Anfang lauf2 Praktische Informatik 1 der Erste der Zweite der Dritte lauf1 R.Zicari / JWG-Universität Frankfurt Kap.3 - Modula-2 S. 164 Lineare Liste Danach wird das entsprechende Element gesucht: REPEAT lauf1 := lauf2; lauf2 := lauf2^.next; UNTIL lauf2^.Data = "der Zweite"; ..... Anfang der Erste der Zweite lauf1 lauf2 Praktische Informatik 1 R.Zicari / JWG-Universität Frankfurt der Dritte Kap.3 - Modula-2 S. 165 Lineare Liste Jetzt wird das Element aus der Kette heraus genommen: lauf1^.next := lauf2^.next; ..... Anfang Praktische Informatik 1 der Erste der Zweite lauf1 lauf2 R.Zicari / JWG-Universität Frankfurt der Dritte Kap.3 - Modula-2 S. 166 Lineare Liste Das Element kann nun auch physikalisch gelöscht werden. (Sein Speicherplatz wird wieder deallokiert (freigegeben). DISPOSE (lauf2); ..... Anfang der Erste NIL lauf1 lauf2 Praktische Informatik 1 der Dritte R.Zicari / JWG-Universität Frankfurt Kap.3 - Modula-2 S. 167 Lineare Liste Problem: Lineare Listen können immer nur in einer Richtung durchlaufen werden. ONE WAY ..... Anfang Praktische Informatik 1 der Erste der Zweite R.Zicari / JWG-Universität Frankfurt der Dritte Kap.3 - Modula-2 S. 168 Stapel (stack) Eine oft genutzte Datenstruktur, die mit einer linearen Liste realisiert werden kann, ist der Stapel (stack), der manchmal auch als Keller bezeichnet wird. Beim Stapel sind nur die folgenden beiden Operationen erlaubt: • pop = hole das oberste Element vom Stapel • push(x) = lege das Element x oben auf den Stapel Beim Stapel kann man immer nur Zugriff auf das oberste Element. Um ein Element aus einem Stapel zu löschen, muss man zuvor alle Elemente, die "über" dem gesuchten Element liegen, entfernen. Praktische Informatik 1 R.Zicari / JWG-Universität Frankfurt Kap.3 - Modula-2 S. 169 Stapel (stack) Beispiel eines Stapels: Abstraktes Modell push(x) pop x Ein Stapel arbeiten nach dem LIFO-Prinzip (Last In First Out). Praktische Informatik 1 R.Zicari / JWG-Universität Frankfurt Kap.3 - Modula-2 S. 170