GNU-Pascal in Beispielen: Routinen

Aus Wikibooks

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 />.