Kapitel 11: Schleifen und Kontrollanweisungen

Inhaltsverzeichnis

Kapitel 13: Datentypen umwandeln

12. Prozeduren und Funktionen

Bei Prozeduren und Funktionen handelt es sich um sogenannte Unterprogramme. Programmteile, die an mehreren Stellen ausgeführt werden sollen, werden in einen eigenen Bereich ausgelagert. Bei Bedarf springt der Programmablauf in das Unterprogramm, führt es aus und springt wieder zurück.

Der Begriff Unterprogramm ist so zu verstehen, dass der Bereich auch eine völlig eigene Speicherverwaltung besitzt. Auf die Variablen, die im Hauptprogramm definiert sind, kann im Unterprogramm in der Regel nicht zugegriffen werden und umgekehrt. Das ist vorteilhaft, weil sowohl Haupt- als auch Unterprogramm nicht wissen müssen, welche Variablen im jeweils anderen Bereich definiert werden, und sich trotzdem nicht versehentlich in die Quere kommen, indem z. B. das Unterprogramm eine wichtige Variable des Hauptprogrammes überschreibt. Trotzdem können Daten zwischen verschiedenen Programmteilen ausgetauscht werden. Das geschieht in der Regel durch den Einsatz von Parametern.

12.1 Einfache Prozeduren

Eine Prozedur wird in FreeBASIC SUB1 genannt. Jede Prozedur benötigt einen Namen; dabei gelten dieselben Regeln wie für die Namen der Variablen, insb. darf eine Prozedur nicht den Namen einer bereits definierten Variablen erhalten (oder umgekehrt, je nachdem …). Eine sehr einfache Prozedur könnte folgendermaßen aussehen:

SUB halloWelt
  PRINT
  PRINT TAB(20); "| Hallo Welt!"
  PRINT TAB(20); "========================="
END SUB

Beginn und Ende der Prozedur wird durch SUB und END SUB definiert. Hinter dem SUB folgt der Name der Prozedur, in diesem Fall halloWelt. Diese erste Zeile wird auch Kopf der Prozedur genannt. Die drei Zeilen zwischen SUB und END SUB stellen den Rumpf der Prozedur dar.

Wenn Sie das Programm abtippen und starten, sehen Sie — dass Sie nichts sehen. Und das liegt nicht nur an einem fehlenden SLEEP. Die Prozedur wurde zwar definiert und steht von nun an im Programm zur Verfügung, sie wurde aber bisher noch nicht aufgerufen. Nach der Definition der Prozedur wird halloWelt genauso wie ein FreeBASIC-Befehl behandelt und kann dementsprechend an beliebiger Stelle des Programmes aufgerufen werden.

Quelltext 12.1: Hallo Welt als Prozedur
SUB halloWelt
  PRINT
  PRINT TAB(20); "| Hallo Welt!"
  PRINT TAB(20); "========================="
END SUB

halloWelt
PRINT "Gut, dass es dich gibt."
PRINT "Daher gruesse ich dich gleich noch einmal:"
halloWelt
SLEEP
Ausgabe:
                   | Hallo Welt!
                   =========================
Gut, dass es dich gibt.
Daher gruesse ich dich gleich noch einmal:

                   | Hallo Welt!
                   =========================

12.2 Verwaltung von Variablen

Wie eingangs erwähnt, kann auf Variablen, die im Hauptprogramm deklariert wurden, im Unterprogramm nicht zugegriffen werden und umgekehrt. Dadurch läuft das Unterprogramm in einer gesicherten Umgebung, und es kann nicht passieren, dass versehentlich der Ablauf des Hauptprogrammes durcheinander gebracht wird. Wie dennoch eine Kommunikation zwischen beiden Programmteilen stattfinden kann, werden wir uns in diesem Abschnitt ansehen.

12.2.1 Parameterübergabe

Die in Quelltext 12.1 genutzte Möglichkeit, die Hallo-Welt-Ausgabe zu formatieren, ist ja schon einmal recht praktisch — immerhin sind in Zukunft statt drei Zeilen Code nur noch eine Zeile nötig. Allerdings will man vermutlich nicht so häufig die Welt grüßen, sondern stattdessen möglicherweise einen anderen Text ausgeben. Diesen Text werden wir jetzt als Parameter übergeben.

Parameter sind Werte, die beim Aufruf der Prozedur an das Unterprogramm übergeben werden und dort in einer Variablen zur Verfügung stehen. Bei der Definition der Prozedur wird hinter dem Prozedurnamen in Klammern die Parameterliste übergeben. Das ist eine durch Komma getrennte Liste aller Parameter, die beim Aufruf der Prozedur übergeben werden müssen, einschließlich Datentyp. Die Prozedur aus Quelltext 12.1 soll nun so verwendet werden, dass sowohl der auszugebende Text als auch die Einrückung vor der Ausgabe von Aufruf zu Aufruf variiert werden kann. Außerdem wird für die Prozedur auch noch ein passenderer Name ausgewählt.

Quelltext 12.2: Prozedur mit Parameterübergabe
SUB ueberschrift(text AS STRING, einrueckung AS INTEGER)
  PRINT
  PRINT TAB(einrueckung); "| "; text
  PRINT TAB(einrueckung); "========================="
END SUB

ueberschrift("1. Unterprogramme", 1)
PRINT "Ein Unterprogramm kann von jedem beliebigen Programmpunkt aus"
PRINT "aufgerufen werden."
ueberschrift("1.1 Parameteruebergabe", 5)
PRINT "An ein Unterprogramm koennen auch Parameter uebergeben werden."
ueberschrift("1.2 Nutzung der Parameter", 5)
PRINT "Parameter sind innerhalb des gesamten Unterprogrammes gueltig."
SLEEP

Die beiden übergebenen Parameter — beim Prozedur-Aufruf in Zeile 7 ist es die Zeichenkette "1. Unterprogramme" und die Zahl 1 — werden innerhalb der Prozedur den Variablen text und einrueckung zugewiesen und können nun genutzt werden. Jedoch nur innerhalb der Prozedur; außerhalb sind die beiden Variablen dem Programm nicht bekannt.

Ausgabe:
| 1. Unterprogramme
=========================
Ein Unterprogramm kann von jedem beliebigen Programmpunkt aus
aufgerufen werden.

    | 1.1 Parameteruebergabe
    =========================
An ein Unterprogramm koennen auch Parameter uebergeben werden.

    | 1.2 Nutzung der Parameter
    =========================
Parameter sind innerhalb des gesamten Unterprogrammes gueltig.

Beim Aufruf einer Prozedur müssen die Parameter nicht in Klammern stehen (sehr wohl aber bei der Definition). Möglich ist also auch ein Aufruf in der Form:

ueberschrift "1.2 Nutzung der Parameter", 5

Ob die Klammern gesetzt werden oder nicht, ist weitgehend Geschmacksache — ohne Klammern entspricht es eher der gewohnten BASIC-Syntax, während in vielen anderen Programmiersprachen die Klammern grundsätzlich gesetzt werden müssen und eine Reihe an Programmierern daher die Klammern auch in FreeBASIC bevorzugt. Die Klammern sind aber nur beim Prozeduraufruf optional; im Prozedurkopf müssen sie gesetzt werden.

Der Vorteil an der Prozedur ist, neben der Einsparung von zwei Zeilen bei jedem Aufruf, dass eine Änderung nur noch an einer einzigen Stelle vorgenommen wird anstatt an vielen verschiedenen Stellen. Nehmen wir etwa an, wir entschließen uns irgendwann, alle Überschriften gelb zu schreiben. Dazu müssen wir lediglich die Prozedur anpassten; im Hauptprogramm kann alles so bleiben, wie es ist.

SUB ueberschrift(text AS STRING, einrueckung AS INTEGER)
  COLOR 14        ' Schriftfarbe gelb
  PRINT
  PRINT TAB(einrueckung); "| "; text
  PRINT TAB(einrueckung); "========================="
  COLOR 15        ' zurueck zu Schriftfarbe weiss
END SUB

Diese Anpassung an jeder Stelle vorzunehmen, an der eine solche Überschrift angezeigt werden soll, wäre deutlich mühsamer — abgesehen davon, dass eine erhöhte Gefahr besteht, eine Stelle zu vergessen.

Note Hinweis:
Auch wenn Prozeduren eine eigene Verwaltung ihrer Variablen besitzen, können sie dennoch Seiteneffekte aufweisen. Eine Änderung der Schriftfarbe innerhalb der Prozedur etwa bleibt auch außerhalb der Prozedur erhalten.

Grundsätzlich kann jeder Datentyp an eine Prozedur übergeben werden, auch UDTs und Pointer. Bei Pointern wird an den Datentypen ein PTR angehängt, wie es auch schon von der Variablendeklaration bekannt ist. Sogar ganze Prozeduren und Funktionen können als Parameter übergeben werden.

12.2.2 Globale Variablen

Innerhalb eines Unterprogrammes können wie gewohnt mit DIM neue Variablen deklariert werden. Es handelt sich bei ihnen um sogenannte lokale Variablen, die nur in der Umgebung gültig sind, in der sie definiert wurden (also innerhalb des Unterprogrammes). Aber auch im Hauptprogramm definierte Variablen sind lokal — sie gelten nur innerhalb des Hauptprogrammes. Allerdings bietet FreeBASIC auch die Möglichkeit, globale Variablen zu deklarieren, auf die sowohl im Hauptprogramm als auch in allen Unterprogrammen uneingeschränkt zugegriffen werden kann. Diese erhalten bei der Deklaration das zusätzliche Schlüsselwort SHARED.

DIM SHARED AS datentyp variable1, variable2
DIM SHARED variable1 AS datentyp1, variable2 AS datentyp2

Die erste Zeile deklariert wieder mehrere Variablen desselben Datentyps, während die Variablen in der zweiten Zeile unterschiedliche Datentypen besitzen dürfen. SHARED kann jedoch nur im Hauptprogramm angegeben werden, nicht in Unterprogrammen. Das bedeutet kurz gesagt: Wenn Sie in einem Unterprogramm globale Variablen verwenden wollen, müssen diese im Hauptprogramm mit SHARED deklariert werden.

Gerade wenn Sie eine Variable in nahezu allen Ihren Unterprogrammen benötigen, ist SHARED ein praktisches Mittel, eine ständige Übergabe als Parameter zu umgehen. Bei Unachtsamkeit kann es jedoch schnell zu großen Problemen kommen. Quelltext 12.3 demonstriert ein solches Problem: In diesem Beispiel wird davon ausgegangen, dass die Laufvariable i in so vielen Unterprogrammen benötigt wird, dass sie als globale Variable deklariert werden kann. Warum das keine gute Idee ist, sieht man in der Ausgabe.

Quelltext 12.3: Probleme mit unachtsamer Verwendung von SHARED
DIM SHARED i AS INTEGER        ' i ist jetzt global (Haupt- und Unterprogramme)

SUB schreibeSumme1bisX(x AS INTEGER)
  DIM AS INTEGER summe = 0     ' summe ist jetzt lokal (nur im Unterprogramm)
  FOR i = 1 TO x
    summe += i
  NEXT
  PRINT summe
END SUB

PRINT "Berechne die Summe aller Zahlen von 1 bis ..."
FOR i = 1 TO 5
  PRINT i; ": ";
  schreibeSumme1bisX i
NEXT
SLEEP
Ausgabe:
Berechne die Summe aller Zahlen von 1 bis ...
 1:  1
 3:  6
 5:  15

Was ist passiert? Im Hauptprogramm startet die Schleife mit i=1. Während des ersten Schleifendurchlaufs wird das Unterprogramm aufgerufen und auch dort eine Schleife durchlaufen — in diesem Fall von 1 bis 1. Wir erinnern uns, dass nach dem Durchlauf die Laufvariable noch einmal erhöht wird; sie hat nun also den Wert 2. Mit diesen Voraussetzungen kehrt das Programm aus der Prozedur zurück in die Schleife des Hauptprogrammes. i hat sich also während des Aufrufs der Prozedur verändert, und mit diesem veränderten Wert arbeitet die Schleife nun weiter.

Quelltext 12.3 arbeitet also nicht alle Werte ab, die es eigentlich hätte abarbeiten sollen. Es hätte bei „geeigneter“ Programmierung der Prozedur sogar passieren können, dass sich das Programm in einer Endlosschleife verfängt und immer dieselben Werte ausgibt. Bei Verzicht auf die globale Variable i wäre das Problem gar nicht aufgetaucht. Natürlich muss dann i in der Prozedur neu deklariert werden.

Quelltext 12.4: Korrekte Berechnung aller Summen
DIM i AS INTEGER               ' i ist jetzt lokal (Hauptprogramm)

SUB schreibeSumme1bisX(x AS INTEGER)
  DIM AS INTEGER summe = 0, i  ' summe und i sind lokal (Unterprogramm)
  FOR i = 1 TO x
    summe += i
  NEXT
  PRINT summe
END SUB

PRINT "Berechne die Summe aller Zahlen von 1 bis ..."
FOR i = 1 TO 5
  PRINT i; ": ";
  schreibeSumme1bisX i
NEXT
SLEEP
Ausgabe:
Berechne die Summe aller Zahlen von 1 bis ...
 1:  1
 2:  3
 3:  6
 4:  10
 5:  15

Im Übrigen sind solche Fallstricke der Grund, warum die Laufvariablen in diesem Buch meist im Schleifenkopf neu deklariert werden (FOR i AS INTEGER). Auch dadurch lässt sich sicherstellen, dass sie nicht versehentlich in Konflikt mit einer gleichnamigen Variablen gerät.

Selbst wenn Sie eine Variable global deklarieren, können Sie sie in einem Unterprogramm oder einer anderen Blockstruktur neu deklarieren. In diesem Fall „vergisst“ das Programm die globale Variable solange, bis die Blockstruktur verlassen wird.

Noch eine letzte Anmerkung: Im Hauptprogramm definierte Konstanten gelten global, sind also auch im Unterprogramm verfügbar. Da Konstanten nicht mehr verändert werden können, kann es hier auch nicht zum Konflikt zwischen zwei Programmteilen kommen.

12.2.3 Statische Variablen

Statische Variablen sind ein weiteres Konzept, das bei Unterprogrammen zum Tragen kommen kann. Man versteht darunter Variablen, die wie lokale Variablen nur innerhalb des Unterprogrammes gültig sind, deren Wert aber nach Beendigung des Unterprogrammes nicht verloren geht, sondern gespeichert wird. Wenn dasselbe Unterprogramm erneut aufgerufen wird, erhält die Variable den zuletzt gespeicherten Wert wieder zurück. In einem einfachen Beispiel soll eine statische Variable eingesetzt werden, um lediglich zu zählen, wie oft die Prozedur bereits aufgerufen wurde.

Quelltext 12.5: Statische Variable in einem Unterprogramm
SUB zaehler
  STATIC AS INTEGER i = 0
  i += 1
  PRINT "Das ist der"; i; ". Aufruf der Prozedur."
END SUB

' Prozedur dreimal aufrufen
zaehler
zaehler
PRINT "und ein letztes Mal:"
zaehler
SLEEP
Ausgabe:
Das ist der 1. Aufruf der Prozedur.
Das ist der 2. Aufruf der Prozedur.
und ein letztes Mal:
Das ist der 3. Aufruf der Prozedur.

Um eine statische Variable zu deklarieren, wird das Schlüsselwort STATIC statt DIM verwendet. Hier besteht die einzige Möglichkeit, der Variablen einen Initialwert zuzuweisen (tut man es nicht, wird die Variable standardmäßig mit dem Wert 0 belegt). Diese Zuweisung wird nur beim allerersten Aufruf der STATIC-Zeile durchgeführt und später ignoriert — aus diesem Grund wird in Quelltext 12.5 i nicht ständig wieder auf 0 gesetzt. Sie können die Auswirkung des Initialwerts gern testen, indem Sie den Wert verändern. Spätere Zuweisungen erfolgen dagegen bei jedem Durchlauf, weshalb folgende Variante nicht wie gewünscht funktioniert:

' ...
  STATIC AS INTEGER i
  i = 0
' ...

Da i bei jedem Durchlauf gleich nach der Deklaration auf 0 gesetzt wird, ist der eigentliche Zweck, den Wert zwischen den Prozedur-Aufrufen zu speichern, hinfällig.

Wenn alle im Unterprogramm deklarierten Variablen statisch sein sollen, kann man der Einfachheit halber das Unterprogramm selbst als statisch deklarieren. Quelltext 12.5 lässt sich dann auch folgendermaßen schreiben:

Quelltext 12.6: Statisches Unterprogramm
SUB zaehler STATIC
  DIM AS INTEGER i = 0
  i += 1
  PRINT "Das ist der"; i; ". Aufruf der Prozedur."
END SUB

' Prozedur dreimal aufrufen
zaehler
zaehler
PRINT "und ein letztes Mal:"
zaehler
SLEEP

STATIC steht in dieser Version ganz am Ende der Kopfzeile des Unterprogrammes — also ggf. hinter der Parameterliste. Die Variablendeklaration kann nun über DIM erfolgen (allerdings ist auch STATIC möglich), und wieder wird der Initialwert nur beim ersten Durchlauf berücksichtigt.

Note Hinweis:
Da statische Variablen ständig im Speicher bereitgehalten werden müssen, sollten Sie nur solche Variablen als statisch deklarieren, die tatsächlich statisch sein sollen.

12.3 Unterprogramme bekannt machen

Wie bereits gesagt wurde, muss ein Unterprogramm im Programm bekannt sein, bevor es aufgerufen werden kann. Wenn die Prozedur, wie in den obigen Beispielen, ganz zu Beginn des Programmes definiert wird, ist sie anschließend auch verfügbar. Es gibt jedoch einige Fälle, in denen das Definieren „ganz am Anfang“ nicht möglich ist.

12.3.1 Die Deklarationszeile

Jedes Unterprogramm vor seinem ersten Aufruf zu definieren, stößt sehr schnell an eine logische Grenze. Wenn das Unterprogramm A das Unterprogramm B aufruft, muss Unterprogramm B zuerst definiert werden, um beim Aufruf in Unterprogramm A bereits bekannt zu sein. Was passiert nun aber, wenn sich beide Unterprogramme gegenseitig aufrufen wollen?

An dieser Stelle kommt die DECLARE-Zeile ins Spiel. Sie ist vom Aufbau identisch mit der Kopfzeile des Unterprogrammes, nur mit vorangestelltem Schlüsselwort DECLARE. Allerdings wird der Inhalt des Unterprogrammes an dieser Stelle noch nicht festgelegt. Die DECLARE-Zeile dient nur dazu, dem Compiler mitzuteilen: Irgendwann später wird die Definition eines Unterprogrammes mit diesem Namen und dieser Parameterliste folgen. Ein Aufruf kann dann auch schon erfolgen, bevor das Unterprogramm festgelegt wird — der Compiler kennt ja bereits seinen Namen.

Quelltext 12.7: Deklarieren einer Prozedur
DECLARE SUB ueberschrift(text AS STRING, einrueckung AS INTEGER)

ueberschrift("1. Unterprogramme", 1)
PRINT "Ein Unterprogramm kann von jedem Programmpunkt aus aufgerufen werden."
ueberschrift("1.1 Parameteruebergabe", 5)
PRINT "An ein Unterprogramm koennen auch Parameter uebergeben werden."
ueberschrift("1.2 Nutzung der Parameter", 5)
PRINT "Parameter sind innerhalb des gesamten Unterprogrammes gueltig."
SLEEP

SUB ueberschrift(text AS STRING, einrueckung AS INTEGER)
  PRINT
  PRINT TAB(einrueckung); "| "; text
  PRINT TAB(einrueckung); "========================="
END SUB

Quelltext 12.7 bewirkt dasselbe wie Quelltext 12.2. In der ersten Zeile wird die Prozedur deklariert (aber noch nicht definiert, d. h. noch nicht inhaltlich festgelegt). Die Definition der Prozedur kann nun (fast) an beliebiger Stelle des Programmes stattfinden: zu Beginn (nach der DECLARE-Zeile) oder ganz am Ende wie in Quelltext 12.7 — sogar irgendwann mitten im Hauptprogramm, was aber im Sinne der Übersichtlichkeit keinesfalls empfehlenswert wäre. Eine sehr häufige Vorgehensweise ist die Deklaration möglichst früh im Programm und die Definition der Unterprogramme ganz am Ende.

Nun lassen sich auch Prozeduren realisieren, die sich wechselseitig aufrufen.2 Quelltext 12.8 ist kein übermäßig sinnvolles Programm, sollte aber das Prinzip veranschaulichen.

Quelltext 12.8: PingPong
DECLARE SUB ping(anzahl AS INTEGER)
DECLARE SUB pong(anzahl AS INTEGER)

ping 3

SUB ping(anzahl AS INTEGER)
  PRINT "ping ist bei"; anzahl
  IF anzahl < 1 THEN                  ' Abbruchbedingung, damit der gegen-
    PRINT "Kein Aufruf von pong"      ' seitige Aufruf nicht ewig laeuft
  ELSE
    PRINT "pong wird aufgerufen"
    pong anzahl-1
  END IF
END SUB
SUB pong(anzahl AS INTEGER)
  PRINT "pong ist bei"; anzahl
  IF anzahl < 1 THEN
    PRINT "Kein Aufruf von ping"
  ELSE
    PRINT "ping wird aufgerufen"
    ping anzahl-1
  END IF
END SUB
Ausgabe:
ping ist bei 3
pong wird aufgerufen
pong ist bei 2
ping wird aufgerufen
ping ist bei 1
pong wird aufgerufen
pong ist bei 0
Kein Aufruf von ping

12.3.2 Optionale Parameter

Uns sind bisher bereits FreeBASIC-eigene Anweisungen begegnet, bei denen nicht alle Parameter angegeben werden mussten; z. B. bei LOCATE und COLOR. Solche optionalen Parameter sind auch bei eigenen Unterprogrammen möglich. Dazu muss dem Compiler mitgeteilt werden, welcher Wert für den Parameter verwendet werden soll, wenn dieser beim Aufruf nicht mit angegeben wurde. Beispielsweise könnte man in Quelltext 12.2 einen zusätzlichen Parameter für die Überschriftsfarbe einführen. Wird sie nicht angegeben, verwendet die Prozedur den Standardwert 14 (gelb).

Quelltext 12.9: Optionale Parameter
DECLARE SUB ueberschrift(txt AS STRING, einrueck AS INTEGER, farbe AS INTEGER = 14)

ueberschrift "1. Unterprogramme",      1, 10  ' Ueberschrift in gruen
ueberschrift "1.1 Parameteruebergabe", 5      ' Ueberschrift in gelb
SLEEP

SUB ueberschrift(text AS STRING, einrueckung AS INTEGER, farbe AS INTEGER = 14)
  COLOR farbe     ' angegebene Schriftfarbe
  PRINT
  PRINT TAB(einrueckung); "| "; text
  PRINT TAB(einrueckung); "========================="
  COLOR 15        ' zurueck zu Schriftfarbe weiss
END SUB

Die Wertzuweisung für den bzw. die optionalen Parameter muss sowohl in der DECLARE-Zeile als auch in der Kopfzeile des Unterprogrammes stehen, und natürlich sollte in beiden Zeilen derselbe Wert angegeben werden.

Optionale Parameter bieten sich vor allem am Ende der Parameterliste an, weil sie dort am einfachsten weggelassen werden können. Um einen optionalen Parameter auszulassen, der mitten in der Liste steht, müssen (genauso wie z. B. beim Auslassen des ersten Parameters von COLOR) die korrekte Anzahl an Kommata gesetzt werden.

Note Hinweis:
In Quelltext 12.9 wurden in der DECLARE-Zeile zwei Parameternamen gekürzt, um den Seitenrand nicht zu sprengen. Die Parameternamen in der DECLARE-Zeile müssen nicht mit denen des Prozedurkopfs übereinstimmen (sie könnten sogar ganz weggelassen werden). Für die Variablen im Funktionsrumpf sind lediglich die Namen im Kopf entscheidend. Wichtig ist aber, dass in der DECLARE-Zeile die korrekten Datentypen angegeben werden.

12.3.3 OVERLOAD

Häufig benötigt man zwei oder mehrere völlig verschiedene Parameterlisten, will jedoch bei einem einheitlichen Namen für das Unterprogramm bleiben. Wir wollen eine weitere Überschrift-Prozedur bauen, bei der die Einrückung nicht durch einen Zahlenwert, sondern durch einen Einrückungs-String festgelegt werden soll, der dunkelgrau ausgegeben wird (das Beispiel ist zugegebenermaßen etwas konstruiert, ist aber zur Veranschaulichung gut geeignet). Die Prozedur soll weiterhin ueberschrift heißen, da es sich ja prinzipiell um dieselbe Art von Anweisung handelt.

SUB ueberschrift(text AS STRING, einrueckung AS STRING, farbe AS INTEGER = 14)
  PRINT
  COLOR 8     : PRINT einrueckung;    ' Einrueckungs-String in dunklem Grau
  COLOR farbe : PRINT "| "; text      ' Text in angegebener Schriftfarbe
  COLOR 8     : PRINT einrueckung;
  COLOR farbe : PRINT "========================="
  COLOR 15        ' zurueck zu Schriftfarbe weiss
END SUB

Wenn Sie nun beide Versionen der Prozedur untereinander schreiben und zu compilieren versuchen, erhalten Sie die Fehlermeldung:

error 4: Duplicated definition

Auch ein Hinzufügen der DECLARE-Zeilen behebt das Problem noch nicht. Sie müssen dem Compiler mitteilen, dass er mehrere Unterprogramme desselben Namens zu erwarten hat. Dazu fügen Sie in der ersten DECLARE-Zeile hinter dem Prozedurnamen das Schlüsselwort OVERLOAD (zu deutsch: Überladung) hinzu. Egal, wie viele Unterprogramme mit gleichem Namen Sie letztlich bereitstellen wollen: Das Schlüsselwort OVERLOAD muss und darf nur in der DECLARE-Zeile des ersten dieser Unterprogramme auftauchen.

Da bei uns beide Überschrifts-Prozeduren größtenteils dasselbe machen, lohnt es sich übrigens, die Arbeit der zweiten Prozedur in die erste auszulagern.

Quelltext 12.10: Überladene Prozeduren (OVERLOAD)
DECLARE SUB ueberschrift OVERLOAD(t AS STRING, e AS INTEGER, f AS INTEGER = 14)
DECLARE SUB ueberschrift(txt AS STRING, einrueck AS STRING, farbe AS INTEGER = 14)

ueberschrift "1. Unterprogramme",      1, 10    ' Ueberschrift in gruen
ueberschrift "1.1 Parameteruebergabe", "~~~~~"  ' Ueberschrift in gelb
SLEEP

SUB ueberschrift(text AS STRING, einrueckung AS INTEGER, farbe AS INTEGER = 14)
  ueberschrift text, SPACE(einrueckung), farbe
  ' SPACE(x) erzeugt einen String, der aus x Leerzeichen besteht.
  ' Diese Prozedur macht also nichts weiter, als die folgende Prozedur
  ' anzuweisen, zur Einrueckung Leerzeichen zu verwenden.
END SUB
SUB ueberschrift(text AS STRING, einrueckung AS STRING, farbe AS INTEGER = 14)
  PRINT
  COLOR 8     : PRINT einrueckung;    ' Einrueckungs-String in dunklem Grau
  COLOR farbe : PRINT "| "; text      ' Text in angegebener Schriftfarbe
  COLOR 8     : PRINT einrueckung;
  COLOR farbe : PRINT "========================="
  COLOR 15        ' zurueck zu Schriftfarbe weiss
END SUB

Hinweis: Die andere Parameterbezeichnung in der ersten DECLARE-Zeile wurde nur deshalb so gewählt, dass der Quelltext nicht den Seitenrahmen sprengt.

Der erste ueberschrift-Aufruf in Zeile 4 springt in das erste der beiden Unterprogramme, der zweite Aufruf in Zeile 5 dagegen in das zweite. Dies ist klar festgelegt, da sich der Datentyp des zweiten Parameters unterscheidet und daher immer nur eine der beiden Prozeduren infrage kommt. Bei der Verwendung von OVERLOAD ist immer darauf zu achten, dass der Compiler zweifelsfrei entscheiden kann, wann er welches der Unterprogramme aufzurufen hat. Das geschieht durch eine unterschiedliche Parameterzahl und/oder durch verschiedene Datentypen. Der Name des Unterprogramms zusammen mit der Parameterliste wird auch als Signatur des Unterprogramms bezeichnet — diese Signatur muss innerhalb eines Programmes eindeutig sein.

Note Hinweis:
Achten Sie auch bei gleichzeitiger Verwendung von OVERLOAD und optionalen Parametern darauf, dass die Eindeutigkeit gewahrt bleibt.

12.4 Funktionen

Funktionen sind, wie Prozeduren, Unterprogramme, nur mit einem Unterschied: Eine Funktion gibt einen Wert zurück. Die Stelle mit dem Funktionsaufruf wird dann durch diesen Rückgabewert ersetzt. Der Funktionsaufruf kann daher an jeder Stelle stehen, an der überhaupt ein Wert mit dem Datentyp des Rückgabewerts stehen kann.

Als Beispiel soll die Funktion mittelwert definiert werden, der zwei Zahlen übergeben werden; die Funktion gibt dann den Wert zurück. der in der Mitte beider Parameter liegt (arithmetisches Mittel). Zuerst müssen wir uns entscheiden, welche Datentypen wir verwenden wollen; zunächst einmal erlauben wir für die Parameter nur die Übergabe von INTEGER-Werten. Der Mittelwert kann natürlich eine Gleitkommazahl sein. Einfache Genauigkeit reicht jedoch aus, da als Nachkomma-Anteil nur .0 oder .5 herauskommen kann. Die Deklaration der Funktion sieht dann so aus:

DECLARE FUNCTION mittelwert(a AS INTEGER, b AS INTEGER) AS SINGLE

Statt SUB steht jetzt FUNCTION, und am Ende hinter der schließenden Klammer ist noch AS datentyp nötig, womit der Datentyp des Rückgabewerts angegeben wird. Innerhalb der Funktion muss der Rückgabewert nun noch festgelegt werden. Das kann z. B. mit RETURN geschehen.

FUNCTION mittelwert(a AS INTEGER, b AS INTEGER) AS SINGLE
  RETURN (a + b) / 2
END FUNCTION

Mit RETURN geschieht zweierlei: Einerseits wird der Rückgabewert zugewiesen, andererseits wird die Funktion verlassen. Weitere Zeilen hinter dem RETURN würden also nicht mehr ausgeführt werden. Es gibt noch zwei weitere Möglichkeiten, den Rückgabewert zuzuweisen: durch die Anweisung FUNCTION = (a + b) / 2 und durch mittelwert = (a + b) / 2. Die erste Version ist ein feststehender Ausdruck, während in mittelwert =… ggf. der Funktionsname angepasst werden muss. In beiden Fällen wird die Funktion zunächst noch nicht verlassen. Diese beiden Zuweisungsmöglichkeiten sind historisch bedingt; sie werden aus Kompatibilitätsgründen weiter unterstützt, aber nicht mehr häufig eingesetzt.

Es fehlt noch ein Aufruf der Funktion, und das Beispielprogramm ist fertig. Da ein SINGLE-Wert zurückgegeben wird, kann dieser in einer passenden Variablen gespeichert oder aber gleich mit PRINT ausgegeben werden.

Quelltext 12.11: Arithmetisches Mittel zweier Werte
DECLARE FUNCTION mittelwert(a AS INTEGER, b AS INTEGER) AS SINGLE
DIM AS INTEGER w1, w2

INPUT "Geben Sie, durch Komma getrennt, zwei Ganzzahlen ein: ", w1, w2
PRINT "Der Mittelwert von"; w1; " und"; w2; " ist"; mittelwert(w1, w2)
SLEEP

FUNCTION mittelwert(a AS INTEGER, b AS INTEGER) AS SINGLE
  RETURN (a + b) / 2
END FUNCTION
Ausgabe:
Geben Sie, durch Komma getrennt, zwei Ganzzahlen ein: 10,3
Der Mittelwert von 10 und 3 ist 6.5

Sämtliche Möglichkeiten, die für Prozeduren zur Verfügung stehen, gibt es auch für Funktionen; der einzige Unterschied zwischen diesen beiden Arten von Unterprogrammen ist der, dass die Funktion einen Wert zurückgeben kann (bzw. muss). Wenn innerhalb der Funktion keine Zuweisung des Rückgabewerts erfolgt, gibt der Compiler eine Warnung aus. Allerdings wird dazu keine Laufzeitüberprüfung durchgeführt, d. h. der Compiler prüft lediglich, ob im Funktionsrumpf eine Zuweisung wie etwa RETURN wert vorkommt. Ob diese Zuweisung dann im speziellen Fall tatsächlich aufgerufen wird, kann er nicht überprüfen.

Der Rückgabewert einer Funktion kann auch verworfen werden. Dazu wird die Funktion genauso aufgerufen wie eine Prozedur, also ohne dass ihr Rückgabewert in einer Variablen gespeichert oder anderweitig ausgewertet wird.

Note Hinweis:
Während beim Aufruf von Prozeduren keine Klammern um die Parameterliste gesetzt werden müssen, sind Klammern bei Funktionen unbedingt erforderlich — außer Sie verwerfen den Rückgabewert.

12.5 Weitere Eigenschaften der Parameter

12.5.1 Übergabe von Arrays

Die Übergabe einer großen Datenmenge — insbesondere auch, wenn die Anzahl der Elemente nicht von vornherein feststeht — kann recht bequem über ein Array erfolgen. Dazu muss in der Parameterliste an den Array-Namen lediglich ein Klammer-Paar angehängt werden — allerdings ohne Angabe der Array-Grenzen. Es ist dabei egal, wie viele Dinensionen das Array besitzt; die Übergabe erfolgt immer nur durch den Array-Namen mit dem angehängten Klammer-Paar.

Den Mittelwert aller Werte eines Arrays könnte man folgendermaßen berechnen:

Quelltext 12.12: Arithmetisches Mittel aller Werte eines Arrays
DECLARE FUNCTION mittelwert(w() AS INTEGER) AS SINGLE

DIM AS INTEGER werte(...) = { 25, 412, -19, 32, 112 }
PRINT "Der Mittelwert der festgelegten Werte ist "; mittelwert(werte())

FUNCTION mittelwert(w() AS INTEGER) AS SINGLE
  IF UBOUND(w) < LBOUND(w) THEN RETURN 0  ' Fehler: Array nicht dimensioniert
  DIM AS INTEGER summe = 0
  FOR i AS INTEGER = LBOUND(w) TO UBOUND(w)
    summe += w(i)
  NEXT
  RETURN summe / (UBOUND(w) - LBOUND(w) + 1)
END FUNCTION
Ausgabe:
Der Mittelwert der festgelegten Werte ist 112.4

Die Funktion hat den Vorteil, dass Start- und Endwert nicht festgelegt sind. Sie reagiert sehr flexibel auf das übergebene Array. Zumindest funktioniert das gut, solange sichergestellt ist, dass ein eindimensionales Array übergeben wird. Die Funktion erkennt die Anzahl der Dimensionen nicht. Bei Übergabe eines mehrdimensionalen Array wird kein Compilerfehler erzeugt, sondern die Funktion behandelt die im Array-Speicher liegenden Werte so, als ob es ein eindimensionales Array wäre. Das zurückgegebene Resultat ist dann mit großer Wahrscheinlichkeit sinnlos.

Natürlich kann die Funktion vor dem Start der Berechnung prüfen, ob die Anzahl der Dimensionen UBOUND(w, 0)=1 beträgt. Man kann aber auch den Compiler anweisen, nur eine bestimmte Anzahl an Dimensionen zuzulassen. Bei einer falschen Dimensionenzahl würde das Programm dann gar nicht compilieren. Wenn man diese Möglichkeit nutzen will, muss für jede Dimension ein ANY angegeben werden.

DECLARE FUNCTION f1(w(ANY) AS datentyp) AS datentyp       ' nur eindimensional
DECLARE FUNCTION f2(w(ANY, ANY) AS datentyp) AS datentyp  ' nur zweidimensional
' und so weiter

Analog dazu muss dann auch die Kopfzeile des Unterprogrammes mit der korrekten Anzahl an ANY versehen werden. Nun kann an die Funktion f1 nur noch ein eindimensionales Array übergeben werden und an die Funktion f2 nur noch ein zweidimensionales Array; eine falsche Dimensionenzahl führt zu einem Compilierfehler.

Als komplettes Programm könnte das dann so aussehen:

Quelltext 12.13: Arithmetisches Mittel für ein eindimensionales(!) Array
DECLARE FUNCTION mittelwert(w(ANY) AS INTEGER) AS SINGLE

DIM AS INTEGER werte(...) = { 25, 412, -19, 32, 112 }
PRINT "Der Mittelwert der festgelegten Werte ist "; mittelwert(werte())

FUNCTION mittelwert(w(ANY) AS INTEGER) AS SINGLE
  IF UBOUND(w) < LBOUND(w) THEN RETURN 0  ' Fehler: Array nicht dimensioniert
  DIM AS INTEGER summe = 0
  FOR i AS INTEGER = LBOUND(w) TO UBOUND(w)
    summe += w(i)
  NEXT
  RETURN summe / (UBOUND(w) - LBOUND(w) + 1)
END FUNCTION
Ausgabe:
Der Mittelwert der festgelegten Werte ist 112.4

Es ist also nicht möglich, die Länge der Array-Parameter festzulegen — nur die Anzahl der Dimensionen kann vorgegeben werden. Wenn es notwendig ist, dass das Array eine genau definierte untere bzw. obere Grenze besitzt, überprüfen Sie die Grenzen mit LBOUND() bzw. UBOUND() und brechen Sie das Unterprogramm ab, wenn die Grenzen nicht passen.

Während die Übergabe eines oder mehrerer Arrays kein Problem darstellt, ist die Rückgabe eines Arrays als Funktionswert nicht möglich. Beim Rückgabewert darf es sich aber selbstverständlich um ein UDT handeln (vgl. Kapitel 7), welches auch ein Array beinhalten kann. Eine weitere Lösung werden wir uns im nächsten Abschnitt ansehen.

12.5.2 BYREF und BYVAL

Über eine wichtige Frage haben wir uns bisher noch keine Gedanken gemacht: Was passiert eigentlich mit einem Parameter, wenn er innerhalb des Unterprogrammes verändert wird? Ändert sich dann auch sein Wert außerhalb der Funktion? Hierbei spielt es eine entscheidende Rolle, ob der Parameter nur als Wert übergeben wird oder als Referenz auf den Variablenwert. Im ersten Fall wird gewissermaßen eine Kopie der Variablen erstellt und diese übergeben. Was auch immer das Unterprogramm mit dieser Kopie anstellt, das Original bleibt davon unberührt. Wird aber die Variable selbst übergeben (oder genauer gesagt eine Referenz auf seine Speicherstelle), dann bleiben Änderungen innerhalb des Unterprogrammes auch nach dessen Beendigung erhalten. Es wurde ja direkt das Original verändert.

FreeBASIC unterstützt beide Konzepte. Parameter können mit den Schlüsselwörtern BYREF oder BYVAL versehen werden, um zu kennzeichnen, ob die Übergabe by reference (also als Original) oder by value (als Wert, also als Kopie) übergeben werden sollen.

Quelltext 12.14: BYREF und BYVAL
SUB doppel(BYREF a AS INTEGER, BYVAL b AS INTEGER, c AS INTEGER)
  ' Werte verdoppeln
  a *= 2 : b *= 2 : c *= 2
  ' Zwischenergebnis ausgeben
  PRINT "In der Prozedur:"
  PRINT "a = "; a,
  PRINT "b = "; b,
  PRINT "c = "; c
  PRINT
END SUB

DIM AS INTEGER a = 3, b = 5, c = 7
PRINT "Vor der Prozedur:"
PRINT "a = "; a,
PRINT "b = "; b,
PRINT "c = "; c
PRINT

doppel a, b, c
PRINT "Nach der Prozedur:"
PRINT "a = "; a,
PRINT "b = "; b,
PRINT "c = "; c
SLEEP
Ausgabe:
Vor der Prozedur:
a =  3        b =  5        c =  7

In der Prozedur:
a =  6        b =  10       c =  14

Nach der Prozedur:
a =  6        b =  5        c =  7

Jeder Parameter wird innerhalb der Funktion verdoppelt. Wie Sie sehen, „vergisst“ das Programm die Änderung von b, wenn die Prozedur verlassen wird, während die Verdopplung von a beibehalten wird. Wird weder BYREF noch BYVAL angegeben, dann wird die Art der Parameterübergabe automatisch ermittelt:

  • Zahlen-Datentypen (Ganzzahlen, Gleitkommazahlen) werden BYVAL übergeben. Das entspricht dem in der Regel gewünschten Verhalten, dass Unterprogramme eine eigene Speicherumgebung verwenden.

  • Strings und UDTs werden BYREF übergeben. Das liegt daran, dass das Kopieren eines Strings bzw. eines UDTs deutlich mehr Arbeit erfordert als das Kopieren eines Zahlen-Datentyps.

  • Arrays, egal welcher Art, werden immer BYREF übergeben. Für sie ist eine Übergabe by value nicht möglich; die Angabe von BYVAL (und auch von BYREF) führt zu einem Compiler-Fehler.

Tip Unterschiede zu QuickBASIC:
Das Standardverhalten von FreeBASIC unterscheidet sich von QuickBASIC und auch von älteren FreeBASIC-Versionen (bis v0.16). Dort wurden alle nicht explizit ausgezeichneten Parameter BYREF übergeben. In den Dialektformen -lang qb, -lang deprecated und -lang fblite wird dieses ältere Verhalten gewählt.

Wenn Sie eine größere Kompatibilität zu anderen Dialektformen oder anderen Sprachen erreichen wollen, empfiehlt es sich, unabhängig vom Standardverhalten alle Parameter, bei denen eine bestimmte Übergabeart erforderlich ist, entsprechend mit BYREF oder BYVAL auszuzeichnen (außer Arrays, bei denen diese Angabe ja nicht erlaubt ist). Betroffen sind dabei alle Parameter, deren Wert innerhalb des Unterprogrammes verändert wird. Parameter, die im Unterprogramm nicht verändert werden, brauchen nicht gesondert ausgezeichnet zu werden.

Beachten Sie auch, dass die Angaben zu BYREF und BYVAL sowohl in der DECLARE-Zeile als auch in der Kopfzeile des Unterprogrammes erfolgen sollten.

12.5.3 Parameterübergabe AS CONST

Auch wenn ein Array nicht BYVAL übergeben werden kann, könnten Sie ein berechtigtes Interesse daran haben, eine Veränderung durch das Unterprogramm zu verhindern. Wenn Sie hinter einem Parameternamen, zwischen dem AS und der Angabe für den Datentyp, ein CONST einfügen, kann auf diesen Parameter innerhalb des Unterprogrammes nur lesend zugegriffen werden. Sollten Sie dennoch versuchen, den Wert des Parameters zu ändern, quittiert der Compiler dies durch eine Fehlermeldung.3 AS CONST ist jedoch nicht auf Arrays beschränkt.

Quelltext 12.15: Parameterübergabe AS CONST
SUB prozedur(a AS INTEGER, b AS CONST INTEGER)
  ' Lesender Zugriff ist auf jeden Fall moeglich.
  PRINT a, b

  ' Auf a kann auch schreibend zugegriffen werden.
  a = 3
  ' Versucht man jedoch, b einen Wert zuzuweisen, erfolgt ein Fehler.
  ' b = 4
END SUB

In der abgedruckten Form stellt Quelltext 12.15 einen korrekten Quellcode dar. Wenn Sie jedoch das Kommentarzeichen in der vorletzten Zeile entfernen, können Sie den Codeschnipsel nicht mehr compilieren.

12.5.4 Aufrufkonvention für die Parameter

Damit die Parameter vom aufrufenden Befehl zum aufgerufenen Unterprogramm gelangen, werden sie zunächst der Reihe nach in den Stack gelegt (das ist ein Stapelspeicher, aus dem immer das zuletzt hineingelegte Element als erstes wieder herausgenommen wird — vergleichbar mit einem Stapel Teller, auf den man immer nur oben einen Teller hinzufügen oder herunternehmen kann) und innerhalb des Unterprogrammes wieder ausgelesen. Da sich FreeBASIC selbständig um die Verwaltung des Stacks kümmert, brauchen wir uns meistens mit den Details nicht auseinanderzusetzen. Gelegentlich werden sie jedoch wichtig.

Zunächst einmal ist wichtig, welcher Parameter zuerst in den Stack gelegt wird (und damit als letztes wieder ausgelesen werden kann). Dazu gibt es drei verschiedene Aufrufkonventionen:

  • STDCALL legt die Parameter von rechts nach links in den Stack, d. h. der letzte Parameter liegt dann ganz unten im Stack. STDCALL ist die Standard-Aufrufkonvention der WinAPI und daher auch von FreeBASIC in Windows-Betriebssystemen.

  • CDECL legt ebenfalls die Parameter von rechts nach links in den Stack. Allerdings muss die Prozedur in dieser Konvention den Stack nicht selbständig abbauen — das ist Aufgabe des aufrufenden Codes. CDECL ist die Standard-Aufrufkonvention unter Linux, BSD und DOS und wird daher auf diesen Plattformen auch von FreeBASIC verwendet.

  • PASCAL legt die Paramter von links nach rechts in den Stack, d. h. der letzte Parameter liegt dann ganz oben im Stack. Auch hier muss die Prozedur den Stack nicht selbständig abbauen. PASCAL ist die Standard-Aufrufkonvention unter QuickBASIC.

Wichtig werden die Unterschiede vor allem bei der Verwendung externer Bibliotheken, da hier natürlich sichergestellt werden muss, dass sich die Bibliothek und das eigene Programm über die Konvention einig sind. Es gibt FreeBASIC-intern aber ebenfalls einen Fall, in dem die Aufrufkonvention wichtig wird, nämlich bei der Verwendung variabler Parameterlisten, auf die wir gleich zu sprechen kommen.

Da nun zwischenzeitlich doch schon eine ganze Menge an Informationen zu den Unterprogrammen zusammengekommen ist (und das sind noch nicht alle), hier noch einmal zusammenfassend der Zwischenstand zum Aufbau der Syntax:

' Deklaration einer Prozedur bzw. einer Funktion
DECLARE SUB Name [Aufrufkonvention] [OVERLOAD] _
        ([{BYVAL|BYREF}] Parameter AS Typ [= Wert] [, ...])
DECLARE FUNCTION Name [Aufrufkonvention] [OVERLOAD] _
        ([{BYVAL|BYREF}] Parameter AS Typ [= Wert] [, ...]) AS Typ

' Definition einer Prozedur bzw. einer Funktion
SUB Name [Aufrufkonvention] ([{BYVAL|BYREF}] Parameter AS Typ [= Wert] [, ...])
  ' Prozedur-Rumpf
END SUB
FUNCTION Name [Aufrufkonvention] ([{BYVAL|BYREF}] Parameter AS Typ [= Wert] _
        [, ...]) AS Typ
  ' Funktions-Rumpf
END FUNCTION
  • Name ist der Name des Unterprogramms, unter dem es angesprochen werden kann.

  • Aufrufkonvention ist einer der Schlüsselwörter STDCALL, CDECL und PASCAL. Wird sie ausgelassen, verwendet FreeBASIC plattformabhängig STDCALL oder CDECL.

  • OVERLOAD wird benötigt, wenn Sie mehrere Unterprogramme mit gleichem Namen definieren wollen. Die Signatur (bestehend aus dem Namen des Unterprogramms, der Anzahl und der Typen der Parameter sowie bei Funktionen der Typ des Rückgabewerts) muss jedoch eindeutig sein.

  • BYREF bzw. BYVAL geben an, ob der folgende Parameter by reference oder by value übergeben wird. Ohne Angabe hängt die Art der Übergabe vom Datentyp ab.

  • Es können beliebig viele Parameter übergeben werden (auch keiner). Die Wertzuweisung ist optional; sie bewirkt, dass der Parameter beim Aufrauf auch ausgelassen werden kann.

  • Bei Funktionen muss noch der Datentyp des Rückgabewertes angegeben werden.

12.5.5 Variable Parameterlisten

12.5.5.1 Abfrage variabler Parameterlisten

Sie können, wenn Sie wollen, auch eine vollkommen freie Parameterliste verwenden, womit Sie beim Aufruf des Unterprogrammes weder in der Anzahl noch bei den Datentypen der Parameter eingeschränkt sind. Der Umgang mit einer variablen Parameterliste ist allerdings deutlich schwerer als mit den bisher behandelten „normalen“ Parameterlisten, und er ist definitiv nicht anfängerfreundlich.

Die Kopfzeile eines solchen Unterprogrammes sieht z. B. folgendermaßen aus:

SUB variabel CDECL (x AS INTEGER, y AS INTEGER, ...)
Warning Achtung:
Beachten Sie die Aufrufkonvention CDECL: Sie ist für die Verwendung von variablen Parameterlisten dringend erforderlich, da bei STDCALL nicht sichergestellt werden kann, dass die Prozedur den erforderten Abbau des Stacks tatsächlich durchführt.

Die drei Punkte (Ellipsis) kennzeichnen den variablen Teil der Parameterliste. Das bedeutet, dass in diesem Beispiel die beiden Parameter x und y festgelegt sind und angegeben werden müssen. Danach kann eine beliebige Anzahl an Parametern folgen (es können auch null sein). Ein Nachteil ist, dass Sie innerhalb des Unterprogrammes lediglich die Lage der Parameter im Speicher ermitteln können, jedoch keine Möglichkeit haben, die Datentypen oder auch nur die Anzahl der Parameter festzustellen.

FreeBASIC stellt für variable Parameterlisten einen eigenen Datentyp CVA_LIST zur Verfügung.4 Zuerst muss innerhalb der Prozedur diese CVA_LIST initialisiert werden. Dazu dient CVA_START, dem zwei Parameter übergeben werden: zum einen die neu zu initialisierende CVA_LIST, zum anderen der letzte Parameter der Parameterliste, der noch nicht zum variablen Anteil gehört.

Anschließend können mit CVA_ARG der Reihe nach die variablen Parameter abgerufen werden. CVA_ARG benötigt zwei Parameter, nämlich die Variable, in die der Parameterwert gelegt werden soll, und der Datentyp des Parameters (den kann das Programm ja, wie oben bereits erwähnt, nicht selbst aus der Parameterliste herauslesen).

Zuletzt muss die CVA_LIST mittels CVA_END wieder freigegeben werden. In Kurzform sehen diese drei Schritte folgendermaßen aus:

SUB variabel CDECL (x AS INTEGER, y AS INTEGER, ...)
  DIM AS CVA_LIST liste          ' zur Speicherung des variablen Listenteils
  DIM p AS LONG, q AS DOUBLE     ' zur Speicherung der variablen Parameter
  CVA_START(liste, y)            ' y ist der letzte feste Parameter der Liste
  p = CVA_ARG(liste, LONG)       ' ersten variablen Parameter holen
  q = CVA_ARG(liste, DOUBLE)     ' zweiten variablen Parameter holen
  ' ...
  CVA_END(liste)                 ' Parameterliste freigeben
END SUB

In diesem Codeschnipsel wird in der vierten Zeile die variable Liste initialisiert und später in der achten Zeile wieder freigegeben. Achten Sie darauf, dass jede Initialisierung mit CVA_START auch wieder mit CVA_END freigegeben werden muss! Bei der Initialisierung wurde im Codeschnipsel y, der zweite Parameter der Parameterliste, übergeben (also der letzte festgelegte Parameter vor dem variablen Anteil). Nun weist ein interner Zeiger auf den Speicherbereich hinter y und damit auf den ersten Parameter des variablen Anteils.

Unter der Annahme, dass es sich beim ersten variablen Parameter um ein LONG handelt, wird nun in der fünften Zeile mit CVA_ARG der erste variable Parameter ausgelesen. Dabei geschieht zweierlei: Zum einen wird der Speicherbereich, auf den der interne Zeiger verweist, als LONG interpretiert und entsprechend ausgelesen. Zum anderen rückt der Zeiger auf den nächsten Parameter weiter. Dazu muss er um die Länge eines LONG weitergesetzt werden. Der übergebene Datentyp ist also sowohl für die richtige Interpretation des Parameterinhalts als auch für das korrekte Weiterrücken des Zeigers nötig.

Wenn Sie die obige Funktion etwa über den Befehl variabel(1, 2, 3, 4.5, 6) aufrufen, wird im Parameter p der Wert 3 und im Parameter q der Wert 4.5 landen. Weitere Variablen würden in diesem Beispiel ignoriert, da sie in der Prozedur nicht abgefragt werden.

Beachten Sie noch zwei Dinge:

  1. Damit variable Parameterlisten verwendet werden können, müssen die Parameter zwingend CDECL übergeben werden. Die Reihenfolge der Parameterübergabe ist hier entscheidend, da ja nur so korrekt von Parameter zu Parameter gesprungen werden kann.

  2. Bei CVA_START, CVA_ARG und CVA_END handelt es sich weder um Funktionen noch Anweisungen, sondern um Makros. Was das genau bedeutet, werden wir später noch besprechen. Für den Augenblick ist nur wichtig, dass die um die Parameter gesetzten Klammern unbedingt erforderlich sind — auch bei CVA_START und CVA_END.

Die entscheidende Frage ist nun, wie der Prozedur Kenntnis über Art und Anzahl der variablen Parameter Kenntnis erlangt. Da sowieso mindestens ein Paramter fest übergeben werden muss, ist es naheliegend, hier die Anzahl der nun folgenden Parameter zu übergeben. Sofern deren Datentypen festgelegt sind — im einfachsten Fall müssen alle denselben festgelegten Datentyp besitzen — reicht das bereits aus. Eine Mittelwertbestimmung mit variabler Parameterliste könnte dann folgendermaßen aussehen:

Quelltext 12.16: Mittelwertsbestimmung mit variabler Parameterliste
FUNCTION mittelwert CDECL (anz AS INTEGER, ...) AS DOUBLE
  DIM AS CVA_LIST liste
  DIM AS INTEGER summe
  CVA_START(liste, anz)

  FOR i AS INTEGER = 1 TO anz
    summe += CVA_ARG(liste, INTEGER)    ' aktuellen Wert holen und aufaddieren
  NEXT
  CVA_END(liste)
  RETURN summe / anz
END FUNCTION

PRINT mittelwert(3, 15, 17, 20)
SLEEP

Der erste Parameter 3 gehört nicht zur Mittelwertsbestimmung dazu, sondern gibt lediglich die Anzahl der zu mittelnden Werte an. liste ist die CVA_LIST, die durchlaufen werden soll. Die einzelnen aus der CVA_LIST gelesenen Werte werden selbst nicht benötigt, sonden können direkt aufsummiert werden. Wenn sichergestellt ist, dass es sich bei allen Werten um INTEGER handelt, liefert CVA_ARG(liste, INTEGER) den Wert dieses Parameters und setzt den Zeiger auf den nächsten Parameter. Wird ein anderer Datentyp verwendet, muss natürlich dieser statt INTEGER eingesetzt werden.

Beim Funktionsaufruf muss auf die korrekten Datentypen geachtet und ggf. auch die automatische Typbestimmung von FreeBASIC berücksichtigt werden. Wenn Sie statt 15 den Wert 15.0 übergeben, liegt er als DOUBLE im Speicher. Da der in der Funktion vorgefundene Speicherwert aber als INTEGER behandelt wird, das eine völlig andere Speicherverwaltung verwendet als Gleitkommazahlen, erhalten Sie falsche Daten.

Wenn Sie sinnvoll mit variablen Parameterlisten arbeiten wollen, bieten sich zwei Konzepte an:

  • Der Datentyp aller Parameter in der variablen Liste muss gleich sein. Zudem übergeben Sie als ersten Parameter die Länge der variablen Liste. Diese Methode ist relativ leicht umzusetzen, und Sie haben in Quelltext 12.16 bereits ein Beispiel dazu gesehen.

  • Die Datentypen können sich unterscheiden. In diesem Fall greift man häufig auf einen Formatstring zurück, der als erster Parameter übergeben wird und der die Anzahl und die Datentypen der verwendeten Parameter enthält. Dazu sehen Sie ein Beispiel in Quelltext 12.17.

Quelltext 12.17: Variable Parameterliste mit Formatstring
SUB varlist CDECL (formatstring AS STRING, ...)
  DIM AS CVA_LIST liste
  CVA_START(liste, formatstring)

  FOR i AS INTEGER = 1 TO LEN(formatstring)
    SELECT CASE MID(formatstring, i, 1)
      CASE "l"
        PRINT "LONG:   " & CVA_ARG(liste, LONG)
      CASE "d"
        PRINT "DOUBLE: " & CVA_ARG(liste, DOUBLE)
      CASE "s"
        PRINT "STRING: " & *CVA_ARG(liste, ZSTRING PTR)
    END SELECT
  NEXT
  CVA_END(liste)
END SUB

DIM s AS STRING = "String2"

varlist "ldss", 1, 3.4, "String1", s
SLEEP
Ausgabe:
INTEGER:  1
DOUBLE:   3.4
STRING:   String1
STRING:   String2

Mit dem Formatstring "idss" wird der Funktion mitgeteilt, dass der Reihe nach ein INTEGER, ein DOUBLE und zwei STRING übergeben werden. Das in der Funktion verwendete MID() dient zum Auslesen des Formatstrings — es gibt einen (in diesem Fall ein Zeichen langen) Teilstring zurück. MID() wird in Kapitel 15.2.3 ausführlich behandelt.

Tip Beachten Sie:
Wie Sie in Quelltext 12.17 sehen, wird nicht der Datentyp STRING, sondern ein ZSTRING PTR eingesetzt. Das hat mit der internen Speicherverwaltung bei der Übergabe von Zeichenketten an Unterprogramme zu tun.
12.5.5.2 Kopie erstellen

Manchmal ist es nötig, eine Kopie der variablen Parameterliste anzulegen. Sie können dann innerhalb der Prozedur gleichzeitig mit zwei Versionen der Liste arbeiten — wichtiger ist aber die Möglichkeit, die Liste an ein anderes Unterprogramm weiterzugeben. Zum Erstellen der Kopie benötigen Sie das Makro CVA_COPY. Auch die Kopie muss am Ende mit CVA_END freigegeben werden.

Quelltext 12.18: Variable Parameterliste kopieren
DECLARE FUNCTION mittelwert CDECL (anz AS INTEGER, ...) AS SINGLE
DECLARE FUNCTION summe CDECL (anz AS INTEGER, varList as CVA_LIST) AS INTEGER

FUNCTION mittelwert CDECL (anz AS INTEGER, ...) AS SINGLE
  DIM AS CVA_LIST liste
  CVA_START(liste, anz)
  DIM AS INTEGER s = summe(anz, liste)    ' Auslagerung der Summenberechnung
  CVA_END(liste)                          ' CVA_LIST freigeben
  RETURN s / anz
END FUNCTION

FUNCTION summe CDECL (anz AS INTEGER, varList AS CVA_LIST) AS INTEGER
  DIM AS CVA_LIST liste
  DIM AS INTEGER rueckgabe = 0
  ' Da nicht sicher ist, dass die aufrufende Prozedur die uebergebene Liste nicht
  ' selbst verwendet (in diesem Fall kommen sich die verschiedenen Aufrufe von
  ' CVA_ARG gegenseitig in die Quere), wird lieber eine Kopie angelegt.
  CVA_COPY(liste, varList)
  ' Mit dieser Kopie wird nun weitergearbeitet:
  FOR i AS INTEGER = 1 TO anz
    rueckgabe += CVA_ARG(liste, INTEGER)
  NEXT
  ' CVA_LIST freigeben und Summenwert zurueckgeben:
  CVA_END(liste)
  RETURN rueckgabe
END FUNCTION

PRINT mittelwert(3, 15, 17, 20)
SLEEP

In diesem Beispiel verwendet die Funktion mittelwert die Liste nicht selbst aus, daher hätte summe auch direkt mit der übergebenen Liste arbeiten können. Sicherer ist es aber, stattdessen eine Kopie zu erstellen, mit dieser zu arbeiten und sie anschließend wieder freizugeben. Natürlich hätte auch in mittelwert bereits eine Kopie erstellt werden können, die dann an summe übergeben (und am Ende ebenfalls wieder durch CVA_END freigegeben) wird — auch dann hätte mittelwert auf die (originale) Liste zugreifen können, ohne den Zugriff durch summe (auf die Kopie) zu beeinträchtigen.

12.5.5.3 Frühere Methoden

CVA_START und die anderen damit zusammenhängenden Makros sind in FreeBASIC recht neu. Viele im Umlauf befindliche Programme, die variable Parameterlisten nutzen, verwenden noch eine ältere Methode, die ich hier der Vollständigkeit halber kurz vorstellen möchte. Auch hier muss die Parameterliste aus mindestens einem festen Parameter bestehen. Der erste variable Parameter wird mit VA_FIRST() bestimmt, der Wert des aktuellen Parameters mit VA_ARG() ausgelesen. Im Unterschied zu CVA_ARG aktualisiert VA_ARG() den Zeiger nicht, sondern der Zeiger muss mit VA_NEXT() weitergerückt werden. Das Erstellen eines eigenen Listenobjekts ist für diese Funktionen nicht nötig.

Der große Nachteil von VA_FIRST(), VA_ARG() und VA_NEXT(): Die hierfür verwendete Methode greift auf spezielle Mechaniken des 32-Bit-Assemblers zurück; insbesondere bedeutet das, dass sie für die 64-Bit-Version von FreeBASIC nicht zur Verfügung steht. Die CVA_LIST arbeitet dagegen plattformübergreifend.

Quelltext 12.19: Mittelwertsbestimmung mit variabler Parameterliste (alte Methode)
' Dieser Quelltext funktioniert nicht unter 64 Bit!
FUNCTION mittelwert CDECL (anz AS INTEGER, ...) AS DOUBLE
  DIM AS ANY PTR param = VA_FIRST     ' Pointer auf den ersten Parameter
  DIM AS INTEGER summe

  FOR i AS INTEGER = 1 TO anz
    summe += VA_ARG (param, INTEGER)  ' aktuellen Wert holen und aufaddieren
    param =  VA_NEXT(param, INTEGER)  ' Zeiger auf den naechsten Parameter setzen
  NEXT
  RETURN summe / anz
END FUNCTION

PRINT mittelwert(3, 15, 17, 20)
SLEEP

12.6 Fragen zum Kapitel

  1. Stellen Sie eine Liste von Argumenten zusammen, die für einen Einsatz von Unterprogrammen sprechen.

  2. Was ist der Unterschied zwischen Prozeduren und Funktionen? Wann bietet sich welcher Typ an?

  3. Welche Möglichkeiten gibt es, Variablen aus dem Hauptprogramm im Unterprogramm zu verwenden?

  4. Welche Arten der Parameterübergabe gibt es und worin unterscheiden sie sich?


Fußnoten:
1) aus dem Lateinischen und auch im Englischen: sub = unter

2) Man spricht hier von einer verschränkten Rekursion.

3) Sie könnten sich jetzt natürlich fragen, warum ein Unterbinden der Wertänderung überhaupt erforderlich ist, wenn Sie stattdessen auch einfach einen schreibenden Zugriff unterlassen können. Bedenken Sie aber, dass oft mehrere Personen an einem Projekt mitarbeiten und dass ein festgeschriebenes CONST eine Garantie dafür ist, dass tatsächlich keine Wertänderung stattfindet.

4) Genau genommen handelt es sich hier — abhängig von der Architektur, für die compiliert wird — um verschiedene Datentypen. Genauere Details sind hier jedoch nicht von Belang, denn es ist für uns nur entscheidend, wie der Datentyp angesprochen wird.


Kapitel 11: Schleifen und Kontrollanweisungen

Inhaltsverzeichnis

Kapitel 13: Datentypen umwandeln