Kurs: Programmieren in Pure Pascal, Teil 2

Unser zweites Etappenziel sind die Unterprogramme von Pascal: Prozeduren und Funktionen. Außerdem beschäftigen wir uns mit der Verwendung von lokalen und globalen Variablen sowie den besonderen Eigenschaften von Zeigern und Zeigervariablen.

Im letzten Teil hatten wir bereits wichtige Grundlagen von Pure Pascal kennengelernt, doch eine grundlegende Methode zum Strukturieren von Programmen wurde nicht betrachtet: Das Zusammenfassen von »funktionalen Einheiten« in Unterprogramme. Pascal bietet uns hier zwei Alternativen: Prozeduren mit dem Schlüsselwort »PROCEDURE«, die an die aufrufende Einheit kein Ergebnis zurückliefern und Funktionen, die ein Ergebnis beliebigen Typs zurückliefern.

Prozeduren und Funktionen sind im Deklarationsteil vereinbart, also vor der BEGIN-Anweisung des Hauptprogramms. Den Anfang jeder Prozedurdefinition bildet das Schlüsselwort PROCEDURE, gefolgt vom gewünschten Namen der Prozedur, den ein Semikolon abschließt. Es folgt der Block mit dem Deklarationsteil und den zusammengesetzten Anweisungen. Eine parameterlose Prozedur rufen wir einfach durch Angabe des Prozedurnamens auf (siehe auch Quelltext »DEM02A.PAS«):

PROGRAM Prozedurtest;
VAR wert, wert2 : INTEGER;  (* Globale Variablen *)
PROCEDURE Prozedur; (* Anfang der Prozedur *)
    VAR wert: INTEGER;  (* Lokale Variable *)
    BEGIN
        wert := 2; wert2 := 2;
        WRITELN (‚Wert in Prozedur:‘, wert)
    END;            (* Ende der Prozedur *)
BEGIN               (* Anfang des Hauptprogramms *)
    wert := 1; wert2 := 1;
    Prozedur;       (* Aufruf der Prozedur *)
    WRITELN (‚Wert1 im Hauptprogramm: ‘, wert);
    WRITELN (‚Wert2 im Hauptprogramm: ‘, wert2);
END.                (* Ende des Hauptprogramms *)

Bei der Ausführung des Programms stellen wir fest, daß »wert« im Hauptprogramm auch nach der Ausführung der Prozedur den Wert 1 annimmt, da wir in der Prozedur eine sogenannte »lokale Variable« gleichen Namens vereinbart haben. Bei Variablen mit gleichem Namen verwendet das Programm die Variable der nächstmöglichen Hierarchiestufe. Mehr zu diesem Thema etwas später.

Trotzdem haben wir auch aus der Prozedur Zugriff auf die Variable »wert« des Hauptprogramms: Die in übergeordneten Hierarchiestufen deklarierten Elemente sind verfügbar. Diese Grundsätze gelten übrigens für alle Festlegungen aus dem Deklarationsteil, also beispielsweise auch für Konstanten- oder Markendeklarationen (Labels).

Doch nun zu dem gerade genannten Begriff der Hierarchiestufen: Pascal-Programme können wie ein Baum hierarchisch gegliedert sein: Hauptbestandteil einer Prozedur ist der Block, der einen Deklarationsteil enthält, aber wiederum Prozedur-Deklarationen enthalten darf:

PROCEDURE WerteEingabe;
    VAR Eingabewert: INTEGER;
    PROCEDURE LiesEin;  (* Anfang von LiesEin *)
    BEGIN
        WRITE (‚Bitte Wert zwischen 1 und 4 eingeben: ‘);
        READLN (Eingabewert);
    END;            (* Ende von LiesEin *)
BEGIN               (* Anfang von WerteEingabe *)
    REPEAT
        LiesEin;    (* Aufruf von LiesEin *)
    UNTIL (Eingabewert >= 1) AND (Eingabewert <= 4);
END;

Die Prozedur »LiesEin« ist innerhalb der Prozedur »WerteEingabe« deklariert und somit nur von dieser aufrufbar, wobei sie Zugriff auf die Variable »Eingabewert« hat. Betrachten Sie das Programm doch einmal mit den Debugging-Funktionen von Pure Pascal: Öffnen Sie im »View«-Menü mit »Variables« die Variablen- und mit »Stack« die Stapelübersicht und gehen Sie das Programm mit < ALT-T > (»Trace into«aus dem Menü »Run«) schrittweise durch. Im Stack-Fenster erkennen Sie die Namen der hierarchisch aufgerufenen Prozeduren, im Variablen-Fenster die Inhalte der Variablen. Der »Step Over«-Befehl arbeitet übrigens ähnlich, führt die aufgerufenen Prozeduren jedoch am Stück aus. Kommen wir nun zu Prozeduren mit Parametern, denen wir Werte weiterreichen dürfen. Dazu übergeben wir eine Parameterliste, die eine in Klammern aufgeführte Folge von Variablendeklarationen enthält. Ähnlich wie im Deklarationsteil geben wir die gegebenenfalls durch Kommata voneinander getrennten Parameternamen, einen Doppelpunkt und den Datentyp an. Enthält die Parameterliste Deklarationen verschiedenen Datentyps, so sind die Deklarationen durch Semikolons voneinander abzugrenzen:

PROCEDURE Fehlermeldung (Text:STRING; Nummer:INTEGER);
BEGIN
    WRITELN (‚Fehler Nummer‘, Nummer,‚ : ‘, Text);
END;

Die Prozedur kann auf die Parameter wie auf andere lokale Variablen zugreifen (»Text« besitzt übrigens Platz für 255 Zeichen, da keine Stringlänge angegeben wurde). Beim Aufruf der Prozedur fügen wir an den Namen einfach die gewünschten, durch Kommata getrennten Aufrufparameter in Klammern an, wobei die Datentypen unbedingt übereinstimmen müssen:

IF fehlernummer = 4812 THEN
    Fehlermeldung (‚Division durch Null‘, fehlernummer);

Ändern wir übrigens einen so deklarierten Parameter, also würden wir beispielsweise im obigen Beispiel den Parameter »Nummer« innerhalb der Funktion »Fehlermeldung« um eins erhöhen, so bleibt die im Aufruf verwendete Variable »fehlernummer« unangetastet. Den umgekehrten Effekt erzielen wir, indem wir vor die Parameterdeklaration das Schlüsselwort »VAR« setzen. Beim Aufruf sind nun unbedingt Variablen und keine Zahlenkonstanten als Parameter anzugeben, die dann gegebenenfalls verändert werden:

PROCEDURE Vertausche (VAR wert1, wert2: INTEGER)
VAR hilf: INTEGER;
BEGIN
    hilf  := wert1;
    wert1 := wert2;
    wert2 := hilf;
END;

Diese Prozedur vertauscht die Inhalte zweier INTEGER-Variablen. Der Aufruf »Vertausche(a,b);« vertauscht demnach die Inhalte der beiden Variablen »a« und »b«. Natürlich kann eine Prozedur auch über einen VAR-Parameter einen Rückgabewert an die aufrufende Stelle zurückliefern, doch dafür gibt es die wesentlich handlicheren Funktionen, die durch das Schlüsselwort »FUNCTION« gekennzeichnet sind. An zweiter Stelle steht der Funktionsname, gefolgt von der optionalen Parameterliste und einem Doppelpunkt, hinter dem wir den Ergebnistyp festlegen. Innerhalb des Funktionsblocks steht uns eine Ergebnisvariable zur Verfügung, die den Namen der Funktion trägt.

FUNCTION Negativ (Wert: INTEGER) :INTEGER;
BEGIN
    Negativ := -Wert;
END

Der Aufruf erfolgt wie bei einer Prozedur über den Namen mit passenden Parametern, wobei der zurückgelieferte Wert zu beachten ist: minuswert:=Negativ(wert);. Interessante Möglichkeiten bieten die in Pascal erlaubten Rekursionen: Prozeduren und Funktionen dürfen sich selbst aufrufen. Hier ein Beispiel zur Fakultätsberechnung fak(n), die wir rekursiv definieren: fak(n):=fak(n-1)*n, wobei fak(1) := 1:

PROGRAM Fakultaet;
FUNCTION fak (n: INTEGER) : INTEGER;
    BEGIN           (* Anfang der Funktion *)
        IF n <= 1 THEN fak := 1 ELSE fak := fak (n-1)*n;
    END;            (* Ende der Funktion *)
BEGIN               (* Anfang des Hauptprogramms *)
    WRITELN (‚Fak(4)=‘,fak(4));
END.                (* Ende des Hauptprogramms *)

Die Funktion ruft sich immer wieder selbst mit einem stets um eins erniedrigten Parameter auf, bis der Parameter kleiner gleich eins ist. Rekursion ist hier aber nicht sehr effizient, wie das iterative Programm FAK2.PAS auf der TOS-Diskette beweist. Unter Umständen kann es erforderlich sein, daß eine Prozedur eine zweite Prozedur aufrufen soll, die aber erst an späterer Stelle im Quelltext deklariert und somit dem Compiler noch nicht bekannt ist. Abhilfe erreichen wir, indem wir vor die aufrufende Funktion eine Kopie des Kopfes der aufzurufenden Funktion kopieren, an die wir das Schlüsselwort »FORWARD;« setzen:

PROCEDURE Ausgabe (Zahl: INTEGER);
FORWARD;    (* Vorwärtsdeklaration *)
PROCEDURE Berechnung;
BEGIN ... Ausgabe(wert); ... END; ...
PROCEDURE Ausgabe (Zahl: INTEGER);
BEGIN ... END)

In Pascal können wir im Deklarationsteil eigene Datentypen erzeugen, die wir mit dem Schlüsselwort »TYPE« einleiten. Der Name des neuen Typs sollte mit einem »T« wie Typ beginnen.

    TYPE TAutomarken = (Audi, Fiat, VW, BMW, Mercedes);

Wir können diesen Datentyp nun wie die vordefinierten Typen verwenden und sparen uns eine Menge Schreibarbeit und Tippfehler: VAR MeinAuto:TAutomarken;. Besonders interessant wird die Typdeklaration aber erst bei strukturierten Datensätzen wie den sogenannten »RECORDs«. In einem Record fassen wir eine beliebige Mixtur von Datensätzen zusammen, wozu wir diese mit den Schlüsselwörtern »RECORD« und »END« eingrenzen. Der Zugriff auf die Elemente erfolgt über den RECORD-Namen, einen Dezimalpunkt und den Elementnamen:

VAR Anschrift : RECORD
    Zuname, Vorname, Strasse, Ort : STRING;
    PLZ : 1000..9999;
END;
Anschrift.PLZ := 8011;

Mit der WITH-Anweisung, auf weiche die Namen der zu bearbeitenden RECORDS (durch Kommata getrennt) und das Schlüsselwort DO folgen, können wir auf direktem Wege auf die RECORD-Elemente zugreifen:

WITH Anschrift DO
    BEGIN Vorname: =‘Otto‘; Zuname: =‘Mustermann‘;
END;

Im Normalfall verwenden wir RECORDS jedoch nicht direkt, sondern werden uns im TYPE-Teil einen maßgeschneiderten Datentyp zusammenstellen. Möchten wir aber in Abhängigkeit eines Datenelements verschiedene Detailinformationen ablegen, so helfen uns die sogenannten Varianten-RECORDs. Nehmen wir an, wir speichern Informationen über einen Wohnort, die unter anderem das Datenelement »Art«enthalten, das festlegt, ob es sich um eine Wohnung oder ein Haus handelt:TYPE TWohntyp (Wohnung,Haus);. Für eine Wohnung wollen wir Miete und Nebenkosten festhalten, für ein Haus nur den Kaufpreis. Dazu deklarieren wir uns einen RECORD mit Basisdaten wie der Anschrift und der »Art«-Variablen, die wir aber hinter das Schlüsselwort CASE setzen, gefolgt von dem Schlüsselwort OF Für jede Erscheinungsform von »Art« können wir nun die zugehörigen Datensätze in runden Klammern hinter den Konstanten angeben, die durch Kommata getrennt und mit einem Doppelpunkt abgeschlossen sind:

TYPE TZuhause = RECORD
    Strasse, Ort : STRING[60];
    CASE Art: TWohntyp OF
        Wohnung : ( Miete, Nebenkosten:REAL);
        Haus: (Kaufpreis : REAL);
END;

Auch Felder beliebiger Dimension sind in Pascal möglich. Wir deklarieren ein eindimensionales Feld, indem wir als Typ die Sequenz »ARRAY[< Indexbereich >] OF < Typ >« verwenden. Der < Indexbereich > stellt entweder einen Zahlenbereich von der Notation »Startwert..Endwert« oder einen Datentyp dar:

VAR Feld1 : ARRAY[1..10] OF TZuhause;
    Feld2 : ARRAY[CHAR] OF INTEGER;

Der Zugriff erfolgt über den entsprechenden Index, den wir in eckige Klammern setzen: Feld2[A‘] := 4812;. Bei mehrdimensionalen Feldern setzen wir zwischen die Indizes jeweils Kommata:

VAR  Feld3: ARRAY[1..3, CHAR] OF INTEGER;
    ...
    Feld3[2,‘A‘] := 123;

Anders als bei C oder Basic können wir in Pascal auch mit Mengen arbeiten, uns allen aus der Mengenlehre der Volksschule wohlbekannt. Eine Mengenvariable deklarieren wir mit Hilfe des Datentyps »SET OF < Typ >«, wobei < Typ > einen beliebigen Datentyp beschreibt, der allerdings höchstens 256 mögliche Ausprägungen haben darf:

TYPE TAutos = (Fiat, BMW, Porsche, Mercedes, VW);
VAR autos, a2, a3: SET OF TAutos;

Eine Menge zeichnet sich dadurch aus, daß jedes mögliche Element entweder enthalten oder nicht enthalten ist. Die Mengenelemente sind bei Operationen wie Zuweisung (:=), Vereinigungsmenge (+), Differenzmenge (-) und Schnittmenge (*) stets in eckige Klammern zu setzen und durch Kommata zu trennen:

a2 := [Fiat,BMW];
a3 := [BMW, Porsche, VM];
autos := a2 + a3;   (* Vereinigungsmenge *)
autos := a2 - a3;   (* Ergibt: [Fiat] *)
autos := a2 * a3;   (* Ergibt: [BMW] *)

Mengen können wir jedoch nicht direkt ausgeben, wohl aber mit dem Operator »IN« testen, ob ein Element enthalten ist:

    IF Fiat IN autos THEN WRITEM( ‚Fiat‘),.

Dieser Operator läßt sich auch auf normale Variablen anwenden:

    IF zeichen IN ‚A‘. . ‚Z‘ THEN ...

Für die Untersuchung von Mengen bieten sich weiterhin Testoperationen auf Gleichheit (=), Ungleichheit (< >), Teilmenge von (<=) und Obermenge von (>=) an, die ein BOOLEAN-Ergebnis liefern:

    IF [Porsche] <= a3 THEN ...

Bislang betrachteten wir Variablen als statische Objekte, auf die wir mit einem fest zugeordneten Namen zugreifen. Wie Sie bestimmt wissen, sind auch die Variablen an bestimmten Positionen (den Adressen) im Speicher untergebracht. Neben dem direkten Zugriff auf eine Variable ist auch ein indirekter Weg möglich, wobei wir uns der sogenannten Zeigervariablen bedienen. Die Deklaration einer Zeigervariablen erfolgt wie die einer Variablen, die wir mit dem Zeichen » ^« versehen.

    VAR WertZeiger: ^INTEGER;

Der so deklarierte Zeiger besitzt wie die Variablen zunächst einen undefinierten Wert. Wollen wir über ihn auf die Variable »Wert« zugreifen, so müssen wir ihn zunächst auf diese ausrichten, wozu wir ihn mit der Adresse der Variablen füllen, die wir mit dem Operator ermitteln:

    WertZeiger := @Wert;

»WertZeiger« enthält jetzt die Adresse der Variablen Wert. Der Zugriff auf den Inhalt dieser Variablen erhalten wir wiederum mit einem nachgestellten »^«:

    WRITELN (WertZeiger^);

Bei RECORD-Zeigern müssen wir übrigens das ^-Symbol hinter den Zeigernamen, aber vor den Dezimalpunkt setzen:

    WRITELN (Adresszeiger^.Ort);

Bislang war die Einführung der Zeiger ohne großen Nutzen. Diesen erhalten diese erst mit der Nutzung von dynamischem Speicher: Bei Bedarf fordern wir Speicher vom Betriebssystem an, verwenden diesen mit Hilfe der Zeiger und geben ihn anschließend wieder frei. Hierzu deklarieren wir uns zunächst eine Zeigervariable und einen Zeigertyp, dem wir zum besseren Erkennen ein »P« für »pointer« voranstellen:

TYPE PPerson:^TPerson;  (*Erlaubte Vorwärtsreferenz *)
TPerson: RECORD Vorname, Zuname: STRING; END;
VAR EinePerson:PPerson;

Wollen wir nun eine dynamische Variable anlegen, so rufen wir die Prozedur »NEW« auf, der wir den Zeiger als Parameter übergeben:

    NEW(EinePerson);

Nun wurde der Speicherraum reserviert und der Zeiger auf diesen gestellt. Auf die Variable greifen wir auf die besprochene Weise zu.

    EinePerson^.Vorname:=‘Otto‘ ;
    EinePerson^. Zuname: =‘Mustermann‘ ;
    WRITELN(EinePerson^.Vorname,‘ ‚, EinePerson^.Zuname);

Benötigen wir die Variable nicht mehr, geben wir sie mit »DISPOSE(EinePerson);« wieder frei. In diesem Beispiel haben wir jedoch ein Problem: Für jedes neu anzulegende Element benötigen wir einen Zeiger, doch unsere Zeigerzahl ist bislang statisch. Dies ändern wir, indem wir unseren Informationsblock einfach um einen Zeiger auf einen gleichartigen Block erweitern:

TYPE PElement = ^TElement;
    TElement =RECORD
        Wert:INTEGER;
        Naechster:PElement; (*Zeiger*)
    END;

Wir merken uns nun einen einzelnen Zeiger, der fortan als Ausgangspunkt dient: VAR Lifo:PElement;. Über diesen Zeiger gelangen wir fortan an das erste Element unserer Daten. In diesem Datenblock befindet sich nun der Zeiger »Naechster«, über den wir an das nächste Element gelangen und so weiter. Mit der vordefinierten Konstante NIL markieren wir im Zeiger des letzten Elementes das Ende der Elementkette. Ist kein Wert vorhanden, so setzen wir den Einstiegszeiger auf NIL: Lifo:=NIL;

Betrachten wir nun aber ein konkretes Beispiel (»DEM02H.PAS« auf der TOS-Diskette): Wir realisieren einen Last-In-First-Out-Stapel, auf den wir Werte legen können (»Push«) und von dem wir Werte nehmen können, wobei das zuletzt abgelegte Element zuerst zurückgeholt wird. Zum Ablegen legen wir zunächst ein neues Element mit »NEW« an und speichern den Wert dort ab:

    NEW(Hilf) ; Hilf‘.Wert:=NeuWert;

Das neue Element wird nun zum ersten Element unserer Datenverkettung, weshalb das bislang erst Element »Lifo^« nun nach dem neuen Element als »Hilf^.Naechster^« folgt. Das neue Element wird zum ersten Element:

    Hilf^.Naechster := Lifo;
    Lifo:=Hilf;

Das Zurückholen (»Pop«) erfolgt umgekehrt: Zunächst lesen wir den Inhalt aus »Lifo^«, merken uns einen Zeiger auf den Nachfolger, löschen das Element und stellen den »Lifo«-Zeiger auf das bislang zweite Element:

Pop := Lifo^.Wert;
Hilf := Lifo^.Naechster;
DISPOSE(Lifo);
Lifo:=Hilf;

Mit der Funktion »Empty« stellen wir anhand der »Lifo«-Zeigers fest, ob die Liste leer ist:

FUNCTIONEmpty:BOOLEAN;
BEGIN
    Empty:=(Lifo=NIL);
END;

In unserem Beispielprogramm legen wir zwanzig Zahlen ab und lesen den Stapel komplett aus:

FOR i:=1 TO 20
    DO
        Push(i);    (* Werte von 1 bis 20 auf Stapel *)
    WHILE NOT Empty DO WRITELN(Pop) ;   (* Werte einlesen *)

Dies ist nur ein Beispiel zur dynamischen Speicherverwaltung, auf ähnliche Weise lassen sich auch verkettete Listen und ähnliches verwirklichen. Interessant sind dabei noch folgende Funktionen: »SIZEOF (Variable Typ)« liefert uns die Größe eines Datenelementes in Bytes, »MAXAVAIL« die Größe des größten freien Datenblocks und »MEMAVAIL« die Gesamtgröße der noch verfügbaren Speichers. Interessant bei direkten Hardwaremanipulationen ist die Funktion PTR, mit der wir einer Zeigervariablen eine numerische Adress zuordnen können: zeiger:=PTR($FF8040);.

In der nächsten Kursfolge werden wir Funktionen zu Ein- und Ausgabe kennenlernen und uns mit dem Modulkonzept von Pure Pascal befassen. (ah)
Frank Mathy


Links

Copyright-Bestimmungen: siehe Über diese Seite
Classic Computer Magazines
[ Join Now | Ring Hub | Random | << Prev | Next >> ]