Programmaufbau
subprogram_declaration ::= [overriding_indicator] subprogram_specification ; subprogram_specification ::= procedure_specification | function_specification procedure_specification ::= procedure defining_program_unit_name parameter_profile function_specification ::= function defining_designator parameter_and_result_profile subprogram_body ::= [overriding_indicator] subprogram_specification is declarative_part begin handled_sequence_of_statements end [designator] ; overriding_indicator ::= [not] overriding
Lexikalische Einheiten
Zeichensatz: Früher wurden Ada-Programme in Latin-1 (ISO 8859-1) geschrieben, der den ASCII-Code enthält. Ab Ada 2005 wird ISO/IEC 10646:2003 verwendet, der äquivalent zu Unicode ist.
Lexikalische Einheit: Ein Programm ist eine Folge von Zeichen aus dem Zeichensatz.
Die Zeichen bilden eine Folge von sog. lexikalischen Einheiten. Zu diesen gehören:
Bezeichner zur Identifizierung von Programmobjekten
Literale zur Bezeichnung von festen Werten (Zahlen, Zeichen, Strings)
Begrenzer (delimiter), die eine spezielle Bedeutung besitzen und gleichzeitig lexikalische Einheiten voneinander trennen (z. B. &, ’, +, (, ), :=, >= usw.)
Wortsymbole (reservierte Wörter der Sprache) mit spezieller Bedeutung (for, if,
procedure, …), dürfen nicht als Bezeichner verwendet werdenTrennzeichen (separator) zur Trennung von lexikalischen Einheiten aus Lesbarkeitsgründen (Zwischenraum, Whitespace)
Kommentare dienen der Erläuterung und Lesbarkeit (in Ada Zeichenfolgen ab --)
Zeigertypen
Zeiger: Zeiger auf Variablen enthalten nicht einen bestimmten Wert, sondern die Speicheradresse der Variable im Arbeitsspeicher.
Interne/externe Namen: Jede Variable hat einen internen (Speicheradresse) und einen externen Namen (symbolische Bezeichnung für die Speicheradresse).
In Ada dürfen „normale“ Zeigervariablen nur auf Variablen zeigen, die ausschließlich über Zeiger ansprechbar sind (sich also im Heap befinden).
Speicherbereiche: Es gibt drei große Speicherbereiche: Programmspeicher (Programmcode), Stack/Keller (Speicher für deklarierte Variablen, Blockstruktur) und Heap/Haufen (Variablen ohne externen Namen).
Eine Zeigervariable wird bei Deklaration im Stack angelegt. Mittels new wird dann eine Variable im Heap allokiert, bei Wertzuweisung erhält der Zeiger die Adresse des erzeugten Objekts.
Dynamische Datenstrukturen in Ada: Dynamische Datenstrukturen sind durch Zeiger „zusammengehaltene“ Daten. Mit Zeiger können sich ständig verändernde Daten gut beschrieben werden. In Ada geht dies z. B. so:
type Zelle; -- Vorwaertsdeklaration type Ptr_Zelle is access Zelle; -- Zeigertyp type Zelle is record -- Datentyp Inhalt : Character; Naechster : Ptr_Zelle; end record; ... Anker : Ptr_Zelle; -- Zeigervariable Test : constant Ptr_Zelle := new Zelle; -- konstante Zeigervariable ... Anker := new Zelle'('a', null); -- Allokation eines neues Objekts, Nullpointer Put (Anker.Inhalt); -- oder Anker.all.Inhalt (automatische Dereferenzierung bei Records!)
Wertzuweisung bei Zeigern: Bei einer Zuweisung zwischen Zeigern müssen linke und rechte Seite den gleichen Datentyp (Zeigertyp) besitzen. Prinzipiell muss in Ada jede Dereferenzierung mittels .all angegeben werden. Bei Records darf dies jedoch weggelassen werden.
Gleichheit bei Zeigern: Zwei Zeiger sind gleich, wenn sie auf dieselbe Variable zeigen.
Listen
Lineare Liste: In einer linearen Liste sind die Elemente linear angeordnet, es gibt keine Verzweigungen. Lineare Listen können als einfach (jedes Element zeigt auf seinen Nachfolger) oder doppelt verkettete Liste (Zeiger für den Vorgänger) realisiert werden. Bei einer zyklischen Liste zeigt das letzte Element nicht auf null, sondern auf das erste Element der Liste.
Operationen der linearen Liste: Empty (gibt leere Liste zurück, Nullpointer),
Isempty(A) (überprüft, ob A leer ist), First(A) (gibt das erste Element der Liste zurück), In_Front(E, A) (fügt
am Anfang ein Element hinzu), Append(E, A) (fügt am Ende ein Element hinzu), Delete(E, A) (löscht alle Elemente mit dem Inhalt aus der Liste).
Stack (Stapel): Ein Stack ist eine lineare Liste mit genau den Operationen Empty(A) (leert eine Liste durch Setzen auf Nullpointer),
Isempty(A) (überprüft, ob A leer ist), Top(A) (gibt das letzte Element zurück), Push(A, E) (fügt ein Element ans
Ende an), Pop(A) (löscht das letzte Element der Liste).
Ein Stack ist eine Liste, die nach dem LIFO-Prinzip (last in first out) arbeitet.
Queue (Schlange): Eine Queue ist eine lineare Liste mit genau den Operationen Empty(A) (leert eine Liste durch Setzen auf
Nullpointer), Isempty(A) (überprüft, ob A leer ist),
First(A) (gibt das erste Element zurück), Enter(A, E) (fügt ein Element ans Ende an), Remove(A) (löscht das erste Element der Liste).
Eine Queue ist eine Liste, die nach dem FIFO-Prinzip (first in first out) arbeitet.
Graphen (Geflechte): Ein Graph ist ein beliebig vernetztes Gebilde (also i. A. keine lineare Liste). Dieses besteht aus Knoten (Elemente des Datentyps) und Kanten (Verweise zwischen ihnen).
Zeiger auf Stackvariablen: Dies ist in Ada möglich, falls der Zeigertyp mit all und die referenzierte Variable mit aliased (Warnung für den Programmierer und Compiler optimiert nicht) deklariert wurde:
type Ptr_Integer is access all Integer; I : aliased Integer; Zeiger : Ptr_Integer := I'Access;
Dangling pointers sind Zeiger, deren referenzierte Variable irgendwann nicht mehr existiert. Daher sollte die Lebensdauer des referenzierten Objekts mindestens so groß sein wie die des Zeigers.
Referenzkonzept
Interne Namen: Jedes Objekt erhält in der Programmierung einen Namen, auch wenn dies nicht im Programmtext geschieht (z. B. implizite Typdeklaration bei Variablendeklaration). Im Programm ist es wichtig, dass alle Namen unterschieden werden können, da man sonst die Objekte nicht auseinander halten kann.
Konstanten im Speicher: Bisher wurden Konstanten als Inhalte der Variablen aufgefasst. Diese Vorstellung kann modifizieren, indem man die normalen Variablen als „Zeiger auf Konstanten“ ansieht. Man nimmt dabei an, dass sich alle Konstanten im Speicher befinden und die Variablen nur noch Adressen enthalten, wo sich die Konstanten befinden. Eine Wertzuweisung bewirkt dann nur noch eine Änderung des Zeigers.
Referenzkonzept: Jedes Objekt erhält eine Referenzstufe. Konstanten erhalten hierbei die Referenzstufe
Bäume
Gerichteter Graph, Weg:
Eine Folge von Knoten
Wurzel, Baum: Ein Knoten
Ein gerichteter Graph heißt Baum, falls er eine Wurzel
Ein Knoten ohne direkten Nachfolger heißt Blatt.
Ein Graph heißt Wald, wenn er sich als disjunkte Vereinigung von Bäumen schreiben lässt.
Ein Baum mit
Jedem Knoten ist ebenfalls eindeutig ein Level/Niveau zugeordnet:
Rekursive Definition für Bäume: Die leere Menge ist ein Baum. Wenn
Binäre Bäume: Die leere Menge ist ein binärer Baum. Wenn
Suchbaum: Ein binärer Baum mit Knoten, die Werte eines geordneten Datentyps beinhalten, heißt Suchbaum, falls für jeden
Knoten
Durchlauf von binären Bäumen:
Inorder | Preorder | Postorder |
procedure Inorder (b : Ref_BinBaum) is begin if b /= null then Inorder (b.L); -- Knoten b bearbeiten Inorder (b.R); end if; end Inorder; |
-- Knoten b bearbeiten Preorder (b.L); Preorder (b.R); |
Postorder (b.L); Postorder (b.R); -- Knoten b bearbeiten |
Ist
Im ungünstigsten Fall benötigt man
Relationen und Graphen
Relation: Seien
Eigenschaften: Sei
Relationsarten: Eine reflexive, symmetrische und transitive Relation heißt Äquivalenzrelation. Eine reflexive, antisymmetrische und transitive Relation heißt Ordnung. Eine irreflexive, antisymmetrische und transitive Relation heißt echte Ordnung. Eine Ordnung heißt totale/lineare Ordnung, falls sie alternativ ist.
Gerichteter Graph:
Ungerichteter Graph:
Umwandeln von Graphen: Ist
Ist
Teilgraph: Ist
ungerichteten Fall
Nachbarn: Jede Kante
Die Menge
Grad: Ist
Ist
Ein Graph heißt geordnet, falls für jeden Knoten
Adjazenzmatrix: Sei
Die erweiterte Adjazenzmatrix
(
Adjazenzliste: Man erstellt eine Liste der Knoten (mit Inhalt und ID-Nummer). Jeder Knoten enthält wieder eine Liste der inzidenten Kanten, deren Einträge das Gewicht und einen Verweis auf den Endknoten enthalten.
Inzidenzliste: Man erstellt jeweils eine (lineare) Liste der Knoten und eine Liste der Kanten.
Die Einträge der Kanten entalten zwei Verweise auf die zugehörigen Knoten.
Graphendurchlauf: Ein Graphendurchlauf lässt sich durch Adjazenzlisten realisieren. Man kann alle Knoten nacheinander durchgehen und bei jedem Knoten die entsprechenden Kanten ablaufen, um den Algorithmus mit dem Zielknoten rekursiv aufzurufen.
Ungerichtete Wege und Pfade: Eine Folge von Knoten
Zusammenhang für ungerichtete Graphen: Ein Weg heißt doppelpunktfrei/einfach, falls
Ein Graph heißt zusammenhängend, falls jeder Knoten mit jedem Knoten verbunden ist.
Gerichtete Wege und Pfade: Eine Folge von Knoten
(gerichteter) Weg in
Zusammenhang für gerichtete Graphen: Ein Weg heißt doppelpunktfrei/einfach, falls
Ein Graph heißt stark zusammenhängend, falls jeder Knoten mit jedem Knoten verbunden ist.
Transitive Hülle: Zu einem gerichteten bzw. ungerichteten Graphen
Ein vollständiger Graph ist ein Graph, in dem zwei verschiedene Knoten durch nur eine Kante miteinander verbunden sind. Ein Graph, bei dem