TO DO: Beispiel 4 (7×5-Rechteck) im ZIP-File ändern!!

SUB und FUNCTION komplett erläutert

Einleitung und Motivation

SUB und FUNCTION ist eines der wichtigsten Konzepte innerhalb der Software-Entwicklung, um überhaupt komplexe Projekte realisieren zu können.

Komplexität und Software-Grossprojekte

Beruflich arbeite ich ebenfalls an einem solchen Grossprojekt, welches aus insgesamt rund 500'000(!) Codezeilen (einige hundert C++-Klassen) besteht. Der Compiler braucht übrigens gut zwei Stunden, um aus sämtlichen Quellcodes die fertigen Executables (=ausführbare .EXE-Programmdateien) bilden zu können.

In einem Zeitschriftenartikel las ich etwas über Star Office (Alternative zu Microsoft Office) von Sun (ehemals Star Division GmbH): Um dort die Software so zu compilieren, wie Sie sie als Endverbraucher auf der Installations-CD-ROM vorfinden, arbeiten 5(!) Rechner gleichzeitig (200 MHz-Pentiums) pausenlos während 2 Tagen (48 Stunden)! Sie können sich nun selber vorstellen, was die Korrektur eines Fehlers am Quellcode an Aufwand bedeutet... Dies nur, um einen kurzen Einblick in sog. Large Scale-Software-Pojekte geben zu können.

Sämtliche Beispiele können Sie hier herunterladen.

Sinn und Zweck von SUB/FUNCTION-Unterprogrammen

SUB und FUNCTION dienen zur Erstellung gekapselter Unterprogramme. Beispiel: Statt

' Beispiel 1a

PRINT "******"
PRINT "---"
PRINT "Andreas"
PRINT "******"
PRINT "---"
PRINT "Meile"
PRINT "******"
PRINT "---"

schreibt man eleganter

' Beispiel 1b

DECLARE SUB DruckAbstand ()

DruckAbstand
PRINT "Andreas"
DruckAbstand
PRINT "Meile"
DruckAbstand

SUB DruckAbstand
  PRINT "******"
  PRINT "---"
END SUB

d.h. Sie ersetzen gleichartige Programmteile durch einen SUB-Programmaufruf!

Hinweise zur Programmeingabe in QBASIC.EXE: Ein neues SUB-Unterprogramm erstellen Sie über das Pulldown-Menü mit Bearbeiten, Neue SUB..., um DruckAbstand eingeben zu können. Ausserdem zeigt Ihnen QuickBASIC nur immer ein Modul (Hauptprogramm oder Unterprogramm) im Editor an, so dass Sie mit Ansicht, SUBs... wechseln können.

Aufrufe können mit Hilfe von Übergabeparameter auch parametrisiert werden. Beispiel: Statt

' Beispiel 2a

PRINT "ABC"
PRINT "Meile"
PRINT "ABCDEFG"
PRINT "ABCDEFGHIJKLMN"

verwendet man besser etwas wie

' Beispiel 2b

DECLARE SUB DruckAlphabet (nBuchst%)

DruckAlphabet 3
PRINT "Meile"
DruckAlphabet 7
DruckAlphabet 14

SUB DruckAlphabet (nBuchst%)
  FOR i% = 1 TO nBuchst%
    PRINT CHR$(i% + 64);
  NEXT i%
  PRINT
END SUB

d.h. wir haben die Anzahl Buchstaben, die vom Alphabet ausgegeben werden sollen, als frei wählbaren Übergabeparameter definiert.

Lokale Variablen und Sichtbarkeitsbereiche

Eine weitere wichtige Eigenschaft von SUB- und FUNCTION-Prozeduren sind nur lokal innerhalb der Prozedur sichtbare Hilfsvariablen. Somit liefert

' Beispiel 3

DECLARE SUB prozA ()

var% = 50
PRINT var%
PRINT "Noch im Hauptprogramm"
prozA
PRINT "Wieder im Hauptprogramm"
PRINT var%

SUB prozA
  PRINT "Im SUB-Programm"
  PRINT var%
  var% = 30
  PRINT var%
  PRINT "Schluss Subprogramm"
END SUB

beim Start nicht etwa 30, sondern 50 auf den Bildschirm am Schluss:

50
Noch im Hauptprogramm
Im SUB-Programm
0
30
Schluss Subprogramm
Wieder im Hauptprogramm
50

Die Null nach der Im SUB-Programm-Stelle beweist ausserdem, dass wirklich ein neues var% im Speicher erzeugt wurde. Durch diese Eigenschaft sind unbeabsichtigte Seiteneffekte nicht mehr möglich. Beispiel: Ausgabe eines Rechteckes als Zeilen:

' Beispiel 4

DECLARE SUB DruckZeile (n%)

' Rechteck 7 Sternchen hoch und 5 breit
FOR i% = 1 TO 7
  DruckZeile 5
NEXT i%

SUB DruckZeile (n%)
  FOR i% = 1 TO n%
    PRINT "*";
  NEXT i%
  PRINT
END SUB

Kleine Übung an den Leser: Versuchen Sie sich vorzustellen, was passieren würde, wenn dasjenige i% vom SUB-Programm dasselbe i% vom Hauptprogramm wäre! Lösung: Es würde eine Endlosschleife entstehen, weil Ihnen das SUB-Programm i% ständig verstellen würde! Im alten Zeilennummern-BASIC war genau dies das grosse Problem: In GWBASIC.EXE gab es nur globale Variablen und GOSUB/RETURN für Unterprogramme (GOSUB existiert in QuickBASIC auch immer noch), so dass eben gekapselte Module noch nicht möglich waren => keine oder nur sehr schlechte Software-Wiederverwendbarkeit => man hatte gegen viele sog. side effects (=unbeabsichtigtes Überschreiben von Variablen) zu kämpfen... :-(

Gemeinsame und globale Variablen

In gewissen Fällen möchte man aber trotzdem innerhalb eines SUB-Unterprogramms auf bestimmte, einzelne Variablen des Hauptprogramms zurückgreifen können. Dazu existiert DIM SHARED:

' Beispiel 5

DECLARE SUB AendereName()

DIM SHARED n$

n$="Andreas"
AendereName
PRINT n$

SUB AendereName
  n$ = "Meile"
END SUB

Übungsfrage: Was gibt dieses Programm aus? Und was passiert, wenn Sie DIM SHARED weglassen? Die Lösung sollte nicht schwerfallen!

Parameterübergabe als Referenz und Wert

Ein ebenfalls interessanter Aspekt ist die Art der Parameterübergabe: In Pascal unterscheidet man zwischen by value, also als reinen Wert und by reference, also als sog. Referenz (Verweis). Das folgende Beispiel zeigt die Bedeutung dazu auf:

' Beispiel 6a

DECLARE SUB ErhoeheUmEins(k%)

h% = 16
PRINT h%
ErhoeheUmEins h%
PRINT h%
ErhoeheUmEins h%
PRINT h%

SUB ErhoeheUmEins(k%)
  k% = k% + 1
END SUB

Als Ergebnis werden Sie

16
17
18

bekommen! Erklärung: QuickBASIC hat bei diesen beiden Aufrufen die Variable by reference übergeben => die Variable k% im Unterprogramm vertritt die Variable h% im Hauptprogramm, so dass die Zuweisung an k% (Zeile k% = k%+1) eben unmittelbar auch das h% vom Hauptprogramm mitändert. Ändern Sie nun das Programm wie folgt ab durch Anhängen von + 23 bei den Aufrufen:

' Beispiel 6b

DECLARE SUB ErhoeheUmEins(k%)

h% = 16
PRINT h%
ErhoeheUmEins h% + 23
PRINT h%
ErhoeheUmEins h% + 23
PRINT h%

SUB ErhoeheUmEins(k%)
  k% = k% + 1
END SUB

Ergebnis diesmal:

16
16
16

Erklärung: h% + 23 ist ein Ausdruck, und keine Variable mehr. QuickBASIC hat das Ergebnis (in diesem Fall also 39) als Wert, d.h. by value dem Unterprogramm übergeben! => k% bildet dann eine von h% unabhängige Variable. Somit bewirkt k% = k% + 1 keine Änderung von h% mehr! Tip: Ändern Sie das Unterprogramm zu (SUBBSP6C.BAS)

SUB ErhoeheUmEins(k%)
  PRINT k%
  k% = k% + 1
  PRINT k%
END SUB

ab => danach bekommen Sie

16
39
40
16
39
40
16

als Ergebnis. Dies, um zu zeigen, dass jetzt k% und h% unabhängig sind. Als Schlussfolgerung müssen Sie sich daher folgendes sehr gut merken:

Eine Variable als Parameter (auch Element einer Feldvariable) wird immer by reference übergeben, während Ausdrücke by value übergeben werden.

Kurzer Ausblick über den Tellerrand von QuickBASIC hinaus

Bei vielen anderen Programmiersprachen ist dies viel besser :-) gelöst als in QuickBASIC: So schreibt man beispielsweise in Pascal das Schlüsselwort VAR davor, wenn man etwas by reference übergeben möchte, sonst ist es immer by value:

PROCEDURE erhoehe(a : INTEGER; VAR b : INTEGER);

In ADA sind es die Vorsätze in, in out und out:

procedure erhoehe(a : in integer; b : in out integer);

Auch C++ kennt das Referenzierungszeichen &:

void erhoehe(int a, int &b);

Wie Sie also sehen, hat Microsoft dies in QuickBASIC nicht besonders klug :-( gelöst im Vergleich zu den übrigen Programmiersprachen, denn im Funktionsprototypen (=Begriff aus C/C++)

DECLARE SUB erhoehe(a%, b%)

sehen Sie nicht mehr so ohne weiteres heraus, was verändert wird, also welcher Parameter ein Berechnungsergebnis darstellt, und welches nur reine Eingabeparameter sind, während dies bei den übrigen Programmiersprachen sofort ersichtlich ist: Der Prozedurkopf erfüllt in der Praxis eine wichtige Dokumentationsfunktion, weil die Anwendungssyntax sofort klar ersichtlich ist! Beim QuickBASIC-Compiler hat Microsoft zwar ein BYVAL ergänzt für by value-Parameter, allerdings nur für Nicht-BASIC-Modulprozeduren (z.B. QuickBASIC mit ANSI C, Pascal oder Assembler kombiniert) vorgesehen.

FUNCTION-Prozeduren

FUNCTION-Prozeduren sind beinahe identisch mit SUB-Prozeduren mit dem einzigen Unterschied, dass sie immer etwas zurückgeben. Beispiel:

' Beispiel 7
' Berechnung des Quadrates einer Zahl

DECLARE FUNCTION Quadriere!(s!)

PRINT Quadriere(5!)
PRINT Quadriere(1.5)
PRINT 1! + Quadriere(4!) - 13!
PRINT Quadriere(10! + 2!) - 1!

FUNCTION Quadriere!(s!)
  Quadriere! = s! * s!
END FUNCTION

Auch hier gilt bei der Eingabe: Bearbeiten, Neue FUNCTION... im QuickBASIC-Editor verwenden!

Als Resultat erhalten Sie

25
2.25
4
143

Noch einen historischen Hinweis: In den alten Zeilennummern-BASIC-Dialekten gab es das Sprachelement DEF FN als Vorläufer zu FUNCTION, welches aber nur eine reine Funktionssubstitution darstellt, also noch keine echten Prozeduren zulässt.

Über den Tellerrand hinaus: Vergleich mit anderen Programmiersprachen

Auch hier wiederum einen Vergleich mit anderen Programmiersprachen:

QuickBASIC kennt zwar auch RETURN, bedeutet dort jedoch etwas ganz anderes: Rücksprung nach einem GOSUB. Wohl existiert EXIT FUNCTION für das explizite Verlassen der Funktion, aber man kann dort keinen Rückgabewert angeben, d.h.

FUNCTION Quadriere!(s!)
  ' dies geht leider :-( nicht!
  EXIT FUNCTION s! * s!
END FUNCTION

ist nicht möglich und daher unzulässig.

Datentyp beim Rückgabewert

Ein wichtiges Detail ist der Typ; Sie müssen also bei der Deklaration (DECLARE) sowie im Prozedurkopf immer das zutreffende Zeichen %, &, !, # oder $ entsprechend dem Datentyp angeben. Es sind also auch Stringfunktionen möglich, beispielsweise alle Buchstaben vervielfachen:

' Beispiel 8

DECLARE FUNCTION VervielfacheZeichen$(a$, n%)

PRINT VervielfacheZeichen$("Andreas", 2)
b$ = "* " + VervielfacheZeichen$("Wichtig!", 3) + " *"
PRINT b$

FUNCTION VervielfacheZeichen$(a$, n%)
  h$ = ""
  FOR i% = 1 TO LEN(a$)
    h$ = h$ + STRING$(n%, MID$(a$, i%, 1))
  NEXT i%
  VervielfacheZeichen$ = h$
END FUNCTION

würde

AAnnddrreeaass
* WWWiiiccchhhtttiiiggg!!! *

liefern. Als weiteres Beispiel ein oberprimitiver Deutsch-Englisch-Übersetzer:

' Beispiel 9

DECLARE FUNCTION UebersetzeDeutschNachEnglisch$(Deutsch$)

INPUT "Deutscher Text"; t$
PRINT "Englische Übersetzung: "; UebersetzeDeutschNachEnglisch$(t$)

FUNCTION UebersetzeDeutschNachEnglisch$(Deutsch$)
  SELECT CASE Deutsch$
  CASE "Brot"
    UebersetzeDeutschNachEnglisch$ = "bread"
  CASE "Wasser"
    UebersetzeDeutschNachEnglisch$ = "water"
  CASE "Hund"
    UebersetzeDeutschNachEnglisch$ = "dog"
  CASE ELSE
    ERROR 5    ' soll einen Laufzeitfehler bewirken
  END SELECT
END FUNCTION

Verschachtelte Unterprogrammaufrufe

Ein weiterer wichtiger Aspekt bei SUB- und FUNCTION-Unterprogrammen ist der verschachtelte Aufruf, also der Aufruf einer Prozedur aus einer anderen heraus. Beispiel:

' Beispiel 10

DECLARE SUB ZeichneLinie(n%)
DECLARE SUB ZeichneRechteck(b%, h%)

ZeichneRechteck 7, 5

SUB ZeichneLinie(n%)
  FOR i% = 1 TO n%
    PRINT "*";
  NEXT i%
  PRINT
END SUB

SUB ZeichneRechteck(b%, h%)
  FOR i% = 1 TO h%
    ZeichneLinie b%    ' hier rufen wir eine andere SUB-Prozedur auf
  NEXT i%
END SUB

Rekursion

Wenn eine SUB-Prozedur sich selber aufruft, so spricht man von sog. Rekursion. Beispiel:

' Beispiel 11

DECLARE SUB RekursivAufruf(n%)

RekursivAufruf 5

SUB RekursivAufruf(n%)
  PRINT "Jetzt sind wir auf Aufrufebene"; n%
  IF n% > 0 THEN
    RekursivAufruf n% - 1
  END IF
  PRINT "Jetzt sind wir wieder auf Ebene"; n%
END SUB

Ausgabe:

Jetzt sind wir auf Aufrufebene 5
Jetzt sind wir auf Aufrufebene 4
Jetzt sind wir auf Aufrufebene 3
Jetzt sind wir auf Aufrufebene 2
Jetzt sind wir auf Aufrufebene 1
Jetzt sind wir auf Aufrufebene 0
Jetzt sind wir wieder auf Ebene 0
Jetzt sind wir wieder auf Ebene 1
Jetzt sind wir wieder auf Ebene 2
Jetzt sind wir wieder auf Ebene 3
Jetzt sind wir wieder auf Ebene 4
Jetzt sind wir wieder auf Ebene 5

Gleichzeitig sieht man wiederum etwas wichtiges: Jede Aufrufebene verwendet ihren eigenen Variablensatz, d.h. bei jedem Aufruf wird ein neues, unabhängiges n% erzeugt!

Sehr viele Algorithmen in der Informatik benötigen Rekursion.

Anwendungsbeispel 1: Puzzlelöser

Ein gutes Beispiel ist der Puzzlelöser. Dort lautet der Algorithmus (folgendes Programm stellt nur einen sog. Pseudocode, welcher also nicht lauffähig ist, dar! => Daher nicht in den Beispielen enthalten):

TYPE Rahmen
  ' .. nicht im Detail aufgeführt
END TYPE

TYPE TeileSatz
  ' .. nicht im Detail aufgeführt
END TYPE

DECLARE SUB LoesePuzzle(TeileSatz() AS Teile, Auffuellrahmen AS Rahmen, n%)

INPUT "Name einer Teilesatzdatei"; t$
LadeTeileAbDiskette(t$, Teile(), Fuellrahmen, AnzTeile%)

LoesePuzzle Teile(), Auffuellrahmen, AnzTeile%

SUB LoesePuzzle(TeileSatz() AS Teile, Auffuellrahmen AS Rahmen, n%)
  IF n% = 0 THEN
    PRINT "Hurra! :-) Wir haben eine Lösung gefunden:"
    PRINT AuffuellrahmenMitTeile
  ELSE
    ' Es hat noch Teile auf der Seite
    FOR i = AlleMoeglichenPositionenUndStellungen
      IF EsHatPlatz(Teil(n%), i) THEN
        PlaziereTeilInRahmen Auffuellrahmen, TeileSatz(n%), i
        LoesePuzzle TeileSatz(), Auffuellrahmen, n% - 1
        NimmTeilWiederHeraus Auffuellrahmen, TeileSatz(n%), i
      END IF
    NEXT i
  END IF
END SUB

Da POLYLOES.BAS noch in GWBASIC implementiert ist, musste ich damals die Rekursion recht umständlich :-( durch geeignete GOTO-Konstrukte und Feldvariablen für den sog. Aufrufstapel (call stack) ersetzen.

Anwendungsbeispiel 2: DOS/Windows-Laufwerk durchsuchen

Ebenfalls ein gutes Beispiel für Rekursion ist Ihre Festplatte, wenn Sie die von Windows 95 bekannte Suchen nach Dateien/Ordner-Funktion in QuickBASIC implementieren wollen: Dort gilt ebenfalls

INPUT "Dateiname, nach dem gesucht werden muss"; d$
INPUT "Laufwerk, z.B. C:\"; lw$
Suche d$, lw$

SUB Suche(dn$, pfad$)
  FILES pfad$ TO ListeMitDateinamen()   ' Nur Pseudo-Anweisung!
  FOR i% = 1 TO UBOUND(ListeMitDateinamen)
    IF Dateityp(ListeMitDateinamen(i%)) = Verzeichnis THEN
      Suche dn$, pfad$ + ListeMitDateinamen(i%) + "\"
    ELSE IF ListeMitDateinamen(i%) passtZuMuster dn$ THEN
      PRINT "Dokument "; dn$; " gefunden in Verzeichnis"; pfad$
    END IF
  NEXT i%
END SUB

Anmerkung: Falls Sie beim hier als Pseudocode dargestellten Befehl FILES eine Lösung für das Einlesen in eine Feldvariable suchen, verweise ich Sie auf den Artikel Verwendung von DOS-Interrupts.

Wenn wir also auf ein Unterverzeichnis bzw. Ordner stossen, so ruft sich die Suche einfach für diesen Ordner noch einmal auf!

In einem Strategiespiel wie Schach, Mühle usw. kann man den sog. Backtracking-Algorithmus, wie ihn der Computergegner üblicherweise verwendet, ebenfalls recht hübsch mit Rekursion implementieren!

Ein häufiger Programmierfehler bei Rekursion: Stapelüberlauf

Zum Abschluss der Rekursion noch ein ganz fatal fehlerhaftes :-( Beispiel:

' So besser nicht!
DECLARE SUB UnendlichRekursiv(i%)

UnendlichRekursiv 5

SUB UnendlichRekursiv(i%)
  UnendlichRekursiv 15 - i%
END SUB

Dieses Programm ruft sich ständig auf, dabei wird laufend ein neues i% erzeugt. Da keinerlei Abbruchbedingung besteht, überläuft der gesamte Aufrufstapel (call stack). Daher wird dieses Beispiel mit einem Out of Memory enden...

Lebensdauer und -zyklus von lokalen Variablen

Beim verschachtelten Aufruf hatten wir festgestellt, dass bei jedem Aufruf neue Variablen entstehen, die aber auch wieder gelöscht werden, sobald das Unterprogramm beendet wird. Beispiel für die Löschung:

' Beispiel 12a

DECLARE SUB SetzeWert()

i% = 7
PRINT i%
SetzeWert
PRINT i%
SetzeWert
PRINT i%

SUB SetzeWert
  PRINT i%
  i% = i% + 5
  PRINT i%
END SUB

Beim Start erhält man

7
0
5
7
0 <= ist wieder Null!
5
7

Erläuterungen dazu: Überall bei i% = 7 haben wir dasjenige i% vom Hauptprogramm ausgegeben. Im Unterprogramm erkennt man aufgrund des Wertes 0, dass bei jedem Aufruf jeweils wieder ein neues i% erzeugt wird. Somit verschwindet nach END SUB das lokale i% wieder.

QuickBASIC erlaubt als Alternative die Verwendung von sog. persistenten Lokalvariablen mit STATIC. Ändern Sie nun das obige Beispiel durch Anfügen von STATIC beim Prozedurkopf zu

' Beispiel 12b

DECLARE SUB SetzeWert()

i% = 7
PRINT i%
SetzeWert
PRINT i%
SetzeWert
PRINT i%

SUB SetzeWert STATIC
  PRINT i%
  i% = i% + 5
  PRINT i%
END SUB

Als Ergebnis bekommen Sie diesmal

7
0
5 <-----+
7       |
5 <= jetzt haben wir hier 5 wie oben
10
7

d.h. die die lokale Variable i% hat diesmal »überlebt«, was sich klar beim zweiten Aufruf äussert. Bei einzelnen BASIC-Dialekten wie AmigaBASIC und MaxonBASIC kennzeichnet STATIC gleichzeitig, dass nicht mehr Rekursion erlaubt ist, d.h.

SUB SetzeWert STATIC
  PRINT i%
  i% = i% + 5
  SetzeWert
  PRINT i%
END SUB

würde dort einen Fehler produzieren. QuickBASIC lässt auch hier noch Rekursion zu, aber es macht wenig Sinn. Dies liegt darin begründet, dass jetzt eben keine neuen lokalen Variablen erzeugt werden können, denn i% ist ja jetzt statisch (STATIC!) im Speicher, d.h. es gibt nur noch eine Version von i% für dieses Unterprogramm!

Vergleich zu anderen Programmiersprachen

Auch wiederum einen kurzen Ausflug in die übrigen Programmiersprachen: C und C++ kennen ebenfalls static, um eine Variable statisch zu kennzeichnen. Bei Pascal und Ada ist mir so etwas dagegen nicht bekannt.

Feldvariablen als Aufrufparameter

QuickBASIC erlaubt Ihnen auch die Übergabe ganzer Felder als Parameter:

' Beispiel 13

DECLARE SUB Sortieren(d$())

DIM n$(1 TO 5), s$(1 TO 3)

FOR i% = 1 TO 5
  READ n$(i%)
NEXT i%
DATA "Mario", "Andreas", "Uwe", "Robert", "Ralf"

FOR i% = 1 TO 3
  READ s$(i%)
NEXT i%
DATA "Stuttgart", "Ulm", "Hamburg"

PRINT "Vor dem Sortieren:"
PRINT "Namen:";
FOR i% = 1 TO 5
  PRINT " "; n$(i%);
NEXT i%
PRINT
PRINT "Städte:";
FOR i% = 1 TO 3
  PRINT " "; s$(i%);
NEXT i%
PRINT

Sortieren n$()
Sortieren s$()

PRINT "Nach dem Sortieren:"
PRINT "Namen:";
FOR i% = 1 TO 5
  PRINT " "; n$(i%);
NEXT i%
PRINT
PRINT "Städte:";
FOR i% = 1 TO 3
  PRINT " "; s$(i%);
NEXT i%
PRINT

SUB Sortieren (d$())
  FOR i% = LBOUND(d$) TO UBOUND(d$) - 1
    FOR j% = i% + 1 TO UBOUND(d$)
      IF d$(i%) > d$(j%) THEN
        SWAP d$(i%), d$(j%)
      END IF
    NEXT j%
  NEXT i%
END SUB

Dieses Beispiel zeigt noch gleichzeitig einige wichtige Details:

Vergleich mit anderen Programmiersprachen

Kurzer Vergleich mit anderen Programmiersprachen: Bei ADA kann man ebenfalls ganze ARRAYs übergeben und mit 'FIRST und 'LAST die Grösse ermitteln. In C++ übergibt man einen sog. Adresszeiger (engl. pointer) an das Unterprogramm, wobei dieses jedoch nicht die Grösse bestimmen kann.

Feldvariable oder nur Einzelvariable?

Achtung: In diesem Beispiel

' Beispiel 14a

DECLARE SUB ErhoeheUmEins(k%)

DIM t%(1 TO 5)
t%(2) = 2
ErhoeheUmEins t%(2)
PRINT t%(2)    ' liefert in diesem Fall 3

SUB ErhoeheUmEins(k%)
  k% = k% + 1
END SUB

wird nicht das ganze Feld t%() übergeben, sondern nur ein einzelnes Element von t%(), nämlich das zweite! Gleichzeitig zeigt dieses Beispiel, dass der Wert auch wieder by reference übergeben wird, d.h. QuickBASIC hat auch hier wieder eine Variable erkannt. Bei

' Beispiel 14b

DECLARE SUB ErhoeheUmEins(k%)

DIM t%(1 TO 5)
t%(2) = 2
ErhoeheUmEins t%(2) + 5
PRINT t%(2)    ' liefert in diesem Fall 2

SUB ErhoeheUmEins(k%)
  k% = k% + 1
END SUB

wurde dagegen der Parameter by value übergeben, da t%(2) + 5 ein ganz normaler Ausdruck darstellt.

Benutzerdefinierte Recordtypen als Übergabeparameter

Ebenfalls noch kurz nennenswert sind eigene Datentypen:

' Beispiel 15

TYPE Punkt
  x AS SINGLE
  y AS SINGLE
END TYPE

DECLARE SUB ZeichneLinie(p1 AS Punkt, p2 AS Punkt)

DIM SHARED Anfang AS Punkt, Ende AS Punkt

SCREEN 9
CLS

Anfang.x = 5!
Anfang.y = 7!
Ende.x = 155!
Ende.y = 183!

ZeichneLinie Anfang, Ende

WHILE INKEY$=""
WEND
SCREEN 0

SUB ZeichneLinie(p1 AS Punkt, p2 AS Punkt)
  LINE(p1.x, p1.y)-(p2.x, p2.y)
END SUB

Mit Hilfe des AS-Schlüsselwortes können Sie auch zusammengesetzte Datentypen (=sog. Records) übergeben. Die Übergabe erfolgt auch hier immer by reference.

Möglichkeitsgrenzen im Unterprogrammkonzept von QuickBASIC

Einleitung

Diese Kapitel erscheint auf den ersten Blick überflüssig (»Was nützen mir Beispiele, die nicht gehen?« denken Sie vielleicht). Dies trifft jedoch nicht zu, denn gerade eine gute Beherrschung des bis hierhin präsentierten Wissens umfasst genau so auch die Kenntnisse des nicht mehr Möglichen. Daher werden an dieser Stelle noch einige Sprachkonstrukte diskutiert, die QuickBASIC nicht hat, jedoch in diversen anderen Programmiersprachen wie C++ und ADA vorhanden sind.

Keine zusammengesetzte Datentypen als Rückgabewerte

Bei FUNCTION-Prozeduren lassen sich keine zusammengesetzten Datentypen zurückliefern:

' Ausschnitt aus einem geometrischen CAD-Konstruktionsprogramm
TYPE Punkt
  x AS SINGLE
  y AS SINGLE
END TYPE

TYPE Linie
  p1 AS Punkt
  p2 AS Punkt
END TYPE

' Dies ist leider :-( nicht möglich!
FUNCTION Schnittpunkt AS Punkt(l1 AS Linie, l2 AS Linie)
  ' Hier der Berechnungsalgorithmus
  Schnittpunkt.x = .. l1.p1.x .. l1.p1.y .. l1.p2.x .. l1.p2.y .. l2.p1.x .. l2.p1.y .. l2.p2.x .. l2.p2.y
  Schnittpunkt.y = .. l1.p1.x .. l1.p1.y .. l1.p2.x .. l1.p2.y .. l2.p1.x .. l2.p1.y .. l2.p2.x .. l2.p2.y
END FUNCTION

In einer solchen Situation müssen Sie die Funktion in eine SUB-Routine umwandeln mit by reference-Argument am Schluss:

SUB BerechneSchnittpunkt(l1 AS Linie, l2 AS linie, schnittpkt AS Punkt)
  ' Hier der Berechnungsalgorithmus
  schnittpkt.x = .. l1.p1.x .. l1.p1.y .. l1.p2.x .. l1.p2.y .. l2.p1.x .. l2.p1.y .. l2.p2.x .. l2.p2.y
  schnittpkt.y = .. l1.p1.x .. l1.p1.y .. l1.p2.x .. l1.p2.y .. l2.p1.x .. l2.p1.y .. l2.p2.x .. l2.p2.y
END FUNCTION

Mehrere Unterprogramme mit gleichem Namen

QuickBASIC erlaubt Ihnen auch kein sog. Überladen (engl. overloading) von SUB- und FUNCTION-Prozeduren:

' Dies ist leider :-( auch nicht möglich
DECLARE SUB Sortiere(a$())
DECLARE SUB Sortiere(a%())

In solchen Situationen müssen Sie eigene Namen geben, ähnlich wie abs und fabs aus ANSI-C:

DECLARE SUB SortiereStrings(a$())
DECLARE SUB SortiereInteger(a%())

In C++ und ADA ist dies dagegen erlaubt und möglich. Allerdings ist zu sagen, dass QuickBASIC innerhalb des fest eingebauten Befehlssatzes Überladung (overloading) unterstützt. So existieren von der Quadratwurzelfunktion SQR() in Wirklichkeit zwei Varianten:

DECLARE FUNCTION SQR!(s!)
DECLARE FUNCTION SQR#(s#)

Deswegen liefern Ihnen die mathematischen Funktionen das Ergebnis auch immer in der entsprechenden Genauigkeit zurück. Ebenso MID$() und INSTR():

DECLARE FUNCTION MID$(s$, start%)
DECLARE FUNCTION MID$(s$, start%, anzahl%)

DECLARE FUNCTION INSTR%(zk$, suchstr$)
DECLARE FUNCTION INSTR%(start%, zk$, suchstr$)

Unterbereiche bei Feldparameter

In ADA können Sie bei Feldparametern auch nur einen Unterbereich übergeben, was beispielsweise bei einem Menüunerprogramm recht nützlich sein könnte:

DECLARE SUB Pfeiltastenmenue(mp$(), gewaehlt%)

DIM menupkt$(1 TO 7)
FOR i%=1 TO 7
  READ menupkt$(i%)
NEXT i%
DATA "Erfassen", "Ändern", "Löschen", "Ende", "Artikelstamm", "Auftrag", "Zurück"

gew% = 1
DO
  ' Dies geht leider :-( nicht!
  Pfeiltastenmenue menupkt$(1 TO 4), gew%
  SELECT CASE gew%
  CASE 1
    gew2% = 5
    DO
      Pfeiltastenmenue menupkt$(5 TO 7), gew2%
      SELECT CASE gew2%
      CASE 5
        ' Artikelstamm erfassen
      CASE 6
        ' Auftrag erfassen
      END SELECT
    LOOP UNTIL gew2% = 7
  CASE 2
    ' Ändern
  CASE 3
    ' Löschen
  END SELECT
LOOP UNTIL gew% = 4

Als Ersatz müssen Sie innerhalb des Bildschirmmenü-Unterprogramms auf LBOUND() und UBOUND() verzichten, und stattdessen wie bei der Windows-Bitmap-Bibliothek den Start- und Endindex jeweils als separate Parameter übergeben:

' Dies geht :-)
DECLARE SUB Pfeiltastenmenue(mp$(), lbnd%, ubnd%, gewaehlt%)
' .. (weggelassen)
  Pfeiltastenmenue menupkt$(), 1, 4, gew%
' ..
      Pfeiltastenmenue menupkt$(), 5, 7, gew2%
' ..

Dagegen arbeitet umgekehrt der interne Befehlssatz mit Unterbereichen, denn Sie können beispielsweise

' Grafikbereich kopieren: Ganzes Feld übergeben
PUT(12, 17), GanzeGrafik%, PSET
' Nur Teilbereich übergeben
t! = TIMER
FOR i%=0 TO 69
  ' Nur Unterbereich übergeben
  PUT(117, 52), Trickfilmbild%(i% * 462), PSET
  t! = t! + .2
  WHILE TIMER < t!
  WEND
NEXT i%

verwenden.

Vorgabewerte bei Übergabeparameter

QuickBASIC erlaubt Ihnen auch keine Vorgabewerte bei Parameter, die man dann bei Aufrufen weglassen kann:

' Geht leider :-( auch wiederum nicht
DECLARE SUB ZeichneLinie(x1%, y1%, x2%, y2%, f% = 15)

' Hauptprogramm
ZeichneLinie 12, 3, 6, 2
ZeichneLinie 12, 3, 6, 2, 3

In C++ und Ada ist dagegen so etwas wiederum möglich.

Operatoren als Funktionsprozedurnamen

C++ und ADA erlauben die Verwendung von Operatoren wie +, -, *, AND usw., um beispielsweise eine Additionsoperation für eigene Datentypen zu definieren:

-- Vektorarithmetik in ADA
type vektor;

function "+"(v1, v2 : vektor) return vektor;

QuickBASIC kennt diese Möglichkeit überhaupt nicht, so dass Sie mit Prozeduren im Stil von

SUB Addiere(v1 AS Vektor, v2 AS Vektor, ergebnis AS Vektor)
  ergebnis.x = v1.x + v2.x
  ergebnis.y = v1.y + v2.y
  ergebnis.z = v1.z + v2.z
END SUB

arbeiten müssen. An dieser Stelle möchte ich Sie auf die 3D-Animation von Pyramiden verweisen, wo Sie wunderbar :-) die Unterschiede zwischen der C++-Umsetzung sowie der QuickBASIC-Version vergleichen können: Während die C++-Version eine eigene Vektorklasse besitzt, mit welcher die gesamten Vektorberechnung wie im Lehrbuch zur Linearen Algebra im Code formuliert werden konnte, mussten in der QuickBASIC-Version diese Berechnungen im Assembler-Programmierstil mit normalen SUB-Aufrufen auscodiert werden.

Visual Basic kenne ich zu wenig gut, um beurteilen zu können, ob das eine oder andere in QuickBASIC nicht vorhandene Sprachkonstrukt dort existiert. Ergänzungen Ihrerseits zu diesem Thema werden daher dankend entgegengenommen, um diesen Artikel ergänzen zu können.

Einsatzgebiet von SUB- und FUNCTION-Unterprogrammen

Einführung

Bis dahin lernten Sie eigentlich nur die reine Grammatik von SUB und FUNCTION kennen. Das folgende Kapitel ist daher mehr praxisorientiert und soll Ihnen diverse Anregungen für ein schönes :-) und sauberes Software-Design auf den Weg geben.

Die sog. Analyse und Ausarbeitung des Software-Designs ist das A und O des Gelingens eines Software-Projektes! Wenn Sie also eine gute Idee haben, versuchen Sie diese sauber in Teilprobleme zu zerlegen, welche dann häufig einer SUB- bzw. FUNCTION-Prozedur entsprechen.

Beispiel zum Software-Entwurf: CAD-Konstruktionsprogramm

Angenommen, Sie wollen ein CAD-Zeichenprogramm schreiben, welches einen elektronischen Zirkel und Lineal zur Verfügung stellt, so dass Sie damit geometrisch konstruieren können. Für diesen Fall definieren wir zuerst einmal das sog. Datenmodell, in unserem Fall also ein Zeichnungsdokument.

Ein Zeichnungsdokument besteht aus einer beliebigen Anzahl geometrischer Objekte:

TYPE Punkt
  x AS DOUBLE
  y AS DOUBLE
END TYPE

TYPE Linie
  p1 AS Punkt
  p2 AS Punkt
  farbe AS INTEGER
END TYPE

TYPE Kreis
  m AS Punkt
  r AS DOUBLE
  farbe AS INTEGER
END TYPE

Unser QuickBASIC-CAD-Programm soll jeweils nur ein Dokument im Arbeitsspeicher behalten können. Daher lösen wir die Speicherverwaltung ganz einfach mit globalen Feldvariablen:

CONST maxAnzahlLinien% = 1000
CONST maxAnzahlKreise% = 800

DIM SHARED linSeg(1 TO maxAnzahlLinien%) AS Linie, kreisSeg(1 TO maxAnzahlKreise%) AS Kreis
DIM SHARED AnzLinien%, AnzKreise%

Damit haben wir unser Datenmodell im Speicher! => Jetzt können wir dazu passende Verarbeitungsoperationen festlegen:

' Allgemeine Operationen, die die ganze Zeichnung betreffen
DECLARE SUB LadeZeichnungVonFestplatte(dateiname$, fehlercode%)
DECLARE SUB SpeichereZeichnungAufFestplatte(dateiname$, fehlercode%)
DECLARE SUB LoescheZeichnungImArbeitsspeicher()

' Operationen auf einzelne Elemente
DECLARE SUB AddiereLinie(l AS Linie)
DECLARE SUB AddiereKreis(k AS Kreis)
DECLARE SUB ModifiziereLinie(id%, l AS Linie)
DECLARE SUB ModifiziereKreis(id%, k AS Kreis)
DECLARE SUB LoescheLinie(id%)
DECLARE SUB LoescheKreis(id%)

' Berechnungen
DECLARE SUB BerechneSchnittpunktLinieLinie(l1 AS Linie, l2 AS Linie, sp AS Punkt, Anzahl%)
DECLARE SUB BerechneSchnittpunkteLinieKreis(l AS Linie, k AS Kreis, sp() AS Punkt, Anzahl%)
DECLARE SUB BerechneSchnittpunkteKreisKreis(k1 AS Kreis, k2 AS Kreis, sp() AS Punkt, Anzahl%)
DECLARE SUB TangentenKreisDurchPunkt(k AS Kreis, p AS Punkt, tl() AS Linie, Anzahl%)
DECLARE SUB TangentenAn2Kreise(k1 AS Kreis, k2 AS Kreis, tl() AS Linie, Anzahl%)
DECLARE SUB KreisDurchDreiPunkte(p1 AS Punkt, p2 AS Punkt, p3 AS Punkt, k AS Kreis, fehler%)
' ^_ fehler% = -1, wenn alle Punkte auf einer Geraden liegen
DECLARE FUNCTION Abstand#(p1 AS Punkt, p2 AS Punkt)
DECLARE FUNCTION Laenge#(l AS Linie)

' Benutzereingaben
DECLARE SUB EingabePunkt(PromptText$, p AS Punkt, abschlusscode%)
' abschluscode% = 0  => Eingabe erfolgt
' abschlusscode = -1 => Abbruch mit <Esc>
DECLARE SUB Cursortastenmenu(Menutexte$(), startInd%, AnzPkt%, auswahl%, abschlusscode%)
DECLARE SUB Sicherheitabfrage(Fragentext$, Antwort%)
' usw...

d.h. Sie definieren so ziemlich alle Teilaufgaben, die in einem solchen Falle vorkommen, zunächst einmal nur als Prozedurkopf (Prototyp). Und nun kommt der entscheidende Vorteil: Beim Implementieren der einzelnen Funktionen müssen Sie sich jeweils nur auf einen Teilbereich, also in der Regel eine Funktion konzentrieren:

SUB AddiereLinie(l AS Linie)
  IF AnzLinien% < maxAnzahlLinien%
    AnzahlLinien% = AnzahlLinien% + 1
    linSeg(AnzahlLinien%) = l
    ZeichneLinie l
  ELSE
    Fehlermeldung "Zeichenspeicher voll!"
  END IF
END SUB

FUNCTION Abstand#(p1 AS Punkt, p2 AS Punkt)
  dx# = p2.x - p1.x
  dy# = p2.y - p1.y
  ' Berechnung mit Satz von Pythagoras
  Abstand# = SQR(dx# * dx# + dy# * dy#)
END FUNCTION

Wie Sie also sehen, entstehen klare, überschaubare Programmteile, die dann am Schluss ein Ganzes ergeben :-). Auf diese Art und Weise arbeiten übrigens alle Profis auch!

Auslagerung von SUB-Prozeduren in Bibliotheken

In guten Entwicklungsumgebungen (bei QuickBASIC über sog. Libraries, dort nur beim Compiler!) ist es zusätzlich noch möglich, einzelne solche SUB- und FUNCTION-Prozeduren als separate Datei mit der Endung .LIB (allgemein) oder .QLB (spezifisch für QuickBASIC) auszulagern, die man dann beliebig oft wiederverwenden kann. Um Module zu verwenden, muss man wie in C und C++ auch mit sog. Headerdateien arbeiten, in welchen man die Funktionsdeklarationen (Prototypen) in eine separate Datei .BI (steht vermutlich für basic include) und ist mit C und C++ zu vergleichen:

ProgrammierspracheDateiendung HeaderDateiendung Implementation
C.h.c
C++.hpp.cpp
QuickBASIC.bi .bas

Für dieses gesamte Kapitel ist der QuickBASIC-Compiler nötig, d.h. die Interpreterversion QBASIC.EXE genügt nicht mehr!

Anwendungsbeispiel: HTML-Bibliothek

Angenommen, Sie möchten mit QuickBASIC HTML-Dateien verarbeiten können und benötigen zu diesem Zweck eine Möglichkeit, bei normalem Text die Sonderzeichen in die bekannte &xxx;-Syntax zu konvertieren und umgekehrt.

Dazu erstellen wir zuerst eine separate Datei HTMLCONV.BI:

' HTML-Library
' Headerdatei

DECLARE FUNCTION TextNachHTML$(t$)
DECLARE FUNCTION HTMLNachText$(ht$)

In diese Datei würde man auch eigene Typen, globale Variablen usw. plazieren. Für unsere HTML-Konvertierungsbibliothek brauchen wir natürlich noch die sog. Implementation (=vollständige Ausprogrammierung) dazu:

' HTML-Library
' Implementation

' $INCLUDE 'htmlconv.bi'

FUNCTION TextNachHTML$(t$)
  h$ = ""
  FOR i%=1 TO LEN(t$)
    z$ = MID$(t$, i%, 1)
    SELECT CASE z$
    CASE "ä"
      h$ = h$ + "&auml;"
    CASE "ö"
      h$ = h$ + "&ouml;"
    CASE "ü"
      h$ = h$ + "&uuml;"
    CASE "ß"
      h$ = h$ + "&szlig;"
    CASE "Ä"
      h$ = h$ + "&Auml;"
    CASE "Ö"
      h$ = h$ + "&Ouml;"
    CASE "Ü"
      h$ = h$ + "&Uuml;"
    CASE CHR$(34)
      h$ = h$ + "&quot;"
    CASE "&"
      h$ = h$ + "&amp;"
    CASE "<"
      h$ = h$ + "&lt;"
    CASE ">"
      h$ = h$ + "&gt;"
    CASE ELSE
      h$ = h$ + z$
    END SELECT
  NEXT i%
  TextNachHTML$ = h$
END FUNCTION

FUNCTION HTMLNachText$(ht$)
  p% = 1
  h$ = ""
  DO
    q% = INSTR(p%, ht$, "&")
  IF q% = 0 THEN EXIT DO
    ' Teil zwischen den Sonderzeichen einfügen
    h$ = h$ + MID$(ht$, p%, q% - p%)
    q2% = INSTR(q% + 1, ht$, ";")
    IF q2% = 0 THEN
      PRINT "Syntaxfehler in HTML-String!"
      ERROR 5
    END IF
    SELECT CASE MID$(ht$, q% + 1, q2% - q% - 1)
    CASE "auml"
      h$ = h$ + "ä"
    CASE "ouml"
      h$ = h$ + "ö"
    CASE "uuml"
      h$ = h$ + "ü"
    CASE "szlig"
      h$ = h$ + "ß"
    CASE "Auml"
      h$ = h$ + "Ä"
    CASE "Ouml"
      h$ = h$ + "Ö"
    CASE "Uuml"
      h$ = h$ + "Ü"
    CASE "quot"
      h$ = h$ + CHR$(34)
    CASE "amp"
      h$ = h$ + "&"
    CASE "lt"
      h$ = h$ + "<"
    CASE "gt"
      h$ = h$ + ">"
    CASE ELSE
      PRINT "Ungültiges Sonderzeichen!"
      ERROR 5
    END SELECT
    p% = q2% + 1
  LOOP
  HTMLNachText$ = h$ + MID$(ht$, p%)
END FUNCTION

Dieses Programm speichern Sie als HTMLCONV.BAS ab. Diese Programmdatei für sich alleine betrachtet, macht natürlich noch nichts, da ja kein Hauptprogramm vorhanden ist. Aber Sie erzeugen davon eine separate Bibliothek mit Run, Make Library.... Es entstehen daraufhin die beiden Dateien HTMLCONV.LIB und HTMLCONV.QLB (dies entspricht den bekannten LIBxx.A-Dateien von C/C++), welche Ihre Routine in Maschinensprache enthält.

Verwendung in einem Programm

Nun können Sie diese Bibliothek überall dort verwenden, wo Sie eine Funktion benötigen, um einen String in seine HTML-Darstellung umzuwandeln, beispielsweise

' Meile's WebEdit (Microsoft FrontPage hat ausgedient!)

' $INCLUDE: 'htmlconv.bi'

' Hauptprogramm
INPUT "Titel der Seite"; t$
HTML$ = "<HTML><HEAD><TITLE>" + TextNachHTML$(t$) + "</TITLE></HEAD>"
' ...
PRINT HTML$

Dieses Beispiel als MEILE_WE.BAS speichern (dies ist unser Hauptprogramm!). Dabei müssen Sie QuickBASIC kurz verlassen und von der DOS-Kommandozeile mit

C:\BASICPRG>qb /l htmlconv meile_we

aufrufen. /L bewirkt das Laden beliebiger solcher .QLB-Dateien, so dass QuickBASIC auch wirklich unsere FUNCTION-Prozeduren kennt und linken (=Fachausdruck beim Compilieren für das Verknüpfen einzelner Module) kann. Wenn alles geklappt hat, sollten Sie dieses Programm starten können:

Titel der Seite? Äußere Ölbüchse
<HTML><HEAD><TITLE>&Auml;u&szlig;ere &Ouml;lb&uuml;chse</TITLE></HEAD>

Auf diese Art und Weise müssen Sie jetzt nicht mehr überall denselben Prozedurcode einfügen (und allfällige Änderungen gleich an haufenweise Dateien vornehmen!), sondern Sie binden es in die Anwendung ein! Das Projekt, an dem ich beruflich arbeite, besteht auch aus rund 400 solcher Bibliotheksdateien! Auf diese Weise schaffen Sie sich wiederverwendbare Software-Bausteine. Wenn Sie jetzt also in einem total anderen Projekt wie einer Active Server Page (ASP) auf einem Webserver unter Windows NT wieder einmal vor dem Problem »Wie mache ich jetzt den Text aus der Datenbank zu HTML?« stehen, so brauchen Sie nur noch HTMLCONV.LIB zu $INCLUDEn und anwenden.

Gute Software-Entwickler schreiben übrigens zu sämtlichen solchen Modulen eine entsprechende Dokumentation, beispielsweise in HTML wie diese Web-Seite oder mit Textverarbeitung, damit ein Nachschlagewerk (Bibliothek!) entsteht.


Wieder zurück zur Übersicht


© 2000 by Andreas Meile