Buchgenerator (deaktivieren)

Programmierkurs: Delphi: Pascal: Prozeduren und Funktionen

Aus Wikibooks

Wechseln zu: Navigation, Suche

Inhaltsverzeichnis

[Bearbeiten] Prozeduren und Funktionen

[Bearbeiten] Prozeduren ohne Parameter

Oft benötigt man ein und den selben Code mehrere Male in einem Programm, - was liegt also näher als ihn nur ein mal zu schreiben, unter einem Namen zu speichern und diesen dann im Programm aufzurufen? Wohl nichts, und deshalb leisten Prozeduren genau dies. Will man etwa die Anweisungen aus dem Eingangsbeispiel


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


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


var
  Vorname: string;

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

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


Das Schlüsselwort procedure leitet dabei den Prozedurkopf ein, der im Beispiel nur aus dem Prozedurnamen besteht. Der Anfang des Codes der Prozedur wird durch begin gekennzeichnet, und im Anschluss daran folgt end; um das Prozedurende zu kennzeichnen. Der String Vorname ist hierbei global, also am Programmanfang außerhalb der Prozedur deklariert, wodurch er im gesamten Programm samt aller Prozeduren Gültigkeit besitzt, also an jeder Stelle gelesen und beschrieben werden kann. 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
  Vorname: string;
begin
  Writeln('Wie heisst du?');
  Readln(Vorname);
  Writeln('Hallo '+Vorname+'! 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.

[Bearbeiten] Prozeduren mit Wertübergabe (call by value)

Oft ist es jedoch nötig, der Prozedur beim Aufruf Werte zu übergeben, um Berechnungen auf unterschiedliche Daten anzuwenden oder individuelle Nachrichten auszugeben. Ein Beispiel:


var
  MusterName: string;
  GeschoentesAlter: Integer;

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

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


Der Prozedur NameUndAlterAusgeben werden in runden Klammern zwei durch Semikolon getrennte typisierte Variablen (Vorname: string; AlterMonate: Integer) bereitgestellt, die beim Prozeduraufruf mit Werten belegt werden müssen. Dies kann wie im Beispiel demonstriert mittels Konstanten 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 GeschoentesAlter nicht.

[Bearbeiten] Prozeduren mit Variablenübergabe (call by reference)

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 kubik(var zahl: Double);
begin
  zahl := zahl * zahl * zahl;
end;

begin
  eingabe := 2.5;
  kubik(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 werden in Delphi immer als Zeiger übergeben. Man kann also auf Eigenschaften und Methoden eines Objekts direkt zugreifen:

 procedure ClearLabel(label: TLabel);
 begin
    label.Caption := ''
 end;

[Bearbeiten] Prozeduren/Funktionen mit Const Parameter

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.

[Bearbeiten] Erste Zusammenfassung

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 (genauer: die gesamte Unit); somit besitzen auch Prozeduren Zugriff darauf.
Innerhalb von Prozeduren deklarierte Variablen besitzten 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.

[Bearbeiten] Funktionen

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 Ergebnis-Typ. 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.


Der Funktion wird also 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. Tatsächlich gibt es z.B. in der Windows-Programmierung die Funktion GetForegroundWindow, die einen Zeiger auf das aktive Fenster zurückgibt und dafür keine Werte vom Benutzer benötigt.

[Bearbeiten] Unterprozeduren / Unterfunktionen

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 viel 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, wenn noch nicht geschehen die 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 deren Einsatz sich dadurch noch weiter reduzieren lässt. Im Zusammenhang mit Rekursionen 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.


[Bearbeiten] forward Deklaration

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 Anweisung forward; . Der Prozedur-Rumpf folgt dann 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;

...

[Bearbeiten] Überladene Prozeduren / Funktionen

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 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:

procedure NumberToString(zahl: Int64; var Ausgabe: string); overload;
begin
  Ausgabe := IntToStr(zahl);
end;

procedure NumberToString(zahl: Double; var Ausgabe: string); overload;
begin
  Ausgabe := FloatToStr(zahl);
end;

procedure NumberToString(zahl: Double; var 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.

Noch weitergehende Fähigkeiten kann man mit dem Typ Variant realisieren.

[Bearbeiten] Vorzeitiges Beenden

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:

Info bulb.png

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.


Arrow left.png Pascal: Schleifen Inhaltsverzeichnis Pascal: Typdefinition Arrow right.png
Persönliche Werkzeuge