GNU-Pascal in Beispielen: Typen im Eigenbau
zurück zu GNU-Pascal in Beispielen
Typen im Eigenbau
BearbeitenDie grundlegenden Typen wurden bereits im Kapitel Variablen und Typen besprochen. Dieses Kapitel beschäftigt sich damit, auf der Basis dieser grundlegenden Typen eigene Typen zu entwickeln.
Üblicherweise wird selbstdefinierten Typen immer ein "T" vorangestellt.
Eigene Stringtypen
BearbeitenGrundsätzlich ist an dieser Stelle bekannt, wie Strings mit einer bestimmten Kapazität erzeugt werden. Statt aber, wie in Kapitel Zeichenketten alle Strings mit ihrer Kapazität anzugeben, ist es viel bequemer, sich eigene Stringtypen selbst zu erzeugen:
Program: Stringtyp
Bearbeitenprogram Stringtyp;
const
Kapazitaet = 20;
type
TMeinString = String (Kapazitaet);
var
MeinString: TMeinString;
begin
MeinString := 'Hallo, Welt!';
WriteLn (MeinString)
end.
Erklärung
BearbeitenIm Bereich von type werden eigene Typen definiert. TMeinString ist damit ab sofort ein Synonym für String (20). Dieser Typ kann im var-Bereich sofort genutzt werden.
Die hier gezeigte Reihenfolge von const, type und var ist die natürlichste Art der Anordnung. Wenn keine Gründe gegen diese Reihenfolge sprechen, so sollte sie beibehalten werden.
Eigene ganzzahlige Typen
BearbeitenManchmal ist es nützlich, eigene ganzzahlige Typen zu definieren. Das folgende Programm demonstriert die Vorgehensweise:
Program: Integertyp
Bearbeitenprogram Integertyp;
type
TMeinInteger = Integer (3);
TMeinCardinal = Cardinal (3);
begin
WriteLn ('Kleinster TMeinInteger Wert: ', Low (TMeinInteger));
WriteLn ('Groesster TMeinInteger Wert: ', High (TMeinInteger));
WriteLn ('Kleinster TMeinCardinal Wert: ', Low (TMeinCardinal));
WriteLn ('Groesster TMeinCardinal Wert: ', High (TMeinCardinal))
end.
Erklärung
BearbeitenEs werden zwei verschiedene ganzzahlige Typen definiert, wobei einer vorzeichenbehaftet, der Andere vorzeichenlos ist. Beide haben eine Kapazität von drei Bits. Die Funktion Low liefert den kleinsten Wert, den eine Variable von diesem Typ annehmen kann, die Funktion High den Höchsten.
Records
BearbeitenRecords sind aus beliebigen Typen in beliebiger Anzahl zusammengesetzte Typen. Ein Record könnte so beispielsweise alle Anschriftendaten über Personen zusammenfassen, geometrische Figuren organisieren oder die Informationen einer /etc/passwd-Datenzeile gruppieren.
Arbeiten mit Records
BearbeitenEin Record wird definiert, indem alle "Record-Felder" aufgeführt werden:
Program: Record1a
Bearbeitenprogram Record1a;
type
TEintrag = String (100);
TPostleitzahl = String (5);
TMitarbeiter = record
PersonalNummer: Integer;
Vorname, Nachname, Strasse, Stadt: TEintrag;
PLZ: TPostleitzahl
end;
var
Person: TMitarbeiter;
begin
Person.PersonalNummer := 7;
Person.Vorname := 'Hans';
Person.Nachname := 'Dampf';
Person.Strasse := 'In den Gassen 1-100';
Person.Stadt := 'Ueberall';
Person.PLZ := '12345'
end.
Erklärung
BearbeitenGefolgt von dem Schlüsselwort record werden die einzelnen Felder wie bei der Variablendeklaration aufgeführt. Eine Initialisierung während der Typendefinition ist nicht möglich. Der Record endet mit dem abschließenden end. Auf die einzelnen Felder eines Records wird mit Hilfe der Punkt-Schreibweise zugegriffen. So meint Person.Stadt das "Stadt-Feld" des Records.
Records können bei der Variablendeklaration initialisiert werden, wie folgendes Beispiel zeigt:
Program: Record2
Bearbeitenprogram Record2;
type
TPunkt = record
X, Y: Integer
end;
TKreis = record
Mitte: TPunkt;
Radius: Real
end;
var
Ort: TPunkt = (100, 50);
Kreis: TKreis = ((19, 23), 30.1);
begin
WriteLn ('Ort = (', Ort.X, ';', Ort.Y, ')');
WriteLn ('Kreis = (', Kreis.Mitte.X, ';', Kreis.Mitte.Y,') Radius = ',
Kreis.Radius : 0 : 2)
end.
Erklärung
BearbeitenEs werden zwei Record-Typen erzeugt, TPunkt und TKreis, wobei TKreis TPunkt als Feld Mitte enthält. Die Variablendeklaration Ort initialisiert die zwei Feldvariablen mit Hilfe von (100, 50), sodass Ort.X = 100 und Ort.Y = 50 ist.
Kreis hingegen wird so initialisiert, dass Kreis.Mitte wie Ort initialisiert wird, diesmal allerdings zu (19, 23). Die Feldvariable Radius wird mit der Zahl 30.1 initialisiert. Beachten Sie dabei die geschachtelte Klammerung.
Der Zugriff auf die Record-Felder erfolgt wieder in der Punkt-Schreibweise. Ort.X ist die X-Koordinate des Punktes, Kreis.Radius der Radius des Kreises. Der Mittelpunkt des Kreises ist Kreis.Mitte, seine X-Koordinate lautet Kreis.Mitte.X.
Ein häufiges Einsatzgebiet von Records ist das Speichern von Datensätzen innerhalb von Arrays. Folgendes Beispiel demonstriert das Vorgehen:
Program: Record3
Bearbeitenprogram Record3;
const
IndexEnde = 10;
type
TPunkt = record
X, Y: Integer
end;
TPunkte = array [1..IndexEnde] of TPunkt;
var
Orte: TPunkte;
Index: Integer;
begin
for Index := 1 to IndexEnde do
begin
Orte[Index].X := Index;
Orte[Index].Y := Index * 2
end;
for Index := 1 to IndexEnde do
WriteLn ('Orte[', Index, '] = (',
Orte[Index].X, ';', Orte[Index].Y, ')')
end.
Erklärung
BearbeitenDie Deklaration des Arrays erfolgt wie bei elementaren Typen, lediglich die Initialisierung ist verschieden: Auf die einzelnen Record-Felder wird gesondert zugegriffen.
With
BearbeitenDer Zugriff auf die einzelnen Komponenten des Arrays aus Beispiel Record1 ist schon bei wenigen Feldern sehr mühsam zu schreiben. Jedes Mal musste "Person." vor das gemeinte Feld notiert werden. Eine abkürzende Schreibweise auf Record-Komponenten demonstriert folgendes Beispiel:
Program: Record1b
Bearbeitenprogram Record1b;
type
TEintrag = String (100);
TPostleitzahl = String (5);
TMitarbeiter = record
PersonalNummer: Integer;
Vorname, Nachname, Strasse, Stadt: TEintrag;
PLZ: TPostleitzahl
end;
var
Person: TMitarbeiter;
begin
with Person do
begin
PersonalNummer := 7;
Vorname := 'Hans';
Nachname := 'Dampf';
Strasse := 'In den Gassen 1-100';
Stadt := 'Ueberall';
PLZ := '12345'
end
end.
Erklärung
BearbeitenDie abkürzende Schreibweise mit with erspart das mehrfache Schreiben von "Person.". Trotz dieser Struktur darf innerhalb des with-Blockes weiterhin Person.Nachname geschrieben werden, wenn es der Umstand erfordert. With-Blöcke dürfen beliebig geschachtelt werden, wobei darauf zu achten ist, dass nicht mehrere Record-Variablen die gleichen Feldbezeichner haben. Fehler, die daraus resultieren, sind schwer zu finden.
Variante Records
BearbeitenErweiterten wir die Menge der geometrischen Objekte aus Record2 um einige weitere Typen, so würden wir schnell feststellen, dass sie gemeinsame Eigenschaften haben die es nahelegen, alle diese Typen in einem gemeinsamen Record zu vereinigen. Eine Möglichkeit könnte darin bestehen, alle diese Typen einzeln in das Record aufzunehmen, wie folgendes Beispiel zeigt:
Program: Variant1
Bearbeitenprogram Variant1;
type
TObjektTyp = (Punkt, Kreis, Rechteck);
TPunkt = record
X, Y: Integer
end;
TKreis = record
X, Y: Integer;
Radius: Real
end;
TRechteck = record
X, Y, Breite, Hoehe: Integer
end;
TGeometrie = record
Typ: TObjektTyp;
Punkt: TPunkt;
Kreis: TKreis;
Rechteck: TRechteck
end;
begin
WriteLn ('Größe TObjektTyp = ', SizeOf (TObjektTyp));
WriteLn ('Größe TPunkt = ', SizeOf (TPunkt));
WriteLn ('Größe TKreis = ', SizeOf (TKreis));
WriteLn ('Größe TRechteck = ', SizeOf (TRechteck));
WriteLn ('Größe TGeometrie = ', SizeOf (TGeometrie))
end.
Erklärung
BearbeitenIn diesem Beispiel werden alle Typen in einem Record zusammengefasst. die Funktion SizeOf liefert uns die Größe eines Objektes in Bytes. Bei diesem Beispiel stellt sich der Nachteil ein, dass TGeometrie die gleiche Größe hat, wie die Summe der einzelnen Typen.
Eine wesentlich elegantere Art, TGeometrie zu definieren ist folgende:
Program: Variant2
Bearbeitenprogram Variant2;
type
TObjektTyp = (Punkt, Kreis, Rechteck);
TGeometrie = record
X, Y: Integer; { gemeinsam für alle Geometrie-Typen }
case Typ: TObjektTyp of
Kreis: (Radius: Real);
Rechteck: (Breite, Hoehe: Integer)
end;
var
MeinKreis, MeinPunkt: TGeometrie;
begin
WriteLn ('Größe TGeometrie = ', SizeOf (TGeometrie));
with MeinKreis do
begin
Typ := Kreis;
X := 10;
Y := 10;
Radius := 3.0
end;
with MeinKreis do
WriteLn ('(', X, ';', Y, ') R= ', Radius);
with MeinPunkt do
begin
Typ := Punkt;
X := 20;
Y := 20
end;
with MeinPunkt do
WriteLn ('(', X, ';', Y, ')')
end.
Erklärung
BearbeitenDiejenigen Record-Felder, die allen Geometrietypen gemeinsam waren, wurden in der neuen Definition von TGeometrie an den Anfang des Records gestellt. Danach folgt eine Auswahl der Record-Felder mittels case Typ: TObjektTyp of. Das Record-Feld Typ wird tatsächlich zum Bestandteil des Records, diese Art der Darstellung erlaubt es, aus der Menge von Varianten Kreis und Rechteck beschreibend auszuwählen. Wenn Typ = Kreis ist, so steht uns das Record-Feld Radius zur Verfügung, ist Typ = Rechteck, dann sind Breite und Hoehe verfügbar [1].
Der Fall Typ = Punkt braucht nicht gesondert abgefangen zu werden, seine Beschreibung liegt außerhalb der Varianten im oberen Bereich des Records. Die Größe des jetzigen TGeometry-Typs ist nur noch das Maximum der Größe der Varianten plus der Größe der außerhalb des Varianten-Teils liegenden Record-Felder. Eine Variable vom Typ TGeometry benötigt mit dieser Methode ungefähr die Hälfte des Speichers, den eine entsprechende Variable aus Variant1 belegen würde.
Zeiger
BearbeitenAlle bisher beschriebenen Variablendeklarationen gingen davon aus, dass wir eine zum Zeitpunkt des Programmierens bekannte Anzahl von Datenelementen speichern wollen. Ist die Anzahl der Elemente, die wir speichern wollen aber beliebig und die Typen vielleicht unterschiedlich, so helfen Zeiger dabei, eine dynamische Speicherverwaltung zu erzeugen. Ein Zeiger ist lediglich ein Verweis auf die Speicherstelle einer Variablen [2]. Ein einführendes Beispiel mit Zeigern zeigt folgendes Listing:
Program: Zeiger
Bearbeitenprogram Zeiger;
type
PInteger = ^Integer;
var
RefZahl: PInteger = nil;
begin
New (RefZahl);
RefZahl^ := 42;
WriteLn ('Inhalt = ', RefZahl^);
WriteLn ('Adresse = ', Cardinal (RefZahl));
Dispose (RefZahl)
end.
Erklärung
BearbeitenEin Zeigertyp wird definiert, indem ein Dach "^" vor den Grundtyp gestellt wird. Das Bedeutet in obigem Fall "Zeiger auf Integer". Allgemeine Zeiger auf nicht näher spezifizierte Typen sind Pointer.
Deklariert werden Variablen von Zeigertypen genauso wie von allen anderen Typen, neu ist die Initialisierung mit dem Schlüsselwort nil. Es sollte hier am Anfang des Kapitels nur der Vollständigkeit aufgeführt werden und bedeutet einen Zeiger auf "nichts", also einen leeren Zeiger [3]. Die Prozedur New erzeugt den Speicherplatz, auf dem in Zukunft ein Wert vom Typ Integer liegen soll. Dieser Wert kann zugewiesen werden, wobei das Dach diesmal nachgestellt wird. Diese Art des Zugriffs nennt man dereferenzieren. Auf die gleiche Weise kann der Inhalt des Zeigers ausgegeben werden.
Der Zeiger selbst kann mit Hilfe eines Casts ausgegeben werden, wobei hier die Speicheradresse ermittelt [4] wird, an der die Zahl 42 abgelegt wurde [5].
Dispose gibt den Speicherplatz, der mit New erzeugt wurde, wieder frei. Obwohl der Speicher freigegeben wurde, verweist der Zeiger möglicherweise noch an die Position, wo die Zahl 42 abgelegt wurde. Verlassen sollte man sich darauf auf keinen Fall, denn ein freigewordener Speicher kann jeden beliebigen Wert haben, auch den ursprünglichen. Erst beim erneuten Überschreiben der Speicheradresse durch das Betriebssystem ändert sich dieser Wert.
Zeigertypen sollte, so lange nichts dagegen spricht, immer ein "P" vorrangestellt werden.
Ein weitaus komplexeres Beispiel der Speicherverwaltung stellen so genannte Stapel dar. Ein Stapel ist eine Datenstruktur, bei der neu eingefügte Daten über [6] alle anderen Daten gestellt werden. Diejenigen Daten, die zuerst eingegeben wurde sind die Untersten des Stapels. Schaut man sich die einzelnen Elemente des Stapels an, so beginnt man von oben, also in umgekehrter Reihenfolge der Eingabe der Daten. Das folgende Programm liest einen "Stapel Namen" ein, gibt ihn aus und löscht ihn anschließend wieder:
Program: Stapel1
Bearbeitenprogram Stapel1;
type
TNamenString = String(100);
PNamen = ^TNamen;
TNamen = record
Name: TNamenString;
Naechster: PNamen
end;
var
NamenStapel, TempName: PNamen = nil;
Abbruch: Boolean = False;
Name: TNamenString;
Nummer: Integer;
begin
Write ('Erzeugt eine Namensliste. ');
WriteLn ('Abbruch durch leere Zeile');
{ Namensstapel aufbauen }
repeat
Write ('Geben Sie einen Namen ein: ');
ReadLn (Name);
if Length (Name) = 0 then
Abbruch := True
else
begin
{ neuen Speicherplatz reservieren }
New (TempName);
{ Namen eintragen }
TempName^.Name := Name;
{ alten Stapel unter den Neuen legen }
TempName^.Naechster := NamenStapel;
{ Alter Stapel ist neuer Stapel }
NamenStapel := TempName
end
until Abbruch;
TempName := NamenStapel; { oberstes Stapelelement }
Nummer := 1;
{ Namensstapel ausgeben }
while TempName <> nil do
begin
WriteLn (Nummer, 'ter Name: ', TempName^.Name);
Inc (Nummer);
TempName := TempName^.Naechster
end;
{ Namensstapel loeschen }
TempName := NamenStapel; { oberstes Stapelelement }
while TempName <> nil do
begin
{ Namenstapel zeigt auf zweites Element }
NamenStapel := NamenStapel^.Naechster;
WriteLn ('entferne ', TempName^.Name);
Dispose (TempName); { Speicher wieder freigeben }
TempName := NamenStapel { oberstes Stapelelement }
end
end.
Erklärung
BearbeitenIm Typenbereich werden drei Typen deklariert, einer für Strings mit einer festen Größe (TNamenString), ein Zeiger auf einen Record (PNamen) und der Record selber (TNamen). Auffällig ist, dass der Zeigertyp (PNamen) vor dem Typen (TNamen), dessen Verweis er ist, deklariert wurde. Dies ist erlaubt und an dieser Stelle auch durchaus erwünscht, denn innerhalb der Definition des Records TNamen wird dieser Zeigertyp als Typ des Record-Feldes Naechster eingesetzt. Verweist ein Record-Feld auf diese Weise auf sich selbst, so spricht man von einer "rekursiven Datenstruktur".
Der Namensstapel wird wie folgt aufgebaut: Wurde ein "richtiger Name" eingegeben, so wird mit Hilfe der Prozedur New ein Speicherplatz für TNamen erzeugt. Der Name wird dem Record-Feld Name zugewiesen. Dabei wird mit Hilfe des Daches der Zeiger dereferenziert, so dass eine Variable vom Typ TNamen vorliegt.
Auf die Record-Felder dieser Variablen greift man mit der bekannten "Punkt-Schreibweise" zu. Das Feld Naechster wird mit dem Zeiger auf den alten Stapel belegt, wobei dieser "alte Stapel" beim ersten Durchlauf nil ist. Der Zeiger Namensstapel, welcher soeben noch einen Zeiger auf das erste Element des restlichen Stapels war, wird nun zu einem Zeiger auf das oberste Element des Stapels. Auf diese Weise wird fortwährend der alte Stapel unter das neu erzeugte Element TempName gelegt.
Die Daten des Namensstapels werden ausgegeben, indem der Stapel von "oben nach unten" durchlaufen wird. Am Anfang zeigt TempName auf das oberste Element des Stapels und wird bei jedem Durchlauf auf seinen Nachfolger gesetzt, die Anzahl der Stapelelemente wird zwecks Anzeige mitprotokolliert. Beachten Sie, dass die Reihenfolge der Ausgabe in umgekehrter Reihenfolge der Eingabe erfolgt. Speicher, der dynamisch alloziert [7] wurde, muss spätestens zum Ende des Programms wieder freigegeben werden. TempName verweist zu Beginn auf das oberste Stapelelement. Solange der Stapel noch nicht vollständig abgebaut wurde, wird die Variable NamenStapel auf das Element hinter TempName gesetzt. Damit enthält NamenStapel wieder den "Rest" des Stapels. Der Speicher des obersten Elementes wird mit Dispose entfernt. Dieser nun freigewordene Zeiger kann anschließend wieder auf das erste Element des Stapels gesetzt werden und die Schleife kann erneut durchlaufen werden.
Schemata
BearbeitenSchemata dienen ebenso wie Zeiger dazu, Speicher dynamisch zu verwalten. Im Gegensatz zu Zeigern muss die maximale Anzahl der Elemente, die gespeichert werden sollen, bekannt sein, wobei diese Anzahl erst zur Laufzeit feststehen muss. Einen Schematypen kennen Sie bereits, es handelt sich dabei um Strings. Schemata und Zeiger lassen sich auch kombinieren. Das einführende Beispiel zeigt den grundsätzlichen Umgang mit Schemata:
Program: Schema1
Bearbeitenprogram Schema1;
type
TSchemaType (Anz: Byte) = array [1..Anz] of Integer;
var
MeinSchema: TSchemaType (4);
i: Integer;
begin
WriteLn ('MeinSchema hat maximal ', MeinSchema.Anz, ' Elemente.');
for i := 1 to MeinSchema.Anz do
MeinSchema[i] := 5 * i;
for i := 1 to MeinSchema.Anz do
WriteLn (MeinSchema[i])
end.
Erklärung
BearbeitenSchematypen werden definiert, indem mindestens eine Variable, in unserem Beispiel Anz, dem Typenname folgt. Diese Variable, die auch als Diskiminante bezeichnet wird, wird als eine der Grenzen eines Arrays [8] benutzt. Das Deklarieren eines Schemas erfolgt analog zum Deklarieren eines Strings.
Die maximale Anzahl der Elemente wird angegeben, was in unserem Fall bedeutet, dass MeinSchema ein Array von vier Integer-Variablen ist. Die Anzahl der Elemente lässt sich nachträglich während des Programmlaufes herausfinden, indem das zum Schema passende Feld Anz in der bekannten Weise ausgelesen wird. Auf die Variable MeinSchema kann zugegriffen werden, wie auf ein Array.
Schemata dienen der Bequemlichkeit. Benötigt man mehrere Variablen vom gleichen Array-Typ, jedoch mit möglicherweise unterschiedlicher Anzahl von Variablen, so sind sie genau die richtige Wahl.
mehrdimensionale Arrays zu erzeugen oder den Anfang und das Ende des Arrays festzulegen, wie folgendes Beispiel zeigt:
Program: Schema2
Bearbeitenprogram Schema2;
type
TSchemaType (Anfang, Ende: Cardinal) =
array [Anfang..Ende] of Integer;
var
MeinSchema: TSchemaType (1, 4) = (5, 6, 7, 8);
i: Integer;
begin
for i := MeinSchema.Anfang to MeinSchema.Ende do
WriteLn (MeinSchema[i])
end.
Erklärung
BearbeitenIn diesem Beispiel werden mit Hilfe der beiden Diskriminanten Anfang und Ende die Grenzen des Arrays festgelegt. Diese Grenzen müssen bei der Deklaration der Variablen angegeben werden. Wie auf diese Diskriminanten zugegriffen wird, zeigt die for-Schleife.
Schemata können mit Zeigern kombiniert werden und zur Laufzeit dynamisch erzeugt werden. Das folgende Programm ist eine Abwandlung von Schema2 welches diese Fähigkeit demonstriert:
Program: Schema3
Bearbeitenprogram Schema3;
type
PSchemaType = ^TSchemaType;
TSchemaType (Anfang, Ende: Cardinal) = array [Anfang..Ende] of Integer;
var
MeinSchema: PSchemaType;
i: Integer;
begin
New (MeinSchema, 4, 6);
for i := MeinSchema^.Anfang to MeinSchema^.Ende do
MeinSchema^[i] := 10 * i;
for i := MeinSchema^.Anfang to MeinSchema^.Ende do
WriteLn (MeinSchema^[i]);
Dispose (MeinSchema)
end.
Erklärung
BearbeitenEs wird ein Zeiger auf den Schematyp definiert, der genutzt wird, um MeinSchema zu deklarieren. Der Speicher für MeinSchema wird dynamisch angefordert, wobei der Prozedur New beide Grenzen des zugehörigen Arrays übergeben werden. Auf Anfang und Ende wird mit der bekannten Zeiger-Schreibweise zugegriffen, als sei MeinSchema ein Record [9]. Das Array erhält man, indem der Schemazeiger dereferenziert wird. Auf die einzelnen Variablen kann sodann mit der Index-Schreibweise zugegriffen werden.
Anmerkungen
Bearbeiten- ↑ Aktuell ist es prinzipiell möglich, alle verfügbaren Record-Felder zu überschreiben, unabhängig davon, wie Typ belegt wurde. Hierauf muss bei der Programmierung selbst geachtet werden.
- ↑ So wie die Adresse einer Person nicht die Person selber bedeutet sondern einen Verweis auf sie.
- ↑ Stellen Sie sich ruhig einen Zeiger auf "nichts" wie eine leere, noch nicht ausgefüllte Anschrift vor.
- ↑ An die wir einen Brief schicken könnten mit der Frage nach ihrem Inhalt.
- ↑ Die wir mit Hilfe der verschachtelten Typencasts wieder erhalten könnten.
Integer (Pointer (Cardinal (RefZahl))^)
- ↑ Wie bei einem Stapel Teller: Der zuletzt auf den Stapel gelegte Teller ist der Oberste.
- ↑ So nennt man das Zuteilen von Speicher, zum Beispiel mit der Prozedur New.
- ↑ Schemata müssen nicht, so wie in unseren Beispielen, auf Arrays basieren, aber es ist vermutlich das häufigste Einsatzgebiet.
- ↑ Tatsächlich sind Schemata wie Records implementiert.