GNU-Pascal in Beispielen: Routinen

Aus Wikibooks
Wechseln zu: Navigation, Suche

zurück zu GNU-Pascal in Beispielen

Routinen[Bearbeiten]

Routinen dienen dazu, wiederkehrende Aufgaben zu gruppieren und den Quelltext übersichtlich und wartungsfreundlich zu gestalten. Es ist einfacher, an genau einer Stelle einen bestimmten Code zu ändern als an vielen Stellen immer wieder den gleichen Quelltext zu bearbeiten, was schnell zu Fehlern führt. Es gibt drei verschiedene Sorten von Routinen: Prozeduren, Funktionen und Operatoren [1].

Routinen können ihre eignen, nur für diese Routine geltenden Konstanten, Typen und Variablen definieren und deklarieren. In diesem Fall spricht man von lokalen Konstanten, lokalen Typen und lokalen Variablen. Variablen, auf die alle Teile des Programms zugreifen können nennt man im Gegensatz dazu "global".


Prozeduren[Bearbeiten]

Prozeduren wurden in den vergangen Kapitel benutzt, ohne dass über ihr Wesen gesprochen wurde. Diese Prozeduren waren WriteLn, Inc, New und viele andere. Prozeduren sind eigenständige Anweisungsfolgen, wobei sie durchaus globale Variablen verändern können. Das Beispiel aus Kapitel Stapel1 sieht umgeschrieben mit Prozeduren aus wie folgt:

Programm: Stapel2[Bearbeiten]

 program Stapel2;
 
 type
   TNamenString = String(100);
   PNamen = ^TNamen;
   TNamen = record
     Name: TNamenString;
     Naechster: PNamen
   end;
 
 var
   NamenStapel: PNamen = nil;
 
 procedure Stapeln;
 var
   Abbruch: Boolean = False;
   Name: TNamenString;
   TempName: PNamen = nil;
 
 begin
   repeat
     Write ('Geben Sie einen Namen ein: ');
     ReadLn (Name);
     if Length (Name) = 0 then
       Abbruch := True
     else
       begin
         New (TempName);
         TempName^.Name := Name;
         TempName^.Naechster := NamenStapel;
         NamenStapel := TempName
       end
   until Abbruch
 end;
 
 procedure StapelAusgeben;
 var
   Nummer: Integer;
   TempName: PNamen = nil;
 
 begin
   TempName := NamenStapel;
   Nummer := 1;
   while TempName <> nil do
     begin
       WriteLn (Nummer, 'ter Name: ', TempName^.Name);
       Inc (Nummer);
       TempName := TempName^.Naechster
     end
 end;
 
 procedure StapelLoeschen;
 var
   TempName: PNamen = nil;
 
 begin
   TempName := NamenStapel;
   while TempName <> nil do
     begin
       NamenStapel := NamenStapel^.Naechster;
       WriteLn ('entferne ', TempName^.Name);
       Dispose (TempName);
       TempName := NamenStapel
     end
 end;
 
 begin
   WriteLn ('Erzeugt eine Namensliste. Abbruch durch leere Zeile.');
   Stapeln;
   StapelAusgeben;
   StapelLoeschen;  
 end.


Erklärung[Bearbeiten]

Der Datentyp ist geblieben wie im Beispiel von Kapitel Stapel1 . Die Anzahl der globalen Variablen hat sich drastisch reduziert, übriggeblieben ist die Variable NamenStapel, da sie sozusagen den gesamten Stapel festhält.

In diesem Beispiel werden drei Prozeduren deklariert, Stapeln, StapelAusgeben und StapelLoeschen. Allen Prozeduren ist gemeinsam, dass sie über mindestens eine lokale Variable verfügen, die nur innerhalb dieser Prozedur Gültigkeit hat. Ähnlich wie bei Programmen werden die Anweisungen, welche die Prozedur ausführt, in einem Block zwischen begin und end gruppiert. Den Inhalt der Prozeduren kennen sie bereits aus einem früheren Beispiel. Durch den Einsatz von Prozeduren verringert sich die Zeilenzahl des Hauptprogramms drastisch, in unserem Beispiel wird die Zeilenzahl bei vollem Funktionsumfang auf Drei reduziert. Sollte der Stapel ein zweites Mal ausgeben werden, so müsste lediglich eine weiter Zeile zum Hauptprogramm hinzugefügt werden.

Prozeduren können eine Argumentenliste haben, denn sonst ließen sich Routinen wie WriteLn und New nicht realisieren:

Programm: Proc[Bearbeiten]

 program Proc;
 
 procedure Summe (Von, Bis: Integer; Zwischensumme: Boolean);
 var
   i, Summe: Integer = 0;
 begin
   for i := Von to Bis do
     begin
       Summe := Summe + i;
       if Zwischensumme then
         WriteLn ('Zwischensumme = ', Summe)
     end;
   WriteLn ('Summe = ', Summe)
 end;
 
 begin
   Summe (-2, 3, True)
 end.

Erklärung[Bearbeiten]

Dieses Programm berechnet die Summe zwischen Von und Bis, wobei auf Wunsch Zwischenergebnisse ausgegeben werde. Die Parameterliste einer Prozedur kann beliebig lang sein oder aber ganz weggelassen werden. Parameter einer Prozedur werden analog zu einer Variablendeklaration aufgezählt, wobei das Schlüsselwort var entfällt, da es in Parameterlisten eine besondere Bedeutung hat, auf die wir im Abschnitt Call by Reference zu sprechen kommen.


Funktionen[Bearbeiten]

Funktionen unterscheiden sich dadurch von Prozeduren, dass sie nie alleine auftreten, sondern immer einen Teil eines Ausdrucks bilden. Der Grund dafür liegt darin, dass Funktionen immer einen Rückgabewert haben. Ansonsten gilt alles, was über Prozeduren geschrieben wurde auch hier. Einige Funktionen sind bereits bekannt: Card, Ord, Length und weitere. Funktionen werden ähnlich wie Prozeduren deklariert:

Programm: Funk[Bearbeiten]

 program Funk;
 
 var
   MeineSumme: Integer;
 
 function Summe (Von, Bis: Integer): Integer;
 var
   i, ZwSumme: Integer Value 0;
 begin
   for i := Von to Bis do
     ZwSumme := ZwSumme + i;
   Summe := ZwSumme
 end;
 
 begin
   MeineSumme := Summe (1, 100);
   WriteLn ('Die Summe der ersten 100 Zahlen ist ', MeineSumme)
 end.

Erklärung[Bearbeiten]

Funktionen werden deklariert, indem das Schlüsselwort function vor den Bezeichner geschrieben wird. Darauf folgt eine optionale Parameterliste und der Typ, den diese Funktion zurückliefert. In obigem Beispiel liefert die Funktionen einen Integer-Wert zurück, der gleich der Summe ist. Innerhalb der Funktion entspricht dies einer Variablen mit dem Funktionsnamen (Summe), die den Typ des Rückgabewertes hat.

Forward-Deklarationen[Bearbeiten]

Benötigt eine Routine eine Andere, die jedoch erst zu einem späteren Zeitpunkt innerhalb des Quelltextes deklariert wird, so wird das Übersetzen des Quelltextes fehlschlagen. Eine Lösung besteht darin, die Reihenfolge aller deklarierten Routinen innerhalb des Quelltextes zu verändern. Oft möchte man dies nicht, da gerade diese Reihenfolge eine besondere Lesbarkeit [2] gewährleistet. Ein Grund dafür könnte sein, dass alle Routinen alphabetisch oder thematisch sortiert wurden. Forward-Deklarationen dienen dazu, die Reihenfolge beizubehalten und die gegenseitigen Abhängigkeiten aufzulösen. Das folgende Beispiel demonstriert den Mechanismus:

Programm: ForW[Bearbeiten]

 program ForW;
 
 procedure SchreibeA;
 begin
   WriteLn ('A')
 end;
 
 procedure SchreibeB; forward;
 procedure SchreibeAB;
 begin
   SchreibeA;
   SchreibeB
 end;
 
 procedure SchreibeB;
 begin
   WriteLn ('B')
 end;
 
 begin
   SchreibeAB
 end.

Erklärung[Bearbeiten]

In diesem Beispiel wurden drei alphabetisch sortierte Prozeduren deklariert, wobei SchreibeAB die Prozedur SchreibeB aufruft, von der sie noch keine Kenntnis haben kann. Aus diesem Grund wurde der Prozedurkopf von SchreibeB deklariert, indem die Direktive forward nachgestellt wurde.


Call by Reference[Bearbeiten]

Bei manchen Prozeduren ist es sinnvoll, wenn sie einen Rückgabewert haben. Eine solche Prozedur kennen Sie bereits, es ist Inc. Diese Prozedur verändert den ihr übergebenen Wert dahingehend, dass sie diesen um Eins erhöht. Weitere Einsatzgebiete für diese Technik, die im folgenden Abschnitt vorgestellt wird, sind Routinen, die mehr als einen Wert zurückliefern sollen. Diese Art, Parameter von Routinen zu deklarieren nennt man "Call By Reference".

Zuerst wird eine Prozedur implementiert, die ähnlich wie Inc arbeitet:

Programm: CBR1[Bearbeiten]

 program CBR1;
 
 var
   MeineZahl: Integer;
 
 procedure PlusDrei (var Zahl: Integer);
 begin
   Zahl := Zahl + 3
 end;
 
 begin
   MeineZahl := 10;
   WriteLn ('MeineZahl = ', MeineZahl);
   PlusDrei (MeineZahl);
   WriteLn ('MeineZahl = ', MeineZahl)
 end.

Erklärung[Bearbeiten]

Das zusätzliche Schlüsselwort var bewirkt, dass das Original des übergebenen Parameters verändert wird. Allen bisherigen Beispielen aus dem Bereich Funktionen und Prozeduren war gemeinsam, dass als Parameter nur Kopien der Argumente übergeben wurde. Das ist bei den vorgestellten Beispielen nicht aufgefallen, da nie die Notwendigkeit bestand, das Argument selbst zu ändern. Ein anderes Beispiel ist eine Funktion, die zwei Argumente tauscht:

Programm: CBR2[Bearbeiten]

 program CBR2;
 
 var
   Zahl1, Zahl2: Integer;
 
 procedure Zahlentauschen (var Param1, Param2: Integer);
 var
   Hilfsvariable: Integer;
 begin
   Hilfsvariable := Param1;
   Param1 := Param2;
   Param2 := Hilfsvariable
 end;
 
 begin
   Zahl1 := 10;
   Zahl2 := 20;
   WriteLn ('Vorher : Zahl1 = ', Zahl1, ' Zahl2 = ', Zahl2);
   Zahlentauschen (Zahl1, Zahl2);
   WriteLn ('Nachher: Zahl1 = ', Zahl1, ' Zahl2 = ', Zahl2)
 end.

Erklärung[Bearbeiten]

Die Prozedur Zahlentauschen vertauscht die Werte der ihr übergebenen Argumente. Ohne das Schlüsselwort var würde sich an den Variablen Zahl1 und Zahl2 nichts ändern. Gleichzeitig ist diese Prozedur ein Beispiel für eine Routine, die mehr als einen Rückgabewert hat.

Statische Variablen[Bearbeiten]

Statisch deklarierte Variablen dienen dazu, die Dauer der Gültigkeit dieser Variablen zu verlängern. In bisherigen Fällen verloren die lokalen Variablen ihre Gültigkeit, sobald die Routine abgearbeitet war. Hierzu ein Beispiel:

Programm: Statisch[Bearbeiten]

 program Statisch;
 
 procedure SchreibeZahl;
 var
   Zahl: Integer = 0; attribute (static);
 begin
   WriteLn ('Zahl ist jetzt = ', Zahl);
   Inc (Zahl)
 end;
 
 begin
   SchreibeZahl;
   SchreibeZahl;
   SchreibeZahl
 end.

Erklärung[Bearbeiten]

Trotz des dreifachen Aufrufs von SchreibeZahl wird die statische Variable nur einmal deklariert und initialisiert. Bei jedem Aufruf wird die lokale Variable Zahl um Eins erhöht, beim dritten Aufruf der Prozedur SchreibeZahl ist der Wert bereits auf 2 angewachsen. Weitere Attribute werden in späteren Abschnitten erläutert.


Rekursion[Bearbeiten]

Rekursive Routinen sind solche, die sich selber aufrufen. Üblicherweise werden sie aus Gründen der Eleganz als Funktionen implementiert. Diese Technik gestaltet Quellcode besonders übersichtlich, weil generell weniger Zeilen zur Lösung eines Problems verwendet werden. Hierzu ein Beispiel, wobei die rekursive Funktion die Summe von Zahlen berechnet:

Programm: Rekurs[Bearbeiten]

 program Rekurs;
 
 var
   DieSumme: Integer;
 
 function Summe (Von, Bis: Integer): Integer;
 begin
   if Von = Bis then
     Summe := Von
   else
     Summe := Von + Summe (Von + 1, Bis)
 end;
 
 begin
   DieSumme := Summe (1, 100);
   WriteLn ('Das Ergebnis lautet: ', DieSumme)
 end.

Erklärung[Bearbeiten]

Die gesamte Funktion Summe besteht aus einer if-Anweisung mit vier Zeilen. Es ist im Gegensatz zu vorherigen Implementationen dieser Funktion keine lokale Variable nötig. Der Algorithmus funktioniert so: Eine Summe von 1 bis 100 ist gleich 1 plus der Summe aus den Zahlen 2 bis 100 und das ist gleich der Summe der Zahlen 1 plus 2 plus der Summe der verbleibenden Zahlen und das ist gleich 1 + 2 + 3 plus der Summe der verbleibenden Zahlen und so fort. Wenn die letzte Zahl, in unserem Beispiel 100 erreicht ist und damit Von und Bis übereinstimmen, so wird die Summe von hinten tatsächlich berechnet, also usw.

Die Tatsache, dass eine Funktion sich selbst aufrufen kann mag ungewöhnlich erscheinen, ist aber tatsächlich ein sehr mächtiges Instrument um geeignete Problemlösungen elegant zu formulieren. Alle mit Rekursion lösbaren Probleme lassen sich auch auf die herkömmliche Weise lösen.


Funktionsergebnisse ignorieren[Bearbeiten]

In einigen Fällen ist es sinnvoll, den Erfolg einer Operation mitgeteilt zu bekommen. Im Falle einer Division zum Beispiel kann so bemerkt werden, dass der Nenner Null ist und das Ergebnis kann anschließend verworfen werden:

Programm: Ignore1[Bearbeiten]

 program Ignore1;
 
 var
   Geteilt: Real;
   Erfolg: Boolean;
 
 function Division (Zaehler, Nenner: Integer; var Ergebnis: Real): Boolean;
 begin
   if Nenner = 0 then
     Division := False
   else
     begin
       Ergebnis := Zaehler / Nenner;
       Division := True
     end
 end;
 
 begin
   Erfolg := Division (4, 0, Geteilt);
   WriteLn (Erfolg)
 end.

Falls ein Programm auf der Grundlage einer Vorbedingung die Null als Nenner ausschließt, kann es interessant sein, sich die dann überflüssige Variable Erfolg zu sparen. Damit in diesem Fall der Compiler bei der Übersetzung nicht warnt, dass der Funktionswert nicht zugewiesen wurde, hilft folgende Konstruktion:

Programm: Ignore2[Bearbeiten]

 program Ignore2;
 
 var
   Z: Integer;
   Geteilt: Real;
 
 function Division (Zaehler, Nenner: Integer;
   var Ergebnis: Real): Boolean; attribute (ignorable);
 begin
   if Nenner = 0 then
     Division := False
   else
     begin
       Ergebnis := Zaehler / Nenner;
       Division := True
     end
 end;
 
 begin
   for Z := 1 to 10 do
     begin
       Division (Z, 3, Geteilt);
       WriteLn (Z, '/3 = ', Geteilt : 0 : 5)
     end
 end.

Erklärung[Bearbeiten]

Das Attribut ignorable schaltet die Warnung des Compilers, die sich auf das fehlende Zuweisen des Funktionsergebnisses bezieht, aus. Dies ist in obigem Programm sinnvoll, da wir davon ausgehen können, dass der Nenner niemals den Wert Null annehmen kann. Darüberhinaus können wir so zum Zeitpunkt des Programmierens bestimmen, ob wir die Routine lieber als Funktion oder als Prozedur benutzen wollen.

Operatoren[Bearbeiten]

Die folgenden Operatoren sind in GNU-Pascal enthalten [3], wobei die hier gezeigte Reihenfolge die Rangfolge widerspiegelt:

< = > in
<> >= <=
+ - or
* / div Mod
and shl shr xor
pow ** ><
not @

Einigen dieser Operatoren lässt sich eine neue Bedeutung geben. Das folgende Beispiel definiert den +-Operator für Integer-Zahlen zu einem --Operator um:

Programm: NeuPlus[Bearbeiten]

 program NeuPlus;
 
 operator + (A, B: Integer) R: Integer;
 begin
   R := A - B
 end;
 
 begin
   WriteLn ('10 + 7 = ', 10 + 7)
 end.

Erklärung[Bearbeiten]

Das Schlüsselwort operator leitet die Definition eines Operators ein. Statt eines Bezeichners wird das Symbol eines Operators benutzt, der umdefiniert werden soll. Das Ergebnis der Operation wird wie eine Variable deklariert, die im Körper der Funktion genutzt werden kann.

Es können alle Operatoren umdefiniert werden, die über genau zwei Operanden verfügen. Aus obiger Aufzählung fallen das unäre Minus (z.B. Wert := -4;) wie auch das unäre Plus heraus, not und @ ebenso. Parameterlisten von Operatoren dürfen das Schlüsselwort var enthalten, so dass die Operanden auf diese Weise einen Wert zurückgeben können. Des weiteren können innerhalb von Operatoren eigene Typen, Konstanten und Variablen definiert und deklariert werden. Es lassen sich keine zusätzlichen Operatoren definieren die nicht in obiger Liste und zugehöriger Fußnote sind. Wohl aber lassen sich Operatoren mit Hilfe eines Bezeichners definieren, z. B. operator Plus (A, B: Integer) = R: Integer;. Anwendungsbereiche für Operatoren sind Operationen auf selbst definierte Strukturen wie z. B. Vektoren. Auch lässt sich ein Operator definieren, der zu einem Stapel ein Element hinzufügt.

Programmierbeispiel: Logic-Spiel[Bearbeiten]

Das folgende Programm ist eine vereinfachte Version des beliebten Spieles "Logic", welches mittlerweile auf jedem Mobiltelefon verfügbar ist. Das Ziel des Spieles ist es, eine bestimmte Folge von Symbolen, in unserem Beispiel Buchstaben, zu erraten, wobei das Programm lediglich Informationen darüber ausgibt, wie viele Buchstaben an der richtigen Position erraten wurden und wie viele Buchstaben an der falschen Stelle übereinstimmen.

Programm: Logical[Bearbeiten]

 program Logical;
 
 const
   AnzZeichen = 4;  { max. Anz. der versch. Zeichen im String }
   StrLaenge  = 4;  { Länge des Strings }
   ErstesZeichen = 'a';
   UnbenutztesZeichen = '.';
 
 type
   TRateString = String (StrLaenge);
 
 var
   RateString, GeheimString: TRateString;
   Versuche, RichtigeZeichen, RichtigeStelle: Integer = 0;
   Gefunden: Boolean = False;
 
 function InitGeheimString: TRateString;
 var
   i: Integer;
   TmpString: TRateString;
 begin
   for i := 0 to StrLaenge do
     TmpString[i] := Chr (Random (AnzZeichen) +
       Ord (ErstesZeichen));
   InitGeheimString := TmpString
 end;
 
 function AnzRichtigeStelle (Geheim, Rate: TRateString): Integer;
 var
   Richtig, i: Integer = 0;
 begin
   for i := 1 to StrLaenge do
     if Geheim[i] = Rate[i] then
       Inc (Richtig);
   AnzRichtigeStelle := Richtig
 end;
 
 function AnzRichtige (Geheim, Rate: TRateString): Integer;
 var
   i, j, Richtig: Integer = 0;
   TmpRate: TRateString;
 begin
   TmpRate := Rate;
   for i := 1 to StrLaenge do
     for j := 0 to StrLaenge do
       if Geheim[i] = TmpRate[j] then
         begin
           Inc (Richtig);
           TmpRate[j] := UnbenutztesZeichen;
           Break
         end;
   AnzRichtige := Richtig
 end;
 
 begin
   GeheimString := InitGeheimString;
   repeat
     Write ('Geben Sie ', StrLaenge, ' Zeichen ein von ',
       ErstesZeichen, ' bis ',
       Chr (Ord (ErstesZeichen) + AnzZeichen - 1), ' :');
     ReadLn (RateString);
     if Length (RateString) <> StrLaenge then
       Continue;
     Inc (Versuche);
     if RateString = GeheimString then
       Gefunden := True
     else
       begin
         RichtigeStelle  := 
           AnzRichtigeStelle (GeheimString, RateString);
         RichtigeZeichen :=
           AnzRichtige (GeheimString, RateString);
         WriteLn (RichtigeStelle, ' an der richtigen Stelle. ',
           RichtigeZeichen - RichtigeStelle, ' richtig.')
       end
   until Gefunden;
   WriteLn ('Gefunden nach ', Versuche, ' Versuchen.')
 end.

Erklärung[Bearbeiten]

Die Funktion InitGeheimString erzeugt einen Zufallsstring, wobei jeder Buchstabe aus der Menge der ersten AnzZeichen Kleinbuchstaben ist. In unserem Beispiel 'a'..'d'. AnzRichtigeStelle bestimmt bei zwei Strings die Anzahl der übereinstimmenden Buchstaben, wobei die Stellung mitberücksichtigt wird. AnzRichtige überprüft die Anzahl der insgesamt übereinstimmenden Buchstaben, unabhängig von der Stelle. Dabei wird, sobald ein Buchstabe übereinstimmt, diese Stelle in ein unbenutztes Zeichen verwandelt, damit doppelt übereinstimmende Buchstaben nicht gezählt werden können. Das Programm selbst liest einen String ein. Wenn dieser String identisch mit dem zu ratenden ist, so ist das Programm beendet. Wenn nicht, so werden zuerst die in der Stellung übereinstimmenden Buchstaben gezählt (RichtigeStelle).

Anschließend werden die insgesamt übereinstimmenden Buchstaben gezählt, wobei darin auch solche Übereinstimmungen gezählt werden, die schon mit AnzRichtigeStelle berücksichtigt wurden. RichtigeZeichen - RichtigeStelle ergibt somit die Anzahl der Buchstaben, die an der falschen Stelle übereinstimmen.


Anmerkungen[Bearbeiten]

  1. Eigentlich gehören Operatoren nicht in diese Klasse von Sprachelementen, sondern bilden eine Eigene. Sie werden hier aufgeführt, weil sie eine Reihe von Gemeinsamkeiten mit Funktionen und Prozeduren haben.
  2. Eine implizite Forward-Deklaration kennen Sie schon aus dem Kapitel Stapel1. Dort wurde PNamen vor TNamen definiert.
  3. Darüber hinaus existieren noch die Operatoren der PXSC-Pascal Erweiterung: +<, +>, -<, ->, *<, *>, /< und />.


Wikibooks buchseite.svg Zurück zu Typen im Eigenbau | One wikibook.svg Hoch zu Inhaltsverzeichnis | Wikibooks buchseite.svg Vor zu Units