Programmierkurs: Delphi: Pascal: Prozeduren und Funktionen

Bisher haben wir alle Anweisungen in unserem Hauptprogramm ausgeführt. Sobald wir jedoch bestimmte Programmteile wiederverwenden möchten, z.B. um eine Berechnung mit verschiedenen Zahlen mehrmals auszuführen oder um Abfragen zu wiederholen, wird dies unpraktisch. Wir müssten dann jedes Mal den gesamten Programmtext erneut schreiben. Auch Kopieren & Einfügen wäre keine wirklich schöne Alternative.

Allgemeines

Bearbeiten

Um dies zu vermeiden, speichern wir einfach den sich wiederholenden Programmtext unter einem eigenen Namen zusammengefasst ab und rufen dann im Hauptprogramm nur noch diesen Namen auf. Dies funktioniert genauso wie bei den bisher bekannten und von Delphi mitgelieferten Routinen.

Bei diesen Aufrufen wird zwischen Prozeduren und Funktionen unterschieden. Dabei übermitteln Funktionen nach deren Beendigung einen Rückgabewert, den man dann weiter verarbeiten kann, Prozeduren jedoch nicht. Zur Unterscheidung im Programmtext dienen die Schlüsselwörter procedure und function.

Wenn in diesem Kapitel der Begriff Routinen verwendet wird, sind gleichermaßen Prozeduren und Funktionen gemeint.

Prozeduren und Funktionen haben folgenden allgemeinen Aufbau:

procedure Prozedurname[(Parameterliste)];               // Prozedurkopf
[Deklaration lokaler Typen und Variablen]
begin
  <Anweisungen>
end;

function Funktionsname[(Parameterliste)]: Rückgabetyp;  // Funktionskopf
[Deklaration lokaler Typen und Variablen]
begin
  <Anweisungen>
end;

Die Verwendung von Parametern ist optional, genauso die Deklaration von lokalen Typen Variablen. Die Parameterliste besteht aus einer durch Semikolons getrennte Liste der Form Parametername: Parametertyp.

Routinen werden wie die globalen Typen, Konstanten und Variablen im Deklarationsblock oberhalb des begin vom Hauptprogramm deklariert. Die Routinen können sich auch untereinander aufrufen. Dabei gilt die Lesereihenfolge von oben nach unten: alle oben genannten Routinen sind denen darunter bekannt und können von diesen aufgerufen werden.

Prozeduren ohne Parameter

Bearbeiten

Dies ist die einfachste Form von Unterprogrammen. Diese nehmen keinen Wert entgegen und geben auch keinen zurück.

Will man nun die Anweisungen aus dem Eingangsbeispiel:

var
  Name: string;
begin
  Writeln('Wie heisst du?');
  Readln(Name);
  Writeln('Hallo ' + Name + '! Schoen, dass du mal vorbeischaust.');
  Readln;
end.


gleich zweimal aufrufen und dafür eine Prozedur verwenden, dann sähe das Ergebnis so aus:

var
  Name: string;

procedure Beispielprozedur;
begin
  Writeln('Wie heisst du?');
  Readln(Name);
  Writeln('Hallo ' + Name + '! Schoen, dass du mal vorbeischaust.');
  Readln;
end;  // Ende procedure Beispielprozedur

begin // Beginn des Hauptprogramms
  Beispielprozedur;
  Beispielprozedur;
end.  // Ende Hauptprogramm


In diesem Beispiel besteht der Prozedurkopf nur aus dem Schlüsselwort procedure und dem Prozedurnamen.

Die Variable Name ist hierbei global, also am Programmanfang außerhalb der Prozedur deklariert. Dadurch besitzt sie im gesamten Programm samt aller (nachfolgenden) Prozeduren Gültigkeit, kann also an jeder Stelle gelesen und beschrieben werden. Dies hat jedoch Nachteile und birgt Gefahren, da verschiedene Prozeduren eventuell ungewollt gegenseitig den Inhalt solcher globaler Variablen verändern. Die Folge wären Fehler im Programm, die schwer zu erkennen, aber glücklicherweise leicht zu vermeiden sind. Man kann nämlich auch schreiben:

procedure Beispielprozedur;
var
  Name: string;
begin
  Writeln('Wie heisst du?');
  Readln(Name);
  Writeln('Hallo ' + Name + '! Schoen, dass du mal vorbeischaust.');
  Readln;
end;  // Ende procedure Beispielprozedur

begin // Beginn des Hauptprogramms
  Beispielprozedur;
  Beispielprozedur;
end.  // Ende Hauptprogramm

Jetzt ist die Variable innerhalb der Prozedur deklariert, und sie besitzt auch nur dort Gültigkeit, kann also außerhalb weder gelesen noch beschrieben werden. Man nennt sie daher eine lokale Variable. Mit Erreichen der Stelle end// Ende procedure Beispielprozedur verliert sie ihren Wert, sie ist also auch temporär.

Lokale Variablen besitzen zu Beginn einen Zufallswert. Sie müssen daher vor dem ersten Auslesen mit einem Wert belegt werden. Leider kann man sie nicht wie die globalen Variablen direkt bei der Deklaration initialisieren, sondern muss dies im Prozedurrumpf tun.

Prozeduren mit Wertübergabe (call by value)

Bearbeiten

Oft ist es jedoch nötig, der Prozedur beim Aufruf Werte zu übergeben, um Berechnungen auf unterschiedliche Daten anzuwenden oder individuelle Nachrichten auszugeben. Hierzu geben wir im Prozedurkopf einen oder mehrere Parameter an. Ein Beispiel:

var
  Name: string;
  AlterInMonaten: Integer;

procedure NameUndAlterAusgeben(Name: string; AlterMonate: Integer);
var
  AlterJahre: Integer;
begin
  AlterJahre := AlterMonate div 12;
  AlterMonate := AlterMonate mod 12; 
  Writeln(Name, ' ist ', AlterJahre, ' Jahre und ', AlterMonate, ' Monate alt');
  Readln;
end;

begin
  NameUndAlterAusgeben('Konstantin', 1197);
  Name := 'Max Mustermann';
  AlterInMonaten := 386;
  NameUndAlterAusgeben(Name, AlterInMonaten)
end.

Der Prozedur NameUndAlterAusgeben werden in runden Klammern zwei durch Semikolon getrennte typisierte Variablen (Name: string; AlterMonate: Integer) bereitgestellt, die beim Prozeduraufruf mit Werten belegt werden müssen. Dies kann wie im Beispiel demonstriert mittels konstanten Werten oder auch Variablen des geforderten Typs geschehen. Diese Variablen werden genau wie die unter var deklarierten lokalen Variablen behandelt, nur dass sie eben mit Werten vorbelegt sind. Sie verlieren also am Ende der Prozedur ihre Gültigkeit und sind außerhalb der Prozedur nicht lesbar. Veränderungen an ihnen haben auch keinen Einfluss auf die Variablen mit deren Werten sie belegt wurden, im Beispiel verändert also AlterMonate := AlterMonate mod 12; den Wert von AlterInMonaten nicht.

Weiterhin können Parameter, wie oben gezeigt, den gleichen Namen wie globale Variablen haben. Die Prozedur verwendet in diesem Falle immer den Parameter.

Prozeduren mit Variablenübergabe (call by reference)

Bearbeiten

Was aber, wenn man der Prozedur nicht nur Werte mitteilen will, sondern sie dem Hauptprogramm auch ihre Ergebnisse übermitteln soll? In diesem Fall benötigt man im Hauptprogramm eine Variable, deren Werte beim Prozeduraufruf übergeben werden und in die am Ende das Ergebnis geschrieben wird. Außerdem muss man im Prozedurkopf einen Platzhalter definieren, der die Variable aufnehmen kann (eine Referenz darauf bildet). Dies geschieht, indem man genau wie bei der Wertübergabe hinter dem Prozedurnamen in runden Klammern eine typisierte Variable angibt, ihr aber das Schlüsselwort var voranstellt. Ein Beispiel:

var
  Eingabe: Double;

procedure BerechneKubik(var Zahl: Double);
begin
  Zahl := Zahl * Zahl * Zahl;
end;

begin
  Eingabe := 2.5;
  BerechneKubik(Eingabe);

  { ... }

end.

Als Platzhalter dient hier die Variable Zahl, ihr wird beim Aufruf der Wert von Eingabe übergeben, am Ende wird der berechnete Wert zurückgeliefert. Im Beispiel erhält Eingabe also den Wert 15.625. Bei der Variablenübergabe darf keine Konstante angegeben werden, da diese ja das Ergebnis nicht aufnehmen könnte, es muss immer eine Variable übergeben werden.

Objekte (s. Kapitel Klassen) werden in Delphi immer als Zeiger übergeben. Man kann also auf Eigenschaften und Methoden eines Objekts direkt zugreifen:

 procedure LoescheLabel(Label: TLabel);
 begin
    Label.Caption := ''
 end;

Hinweis: Statt Parameter als Referenz zu übergeben, sollte man lieber Funktionen verwenden, da diese für eben diesen Zweck vorhanden sind. Nur wenn man gleichzeitig mehrere Rückgabewerte benötigt, ist eine Prozedur oder Funktion mit Referenzparametern sinnvoll.

Aber: Der große Vorteil von VAR Parameter ist es, das nicht die Variable selber umkopiert wird (wie es bei einer Funktion der Fall wäre), sondern nur ein Zeiger auf die Variable im Hauptspeicher übergeben wird. Damit können quasi beliebig große Variablen übergeben werden, während bei "normalen" Parametern der Stack die limitierende Größe ist. Außerdem ist diese Art der Werteübergabe sehr schnell, da nichts herumkopiert werden muß.

Routinen mit konstanten Parametern

Bearbeiten

Parameter können mit const gekennzeichnet sein, um zu verhindern, dass ein Parameter innerhalb einer Prozedur oder Funktion geändert werden kann.

 function StrSame(const S1, S2: AnsiString): Boolean;
 begin
   Result := StrCompare(S1, S2) = 0;
 end;

Bei String-Parametern ergibt sich ein Geschwindigkeitsvorteil, wenn diese mit const gekennzeichnet werden.

Standardparameter

Bearbeiten

Es kann manchmal sehr nützlich sein, einer Routine Standardparameter zu übergeben. So ist es möglich, einen Sonderfall mit derselben Routine abzuarbeiten, ohne bei jedem „normalen“ Aufruf einen Mehraufwand zu haben. Im folgenden Beispiel kann die Routine "Meldung" sowohl als Meldung('Text') als auch als Meldung('Text', IsError) aufgerufen werden. Wenn "Meldung" ohne Angabe des Parameters "IsError" aufgerufen wird, wird IsError = 0 gesetzt:

procedure Meldung( text:string; IsError: Integer = 0 );
begin
if IsError <> 0 then Showmessage('Error: '+text)
else Showmessage(text);
end;

Meldung('Hallo') ist gleichbedeutend mit Meldung('Hallo', 0) und wird "Hallo" anzeigen.

Meldung('Hallo', 1) wird "Error: Hallo" anzeigen.

Erste Zusammenfassung

Bearbeiten

Prozeduren können ohne Ein- und Ausgabe, mit einer oder mehreren Eingaben und ohne Ausgabe, oder mit Ein- und Ausgabe beliebig vieler Variablen deklariert werden. Wertübergaben und Variablenübergaben können selbstverständlich auch beliebig kombiniert werden. Bei Variablenübergaben ist zu beachten, dass die Prozedur immer den Wert der Variable einliest. Will man sie allein zur Ausgabe von Daten verwenden, darf deren Wert nicht verwendet werden, bevor er nicht innerhalb der Prozedur überschrieben wurde, beispielsweise mit einer Konstanten oder dem Ergebnis einer Rechnung.

Prozeduren können natürlich auch aus anderen Prozeduren oder Funktionen heraus aufgerufen werden.

Werden Variablen im Hauptprogramm deklariert, so nennt man sie global, und ihre Gültigkeit erstreckt sich demnach über das gesamte Programm bzw. die gesamte Unit; somit besitzen auch Prozeduren Zugriff darauf. Innerhalb von Prozeduren deklarierte Variablen besitzen nur dort Gültigkeit, man nennt sie daher lokale Variablen.

Aber Vorsicht: Konnte man sich bei Zahlen (vom Typ Integer, Int64, Single, Double, ...) in globalen Variablen noch darauf verlassen, dass sie zu Anfang den Wert null haben, so ist dies in lokalen Variablen nicht mehr der Fall, sie tragen Zufallswerte. Außerdem kann man keine typisierten Konstanten innerhalb von Prozeduren deklarieren.

Funktionen

Bearbeiten

Funktionen liefern gegenüber Prozeduren immer genau ein Ergebnis. Dieses wird sozusagen an Ort und Stelle geliefert, genau dort wo der Funktionsaufruf im Programm war, steht nach dem Ende ihr Ergebnis. Das hat den Vorteil, dass man mit den Funktionsausdrücken rechnen kann als ob man es bereits mit ihrem Ergebnis zu tun hätte, welches erst zur Laufzeit des Programms berechnet wird.

Sie werden durch das Schlüsselwort function eingeleitet, darauf folgt der Funktionsname, in runden Klammern dahinter ggf. typisierte Variablen zur Wertübergabe, gefolgt von einem Doppelpunkt und dem Datentyp. Innerhalb der Funktion dient ihr Name als Ergebnisvariable. Ein Beispiel:

var
  Ergebnis: Double;

function Kehrwert(Zahl: Double): Double;
begin
  Kehrwert := 1/Zahl;   // oder: Result := 1/Zahl;
end;

begin
  Ergebnis := Kehrwert(100) * 10; // wird zu:   Ergebnis := 0.01 * 10;

  { ... }

  Kehrwert(100) * 10;
end.


Im ersten Aufruf wird der Funktion der (Konstanten-) Wert 100 übergeben, Sie liefert ihr Ergebnis, es wird mit 10 multipliziert, und in Ergebnis gespeichert. Der nächste Aufruf bleibt ohne Wirkung, und verdeutlicht, dass man mit Funktionsausdrücken zwar beliebig weiterrechnen kann, aber am Ende das Ergebnis immer speichern oder ausgeben muss, da es sonst verloren geht.

Funktionen erfordern nicht notwendigerweise Wertübergaben. Z.B. wäre es möglich, dass eine Funktion mit Zufallszahlen arbeitet oder ihre Werte aus globalen Variablen oder anderen Funktionen bezieht. Eine weitere Anwendung wäre eine Funktion ähnlich einer normalen Prozedur, die jedoch einen Wert als Fehlermeldung zurückgibt. Beispielsweise gibt die Funktion Pi den Wert der Zahl Pi zurück und benötigt dafür keine Werte vom Benutzer.

Die Variable Result ist eine sogenannte Pseudovariable. Sie wird automatisch für jede Funktion erstellt und ist ein Synonym für den Funktionsnamen. Der Wert, der ihr zugewiesen wird, wird von der Funktion zurückgegeben.

function Kehrwert(Zahl: Double): Double;
begin
  Result := 1/Zahl;
end;

ist also gleichbedeutend mit

function Kehrwert(Zahl: Double): Double;
begin
  Kehrwert := 1/Zahl;
end;

Unterprozeduren / Unterfunktionen

Bearbeiten

Prozeduren und ebenso Funktionen können bei ihrer Deklaration auch in einander verschachtelt werden (im Folgenden wird nur noch von Prozeduren gesprochen, alle Aussagen treffen aber auch auf Funktionen zu). Aus dem Hauptprogramm oder aus anderen Prozeduren kann dabei nur die Elternprozedur aufgerufen werden, die Unterprozeduren sind nicht zu sehen. Eltern- und Unterprozeduren können sich jedoch gegenseitig aufrufen.

Die Deklaration dazu an einem Beispiel veranschaulicht sieht folgendermaßen aus:

procedure Elternelement;
var
  Test: string;

  procedure Subprozedur;
  begin
    Writeln(Test);
    Readln(Test);
  end;

{ ...
  Beliebig viele weitere Prozeduren
  ... }

begin  // Beginn von Elternelement
  Test := 'Ein langweiliger Standard';
  Subprozedur;
  Writeln(Test);
  Readln;
end;


Nach dem Prozedurkopf folgen optional die Variablen der Elternprozedur, dann der Prozedurkopf der Unterprozedur, ihr Rumpf, ggf. weitere Variablen der Elternprozedur und schließlich der Rumpf der Elternprozedur.

Sofern die Variablen der Elternprozedur vor den Unterprozeduren deklariert werden, können diese darauf ähnlich einer globalen Variable zugreifen, wodurch sich der Einsatz weiterer Variablen reduzieren lässt. Im Zusammenhang mit Rekursionen, also dem Aufruf einer Routine aus sich selbst heraus, wird ein solcher Einsatz manchmal angebracht sein. Unterprozeduren können auch selbst wieder Unterprozeduren haben, die dann nur von ihrem Elternelement aufgerufen werden können. Es sind beliebige Verschachtelungen möglich.

forward-Deklaration

Bearbeiten

Alle folgenden Aussagen treffen auch auf Funktionen zu.

Eigentlich muss der Code einer Prozedur vor ihrem ersten Aufruf im Programm stehen. Manchmal ist dies jedoch eher unpraktisch, etwa wenn man viele Prozeduren alphabetisch anordnen will, oder es ist gar unmöglich, nämlich wenn Prozeduren sich gegenseitig aufrufen. In diesen Fällen hilft die forward-Deklaration, die dem Compiler am Programmanfang mitteilt, dass später eine Prozedur unter angegebenen Namen definiert wird. Man schreibt dazu den gesamten Prozedurkopf ganz an den Anfang des Programms, gefolgt von der Direktive forward; Der Prozedurrumpf folgt dann weiter unten im Deklarationsteil oder im Implementation-Teil der Unit. Hierbei kann der vollständige Prozedurkopf angegeben werden oder man lässt die Parameter weg. Am Beispiel der Kehrwertfunktion sähe dies so aus:

function Kehrwert(Zahl: Double): Double; forward;

var
  Ergebnis: Double;

function Kehrwert;
begin
  Kehrwert := 1/Zahl;
end;

{ ... }

Überladene Prozeduren / Funktionen

Bearbeiten

Alle bisher betrachteten Prozeduren/Funktionen verweigern ihren Aufruf, wenn man ihnen nicht genau die Zahl an Werten und Variablen wie in ihrer Deklaration gefordert übergibt. Auch deren Reihenfolge und Datentyp muss beachtet werden.

Will man beispielsweise eine Prozedur mit ein- und demselben Verhalten auf unterschiedliche Datentypen anwenden, so wird dies durch die relativ strikte Typisierung von Pascal verhindert. Versucht man also, einer Prozedur, die Integer-Zahlen in Strings wandelt, eine Gleitkommazahl als Eingabe zu übergeben, so erhält man eine Fehlermeldung. Da dies aber eigentlich ganz praktisch wäre, gibt es eine Möglichkeit, dies doch zu tun. Man muss jedoch die Prozedur in allen benötigten Varianten verfassen und diesen den gleichen Namen geben. Damit der Compiler von dieser Mehrfachbelegung des Namens weiß, wird jedem der Prozedurköpfe das Schlüsselwort overload; angefügt. Wichtig ist es zu beachten, dass die Parameter tatsächlich in der Reihenfolge ihrer Datentypen unterschiedlich sind. Es reicht nicht, unterschiedliche Parameternamen zu verwenden.

Ein Beispiel:

function ZahlZuString(Zahl: Int64): string; overload;
begin
  ZahlZuString := IntToStr(Zahl);
end;

function ZahlZuString(Zahl: Double): string; overload;
begin
  ZahlZuString := FloatToStr(Zahl);
end;

procedure ZahlZuString(Zahl: Double; Ausgabe: TEdit); overload;
begin
  Ausgabe.Text := FloatToStr(Zahl);
end;

Die erste Version kann Integer-Werte in Strings wandeln, die zweite wandelt Fließkommawerte, die dritte ebenso, speichert sie dann jedoch in einer Delphi-Edit-Komponenten.

Wenn Typen zuweisungskompatibel sind, braucht man keine separate Version zu schreiben, so nimmt z.B. Int64 auch Integer (Longint)-Werte auf, genau wie Double auch Single-Werte aufnehmen kann.

Der Begriff überladen (overload) beschreibt das mehrmalige Einführen einer Funktion oder Prozedur mit gleichem Namen, aber unterschiedlichen Parametern. Der Compiler erkennt hierbei an den Datentypen der Parameter, welche Version er nutzen soll. Das Überladen ist sowohl in Klassen, für die dort definierten Methoden, als auch in globalen Prozeduren und Funktionen möglich. Wie man in dem Beispiel sieht, können als überladene Routinen sowohl Prozeduren wie auch Funktionen gemischt verwendet werden.

Vorzeitiges Beenden

Bearbeiten

In einigen Situationen kann es sinnvoll sein, den Programmcode einer Prozedur oder Funktion nicht vollständig bis zum Ende durchlaufen zu lassen. Hierfür gibt es die Möglichkeit, mittels der Anweisung Exit vorzeitig hieraus auszusteigen. Exit kann an jeder Stelle im Programmablauf vorkommen, bei Funktionen ist jedoch zusätzlich zu beachten, dass vor dem Verlassen ein gültiges Funktionsergebnis zugewiesen wird.

Stellen Sie sich vor, Sie haben eine Liste von Namen und möchten wissen, an welcher Stelle dieser Liste sich ein Name befindet. Die entsprechende Funktion könnte wie folgt aussehen:

type
  TNamensListe = array[0..99] of string;

function HolePosition(Name: string; Liste: TNamensListe): Integer;
var
  i: Integer;
begin
  Result := -1;         // Funktionsergebnis bei Fehler oder "nicht gefunden"
  if Name = '' then
    Exit;               // Funktion verlassen, wenn kein Name angegeben wurde

  for i := 0 to 99 do   // komplette Liste durchsuchen
    if Liste[i] = Name then
    begin
      Result := i;      // gefundene Position als Funktionsergebnis zurückgeben
      Break;            // verlässt die Zählschleife (oder Exit, verlässt die Funktion)
    end;
end;

In diesem Beispiel wird als erstes das Funktionsergebnis -1 festgelegt. In diesem Falle soll es die Bedeutung „Fehler“ oder „Name nicht gefunden“ besitzen. Da der Index der Liste erst bei 0 beginnt, ist eine Stelle unter 0 als gültiges Ergebnis nicht möglich. Dadurch können wir diesen Wert als so genannte „Magic number“ gebrauchen, sprich, als Ergebnis mit besonderer Bedeutung.

Als nächstes wird getestet, ob überhaupt ein Name angegeben wurde. Wenn die Zeichenkette leer ist, erfolgt der sofortige Ausstieg aus der Funktion. Die anschließende Prüfschleife wird nicht durchlaufen. Als Ergebnis wird der zuvor zugewiesene Wert -1 zurückgegeben. Nur wenn Name nicht leer ist, beginnt die eigentliche Suche. Hierbei werden alle Elemente der Liste geprüft, ob sie dem angegebenen Namen entsprechen. Wenn dies der Fall ist, wird die Position dem Funktionsergebnis zugewiesen und die Suche beendet.

Für den besonderen Fall, dass zwar ein Name angegeben wurde, dieser aber in der Liste nicht enthalten ist, wird ebenfalls -1 zurückgegeben. Da die Bedingung Liste[i] = Name niemals zutrifft, bekommt das Funktionsergebnis in der Schleife keinen neuen Wert zugewiesen. Nach 99 wird die Schleife verlassen und Result ist immer noch -1.

Das folgende Programm verdeutlicht den Ablauf:

var
  Liste: TNamensListe;

begin
  Liste[0] := 'Alfons';
  Liste[1] := 'Dieter';
  Liste[2] := 'Gustav';
  Liste[3] := 'Detlef';

  WriteLn(HolePosition('Gustav', Liste));  // Ergibt:  2
  WriteLn(HolePosition('Peter', Liste));   // Ergibt: -1
end.
Tipp:

 

Ab Delphi 2009 ist es möglich, das Funktionsergebnis als Parameter von Exit anzugeben. Man kann die obige Beispielfunktion ab dieser Version mit Exit(-1) bzw. Exit(i) direkt verlassen und muss das Funktionsergebnis vorher nicht mehr extra zuweisen.


  Pascal: Schleifen Inhaltsverzeichnis Pascal: Typdefinition