TO DO: Beispiel 4 (7×5-Rechteck) im ZIP-File ändern!!
SUB
und FUNCTION
ist eines der wichtigsten
Konzepte innerhalb der Software-Entwicklung, um überhaupt komplexe
Projekte realisieren zu können.
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.
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.
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... :-(
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!
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.
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 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.
Auch hier wiederum einen Vergleich mit anderen Programmiersprachen:
PROCEDURE Quadriere(s : FLOAT) : FLOAT BEGIN Quadriere := s * s; END;Mit dem
: typ
wird der Rückgabewert signalisiert
und gleichzeitig der Typ festgelegt.FUNCTION
geschrieben und mit
RETURN typ
Rückgabewert signalisiert. Innerhalb des
sog. Rumpfes (=Ausprogrammierung der Funktion)
muss dann eine return
-Anweisung vorkommen.
function quadriere(s : float) return float is begin return s * s; end function;
SUB
geben dort einfach void
(=engl. nichts, Leere) zurück.
Ansonsten auch return
wie ADA.
float quadriere(float s) { return s * s; }
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.
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
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
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.
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.
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!
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...
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!
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.
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:
LBOUND()
und UBOUND()
, um ein
Unterprogramm wiederverwendbar zu gestaltenKurzer Vergleich mit anderen Programmiersprachen: Bei ADA kann man
ebenfalls ganze ARRAY
s ü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.
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.
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.
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.
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
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$)
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.
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.
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.
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.
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!
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:
Programmiersprache | Dateiendung Header | Dateiendung 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!
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$ + "ä" CASE "ö" h$ = h$ + "ö" CASE "ü" h$ = h$ + "ü" CASE "ß" h$ = h$ + "ß" CASE "Ä" h$ = h$ + "Ä" CASE "Ö" h$ = h$ + "Ö" CASE "Ü" h$ = h$ + "Ü" CASE CHR$(34) h$ = h$ + """ CASE "&" h$ = h$ + "&" CASE "<" h$ = h$ + "<" CASE ">" h$ = h$ + ">" 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.
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>Äußere Ölbü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 $INCLUDE
n 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.