BlitzBasic mit AmiBlitz3 - ein Tutorial für Amiga-Programmierer www.mbergmann-sh.de

BlitzBasic für Einsteiger – Grundrechenarten und eigene Funktionen

Rechner sind zum Rechnen da – das ist ihr Kerngeschäft und da bildet der Amiga keine Ausnahme. BlitzBasic/AmiBlitz3 bringt bereits eine große Anzahl an mathematischen Bibliotheksfunktionen für alle Anwendungsfälle mit, die sich bei Bedarf jederzeit um selbstgeschriebene, eigene Funktionen erweitern lassen – und darum geht es in diesem Teil unseres Tutorials.

Die Grundrechenarten

Um einfache Berechnungen anzustellen, bedarf es keiner umfangreichen Mathe-Bibliotheken und -funktionen. BlitzBasic beherrscht ohne weiteren Aufwand Addition (+), Subtraktion (-), Multiplikation (*) und Division (/). Um eine Berechnung durchzuführen genügt es, zwei Zahlen mit dem entsprechenden Operator zu verknüpfen und das Ergebnis einer Variablen zuzuweisen. Beispiele:

summe.w = 47 + 11     ; Addition
summe.w = 50 - 8      ; Subtraktion
produkt.w = 33 * 3    ; Multiplikation
quotient.q = 42 / 3   ; Division
quotient.q = 42 / 0   ; Division durch Null nicht erlaubt!

 

Priorität von Rechenoperationen

Tauchen in einem Term (einer Anweisung) unterschiedliche Rechenoperationen auf, so muss man die Priorität der einzelnen Operationen beachten. Die Hierarchie ist wie folgt aufgebaut:

  1. Potenzierung
  2. Multiplikation und Division („Punktrechnung“)
  3. Addition und Subtraktion („Strichrechnung“)

Beachtet man diese Rangordnung nicht, so kann es zu unerwarteten Seiteneffekten aufgrund falscher Ergebnisse kommen. Beispiel:

meinErgebnis.w = 5 + 5 * 3

Erwartetes Ergebnis: 30
Tatsächliches Ergebnis: 20

Überrascht? Wir haben in diesem Beispiel die Faustregel „Punkt- vor Strichrechnung“ nicht beachtet und denken uns: 5 + 5 = 10, 10 * 3 = 30. Der Computer macht alles richtig und rechnet stattdessen: 5 * 3 = 15, 5 + 15 = 20.

Klammerung von Termen

Um die Priorität einer verketteten Berechnung zu ändern, setzt man runde Klammern entsprechend der gewünschten Priorisierung:

meinErgebnis.w = (5 + 5)* 3 

Erwartetes Ergebnis: 20 
Tatsächliches Ergebnis: 20

Jetzt werden tatsächlich 5 + 5 addiert, bevor die Multiplikation durchgeführt wird.

Die Wahl eines „passenden“ Datentyps

Wenn man das Ergebnis einer Berechnung einer Variablen zuweisen möchte, dann muss man sich bereits im Vorfeld Gedanken darüber machen, welche Art von Zahlen und Zahlengrößen dabei anfallen können. Die Zahl muss ja schließlich auch in die Variable hinein passen. Werden nur sehr kleine Zahlen als Ergebnis erwartet? Kann ich mich auf Ganzzahlen beschränken, oder sind Fließkommazahlen zu erwarten? Wie groß kann ein Ergebnis ausfallen, falls sich die Werte der Berechnung ändern?

Der Datentyp einer Variable bestimmt, ob eine abzulegende Zahl fehlerfrei dargestellt werden kann (siehe Anhang A) oder nicht. Ausschlaggebend ist dabei sein Wertebereich. Ist er zu klein bemessen, dann könnte die Variable überlaufen und falsche Resultate liefern. Bemisst man ihn zu groß, so verschwendet man Ressourcen. Weist man eine Fließkommazahl einem Ganzzahlen-Typen zu, dann gehen die Nachkommastellen verloren, was wiederum zu Fehlern im Programm führen kann. Gerade bei der Division zweier Zahlen muss z.B. regelmäßig mit einem Ergebnis in Form einer Fließkommazahl gerechnet werden. Du siehst, es erfordert Hirnschmalz. Der Leitsatz zur Wahl eines passenden Datentyps könnte etwa so lauten: „So viel wie nötig, aber so wenig wie möglich“.

Das Listing „calc1.ab3“ führt die Grundrechenarten vor und zeigt kleine Unterschiede zwischen Berechnungen mit Ganzzahlen und solchen mit Fließkommazahlen auf:

; -----------------------
; File:calc1.ab3
; Grundrechenarten
; Version: 1.0
; -----------------------
OPTIMIZE 1 ; Optimierung für Amiga mit Turbokarte/A1200
SYNTAX 1   ; strenge Syntaxprüfung

; Amiga Version String und das Compilerdatum
!version {"calc1 1.0 (\\__DATE_GER__)"}

; Variablen deklarieren
DEFTYPE .l l_zahl1,l_zahl2, l_result   ; Datentyp: Long
DEFTYPE .f f_zahl1, f_zahl2, f_result  ; Datentyp: Float

; Variablen initialisieren
l_zahl1 = 3201
l_zahl2 = 500
f_zahl1 = 3201.5
f_zahl2 = 500.2

; Addition
l_result = l_zahl1 + l_zahl2
f_result = l_zahl1 + l_zahl2
NPrint "Addition (integer) Ergebnis          : " + Str$(l_result)
NPrint "Addition (float) Ergebnis            : " + Str$(f_result)
NPrint "Addition (float-Werte) Ergebnis      : " + Str$(f_zahl1 + f_zahl2)
NPrint ""

; Subtraktion
l_result = l_zahl1 - l_zahl2
f_result = l_zahl1 - l_zahl2
NPrint "Subtraktion (integer) Ergebnis       : " + Str$(l_result)
NPrint "Subtraktion (float) Ergebnis         : " + Str$(f_result)
NPrint "Subtraktion (float-Werte) Ergebnis   : " + Str$(f_zahl1 - f_zahl2)
NPrint ""

; Multiplikation
l_result = l_zahl1 * l_zahl2
f_result = l_zahl1 * l_zahl2
NPrint "Multiplikation (integer) Ergebnis    : " + Str$(l_result)
NPrint "Multiplikation (float) Ergebnis      : " + Str$(f_result)
NPrint "Multiplikation (float-Werte) Ergebnis: " + Str$(f_zahl1 * f_zahl2)
NPrint ""

; Multiplikation
l_result = l_zahl1 / l_zahl2
f_result = l_zahl1 / l_zahl2
NPrint "Division (integer) Ergebnis          : " + Str$(l_result)
NPrint "Division (float) Ergebnis            : " + Str$(f_result)
NPrint "Division (float-Werte) Ergebnis      : " + Str$(f_zahl1 / f_zahl2)
NPrint ""

End

Ausgabe:

Addition (integer) Ergebnis : 3701
Addition (float) Ergebnis : 3701
Addition (float-Werte) Ergebnis : 3701.699

Subtraktion (integer) Ergebnis : 2701
Subtraktion (float) Ergebnis : 2701
Subtraktion (float-Werte) Ergebnis : 2701.3

Multiplikation (integer) Ergebnis : 1600500
Multiplikation (float) Ergebnis : 1600500
Multiplikation (float-Werte) Ergebnis: 1601390

Division (integer) Ergebnis : 6
Division (float) Ergebnis : 6.401999
Division (float-Werte) Ergebnis : 6.400439

Programmanalyse:

  • in den Zeilen 13 bis 20 deklarieren und initialisieren wir, wie gewohnt, die Variablen des Programms. Verwendet werden Ganzzahlenwerte (Integerwerte) vom Typ Long und große Fließkommawerte vom Typ Float.
  • die Zeilen 23 bis 28 führen Additionen durch und geben anschließend deren Summen aus. Dabei addiert die Berechnung in Zeile 24 zwei Ganzzahlen und weist die Summe einer Float-Variablen zu. Beachte: Die Ausgabe zeigt trotzdem lediglich eine Ganzzahl an.
  • die Ausgabe in Zeile 27 erfolgt mit einer direkt an NPrint übergebenen, mittels Str$ in einen String umgewandelten Berechnung mit zwei Fließkommazahlen. Die Ausgabe zeigt hier, wie erwartet, eine Fließkommazahl als Ergebnis an.
  • Analog zum Gesagten erfolgt in den Zeilen 31 bis 38 die Subtraktion, in den Zeilen 39 bis 44 die Multiplikation und in den Zeilen 46 bis 52 die Division.

Erinnerung: Print und NPrint erwarten bei der Verwendung von aus Zahlen und Strings zusammengesetzten Parametern, dass die Zahlen in einem zusammengesetzten String ebenfalls als String vorliegen! Darum wandeln wir solche Zahlen per Str$ um.

Anstatt des „großen“ Typs Float hätten wir hier auch den kleineren Typ Quick verwenden können. Beide Typen unterscheiden sich allerdings sowohl in ihrem Wertebereich, als auch in ihrer Genauigkeit bezüglich ihrer Anzahl an Nachkommastellen. Das kurze Listing „floatcalc.ab3“ veranschaulicht die Unterschiede beider Datentypen:

; --------------------------
; File:floatcalc.ab3
; Division von Float-Zahlen
; Version: 1.0
; --------------------------
OPTIMIZE 1
SYNTAX 1

; Amiga Version String und das Compilerdatum
!version {"floatcalc 1.0 (\\__DATE_GER__)"}

; Variablen deklarieren
DEFTYPE .q q_zahl1,q_zahl2, q_result   ; Datentyp: Quick
DEFTYPE .f f_zahl1, f_zahl2, f_result  ; Datentyp: Float

; Variablen initialisieren
q_zahl1 = 3201.5123456
q_zahl2 =  500.7123478
f_zahl1 = 3201.5123456
f_zahl2 =  500.7123456

; Division
q_result = q_zahl1 / q_zahl2
f_result = f_zahl1 / f_zahl2
NPrint "Division (quick) Ergebnis: " + Str$(q_result)
NPrint "Division (float) Ergebnis: " + Str$(f_result)
NPrint ""

End

 

Ausgabe:

Division (quick) Ergebnis: 6.3939
Division (float) Ergebnis: 6.393915

Zum Ablauf dieses Programms gibt es nicht viel zu sagen. Wir initialisieren die Variablen jeweils mit den gleichen Inhalten. Quick liefert als Ergebnis der Berechnung eine Zahl mit 4 Nachkommastellen, bei Float sind es 6.

Aufgaben:

  1. Versuche spaßeshalber einmal, alle Variablen mit zusätzlichen Nachkommastellen zu initialisieren und compiliere das Programm erneut. Beachte die Fehlermeldung des Compilers.
  2. Initialisiere eine Quick-Variable mit dem Wert 37900001.5123456 und compiliere das Programm. Was sagt der Compiler?
  3. Schreibe ein Programm mit einer Quick-Variablen und weise dieser den Wert 32767.1 zu. Gib diesen Wert aus. Addiere nun 1.0 zum Wert und gib die Variable erneut aus. Was passiert?

Modulo – der Rest einer Division

Modulo ist eine mathematische Operation, die den Rest einer ganzzahligen Division bezeichnet. Programmierer verwenden diese Operation z.B. zur Prüfung, ob eine ganzzahligen Division einen Nachkommaanteil besitzt. Ist dies der Fall, so liefert die Operation den Wert 1 zurück, andernfalls 0. BlitzBasic verwendet den Befehl MOD, um die Operation durchzuführen. Beispiele:

rest.w = 4 MOD 3  ; Ergebnis: 1, es gibt einen Rest
rest.w = 4 MOD 2  ; Ergebnis: 0, es gibt keinen Rest

Das Listing „modulus.ab3“ demonstriert die Verwendung:

; -----------------------
; File: modulus.ab3
; Rest einer Division
; Version: 1.0
; -----------------------
OPTIMIZE 1
SYNTAX 1

; Amiga Version String und das Compilerdatum
!version {"modulus 1.0 (\\__DATE_GER__)"}

DEFTYPE .w modulus, zahl1, zahl2

; Werte eingeben
NPrint "Rest einer Division testen"
NPrint "Gib zwei Ganzzahlen ein!"
Print "Erste Zahl:  "
zahl1 = Edit(10)
Print "Zweite Zahl: "
zahl2 = Edit(10)
NPrint""

; Modulus-Operation
modulus = zahl1 MOD zahl2

; Auswertung
If (modulus = 0)
  NPrint "Die Division hat keinen Rest."
Else
  NPrint "Die Division weist einen Rest auf."
EndIf

End

Programmanalyse:

  • in den Zeilen 16 bis 20 fragen wir den Benutzer nach der Eingabe von zwei Ganzzahlen und lesen diese jeweils in den Zeilen 18 und 20 ein. Dazu verwenden wir die Funktion Edit(), das Gegenstück für Zahlen zu Edit$() (Letzteres haben wir bereits in anderen Programmen verwendet, um Zeichenketten einzulesen). Der Parameter in Klammern gibt dabei die Anzahl zulässiger Ziffern an – hier sind es 10.
  • Zeile 24 führt die Modulus-Operation aus und weist deren Resultat der Variablen modulus zu.
  • in den Zeilen 27 bis 31 untersuchen wir mit einer if…else…endif-Abfrage, ob ein Rest vorliegt und geben eine entsprechende Meldung aus. WENN der Wert der Variablen modulus Null beträgt, so gibt es keinen Rest – ANDERNFALLS existiert ein Rest (später mehr zu vergleichenden Abfragen).

Potenzierung

Eine Potenz  ist das Ergebnis des Potenzierens (der Exponentiation), das wie das Multiplizieren seinem Ursprung nach eine abkürzende Schreibweise für eine wiederholte mathematische Rechenoperation ist. Wie beim Multiplizieren ein Summand wiederholt addiert wird, so wird beim Potenzieren ein Faktor wiederholt multipliziert. Dabei heißt die Zahl, die zu multiplizieren ist, Basis. Wie oft diese Basis als Faktor auftritt, wird durch den Exponenten angegeben. Man schreibt:

BlitzBasic verwendet zur Potenzierung das Zeichen „^“. Auch diese Rechenoperation schauen wir uns an einem Beispiel an – „power.ab3“:

; -----------------------
; File: power.ab3
; Potenzieren
; Version: 1.0
; -----------------------
OPTIMIZE 1
SYNTAX 1

; Amiga Version String und das Compilerdatum
!version {"power 1.0 (\\__DATE_GER__)"}

DEFTYPE .w potenz, basis, faktor

; Werte eingeben
NPrint "Potenzieren:"
NPrint "Gib zwei Ganzzahlen ein!"
Print "Basis:  "
basis = Edit(5)
Print "Faktor: "
faktor = Edit(5)
NPrint""

; Potenz-Operation
potenz = basis ^ faktor

; Ausgabe
NPrint "Die Potenz von " + Str$(basis) + " hoch " + Str$(faktor) + " ist " + Str$(potenz)

End

Ausgabe:

Potenzieren:
Gib zwei Ganzzahlen ein!
Basis: 2 
Faktor: 16 

Die Potenz von 2 hoch 16 ist 65536

Zum Programmaufbau gibt es nicht viel zu sagen. Den Datentyp der Variablen haben wir mit Long Word (.l) angegeben – Word (.w) wäre zu klein, da beim Potenzieren sehr große Zahlen entstehen können. Den Ablauf dieses Programms solltest du inzwischen ohne weitere Erläuterung verstehen können.

 

Prozeduren, Statements und Funktionen

Eine Prozedur (Procedure) ist eine Möglichkeit, Routinen (wie z.B. wiederkehrende Berechnungen) in eigenständige Teile des Programms zu „verpacken“. Sobald eine Routine in eine Prozedur verpackt ist, kann sie von deinem Hauptcode aus aufgerufen werden. Parameter können übergeben werden, und ein optionaler Wert wird an den Hauptcode zurückgegeben. Da eine Prozedur ihren eigenen lokalen Variablenbereich enthält, kannst du sicher sein, dass keine deiner Haupt- oder globalen Variablen durch den Aufruf der Prozedur verändert wird. Diese Eigenschaft bedeutet, dass Prozeduren sehr portabel sind, d.h. sie können in andere Programme portiert werden, ohne dass es zu Konflikten mit dort verwendeten Variablennamen gibt.

Einfache Prozeduren geben keine Werte an ihren Aufrufer zurück und werden als Statements bezeichnet. Prozeduren, die Werte zurück liefern, heißen unter BlitzBasic Funktionen. Procedure ist also lediglich der Oberbegriff für Beides.

Funktionen und Statements unter BlitzBasic haben die folgenden Eigenschaften:

  • Die Anzahl der Parameter ist auf 6 begrenzt.
  • Gosub und Goto zu Labels außerhalb des Codes einer Prozedur sind streng verboten.
  •  Alle lokalen Variablen, die innerhalb einer Prozedur verwendet werden, werden bei jedem Aufruf neu initialisiert. Ihre Werte sind also nur so lange gültig, wie die Prozedur läuft.

Statements

Ein Statement definiert man nach folgender Schablone:

Statement Name{Parameter}
   ... Anweisungen ...
End Statement

Der Name des Statements ist frei wählbar. Einem Statement können innerhalb der geschweiften Klammern bis zu 6 Parameter mitgegeben werden, die Statement-intern weiterverarbeitet werden. Einem Statement muss nicht zwingend ein Parameter übergeben werden – in dem Fall bleiben die geschweiften Klammern leer. Variablen innerhalb eines Statements sind stets lokal, d.h. nur innerhalb des Statements gültig – es sei denn, man macht sie mit dem Schlüsselwort SHARED allgemein zugänglich.

Das Listing „statement.ab3“ verwendet ein Statement, um den Fakultätsfaktor einer Zahl fünfmal auszugeben.
Beachte, dass bei Verwendung der strengen Syntaxprüfung mittels OPTION 1 sämtliche Variablen im Vorfeld deklariert werden müssen! Außerdem müssen sie in diesem Fall mit ihrem vollen Namen inklusive der Extension (hier: .l) angesprochen werden.

; ----------------------------
; File: statement.ab3
; Funktion ohne Rueckgabewert
; Version: 1.0
; ----------------------------
OPTIMIZE 1
SYNTAX 1

; Amiga Version String und das Compilerdatum
!version {"statement 1.0 (\\__DATE_GER__)"}

DEFTYPE .l k             ; globale Variable: k

; ein Statement definieren
Statement fact{n.l}
  DEFTYPE .l a, k        ; Lokale Variablen: a, k

  a.l = 1
  For k.l = 2 To n.l
    a.l = a.l * k.l
  Next

  NPrint a
End Statement

; Hauptteil
For k.l = 1 To 5
  fact{k.l}      ; Aufruf des Statements
Next

End

Ausgabe:

1
2
6
24
120

Programmanalyse:

  • Zeile 12 deklariert die im Hauptteil (außerhalb des Statements) verwendete globale Variable als Long Word.
  • die Zeilen 15 bis 24 definieren das Statement fact{n.l}.
  • Zeile 15 ist der Kopf des Statements, bestehend aus dem Schlüsselwort Statement, dem Namen (fact) und der Parameterliste in geschweiften Klammern. Sie enthält nur einen Parameter: n. Beachte, dass aufgrund der strengen Syntaxprüfung der Parameter n mit einem Datentyp deklariert werden muss.
  • die Zeilen 16 bis 23 sind der Rumpf des Statements, der in Zeile 24 mit der Anweisung End Statement abgeschlossen wird.
  • Zeile 16 deklariert die lokalen Variablen des Statements als Long Word.
  • Zeile 18 initialisiert die lokale Variable a.l mit dem Wert 1.
  • in den Zeilen 19 bis 21 wird mittels einer for…next-Schleife (später mehr dazu!) der Wert der Variablen a.l mit sich selbst multipliziert.
  • der berechnete Wert wird in Zeile 23 ausgegeben.
  • Auch im Hauptteil ab Zeile 27 wird eine for…next-Schleife verwendet. Sie läuft fünf mal und ruft bei jedem Durchlauf einmal das Statement fact{n.l} auf. Würde man also den Zähler der Schleife erhöhen, so würden dementsprechend mehr Zahlen ausgegeben werden. Versuche es spaßeshalber: Ersetze die Zahl 5 in Zeile 27 mit der Zahl 10 und compiliere und starte das Programm erneut.
    Anmerkung: Das geht nur bis maximal zur Zahl 16 gut – ab 17 läuft die Variable über!

Funktionen

Im Gegensatz zu Statements liefern Funktionen über die Anweisung Function Return einen weiter verarbeitbaren Wert an den Aufrufer zurück. Eine Funktion definiert man nach folgender Schablone:

Funktion Name{Parameter}    
   ... Anweisungen ... 
   Function Return Variable  ; Rückgabewert
End Funktion

Unser Beispiel für ein Statement lässt sich ohne Aufwand auch als Funktion realisieren. Dabei geben wir die berechnete Zahl nicht gleich direkt aus, sondern übergeben sie dem Aufrufer im Hauptteil des Programms. Das Listing „function.ab3“ zeigt, wie es funktioniert:

; ----------------------------
; File: function.ab3
; Funktion mit Rueckgabewert
; Version: 1.0
; ----------------------------
OPTIMIZE 1
SYNTAX 1

; Amiga Version String und das Compilerdatum
!version {"function 1.0 (\\__DATE_GER__)"}

DEFTYPE .l k       ;Globale Variablen deklarieren

; Funktion definieren
Function fact{n.l}
  DEFTYPE .l a, k  ; Lokale Variablen deklarieren

  a.l = 1
  For k.l = 2 To n.l
    a.l = a.l * k.l
  Next

  Function Return a.l ; Rueckgabewert
End Function

; Hauptteil
For k.l = 1 To 5
  NPrint fact{k.l}  ; Rueckgabewert drucken
Next

End

Die Programmlogik ist nahezu identisch zum vorherigen Listing – allerdings geben wir nun in Zeile 23 den berechneten Wert mit Function Return an den Aufrufer (NPrint) in Zeile 28 zurück.

Zugriff auf globale Variablen

Manchmal ist es notwendig, dass eine Prozedur auf eine oder mehrere globale Variablen eines Programms zugreifen kann. Zu diesem Zweck erlaubt der SHARED-Befehl, bestimmte Variablen innerhalb einer Prozedur als globale Variablen zu behandelt. Dazu ein schnelles Beispiel:

Statement example{}
  SHARED k
  NPrint k
End Statement
For k=1 To 5
  example{}
Next

Per SHARED-Befehl teilst du dem Compiler mit, dass die Prozedur die globale Variable k verwenden soll, anstatt eine lokale Variable k zu erzeugen. Versuche dasselbe Programm ohne den SHARED-Befehl: Jetzt ist k innerhalb der Prozedur eine lokale Variable und wird daher jedes Mal 0 sein, wenn die Prozedur aufgerufen wird.

Rekursion

Der von den lokalen Variablen einer Prozedur verwendete Speicher ist nicht nur für die eigentliche Prozedur, sondern für jeden Aufruf der Prozedur reserviert. Jedes Mal, wenn eine Prozedur aufgerufen wird, wird ein neuer Speicherblock zugewiesen und erst nach Beendigung der Prozedur wieder freigegeben. Dies hat zur Folge, dass eine Prozedur sich selbst aufrufen kann, ohne ihre eigenen lokalen Variablen zu beschädigen. Das ermöglicht ein Phänomen, das als Rekursion bekannt ist. Hier ist eine neue Version der faktoriellen Funktion, die Rekursion verwendet:

; ----------------------------
; File: recursion.ab3
; rekursiver Funktionsaufruf
; Version: 1.0
; ----------------------------
OPTIMIZE 1
SYNTAX 1

; Amiga Version String und das Compilerdatum
!version {"recursion 1.0 (\\__DATE_GER__)"}

DEFTYPE .l n       ;Globale Variablen deklarieren

; Funktion definieren
Function fact{n.l}
  If n.l > 2 Then n.l = n.l * fact{n.l - 1} ; Rekursiver Aufruf
  Function Return n.l ; Rueckgabewert
End Function

; Hauptteil
For n.l = 1 To 5
  NPrint fact{n.l}  ; Rueckgabewert drucken
Next

End

Dieses Beispiel beruht auf dem Konzept, dass die Berechnung der Fakultät einer Zahl eigentlich die Zahl, multipliziert mit dem Faktor von Eins weniger als die Zahl darstellt (Zahl – 1, vergl. Zeile 16).

Zusammenfassung

In diesem Teil des Tutorials haben wir gelernt

  • wie man unter BlitzBasic die Grundrechenarten und den Modulo-Operator verwendet und wie man Zahlen potenziert.
  • wie die Hierarchie der Rechenoperationen aufgebaut ist und wie man sie durch Klammerung verändern kann.
  • auf was es bei der Wahl des Datentyps für eine Variable ankommt.
  • wie man einen numerischen Wert mittels des Edit()-Befehls einliest.
  • das bei Zuweisung einer Fließkommavariablen an eine Ganzzahlvariable der Nachkommaanteil verloren geht.
  • das Prozeduren ein Oberbegriff für Statements und Funktionen sind.
  • das man Prozeduren maximal 6 Parameter übergeben kann.
  • das Statements im Gegensatz zu Funktionen keinen Rückgabewert liefern.
  • das Variablen innerhalb von Prozeduren lokaler Natur sind und nur innerhalb der jeweiligen Prozedur Gültigkeit besitzen.
  • Das lokale Variablen für andere Prozeduren und das Hauptprogramm nicht sichtbar sind, mittels des Schlüsselwortes SHARED aber sichtbar gemacht werden können.
  • das Prozeduren sich selbst rekursiv aufrufen können, wobei die Werte lokaler Variablen erhalten bleiben.

Ausblick

Im kommenden Teil des Tutorials werden wir uns Kontrollstrukturen zur Fallunterscheidung und Verzweigung, Wiederholungsschleifen, Wahrheitswerte und Vergleichsoperatoren näher betrachten.


[Zurück zur Übersicht] | [zurück] | [vowärts]

C++ für einsteiger - ein Basis-Tutorial www.mbergmann-sh.de

C++ Tutorial – Funktionen

Schnelleinstieg: Funktionen in C/C++

C++ Funktionen sind kleine Unterprogramme, die Teilprobleme lösen. Funktionen sind ein wichtiges Werkzeug, um den Quelltext eines Programms zu ordnen und wesentliche Algorithmen und zusammenhängende Anweisungsblöcke der main()-Hauptfunktion in einer zusammenhängenden Form auszulagern. Sie werden außerhalb der main()-Hauptfunktion definiert und vereinfachen somit das Verständnis und die Lesbarkeit des Quelltextes.

Deklaration und Definition von Funktionen

Im Grunde bedeutet die Definition einer Funktion nichts Anderes, als eine Code-Block (Anweisungsblock) mit einem Funktionsnamen zu verbinden. Die Definition einer C++ Funktion besteht aus einer Deklaration und einem Anweisungsblock.

Für die Deklaration einer C++ Funktion sind gewisse Angaben erforderlich und gewisse zusätzliche Spezifikationen möglich (z.B. ‚inline‚ oder ‚virtual‚). Um eine Funktion schließlich zu definieren, muss man zusätzlich noch die zu erledigenden Anweisungen in einem Anweisungsblock zusammenfassen. Die allgemeine Definition einer C++ Funktion ist der formalen Definition einer mathematischen Funktion nicht unähnlich und man könnte sie auch wie folgt definieren: „Eine C++ Funktion ist eine Abbildung von dem Datenraum der Argumentenliste in den Datenraum des Rückgabetyps. Die dabei benutzte Abbildungsvorschrift findet sich in ihrem Anweisungsblock. Formal besitzt sie somit die folgende Struktur:“

'Rückgabe TypFunktionsname ('Argumentenliste') { 'Block von Anweisungen' }

  • Der Name der Funktion:
    Hier sollte der Programmierer einen Namen wählen, der die im Anweisungsblock definierten Anweisungen präzise und kurz in einem Wort zusammen fasst.
  • Rückgabe Typ:
    Welchen Datentyp gibt die Funktion an das Hauptprogramm zurück? Der „Rückgabe Typ“ steht vor dem Funktionsnamen.
  • Argumentenliste:
    Die „Argumentenliste“ steht direkt hinter dem Funktionsnamen, im Aufrufoperator „(…)“. Sie spezifiziert die Datentypen der Variablen, die die Funktion zur Berechnung ihrer Aufgabe benötigt.

Der ‚Rückgabe Typ‘ kann hierbei einer der C++ Datentypen (z.B. int oder double) oder ein Daten-Array  sein. Falls der ‚Rückgabe Typ‘ als void gekennzeichnet wird, gibt die Funktion keine Daten an das Hauptprogramm zurück, und führt nur den ‚Block von Anweisungen‘ aus. Die ‚Argumentenliste‘ setzt sich aus einer Liste von Datentypen der formalen Argumente (Parameter) der Funktion zusammen, die jeweils mit einem Komma voneinander getrennt sind.

Soviel zur Theorie – werden wir praktisch:

Funktionen erfüllen also den Zweck, wiederkehrende Aufgaben auszulagern, anstatt jedes Mal den gleichen Code nochmal einzutippen, wenn eine bestimmte Aufgabe ansteht.

Programme werden Zeile für Zeile ausgeführt, in der Reihenfolge, in der du den Quellcode aufgesetzt hast. Bei einem Funktionsaufruf verzweigt das Programm, um die Funktion auszuführen. Ist die Funktion beendet, springt die Programmausführung zurück zu der Zeile in der aufrufenden Funktion, die auf den Funktionsaufruf folgt. Funktionen dürfen andere Funktionen – auch sich selbst (Rekursion) – aufrufen.

Der Funktion main() kommt dabei eine Sonderstellung zu. Normalerweise muss eine Funktion, um etwas leisten zu können, im Verlauf Ihres Programms durch main() oder eine andere Funktion aufgerufen werden. main() ist die Hauptfunktion eines jeden Programms und wird beim Start des Programms vom Betriebssystem aufgerufen. Sie steuert den eigentlichen Ablauf des Programms. Die Hauptfunktion darf sich nicht selbst aufrufen.

Im realen Leben könnte das so aussehen: Stellen wir uns vor, dass du ein Bild von dir selbst zeichnest. Du zeichnest deinen Kopf, die Augen, die Nase – und plötzlich bricht dein Bleistift ab. Du »verzweigst« nun in die Funktion »Bleistift spitzen«. Das heißt, du hörst mit dem Zeichnen auf, stehst auf, gehst zur Spitzmaschine, spitzt den Stift, kehrst an deine Arbeit zurück und setzt sie dort fort, wo du aufgehört hast. Wenn ein Programm eine bestimmte Arbeit verrichtet haben möchte, kann es dafür eine Funktion aufrufen und nach Abarbeitung der Funktion genau an dem Punkt weitermachen, wo es aufgehört hat. Das Listing „demofunction.cpp“ verdeutlicht dieses Konzept:

// Listing: demofunction.cpp
#include <iostream>

// Namensraum für cout:
using namespace std;
// Funktion DemoFunction()
// - gibt eine Meldung aus und gibt danach
// - die Kontrolle zurück an den Aufrufer:   
void DemoFunction(void)  
{     
  cout << "In DemoFunction\n";
}

// Funktion main - gibt eine Meldung aus, ruft
// dann DemoFunction auf, gibt danach
// eine zweite Meldung aus:
int main()
{ 
  cout << "In main\n" ;
  DemoFunction();   // Verzweigung zur Funktion 'DemoFunction()'

  // Wieder in main() nach Abarbeitung der Funktion:    
  cout << "Zurueck in main\n";
  
  return 0;
}

Hinweis: Bei der Funktion DemoFunction() handelt es sich um eine Funktion ohne Rückgabewert (Datentyp für die Rückgabe: void).

Ausgabe:

./demofunction
In main
In DemoFunction
Zurueck in main

Programmanalyse:

  • Zeile 2 inkludiert IOSTREAM. Das ist eine Header-Datei, die Teil der Standardbibliothek von C++ ist. Diese Datei enthält Definitionen für die Ein- und Ausgabestreams, wie cin, cout, cerr und clog. Diese Streams sind Instanzen von Klassen wie istream und ostream, die wiederum von der Basisklasse ios abgeleitet sind.
  • Zeile 5 legt den Namensraum std fest und erspart dir damit Tipparbeit (vgl. std::cout).
  • Zeile 9 ist der Funktionskopf der Funktion DemoFunction().
  • Die Zeilen 10 und 12 beginnen und beenden den Anweisungsblock (Funktionsrumpf) der Funktion.
  • Zeile 11 sorgt für die Ausgabe der Meldung ‚In DemoFunktion‘, wenn die Funktion aufgerufen wird. Danach wird die Funktion ohne Rückgabewert beendet und die Programmkontrolle wieder an den Aufrufer (in diesem Fall: Die main()-Funktion) zurück gegeben.
  • Die Hauptfunktion main() beginnt in Zeile 17 (Funktionskopf). Ihr Anweisungsblock beginnt in Zeile 18 und endet in Zeile 26.
  • Zeile 19 gibt die Meldung ‚In main‘ und einen Zeilenvorschub aus.
  • Zeile 20 ruft die Funktion ‚DemoFunktion()‘ ohne Parameter auf und übergibt ihr die Programmkontrolle, bis die Funktion abgearbeitet ist.
  • Zeile 23 gibt die Meldung ‚Zurueck in main‘, gefolgt von einem Zeilenvorschub aus. Zu diesem Zeitpunkt liegt die Programmkontrolle bereits wieder bei main().
  • Zeile 25 gibt den Integer-Wert ‚0‘ (Null) an den Aufrufer (Das ist das Betriebssystem, bzw. die Konsole, in welcher das Programm gestartet wurde) zurück. Das Programm ist damit beendet.

Funktionen sind Arbeitstiere. Um ihren Aufgaben gerecht zu werden, können sie Daten (z. B. das Ergebnis einer Berechnung) über ihren Rückgabetyp an den Aufrufer zurückgeben. Um flexible Ergebnisse liefern zu können, verfügt eine Funktion über eine Parameterliste, mit der ihr Werte übergeben werden können. Soll eine Funktion zum Beispiel zwei Zahlen addieren, stellen die Zahlen die Parameter für die Funktion dar. Ein typischer Funktionskopf sieht folgendermaßen aus:

int summe (int a, int b)

Das Listing „addition.cpp“ zeigt eine Funktion, die zwei ganzzahlige Parameter übernimmt und einen ganzzahligen Wert zurückgibt. Kümmere dich momentan nicht um die Syntax oder die Einzelheiten, wie man mit Integer-Werten (beispielsweise int x) arbeitet. Wir kommen darauf zurück.

// Listing: addition.cpp
#include <iostream>

using namespace std;

// Funktion: int Add(int x, int y)
// - addiert zwei an die Funktion übergebene
// - Zahlen und gibt das Ergebnis an den
// - Aufrufer zurück:
int Add(int x, int y)
{
  cout << "\nFunktion 'Add()', erhalten: " << x << ", " << y << "\n";
  return(x + y);   // Addieren und zurückgeben
}

// --- Haupt-Funktion --
int main(void)
{
  int summand1, summand2, summe;

  cout << "Zahlen addieren:\n";
  cout << "================\n";
  cout << "Ich bin in 'main()'!\n";
  cout << "Gib die erste Zahl ein: ";
  cin >> summand1;
  cout << "Gib die zweite Zahl ein: ";
  cin >> summand2;

  // Berechnung:
  // Der Variablen 'summe' wird der Rückgabewert
  // der Funktion 'Add()' zugewiesen:
  summe = Add(summand1, summand2); // Funktionsaufruf und Wertzuweisung

  // Ausgabe des Ergebnisses:
  cout << "\nZurueck in 'main()'!\n";
  cout << summand1 << " + " << summand2 << " = " << summe << endl;
  cout << "\nProgramm beendet.\n";

  return 0;
}

Hinweis: Bei der Funktion ‚int Add(int x, int y)‘ handelt es sich um eine Funktion mit Rückgabewert und Parameterliste.

Compiliere das Programm (<F5> in Geany oder Konsole):

  g++ -Wall -s addition.cpp -o addition

Starte es anschließend und gib nach Aufforderung zwei Ganzzahlen ein (z. B. 4700 und 11).

Ausgabe:

./addition
Zahlen addieren:
================
Ich bin in 'main()'!
Gib die erste Zahl ein: 4700
Gib die zweite Zahl ein: 11

Funktion 'Add()', erhalten: 4700, 11

Zurueck in 'main()'!
4700 + 11 = 4711

Programm beendet.

Programmanalyse:

  • Die Zeilen 10 bis 14 definieren die Funktion ‚int Add(int x, int y)‘. Beachte: Dies geschieht vor der Definition von ‚main()‘!
  • Zeile 12 sorgt dafür, dass beim Aufruf von Add() eine Meldung ausgegeben wird, die auch die beiden übergebenen Parameter ausgibt.
  • Die Hauptfunktion main() erstreckt sich über die Zeilen 17 bis 40.
  • In Zeile 19 deklarieren wir drei Variablen vom Typ Ganzzahl (int). Sie dienen später der Aufnahme der beiden Summanden und der berechneten Summe.
  • Zeile 24 fordert zur Eingabe einer Zahl auf.
  • Zeile 25 liest diese Zahl mit dem Eingabestream cin ein. Dieses Schlüsselwort steht für „Console Input“ und wird vom Eingabeoperator „>>“ (dem Gegenstück zum Ausgabeoperator „<<„) gefolgt. Er verweist auf die Variable summand1, die die eingegebenen Daten (die erste Ganzzahl) aufnehmen soll.
  • In den Zeilen 26 und 27 wiederholt sich dieser Vorgang der Dateneingabe für die Variable summand2.
  • Zeile 32 – Hier wird die eigentliche Arbeit geleistet: Der Variablen summe wird das Ergebnis der Berechnung, also der Rückgabewert der Funktion Add() zugewiesen. Hierzu ruft die Zuweisung die Funktion Add() mit den Parametern (summand1, summand2) auf. Überraschung! Hatten wir nicht die Parameterlist von Add() als (int x, int y) definert? Doch, hatten wir – und das ist auch korrekt. Die Parameter x und y sind lokale, nur innerhalb der Funktion selbst sichtbare Variablen. Der Aufruf von außerhalb kann mit jeder passenden Variablen, gleich welchen Namens, erfolgen. Intern arbeitet die Funktion allerdings mit den Variablen x, y. Das kann man gut an den Zeilen 12 und 13 erkennen.
  • Zeile 36 gibt beide eingegebenen Zahlen und ihre Summe in der Form aus, wie du die Berechnung auch auf ein Blatt Papier schreiben würdest:
    4700 + 11 = 4711
  • Zeile 39 beendet das Hauptprogramm mit dem Rückgabewert Null.

Funktionsprototypen

Bisher haben wir eigene Funktionen immer vor der Hauptfunktion main() definiert. Das ist dem Umstand geschuldet, dass in C/C++ eine Variable oder Funktion immer vor ihrer ersten Verwendung deklariert sein muss. Es geht aber auch anders, wenn man dem Compiler eine Funktion durch ihren Funktionsprototypen rechtzeitig – also vor main() – bekannt macht. Dabei entspricht der Funktionsprototyp genau dem Funktionskopf, gefolgt von einem Semikolon:

// Listing: addition2.cpp
#include <iostream>

using namespace std;

// Funktionsprototyp:
int Add(int x, int y);

// -- Hauptprogramm --
int main(void)
{
  int summand1, summand2;

  cout << "Zahlen addieren:\n";
  cout << "================\n";
  cout << "Ich bin in 'main()'!\n";
  cout << "Gib die erste Zahl ein: ";
  cin >> summand1;
  cout << "Gib die zweite Zahl ein: ";
  cin >> summand2;

  // Berechnung und Ausgabe:
  cout << summand1 << " + " << summand2 << " = " << Add(summand1, summand2) << endl; 
  return 0;
}

// -- Funktion: int Add(int x, int y) --
int Add(int x, int y)
{
  return(x + y);   // Addieren und zurückgeben
}

Beachte: Im Listing „addition2.cpp“ erfolgt die Definition der Funktion „Add()“ erst nach der Hauptfunktion main(). Um die Funktion trotzdem bereitstellen zu können, wurde in Zeile 7 der Funktionsprototyp entsprechend dem Funktionskopf von Add() deklariert. Auch erfolgt die Berechnung nicht mehr mit Zwischenspeicherung des Ergebnisses in der Variablen summe – diese wurde eingespart. Stattdessen übergeben wir in Zeile 23 den Rückgabewert von Add() direkt an den Ausgabestream.

Die Verwendung von Prototypen hat übrigens den Vorteil, dass der Compiler die korrekte Verwendung einer Funktion bereits frühzeitig überprüfen und auf etwaige Regelverstöße genauer reagieren kann. Gute C/C++-Programme beinhalten deshalb einen Abschnitt, in dem alle Prototypen für Funktionen vor ihrer ersten Verwendung deklariert werden.

Zusammenfassung:

Funktionen geben entweder einen Wert oder void (das heißt: nichts) zurück. Eine Funktion zur Addition von zwei ganzen Zahlen liefert sicherlich die Summe zurück, und man definiert diesen Rückgabewert vom Typ Integer (int). Eine Funktion, die lediglich eine Meldung ausgibt, hat nichts zurückzugeben und wird daher als void (zu deutsch: leer) deklariert.

Funktionen gliedern sich in Kopf und Rumpf. Der Kopf besteht wiederum aus dem Rückgabetyp, dem Funktionsnamen und den Parametern. Mit Parametern lassen sich Werte an eine Funktion übergeben.

Funktionen müssen (wie Variablen) vor ihrer ersten Verwendung dem Compiler bekannt gemacht werden. Aus diesem Grund definiert man sie entweder vor main(), oder aber man verwendet stattdessen einen entsprechenden Funktionsprototypen an ihrer Stelle.


[Inhaltsverzeichnis] | [zurück] | [vorwärts]