Dies ist der Versuch, zu beweisen, dass Programmieren etwas sehr einfaches ist.
Als ein Leser dieses Tutorials scheinst du vermutlich sehr verzweifelt zu sein, immer noch nicht programmieren zu können und gleichzeitig auch wohl deswegen, dass du gerade einen Text wählst, dessen Titel dich bereits beleidigt. Vielleicht bist du auch ein Programmieranfänger und scheiterst immer wieder bei den ersten Gehversuchen, obwohl du bisher nichtmals richtig krabbeln kannst (dies ist im übertragenen Sinne gemeint).
Die größte Überwindungshürde ist häufig die, damit klar zu werden, dass Programmieren wirklich etwas sehr einfaches ist - und wegzugehen von der Einstellung "Hm, das sieht ja alles ganz trivial aus, aber so extrem simpel kann das doch gar nicht sein, also muss das wohl ganz kompliziert sein, denn diesen komplizierten Inhalt sehe ich nicht."
Hinweis: Dieses Tutorial ist zur Zeit noch nicht vollständig und wird stetig nachbearbeitet und erweitert. (Das Endziel ist die komplette Einführung in die objekt-orientierte Programmierung.) Bitte nicht erschrecken, wenn das Ende etwas plötzlich kommt. Auch bin ich für Tipps deinerseits dankbar, falls du etwas nicht verstanden hast, so dass ich in dem Fall auf das entsprechende Detail noch genauer eingehen kann.
Das wird dich erwarten: (Und falls du denkst, bereits etwas
schon lange zu wissen und verstanden zu haben, kannst du ruhig Kapitel
überfliegen bzw. auslassen.)
Beginnen wir mit dem Anfang:
Programmieren, was ist das eigentlich?
Man könnte allgemein sagen, Programmieren ist das Aufschreiben und Überlegen von Algorithmen.
Da das Schreiben hoffentlich keine Schwierigkeit darstellt und die Schwierigkeiten beim Überlegen sich in Grenzen halten, bleibt noch die Frage, was Algorithmen sind.
Algorithmen sind, grob gesagt, Anleitungen, wie ein bestimmtes Ziel zu einem Problem bzw. einer Aufgabe erreicht werden kann. Diese Anleitung ist dabei in der Art einer Liste von Anweisungen, die abgearbeitet werden müssen. Je nach Kontext ist es natürlich wichtig, die Anweisungen hinreichend primitiv zu halten - eine Anweisung wie "löse das Problem" ist nicht zugelassen. Man beachte dabei: Bei der allgemeinen Definition ist absolut nicht festgelegt, für wen diese Anweisungen sind, also von wem sie ausgeführt werden sollen. Dies kann sowohl ein Computer als auch ein Mensch oder sonst etwas ganz anderes sein. Auch die Problemstellung und das gewünschte Ziel kann alles beliebige sein. Wichtig ist nur, dass in jedem Fall klar definiert ist, was getan werden soll. Es muss klar sein, welches die erste Anweisung ist. Es muss außerdem klar sein nach dem Ausführen einer beliebiger Anweisung, welche Anweisung als nächstes auszuführen ist. Auch nicht definiert ist, in welcher Sprache oder in welcher Form diese Liste von Anweisungen sein muss.
Ein paar Beispiele:
Aufgabe:
In meinem Zimmer befindet sich ein
Haufen Abfall (sagen wir 1
m³), der in den Müllkontainer im Keller soll.
Lösung
1:
Um Schreibarbeit zu sparen, fasst man wiederholende Aktionen häufig zusammen (was vollkommen legitim ist, solange eindeutig bleibt, was genau getan werden soll; man nennt dies eine Schleife):
Nochmal Lösung
1:
(1.) Man nehme meinen
Müllkorb
Führe folgende
Aktionen 3 Mal aus:
(2.) fülle ihn mit 1/3
m³ des Abfalls,
(3.) laufe damit nach unten in den
Keller,
(4.) entleere ihn im
Müllkontainer,
(5.) laufe mit ihm wieder nach oben in
mein Zimmer,
Ende der zu wiederholenden
Aktionen
Man
beachte, dass man, je nach Kontext, evtl. das
Füllen des Müllkorbes auch noch weiter spezifizieren
muss (man kann natürlich an anderer Stelle
definieren, was genau damit gemeint ist).
Da man sich gerne allgemeiner hält (was ist, wenn der
Müllberg doch größer ist oder mein
Müllkorb noch mehr fassen kann?), könnte man den
Algorithmus aus so formalieren:
Lösung
2:
Dies ist nicht exakt der gleiche Algorithmus, denn die Aktionen sind nicht ganz dieselben. Abgesehen von der allgemeineren Fassung ist hier die Anweisung 6 von neuem Charakter. Die Anweisung 7 nennt man eine bedingte Anweisung, die nämlich genau dann ausgeführt wird, wenn die Bedingung der Anweisung 6 erfüllt wird (bzw. sie soll dann ausgeführt werden; ein Algorithmus ist nichts, was getan wird, sondern beschreibt eine Möglichkeit, etwas zu tun). Naiv ist hoffentlich sehr klar, wenn ich z.B. dir diese Anleitung gebe, was du zu tun hast. Entsprechend könnte man bei so einer bedingten Anweisung natürlich noch einen "sonst tue was anderes"-Fall hinzufügen.
"Fahre mit Aktion 2 fort" nennt man einen Sprung und zwar in diesem Fall zu Aktion 2 (deshalb würde man es auch Rücksprung nennen). Auf diese Art wird hier die Schleife realisiert.
In diesem Fall ist die Schleife noch relativ simpel (inklusive dem ganzen Algorithmus). Es könnte aber Fälle geben, in denen das ganze nicht so gut überschaubar ist. Es stellt sich dann die wichtige Frage, ob es bei der Ausführung des Algorithmus' passieren kann, dass man unendlich lange in einer Schleife gefangen ist und nicht heraus kommt. Man nennt dies dann eine Endlosschleife. Insgeheim haben wir bisher angenommen, dass der Ausführer des Algorithmus der einzige ist, der auf den Müll in meinem Zimmer zugreift - wenn beispielsweise aber aus einem unerklärlichen Grund irgendwoher pro Sekunde 1/10 m³ neuer Müll hinzukommt, der Ausführer mit dem Füllen des Mülleimers aber bereits schon eine halbe Minute braucht, sieht man leicht, dass der Ausführer des Algorithmus' in Lösung 2 in einer Endlosschleife ist. In Lösung 1 wäre dies nicht so, allerdings wäre am Ende des Algorithmus das Ziel nicht erreicht.
Lösung
3:
Genug zu diesem Beispiel und zu einem Neuen.
Aufgabe:
Es soll errechnet werden, was n hoch k ist (auch
geschrieben als n^k). n und k
sollen dabei nicht negative, ganzen Zahlen (also ohne Nachkommastellen)
sein. Dem Ausführer wird als Voraussetzung gegeben, dass er
multiplizieren kann.
Im Folgenden kommt ein sehr wichtiges neues Element in dem Algorithmus hinzu. Wir müssen uns gewisse Sachen merken. Wie das Merken genau aussieht, spielt dabei wieder keine Rolle. Ein Computer wird dazu seinen Arbeitsspeicher oder evtl. die Festplatte benutzen. Ein Mensch benutzt einfach sein Gedächtnis oder ein Notizblock oder eine sonstige Erinnerungsstütze. Für jede Sache, die zu merken ist, haben wir dabei einen bestimmten Platz (im Kopf, im Arbeitsspeicher oder wo auch immer). Wir wollen die Möglichkeit lassen, nach gewisser Zeit an dieser Stelle neue Daten/Werte zu merken. Das Merken ist dabei nicht (wie man es als Mensch vielleicht ganz gerne hätte) etwas Ergänzendes, sondern es wird immer die alte Information an dieser Stelle vergessen und überschrieben mit der neuen Information. Vielleicht stellt man sich auch einfach eine Schublade vor, in die genau eine Information reinpasst - und diese kann abgefragt werden oder auch durch eine andere ersetzt werden.
Beispiel:
Wir wollen uns einen Buchstaben
merken. Am Anfang wollen wir uns für den Buchstaben mal das
'A' merken.
Wir erinnern uns nun, dass wir uns für den Buchstaben ein 'A'
gemerkt haben.
Nun wollen wir uns für den Buchstaben
stattdessen ein 'B' merken. Nun haben wir uns also für den Buchstaben ein 'B'
gemerkt und wissen deshalb nichts mehr von dem 'A', denn nun haben wir
stattdessen uns ja das 'B' gemerkt. (Bei der Benutzung eines
Notizblocks heißt das, dass beim neuen Setzen eines neuen
Wertes der alte gestrichen wird. Für eine Sache, in diesem
Fall dem Buchstaben,
kann immer nur ein Wert gemerkt werden.)
Man beachte, dass man einen Anfangswert auch angeben sollte, denn sonst hat man da ein Speicherfeld ohne Ihnalt am Anfang. Dies kann in gewissen Situationen zu Uneindeutigkeiten kommen. (Was soll z. B. die Anweisung bedeuten "multipliziere 5 mit dem Inhalt des Speicherfeldes", wenn dort bisher kein Inahlt ist?)
Lösung
der eigentlichen Aufgabe "n hoch k":
(1.) Man merke sich einen Zähler,
der bei k
beginnen soll. [Zähler entspricht also dabei der Bezeichnung für Schublade 1]
(2.) Man merke sich das Zwischenergebnis,
welches erstmal 1 sein soll. [Zwischenergebnis ist die Schublade 2]
(3.) Wenn der Zähler
nicht 0 ist, tue folgendes:
(4.) Multipliziere das Zwischenergebnis
mit n und
merke es sich als neues Zwischenergebnis.
(5.) Zähle den Zähler um
eins runter (subtrahiere ihn mit 1) und merke es sich als neuen Zähler-Wert.
(6.) Gehe zu Befehl 3.
Ende der im Wenn-Fall
auszuführenden Befehle.
(7.) Das Ergebnis ist nun
der Wert vom Zwischenergebnis. [man könnte das Ergebnis als die Ergebnis-Schublade ansehen]
Um alle bisherigen Prinzipien (die Schleife und die zu merkenden Sachen) noch mal zu verdeutlichen, wollen wir als Mensch den Algorithmus einmal für n=3 und k=2 durchgehen.
Man mache sich einmal klar, dass dieser Algorithmus genau dem entspricht, was man auch sonst im Kopf ausgeführt hätte. (Bei diesen niedrigen Zahlen hätte man es hoffentlich noch im Kopf geschafft.)
Direkt zu einem etwas komplexeren Beispiel:
Aufgabe:
Das Ergebnis der Addition 2er Zahlen
(nicht negative, ganze Zahlen,
also ohne Nachkommastellen), die im Dezimalsystem vorliegen, soll
errechnet werden.
Es ist dabei bekannt, wie 2 Zahlen
von 0 bis 9 inklusive einer 3. Zahl,
die entweder 0 oder 1 ist, zu addieren sind. Diese Addition 2er Ziffern
(mit Übertragsziffer) kann also als Operation im Algorithmus
verwendet werden.
Lösung
(Grundschulmethode aus der 2. Klasse: "schriftlich addieren"):
(Nur ein Beispiel.)
Im gesamten Algorithmus wird der Ausführer sich diesmal wieder viel merken müssen. Diese bereits im letzten kleinen Beispiel erwähnten Merkplätze oder auch Speicherplätze nennt man Variablen. Der kleine Unterschied zwischen den aus der Mathematik bekannten Variablen ist, dass diese regelmäßig im Algorithmus immer wieder neu gesetzt werden können - in der Mathematik wird eine Variable nur ein einziges mal eindeutig definiert. Variablen in Algorithmen sind eher zu vergleichen mit einer Speicherstelle, z.B. einer Textdatei auf dem Computer oder einem Notizblock. Das Entscheidende ist, dass diese Variablen einen Wert enthalten (den sogenannten Inhalt) und dass dieser Inhalt jederzeit neu geschrieben werden kann (genauso wie der Inhalt einer Textdatei entfernt und neu geschrieben werden kann), wobei natürlich dann der alte Inhalt verloren geht. Man sagt dazu auch, dass man einer Variable einen Wert zuweist oder die Variable setzt.
Man beachte, dass dies eine zusätzliche Voraussetzung an den Ausführer darstellt, nämlich die Fähigkeit, sich Daten zu merken/speichern. Wenn nicht anders angegeben, kann man diese Fähigkeit als gegeben annehmen. In gewissen Fällen steht man aber manchmal vor dem Problem, einen Algorithmus speziell für einen Ausführer zu entwerfen, der diese Möglichkeit nicht hat. Der Algorithmus in obiger Form wäre dann so nicht möglich. So eine seltsame Situation betrachten wir hier aber nicht.
Wenn man das Prinzip der Variablen verstanden hat, kann man den Algorithmus auch etwas lesbarer so schreiben:
Nochmal die Lösung:
Die im Algorithmus benutzten Klammern dienen dabei nur dem Zweck, zu verdeutlichen, was zusammengehört (wie in der Mathematik) und nicht dem Zweck, etwas auszuklammern.
In einem Algorithmus, wo ein spezieller Wert als Ergebnis erwartet wird (in diesem Fall die Summe) und evtl. spezielle Werte vorgegeben werden (in diesem Fall die beiden Zahlen), sieht man diese vorgegebenen Werte (auch häufig Argumente oder Parameter genannt) und das Ergebnis (oder auch Rückgabewert genannt) auch als Variablen an, um so die Notation einheitlicher zu halten.
Wir wollen den Algorithmus noch ein letztes Mal, noch ein wenig lesbarer, formulieren. Hierzu ist es praktisch, eine Kurzschreibweise für die i-te Ziffer von einer Zahl einzufügen. Wir wollen diesen Wert also im Folgenden schreiben als Zahl[i]. Außerdem wollen wir mit ZifferAnzahl(Zahl) die Zifferanzahl der Zahl bezeichnen. Das Setzen einer Variable kürzen wir ab mit einem Definitionszeichen. Zahl1 und Zahl2 seien nun die beiden vorgegebenen Zahlen und Ergebnis soll das Ergebnis sein. Da es kein einfaches Zeichen für Kleiner-oder-gleich gibt, verwenden wir hierfür <= (entsprechend >=) (weil es meistens genau so gemacht wird).
Und nochmal die Lösung:
(Ausführung
des Befehls 7 und 8 rot markiert.)
Man bemerke, dass bei allen Angaben des Algorithmus, der Algorithmus selbst sich nie geändert hat, sondern nur die Form, wie er aufgeschrieben wurde. Genauso wie ein "Hallo" sich in die meisten Sprachen dieser und anderer Welten übersetzen lässt und sich auf die verschiedensten Formen hinschreiben oder symbolisieren lässt, aber immer das gleiche bedeutet.
Wir wollen diesen Algorithmus 'Addierer' nennen. Dieser Algorithmus ist von den 2 gegebenen Zahlen abhängig und liefert ein Ergebnis. Alle gegebenen Werte/Informationen nennt man die Parameter. Das Ergebnis nennt man auch den Rückgabewert. Da durch diesen Algorithmus nun bekannt ist, wie man 2 Zahlen addiert, können wir diese Technik natürlich in anderen Algorithmen weiterverwenden. Als Kurzform schreibt man häufig, wenn man 2 Zahlen X und Y addieren will, Addierer(X,Y). Die Parameter werden dabei in den Klammern hintereinander angegeben, bekommen so also eine Reihenfolge. Wir wollen einfach mal den ersten Parameter als Zahl1 und den 2. Parameter als Zahl2 festlegen (wobei es bei der Addition natürlich keine Rolle spielt). Es gilt also z.B.: 5 = Addierer(2,3). Man betrachtet häufig Addierer nun auch als Funktion, denn wie im Mathematischen Sinne einer Funktion bekommt man zu gewissen Parametern einen Rückgabewert.
Betrachten wir zur Verdeutlichung eine neue Aufgabe. In dieser wollen wir diesen Addierer-Algorithmus als bekannt voraussetzen. Außerdem soll bekannt sein, wie man eine Zahl um 1 vermindert.
Aufgabe:
Berechne das Ergebnis der
Multiplikation 2er Zahlen (wieder ganze,
nicht negative Zahlen) Zahl1
und Zahl2.
Lösung
1 (grob formuliert):
Rechne Zahl2
genau Zahl1-mal
aufeinander (also Zahl2
+ Zahl2 + Zahl2 ...).
Lösung
1 (exakte Formulierung):
(1.) Neue Variable: Zwischenergebnis
(2.) Neue Variable: Rest
(3.) Zwischenergebnis
:= 0
(4.) Rest
:= Zahl1
Wiederhole
folgende Befehle, wenn
nicht Rest
= 0 ist:
(5.) Zwischenergebnis :=
Addierer(Zwischenergebnis, Zahl2)
(6.) Rest := Rest - 1
Ende der zu wiederholenden Befehle
(7.) Ergebnis
:= Zwischenergebnis
Die Schleife wurde diesmal wieder ein wenig anders geschrieben. Gemeint ist, dass alle Befehle innerhalb der Schleife immer zusammen einmal hintereinander ausgeführt werden sollen und danach erneut die Überprüfung der Bedingung der Schleife (nicht Rest = 0) durchgeführt werden soll und falls diese Überprüfung negativ ausfällt (wenn Rest = 0 ist), dann sollen die Befehle innerhalb der Schleife nicht noch ein weiteres Mal ausgeführt werden, sondern damit ist die Schleife abgeschlossen (und evtl. Befehle hinter der Schleife sollen nun ausgeführt werden).
Man könnte mit Hilfe von bedingten Sprüngen die Lösung 1 auch äquivalent so formulieren:
Lösung
1:
Im Endeffekt ist aber natürlich die bessere Lesbarkeit zu Empfehlen, die natürlich in der 'Wiederhole, wenn ...'-Variante mehr gegeben ist. Allgemein sind Sprünge irgenwelcher Art etwas verpöhnt unter Programmierern, denn wenn man gleich 2 oder mehr Sprünge in einem Algorithmus hat, welche hin und her und durcheinander irgendwo hin springen, ist für einen Leser häufig nur schwer nachzuvollziehen, was genau der Algorithmus eigentlich tun soll.
Noch einmal zu der Nutzung des anderen Algorithmus' innerhalb
diesem: Ein Algorithmus ist genau dann klar und eindeutig definiert,
wenn ein Ausführer zu jedem Schritt genau weiß,
welcher Schritt als nächstes ausgeführt werden soll.
An der Stelle, wo nun der andere Algorithmus erwähnt und
benutzt wird, was genau hat der Ausführer also
verständlicherweise zu tun? Er arbeitet nun natürlich
aus dem anderen Algorithmus die einzelnen Schritte ab, bis er das
Ergebnis ermittelt hat und kehrt dann wieder zurück (in dem
Fall kann dann damit nun die Variable Zwischenergebnis
gesetzt werden).
Dies ist nicht ungewöhnlich und entspricht genau dem
wirklichen
Leben. Wenn ich z.B. eine Maschine baue, die meine TV-Fernbedienung
benutzt, um den Fernseher auszuschalten und nun diese Maschiene starte
(meine Aktion), ist die Funktion der Fernbedienung exakt die gleiche,
wie wenn ich sie direkt benutzt hätte.
Wenn man sich diesen Algorithmus jetzt so mal betrachtet und zum Spaß ihn mal mit 2 Zahlen ausprobiert, sieht man gleich, dass man bei diesem Algorithmus bei etwas größeren Zahlen (vor allem bei einer großen Zahl1, wenn wir die Addierer-Tätigkeit mal als relativ gering einschätzen für alle Zahlen) ziemlich lange braucht. Wenn man so etwas feststellt, ist das ein Hinweis darauf, dass der Algorithmus evtl. nicht optimal ist. Es stellt sich natürlich immer die Frage, ob es überhaupt einen besseren Algorithmus gibt oder ob man bereits den bestmöglichen hat. Diese Frage kann in komplizierten Fällen sehr schwierig zu beantworten sein.
Wenn man diesen Algorithmus genauer auf seine Geschwindigkeit hin untersuchen will, versucht man eine ungefähre Formel für die Anzahl der Aktionen in Abhängigkeit der Parameter (also der Eingaben, d.h. der vorgegebenen Zahlen) zu ermitteln. Hierbei muss man sich natürlich darauf festlegen, was als eine Aktion betrachtet werden soll (also in diesem Fall: soll die Addierer-Benutzung in Befehl 6 die einzelnen Schritte des Addierer-Algorithmus mitzählen oder selbst nur ein Befehl darstellen?). Wir wollen hier alle Befehle, so wie sie sind, nur jeweils als eine Aktion betrachten, also den Aufwand der Addier-Tätigkeit vernachlässigen (wenn man davon ausgeht, dass der Algorithmus von einem Computer ausgeführt wird, kann man auch durchaus davon ausgehen, dass das Addieren auf Prozessorebene nur einen Befehl kostet). Man kommt zu dem Ergebnis, dass die Befehle 5, 6, 7 und 8 genau Zahl1-mal ausgeführt werden. Sonst hat man noch 5 Befehle, die überhaupt nur einmal ausgeführt werden müssen. Insgesamt hat man also 4*Zahl1+5 auszuführende Befehle. Da diese Formel linear von der Eingabe (nur Zahl1 in diesem Fall) abhängt, sagt man auch, der Algorithmus liegt in der linearen Komplexitätsklasse (der höchste Grad der Formel entscheidet darüber, denn der höchste Grad ist natürlich bei größeren Zahlen immer domminierend).
Wie kann man den Algorithmus verbessern? Eine Möglichkeit wäre, falls Zahl1 > Zahl2, diese beiden zu vertauschen, denn so sind dann weniger Befehle auszuführen. Sind aber beide Zahlen groß, hilft das nicht so viel. Eine andere Möglichkeit hat man bereits in der Grundschule in der 3. Klasse unter dem Namen "schriftlich Multiplizieren" gelernt. Hier eine Möglichkeit, diesen Algorithmus zu formulieren:
Lösung
2:
Neue Variablen: Teil[1], Teil[2] ... bis Teil[ZifferAnzahl(Zahl2)]
Neue Variable: Zwischenergebnis
Neue Variable: Position
Für Position := 1 bis ZifferAnzahl(Zahl2), tue:
(*1)
Teil[Position] := Zahl2[Position] * Zahl1
Zwischenergebnis := 0
Für Position := 1 bis ZifferAnzahl(Zahl2), tue:
(*2)
Zwischenergebnis := Zwischenergebnis + Teil[Position] * 10^(Position-1)
Ergebnis := Zwischenergebnis
'Für Position := Start bis Ende, tue Aktion' soll bedeuten, dass wir Position als erstes auf Start setzen, dann die Aktion ausführen, danach Position um eins erhöhen, erneut die Aktion ausführen und dies solange wiederholen, bis Position bei Ende angelangt ist, wofür wir dann ein letztes Mal die Aktion ausführen (insgesamt sind das Ziel - Start + 1 Ausführungen von Aktion; falls Start > Ende ist, wollen wir die Aktion gar nicht ausführen).
Wir wollen in (*1) die Multiplikation mal als primitiv voraussetzen (man könnte hierfür beispielsweise den in Lösung 1 angegebenen Algorithmus verwenden, denn Zahl2[Position] ist nur eine Ziffer, also eine Zahl von 0-9, so dass auch Lösung 1 hierfür schnell sein sollte). In (*2) soll 10^k für "10 hoch k" stehen. Die Multiplikation mit der 10er-Potenz bedeutet nur das Anhängen von Nullen an der rechten Seite der Zahl, kann also auch als primitiv angesehen werden.
Nachdem hoffentlich klar geworden ist, dass dies exakt dem entspricht, was man bei einer schriftlichen Multiplikation tut (Teil[1], Teil[2] ... bis Teil[ZifferAnzahl(Zahl2)] sind dabei die untereinander hingeschriebenen Teilergebnisse, die man hinterher alle zusammenaddiert), die man aus der Grundschule kennt, wollen wir den Algorithmus etwas verbessern. Störend in ihm ist die ganze Reihe von Variablen (Teil[1], Teil[2] ... bis Teil[ZifferAnzahl(Zahl2)]), denn hierfür muss der Ausführer eine ganze Menge von Informationen sich merken (ein Mensch bei Verwendung eines Blatt Papiers für die Ausführung hat natürlich den Speicherplatz dort zu Verfügung; trotzdem wäre es nicht nötig und wenn er alles im Kopf machen würde, hätte er auch dabei Probleme). Man nennt diese Reihe von Variablen übrigens auch Array. In anderen Fällen von anderen Problemstellungen kommt man nicht drum herum, so ein Array zu benutzen. Hier jedoch geht es einfacher:
bessere Lösung
2:
Neue Variable: Teilmultiplikation
Neue Variable: Zwischenergebnis
Neue Variable: Position
Zwischenergebnis := 0
Für Position := 1 bis ZifferAnzahl(Zahl2), tue:
Teilmultiplikation := Zahl2[Position] * Zahl1
Teilmultiplikation
:= Teilmultiplikation * 10^(Position-1)
Zwischenergebnis := Zwischenergebnis + Teilmultiplikation
Ergebnis := Zwischenergebnis
Nun noch ein etwas anderes, sehr wichtiges Beispiel.
Aufgabe:
Man bekommt ein Kartenblatt
(Anzahl der Karten soll hier nicht weiter festgelegt sein) und soll
dieses Sortieren, wobei am Anfang die kleinste Karte stehen soll und am
Ende die Größte. Wir wollen dabei keine Farben
unterscheiden. Wir wollen folgende Reihenfolge (niedrigste Karte bis
höchste Karte): Ass, 2, 3, 4, 5, 6, 7, 8, 9, 10, Bube, Dame,
König, Joker. Wir wollen Karte1
< Karte2
schreiben, wenn Karte1
niedriger ist. Wir können zur Vereinfachung dem Ass den
Zahlenwert
1 geben und Bube, Dame, König, Joker entsprechend die Werte
11,
12, 13, 14. Wir betrachten also vereinfacht jede Karte als Zahl von
1-14.
Falls euch das verwirrend sollte, warum Ass am niedrigsten ist: Das soll ein Beispiel sein und ist völlig absolut egal und irrelevant hier und jetzt! Außerdem kann jeder selbst entscheiden, wie er gerne seine Karten sortieren will.
Macht euch als erstes einmal bewusst, dass ihr in der Lage sein solltet, diese Aufgabe zu lösen (oder habt ihr noch nie Karten gespielt?). Bei der Überlegung, wie ein Algorithmus aussehen könnte, überlegt einfach, wie ihr selbst denn nun die Karten sortieren würdet. Die Schwierigkeit liegt nun dadrin, das, was ihr sonst immer so total unbewusst und automatisch immer wieder selbst macht, in Worte zu fassen. Ihr solltet an dieser Stelle vielleicht auch wirklich einmal versuchen, es selbst zu überlegen und auch aufzuschreiben (hierbei immer darauf achten: jeder einzelne Schritt muss exakt sein und es muss immer eindeutig sein, welcher Befehl als nächstes kommt).
Hier nun eine der vielen möglichen Möglichkeiten:
Lösung
1 (Insertion-Sort auch genannt) grob formuliert:
Man nehme sich eine Karte und lege sie
in einen
neuen Stapel. Jede weitere Karte ist an die richtige Stelle im neuen
Stapel zu legen, d.h. man geht den neuen Stapel von Anfang an durch und
bei der ersten Position, wo die nächste Karte eine
größere ist, legt man die ausgewählte Karte
hinein.
Dies wiederholt man, bis der ursprüngliche Stapel keine Karten
mehr enthält. Der neue Stapel enthält nun alle Karten
in
sortierter Reihenfolge, also das gewünschte Ergebnis.
Lösung
1 exakt:
Neue Variable: Position
Neue Variable: Karte
Erstelle einen neuen (anfangs leeren)
Stapel, nenne ihn neuerStapel.
Wiederhole folgendes, solange noch
Karten im Kartenblatt
sind:
Nehme die erste Karte aus dem Kartenblatt heraus.
Für Position := 1 bis
(Anzahl Karten in neuerStapel),
tue:
Wenn neuerStapel[Position] > Karte ist,
höre mit dieser Schleife vorzeitig auf.
Ende der "Für"-Schleife.
Füge Karte in neuerStapel an Position ein
(zwischen Position-1
und Position).
Ende der "Wiederholen"-Schleife.
neuerStapel
ist nun das Ergebnis.
Wir wollen den Algorithmus noch etwas vereinfacht aufschreiben. Es bietet sich hier an, gleich eine ganze Reihe von Kurzschreibweisen einzuführen. Wir wollen sowohl Kartenblatt als auch neuerStapel als eine Liste auffassen. Jeder Eintrag besitzt daher eine Position in der Liste und eine Liste besitzt gewöhnlich auch eine Länge, d.h. eine Anzahl von Einträgen (wir wollen ersteinmal keine unendlichen Listen (wie z.B. die Liste aller positiven, ganzen Zahlen 1,2,3,...) betrachten).
Wenn wir nun
schreiben würden:
Karte := Kartenblatt[Position]
Dann stellt sich die Frage, ob das nun ein Rausnehmen aus dem Kartenblatt
bedeuten soll oder aber, dass die Karte
sich noch im Kartenblatt
befindet und die Variable Karte
nur ein Verweis
oder vielleicht auch eine Kopie
auf den Eintrag im Kartenblatt.
Wir wollen uns darauf festlegen, dass in so einem Fall die Liste
niemals geändert wird (es würden an vielen anderen
Stellen,
wenn man sich auch mal die vorherigen Algorithmen betrachtet, sonst
Verwirrungen entstehen).
Es stellt sich immer noch die Frage, ob Karte nun eine Kopie der Karte im Kartenblatt ist oder nur ein Verweis auf diese Karte im Kartenblatt (in diesem Beispiel ist es für einen Mensch als Ausführer natürlich nicht so leicht, die Karte eben mal zu kopieren; wenn es aber um ganz normale Zahlen geht, was wir ja vereinfacht angenommen haben, ist dies natürlich möglich).
Eine Kopie und ein Verweis (manchmal auch Referenz genannt) ist aber natürlich etwas unterschiedliches. Wenn man z.B. auf Karte nun "Hallo" oben links in die Ecke schreibt und es sich um eine Kopie gehandelt hat, dann hat sich bei der Originalkarte im Kartenblatt nichts geändert. Wenn aber die Variable Karte nur ein Verweis auf die Karte im Kartenblatt war und nun auf Karte "Hallo" geschrieben wurde, ist damit die Karte im Kartenblatt beschrieben worden.
In manchen Fällen kann es nun gewünscht sein, dass es sich um einen Verweis handelt, in manchen anderen Fällen ist es eher erwünscht, dass man immer mit Kopien handelt. Man kann dies natürlich immer an jeder Stelle explizit angeben, was gemeint ist. Man hat sich zusätzlich aber darauf geeinigt, dass man bei primitiven Variablenwerten immer implizit eine Kopie meint (wenn nicht anders angegeben) und im Falle von komplexeren Objekten meint man ein Verweis. Primitive Typen sind z.B. Zahlen und kürzere Texte/Buchstabenfolgen. Komplexere Typen sind z.B. ganze Listen oder andere Objekte, die gleich mehrere Sachen enthalten.
Beispiel
1 (implizite Kopie):
Neue Variablen vom Typ Zahl: Zahl1, Zahl2
Zahl1 := 10
Zahl2 := Zahl1
Zahl2 := 5
Ergebnis := Zahl1
Das Ergebnis ist nun 10, da bei der Änderung von Zahl2 die Zahl1 nicht geändert wurde, denn Zahl2 war nur eine Kopie von Zahl1, stand mit ihr also in keinem Zusammenhang nach der Kopie.
Beispiel
2 (impliziter Verweis):
Neue Variablen vom Typ Liste: Liste1, Liste2
Liste1 und Liste2 sollen erstmal leer sein.
Füge die Zahl 5 in Liste1 am Ende hinzu.
Füge die Zahl 3 in Liste1 am Ende hinzu.
Liste2 := Liste1
Füge die Zahl 42 in Liste2 am Ende hinzu.
Ergebnis := Liste1
Das Ergebnis ist nun die Liste, die aus 5, 3 und 42 besteht.
Falls eine Kopie allerdings erwünscht war, könnte man es z.B. folgendermaßen aufschreiben:
Beispiel
3 (explizite Kopie):
Neue Variablen vom Typ Liste: Liste1, Liste2
Liste1 und Liste2 sollen erstmal leer sein.
Füge die Zahl 5 in Liste1 am Ende hinzu.
Füge die Zahl 3 in Liste1 am Ende hinzu.
Liste2 := Copy(Liste1)
Füge die Zahl 42 in Liste2 am Ende hinzu.
Ergebnis := Liste1
Das Ergebnis ist nun die Liste, die aus 5 und 3 besteht.
Zurück zu dem ursprünglichen Beispiel: Da
wir uns darauf
festgelegt haben, dass Zahlen implizit kopiert werden sollen und wir
die Karten als Zahlen darstellen wollten, liegen hier also Kopien vor.
Dies soll uns aber nicht weiter stören. Nach dem Befehl:
Karte := Kartenblatt[1]
Ist es allerdings noch notwendig, die erste Karte aus dem Kartenblatt zu
entfernen. Wir wollen hierzu eine Art Funktion einführen
(übliche Schreibweise, zusätzliche Methoden als
Funktionen anzusehen), die wir Delete
nennen wollen, wobei Delete(Liste, i) die i-te Stelle der Liste löschen
soll. Wie bereits früher wollen wir mit Length(Liste) die Anzahl
der Einträge in der Liste
bezeichnen. Wir wollen außerdem mit der Funktion Add(Liste, i, Eintrag) das
Hinzufügen von Eintrag
an der i-ten
Stelle (d.h. zwischen dem (i-1)-ten
und dem i-ten
Eintrag) in der Liste
bezeichnen. Legen wir uns im Folgenden nun auch darauf fest, dass neue
Variablen für Zahlen anfangs automatisch mit 0 initialisiert
werden sollen und Listen am Anfang nach der Einführung leer
sein
sollen (um nicht immer wieder schreiben zu müssen, dass die
Liste
am Anfang leer sein soll).
Wir können den Algorithmus mit diesen neuen Schreibweisen und Festlegungen nun noch ein wenig exakter so formulieren:
Lösung
1 exakt:
Neue Variable vom Typ Zahl: Position
Neue Variable vom Typ Zahl: Karte
Neue Variable vom Typ Liste: neuerStapel
Wiederhole folgendes, solange Length(Kartenblatt)
> 0 ist:
Karte
:= Kartenblatt[1]
Delete(Kartenblatt, 1)
Für Position := 1 bis
Length(neuerStapel),
tue:
Wenn neuerStapel[Position] > Karte ist,
höre mit dieser Schleife vorzeitig auf.
Ende der "Für"-Schleife.
Add(neuerStapel, Position, Karte)
Ende der "Wiederholen"-Schleife.
Ergebnis
:= neuerStapel
Etwas stillschweigend haben wir hier und auch schon bereits in den vorherigen kleineren Beispielen für Variablen spezielle Typen mitangegeben. Dies war in den allerersten einfachen Beispielen noch nicht notwendig, da sowieso im ganzen Algorithmus nur ein Typ verwendet wurde. Durch solche neuen Fragen wie die Kopie oder dem Verweis und auch durch solche Funktionen wie Length, Delete und Add und die Schreibweise Liste[i] ist es aber sinnvoll, einen Typ im Vorhinein mit anzugeben, da sonst Verwirrungen und Unklarheiten auftreten könnten (was z.B. soll bedeuten: Delete(Zahl, 1) ?). Die meisten Computer z.B. unterscheiden auch zwischen ganzen Zahlen und Zahlen mit Nachkommastellen.
Betrachten wir noch einen weiteren Algorithmus zum Sortieren:
Lösung
2 (Bubble-Sort):
Neue Variablen vom Typ Zahl: x, i, j
Für i := 1 bis Length(Kartenblatt), tue
Für j := 1 bis Length(Kartenblatt)-1, tue
Wenn Kartenblatt[j] > Kartenblatt[j+1], dann tue
x
:= Kartenblatt[j]
Kartenblatt[j] := Kartenblatt[j+1]
Kartenblatt[j+1] := x
Ende "Wenn"-Fall
Ende "Für"-Schleife
Ende "Für"-Schleife
Ergebnis :=
Kartenblatt
Dies ist interessanterweise der Algorithmus, auf den die meisten Leute als erstes kommen, wenn sie sich einen Algorithmus zum Sortieren überlegen sollen. Man beachte, dass offensichtlich immer 3*n*n Befehle (n sei dabei die Anzahl der Einträge im Kartenblatt) notwendig sind. Lösung 1 ist ein abhängig von der bereits gegebenen Ordnung vom Kartenblatt, aber selbst im extremsten Fall ist Lösung 1 schneller.
Es gibt noch viele weitere Algorithmen zum Sortieren von Listen, die teilweise (vor allem für sehr große Listen) noch deutlich schneller sind. Diese sind aber auch etwas umständlicher, deshalb wollen wir hier erstmal nicht darauf eingehen.
Wir wollen nun zu einem neuen Kapitel übergehen. Das Überlegen von Algorithmen bleibt aber immer beim Programmieren die absolute Grundlage. Es kann daher nützlich sein, sich zu bestimmten Sachen selbst Algorithmen zu überlegen.
Ein paar Denkansätze für weitere
Problemstellungen (manche
davon leichter, manche schwerer und bei manchen wurde erst vor
vergleichsweise kurzer Zeit mathematisch bewiesen, dass eine
Lösung nicht möglich ist):
Man beachte auch, dass das Überlegen von Algorithmen etwas sehr altes ist. Jedes Kuchenrezept, jede Bauanleitung und jeder sonstige beschriebene Vorgang ist ein Algorithmus. Wir Menschen haben uns Algorithmen überlegt, seit es uns gibt. Selbst Tiere leben ihr gesamtes Leben nach einem Algorithmus ab, der sich intuitiv in ihnen befindet, programmiert von der Evolution. (Wenn Hunger, dann esse. Wenn Durst, dann trinke. Wenn gewisse sexuellen Bedürftnisse, dann Partnersuche. Wenn müde, dann suche Platz zum Schlafen und schlafe. ... Wiederhole Überprüfungen wieder am Anfang.) Und natürlich ist das Arbeitsprinzip einer Maschiene nichts anderes, als einfach einen festgelegten Algorithmus Schritt für Schritt abzuarbeiten.
Hier noch vom Lehrstuhl 1 der Informatik der RWTH Aachen eine
sehr schöne Beschreibung eines Algorithmus' zum Suchen einer
CD aus einer geordneten Sammlung von CDs:
www-i1.informatik.rwth-aachen.de/~algorithmus/algo1.php
An dieser Stelle sind bereits nun alle Grundlagen zum Programmieren gelegt und wenn ihr alles verstanden habt könnt ihr bereits sagen, dass ihr programmieren könnt, auch wenn das Bisherige vom Wissensumfang wirklich minimal und trivial ist.
Damit sich das Wissen aber auch irgendwie festsetzt und hält, wollen wir nun das Programmieren praktisch anwenden. Wir wollen damit beginnen, geschriebene Algorithmen testen zu lassen und sie in einer einheitlicheren Form aufzuschreiben. Abgesehen von dem Lerneffekt ist es auch ein unbeschreiblich cooles Gefühl zu sehen, wie da der eigene Algorithmus durchläuft und genau das tut, was er soll. Hierfür wurden sogenannte Programmiersprachen entwickelt. In Programmiersprachen sind sehr strikte Regeln vorgegeben, in welcher Form der Algorithmus aufgeschrieben werden muss. Zu dieser Form, die man auch Grammatik nennt, gehören solche Details wie z.B. ein Semikolon am Ende eines Befehls und Ähnliches. Außerdem kann man grundsätzlich in der Programmiersprache nur die von der Programmiersprache bereitsgestellten Befehle benutzen (diese Menge von Befehlen wird häufig auch das Vokabular genannt). Auch wird von der Programmiersprache meistens eine Reihe von Standardvariablentypen bereitgestellt, die meistens Ganzzahlen, Zahlen mit Nachkommastellen, Listen und weiteres enthalten. Der Vorteil dieser sehr strikten Regeln ist, dass die Algorithmen in so einer klarer Form dann geschrieben sind, dass sie ein Computer sehr leicht interpretieren und ausführen kann.
Im Folgenden, vor allem in der Einführung in Object Pascal, werden sehr viele spezielle Details aufgelistet. Es ist nicht notwendig, wirklich alles davon auswendig zu lernen. Sobald man ein wenig dadrin selbst arbeitet, kommt das Lernen automatisch und kurze Zeit später, nachdem man den Anfang hinter sich hat, wo man noch immer wieder alles nachgucken muss und häufig alles nicht auf Anhieb funktioniert, wird es dann ungemein Spaß machen.
Bevor wir die Programmiersprache einführen, ist es allerdings notwendig, zumindest einige wesentliche Grundlagen der Arbeitsweise eines Computers zu kennen und zu verstehen, daher hier eine kurze Zusammenfassung:
Der Kern eines jeden Computers ist sein Prozessor. Dieser führt die eigentlichen Befehle aus. Er kennt dabei eine vorgegebene Menge von relativ primitiven Befehlen, mit denen letztendlich jede Funktion eines Computers realisiert werden muss. Man nennt diese primitiven Befehle die Maschienenbefehle. Diese Befehle sind dabei einfach durchnummeriert, hat einen bestimmten Namen und erwarten jeweils eine bestimmte vorgegebene Anzahl von Parametern. Außerdem besitzt jeder Computer einen begrenzten Arbeitsspeicher, in dem er sich Sachen (Variablen) speichern kann. Dieser Arbeitsspeicher ist kein dauerhafter Speicher, d.h. bei einem Neustart ist alles wieder leer. Für die dauerhafte Speicherung ist die Festplatte gedacht, die genauso eine begrenzte Speichermenge zu Verfügung stellt.
Ein Computerprogramm ist nun nichts anderes, als eine Auflistung von Maschienenbefehlen (d.h. deren zugeordnete Nummer), wobei jedem Maschienenbefehl immer die Parameter folgen. Der Prozessor führt nun einen Befehl nach dem anderen aus.
So und zwar wirklich genau so läuft alles, was man auf dem Monitor erkennen kann, intern in einem Computer auf unterster Ebene ab. (Wozu soll man es auch komplizierter machen, wenn es schon so einfach funktioniert.)
Da diese vom Prozessor unterstützten Maschienenbefehle sehr primitiver Art sind (grob gesagt ist da nicht sehr viel mehr als das Schreiben und Auslesen bestimmter binärer Daten auf die bzw. von der Hardware und dann noch grundlegende mathematische Befehle wie Addieren, Multiplizieren, usw.), hat man sogenannte höhere Programmiersprachen entwickelt, in denen man die Programme in Textform formulieren kann. Man nennt diesen Text Programmcode. Dieser Programmcode kann dann von sogenannten Compiler-Programmen in Maschienencode, d.h. ein vom Prozessor ausführbares Programm, umgewandelt werden. (Diese Umwandlung nennt man auch compilieren.)
Von solchen höheren Programmiersprachen gibt es heutzutage eine fast unendliche Menge, die teilweise sehr verschiedener Art sind. Grundsätzlich hat man sich aber in den meisten Programmiersprachen für Bezeichnungen auf die Sprache Englisch geeinigt (und allgemein auf relativ gut lesbare Konstrukte). Ein erster Unterschied, der auffällt zu den in rein sprachlicher Form aufgeschriebenen Algorithmen, ist die Wichtigkeit der Einhaltung gewisser von der Programmiersprache vorgegebener Details. Diese Details umfassen z.B., dass ein Befehl (je nach Programmiersprache) immer mit einem Semikolon enden muss und Ähnliches.
Wir wollen hier die Programmiersprache Object Pascal für weitere Beispiele verwenden. Object Pascal ist relativ gut lesbar für einen Nicht-Programmierer und auf der anderen Seite aber auch sehr mächtig. Object Pascal ist die objektorientierte Erweiterung von dem ursprünglichen Pascal. Was genau objektorientierte Programmierung ist, wird später behandelt und spielt erstmal keine weitere Rolle. Bekannte Compiler für Object Pascal sind z.B. Delphi oder Lazarus basierend auf Free Pascal. Lazarus bietet den Vorteil, als Open Source Projekt frei verfügbar zu sein und außerdem auf allen gängigen Betriebssystemen und Computerarchitekturen zu laufen. Der unter Windows manuell auszuführende, notwendige Download des Programms kann auf der Homepage www.lazarus.freepascal.org durchgeführt werden.
Wir beginnen direkt mit einem Beispiel, anhand dem wir weitere Details dann erklären wollen. Dieses Beispiel soll den vorhin besprochenen BubbleSort-Algorithmus darstellen:
procedure BubbleSort(var list: array of Integer); var x,i,j: Integer; begin for i := 0 to High(list) do {1} begin for j := 0 to High(list)-1 do {2} begin if list[j] > list[j+1] then {3} begin x := list[j]; {4} list[j] := list[j+1]; {5} list[j+1] := x; {6} end; end; end; end;
Beginnen wir, im Detail die Einzelheiten der Sprache festzuhalten.
Der Programmcode beginnt mit dem Schlüsselwort procedure. Wir hatten bereits erwähnt, dass man einen Algorithmus häufig als Funktion ansieht, denn dieser bekommt in der Regel gewisse Parameter (in diesem Fall nur einen, nämlich list; beachte, dass auch wenn die Liste selbst vielleicht einiges an Informationen enthält, die Liste an sich trotzdem nur ein Objekt ist). Außerdem gibt ein Algorithmus in der Regel einen Wert zurück, den sogenannten Rückgabewert. Im Falle der Addition z.B. war dies das Ergebnis. Es gibt aber auch Fälle, wo im Algorithmus irgendetwas aktiv getan wird, wie z.B. das Entfernen des Mülls oder in diesem Fall das Sortieren der Liste. Einen solchen Algorithmus ohne speziellen Rückgabewert nennt man auch häufig Prozedur oder Methode. In anderen Programmiersprachen gibt es kein spezielles Schlüsselwort für eine Prozedur, da man dort einfach eine Funktion ohne Rückgabewert beschreiben kann. Ich werde deshalb im weiteren häufig auch Prozeduren als Funktionen bezeichnen. Von manchen Leuten wird dies nicht so gerne gesehen, da im mathematischen Sinn eine Funktion immer einen Rückgabewert haben muss, da es sonst keine Funktion ist. Ich hoffe, ihr seit hinreichend gestärkt, um trotzdem damit fertig zu werden.
Der gesamte Code in Pascal besteht aus Funktionen und Prozeduren, in welchen die eigentlichen Aktionen ablaufen (bzw. beschrieben sind). Beim Schreiben einer Prozedur oder einer Funktion muss mit dem ersten Wort gekennzeichnet sein, ob es sich um eine Prozedur (in dem Fall procedure) handelt oder um eine Funktion (in dem Fall function).
Als nächstes legt man den Namen der Funktion fest. In diesem Fall ist er passenderweise BubbleSort.
Falls die Funktion Parameter bekommen soll, werden diese nun in Klammern angegeben. (Eine Funktion muss nicht unbedingt Parameter bekommen. Das Müllentfernen beispielsweise ist eine Aktion. Das Herunterfahren des PCs ist auch eine Aktion. In beiden Fällen sind vermutlich zusätzliche Parameter nicht so sinnvoll. Das bleibt aber natürlich dem Author des Algorithmus überlassen.) Alle in den Klammern angegebenen Parameter sind durch ein Semikolon zu trennen (da in diesem Fall nur ein Parameter da ist, ist auch nichts zu trennen).
Das im Beispiel benutze Schlüsselwort var hat eine besondere Bedeutung. Es symbolisiert, dass der Parameter ein Verweis und keine Kopie sein soll (der Unterschied wurde bereits besprochen). Dies bedeutet, dass Manipulationen des Parameters sich direkt auf das Original auswirken sollen. (Ansonsten wäre der oben angegebene Algorithmus auch nicht sinnvoll, da die kopierte sortierte Liste nirgendwo zurückgegeben wird. Es wäre natürlich in dem Fall dann eine gute Idee, diese kopierte Liste auch zurückzugeben. Das Zurückgeben einer Liste wird aber in der Sprache Pascal nicht erlaubt, da eine Liste ein zu komplexes Objekt ist. Mehr hierzu später und auch, wie man es sonst hätte machen können.) Das Schlüsselwort var kann benutzt werden (hat dann die beschriebene Wirkung), muss aber nicht (in dem Fall ist der entsprechende Parameter eine Kopie).
Der Parameter selbst, genauso wie auch Variablen (dazu gleich), besteht dann aus dem Namen (in diesem Fall list) und, getrennt mit einem Doppelpunkt, dem Typen des Parameters bzw. der Variable. Beachte, dass in Pascal (sowie auch in vielen anderen Programmiersprachen) der Typ immer angegeben werden muss. Dies erleichtert es, den Code zu lesen, gewisse Vorhersagen über das Verhalten des Codes bei der Ausführung machen zu können und außerdem für den Compiler, Fehler im Code zu finden. (Man kann sehr froh sein, wenn der Compiler einen Fehler im Code findet, denn dann kann man diesen leicht beheben. Falls der Compiler den Fehler nicht findet und man dann das Programm startet, es aber irgendwie nicht richtig funktioniert, ist es deutlich schwieriger, eigentlich überhaupt die Fehlerursache zu finden.)
Typ | Bedeutung | Beispiel |
---|---|---|
Integer | eine ganze Zahl | -2 oder 10 |
Real | eine Zahl mit Nachkommastellen | 3.1415 |
Boolean | nur 2 mögliche Werte: true oder false | true |
String | ein Text | 'Einen schönen guten Tag.' |
Char | (genau) ein einzelnes Zeichen | 'b' oder '3' oder '+' oder ' ' |
Typ | Bedeutung | Beispiel |
---|---|---|
array of <Typ> | ein Array (variabler Länge) vom Variablentyp <Typ> | (1, 2) |
array[0..n] of <Typ> | ein Array (fester Länge, nämlich genau n+1 Einträgen) von <Typ> | ('a', 'b', 'c') |
Weitere Typen werden später eingeführt.
Wir haben nun nahezu die erste Zeile des Codes komplett erklärt. Was fehlt ist das Semikolon am Ende der Zeile. Mit dem Semikolon schließt man in Pascal Dinge ab, in diesem Fall die Funktionsbeschreibung. Ansonsten wird es auch benutzt (bzw. muss benutzt werden; auf so etwas legt der Compiler Wert), um 2 Befehle voneinander zu trennen.
Nun
können, wenn Variablen in der Funktion benötigt
werden, diese
festgelegt werden. Wir müssen hier zu Beginn alle in der
Funktion
benutzten Variablen direkt festlegen und können
später im
eigentlichen Code keine weiteren hinzufügen. Das sollte aber
kein
Problem darstellen, denn dann schreiben wir einfach alle Variablen nun
hier hin. Die Variablenliste leitet man mit dem Schlüsselwort var
an dieser Stelle ein. (Man beachte, dass je nach Stelle im Code,
gewisse gleiche Schlüsselwörter unterschiedliche
Bedeutungen
haben können.) Diese werden im gleichen Format
niedergeschrieben,
wie auch die Parameter. Man beachte auch die von Pascal erlaubte
Zusammenfassung von Variablen (bzw. auch Parametern), welche im
Beispiel auch benutzt wurde. Man hätte es auch schreiben
können:
var x:
Integer; i: Integer; j: Integer;
Falls keine Variablen erwünscht werden,
lässt man das Schlüsselwort var
einfach weg.
Man beachte, dass Zeilenumbrüche (so wird es genannt, wenn eine neue Zeile beginnt) und Leerzeichen (auch Tabulatoren) keine Rolle in Pascal spielen. Diese können gesetzt werden, wo sie auch immer zur besseren Lesbarkeit erwünscht werden.
Auch können (sollten) Kommentare im Code hinterlassen werden. Diese werden entweder in geschweifte Klammern gesetzt oder beginnen alternativ mit // (2 mal Schrägstrich) und gelten dann bis zum Ende der Zeile (die Kommentare innerhalb der geschweiften Klammern können über mehrere Zeilen gehen). Alle diese Kommentare werden vollkommen vom Compiler ignoriert und dienen damit nur der Lesbarkeit.
Der eigentliche Code wird nun mit dem Schlüsselwort begin eingeleitet, gefolgt von den eigentlichen Befehlen. Generell gehört zu jedem begin auch immer ein end am Ende. So ein begin/end markiert einen Codeblock. Ein Codeblock ist nichts weiter als ein Block (d.h. eine gewisse Anzahl von Befehlen) Code. Solche Codeblöcke können auch ineinander geschachtelt werden, wie es auch im Beispiel gemacht wurde. Einzelne Befehle sind dabei entweder Zuweisungen (variable := irgendwas), Funktions- oder Prozeduraufrufe, ein Schleifenanfang oder eine if-Abfrage.
Der erste Befehl im Beispiel ist direkt eine Schleife. In diesem Fall wurde
die for-Schleife
verwendet. Die Ausführung der Schleife geschieht dabei exakt
genauso, wie es bereits für den äquivalenten
Pseudocode
beschrieben wurde. Die for-Schleife
sieht in Pascal immer folgendermaßen aus:
for i := start to ende do aktion;
i, start und ende sind dabei
beliebige Variable oder Werte vom Typ Integer. aktion ist entweder
ein einzelner Befehl oder ein Codeblock.
Beachte, dass in Pascal ein Array immer beginnt mit dem 0-ten Eintrag. Die im Beispiel benutzen Funktion High bekommt als Parameter ein Array und liefert als Rückgabewert den Index des letzten Eintrags (der Index eines Eintrags ist die Position in dem Array bzw. in einer Liste). High(list) gibt als eins weniger als die Anzahl der Einträge in list zurück, da der 0-te Eintrag noch hinzukommt.
Der restliche Code entspricht nach allen Erklärungen nun exakt dem, was auch als Pseudocode bereits präsentiert wurde.
Um zu Vergewissern, dass auch klar ist, was im Detail genau passiert (was sehr wichtig für das Verständnis ist), werden wir mal genau im Beispiel durchgehen, was der Computer bei der Ausführung dieser Prozedur genau in welcher Reihenfolge tut, also wie er diesen Code liest. Sei dazu meine_liste = (2,1) gegeben. Wir beschreiben nun, was genau der Computer macht, wenn an anderer Stelle der Befehl BubbleSort(meine_liste); aufgerufen wird.
Ist das alles verstanden, sollte damit klar sein, wie man grundsätzlich Programmcode in Object Pascal schreibt. Falls sich das bei dir im Kopf alles noch nicht so feste gesetzt hat, ist das nicht schlimm und auch nicht weiter verwunderlich. Wirklich festsetzen tut sich sowas erst nach ein wenig praktischer Anwendung - und bei den ersten Gehversuchen (Krabbeln sollte jetzt bei dir jetzt drin sein) und auch überhaupt immer kann man sich ja irgendwelche Referenzen oder Tutorials zur Vorlage nehmen (ist ja völlig egal, wie man an sein Endresultat kommt, nur das Endresultat hinterher zählt wirklich).
Was
noch ein wenig unklar ist, ist wie man grundsätzlich nun ein
lauffähiges Programm in Object Pascal schreibt. Dazu mal ein
einfaches Beispiel:
program hallo_welt_extended;
// BubbleSort aus dem vorherigen Beispiel (nur mit char statt integer)
procedure BubbleSort(var list: array of char);
var
x: char;
i,j: integer;
begin
for i := 0 to High(list) do
begin
for j := 0 to High(list)-1 do
begin
if list[j] > list[j+1] then
begin
x := list[j];
list[j] := list[j+1];
list[j+1] := x;
end;
end;
end;
end;
// Hauptprozedur
procedure main();
var
zeichen: array[0..9] of char;
i: integer;
begin
// Eingabe
write('Gib 10 Zeichen ein: ');
for i := 0 to 9 do
begin
// lese Zeichen vom Benutzer ein
read(zeichen[i]);
end;
// Ausgabe
write('Du hast eingegeben: ');
for i := 0 to 9 do
begin
// gib Zeichen einzeln aus
write(zeichen[i]);
end;
writeln(''); // und noch einen Zeilenumbruch am Ende
// Sortieren
writeln('Wir sortieren nun...');
BubbleSort(zeichen);
// neue Ausgabe
write('Fertig, das Ergebnis ist: ');
for i := 0 to 9 do
begin
write(zeichen[i]);
end;
writeln('');
end;
// Hauptcode unseres Programms
begin
// starte unsere Hauptprozedur
main();
// hier ist bereits nun das Ende des Programms
end.
Dieses Programm ist ein reines Konsolenprogramm. Im Hauptcode wird direkt als einzige Aktion die main-Funktion gestartet. Diese enthält die eigentlichen Aktionen des Programms. Am Anfang liest sie 10 Zeichen vom Benutzer auf der Konsole ein, danach gibt es diese 10 Zeichen wieder aus, danach wird unsere bereits besprochene Prozedur BubbleSort darauf angewendet und letztendlich wird die Liste der 10 Zeichen dann ein weiteres Mal ausgegeben (nun hoffentlich korrekt sortiert).
Die erste richtige Zeile leitet in Object Pascal immer die Datei ein. Das kann dabei sowohl eine Unit (unit name;) oder auch hier die Programm-Deklaration (program name;) selbst sein. Der Name sollte dabei dem Dateinamen (ohne Dateierweiterung) entsprechen, ansonsten könnte es zu Verwirrungen beim Compiler kommen. Diese Einleitung muss ganz am Ende der Datei mit einem end. abgeschlossen werden.
Ein vollständiges Programm besteht immer aus einer solchen Programm-Deklaration und mehreren Units (bei diesem Beispiel war gar keine zusätzliche Unit mehr nötig). Die Units benutzt man, um sein Programm ein wenig aufzuteilen und so eine übersichtlichere Struktur zu bekommen (wir hätten beispielsweise die BubbleSort-Prozedur in eine extra Unit packen können). Die Programm-Deklaration hält man in der Regel auch so klein wie möglich, man will eigentlich immer den gesamten Code in den Units haben.
Programm-Deklarations-Struktur:
// Einleitung der Datei
program my_sample_prog;
// Unit-Abhaengigkeiten
uses
Unit1,
Unit2 {...} ; // nicht vergessen am Ende; dies schliesst den uses-Teil ab
{ irgendwelche Funktionen / Prozeduren }
// Variablendeklarationen
var
zahl: integer;
noch_ne_zahl: integer;
// Beginn des eigentlichen Hauptcodes
begin
{ Hauptcode hier }
end. // absolutes Ende
Unit-Struktur:
// Dateieinleitung fuer eine Unit
unit my_sample_unit;
// Beginn des Interface-Teils
interface
// welche anderen Units brauche ich hier?
uses
my_other_unit, some_graphics_stuff;
// ein paar Konstanten, die ich vielleicht brauche
const
PI = 3.1415;
MIN_NEEDED_CPU_SPEED = 800; // MHz Angabe
MENSCHEN : array[1..3] of string
= ('Linus', 'Florian', 'Albert');
{ ... }
// ein paar eigene Variablentypen definieren
type
{ spaeter mehr zur eigenen Variablentypen ... }
// hier noch irgendwelche Funktionen, aber ohne Code
function BubbleSort(list: array of integer);
{ ... weitere Funktionen hier ... }
// ein paar globale Variablen
var
AnzahlSpieler: integer;
txt1, txt2, txt3: string;
{ ... }
// Beginn des Implementierungsteils
implementation
// welche anderen Units brauche ich hier?
uses
super_mega_unit;
// Code fuer die deklarierten Funktionen ...
function BubbleSort(list: array of integer);
begin
{ ... }
end;
{ ... weitere Implementierungen der oben eingefuehrten Sachen ... }
// Beginn des Initialisierungscodes
initialization
AnzahlSpieler := 0;
txt1 := 'Hallo Welt';
{ ... }
end. // Ende der Unit
Im Prinzip sind viele alle Teile optional, d.h. wenn ein Teil nicht benötigt wird, lässt man ihn einfach weg (nicht immer muss man Konstanten einführen oder benötigt für die ganze Unit globale Variablen oder braucht einen Initialisierungscode usw.). Nur die Grundstruktur, d.h. bei einer Unit der Interfaceteil und der Implementationteil, muss vorhanden sein.
Die Unterteilung übrigens zwischen Interface und Implementierung hat seinen Sinn: Unter einem Interface versteht man die genaue Angabe der Schnittstelle der Unit, d.h. die nach außen hin (für andere Units) zu Verfügung gestellten Sachen. Dies sind in erster Linie eigene Funktionen und eigene Variablentypen, aber auch Konstanten und ähnliches (alles das, was unter interface steht). Nicht alles muss man nach außen hin zu Verfügung stellen - nur das, was man als sinnvoll erachtet. Unter einer Implementierung versteht man einen Programmcode für eine spezielle Sache, z.B. eine im Interface deklarierte Funktion. Der Grund zur Unterteilung zwischen Interface und Implementierung ist, dass man als menschlicher Leser schneller erkennen kann, was einem eine Unit anbietet. Die Implementierung ist in der Regel um ein vielfaches größer und länger als das Interface.
Das letzte Beispiel war ein reines Konsolenprogramm. Angenommen, der Dateiname der Programmdeklaration war hallo_welt_extended.pas. In der Konsole gibt man einfach folgendes ein, um das Programm zu kompilieren:
fpc hallo_welt_extended.pas
Man erhält dann die ausführbare Datei hallo_welt_extended. Die noch gestartet und das ganze sieht dann so aus:
Da es Dank Lazarus aber nicht wirklich schwieriger ist, direkt ein Programm mit grafischer Oberfläche zu programmieren und dies außerdem unter Windows, Linux oder sonst wo auf gleiche Art und Weise ohne weitere Probleme durchführbar ist und man außerdem so direkt, auch als Anfänger, zu viel netteren Ergebnissen kommen kann, wollen wir im weiteren das auch tun.
So sieht Lazarus direkt nach dem (ersten) Start aus:
Ganz kurz zur Erklärung: Oben sieht man das Standardmenü (Lazarus-Editor ...), wo man so Standardsachen machen kann (Speichern, neue Units anlegen, neues Projekt erstellen, usw.). Dieser grüne Playknopf () startet das Programm. Außerdem findet man hier die Liste aller verfügbaren Steuerelemente (z.B. Buttons, Textfelder, usw., die man nach Auswahl auf der Form platzieren kann). Links der Objektinspektor listet die auf der aktuell ausgewählten Form verfügbaren Objekte/Steuerelemente auf. Hier gibt man auch erweiterte Eigenschaften an (wie z.B. die Beschriftung von einem Button, den internen Namen (über den das Objekt im Code angesprochen wird), die Farbe, die Schriftart und sonstiges). In der Mitte im Hintergrund ist der Lazarus-Quelltexteditor, in dem man immer den kompletten Code der gerade ausgewählten Unit sieht und editieren kann. Die Form rechts im Bild ist das Fenster, welches später vom erstelten Programm benutzt wird. Zur Entwicklungszeit kann man hier die Steuerelemente einfügen, wie man gerade Lust hat. Nach einem Doppelklick auf ein Steuerelement (z.B. auf ein dort platzierten Button) gelangt man automatisch zu dem entsprechenden Code des Standardereignisses des Steuerelements (z.B. ist sehr häufig das Standardereignis ein einfacher Mausklick).
Packen wir nun mal ein Button (), ein Label () und ein Timer (der findet sich unter der Kategorie 'System': ) auf unsere Form:
Ein Button ist dabei eins dieser wohlbekannten Klickfelder. Ein Label tut nichts anderes als einen Text anzuzeigen. Und ein Timer ist dafür da, irgendwelchen Code in regelmäßigen Abständen auszuführen. Nun wählen wir den Button aus und geben ihm ein paar vernünftige Eigenschaften. Es empfiehlt sich, die Caption-Eigenschaft (die Beschriftung) zu ändern. Damit du in etwa das gleiche Ergebnis wie ich im Folgenden bekommst, solltest du beim Timer das Intervall auf 100 setzen. Auch die internen Namen (Button1, Label1, Timer1) habe ich soweit erstmal gelassen.
Der aktuelle Code sieht nun folgendermaßen aus:unit Unit1;
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, LResources, Forms, Controls, Graphics, Dialogs, ExtCtrls,
Buttons, StdCtrls;
type
{ TForm1 }
TForm1 = class(TForm)
Button1: TButton;
Label1: TLabel;
Timer1: TTimer;
private
{ private declarations }
public
{ public declarations }
end;
var
Form1: TForm1;
implementation
initialization
{$I unit1.lrs}
end.
Lazarus hat für uns, wie man sieht, bereits gute Arbeit geleistet. Diese Unit1 ist die entsprechend zu der Form1 gehörige Unit. Es wurden bereits alle abhängigen Units im uses-Teil aufgelistet. Unsere Form wurde im type-Bereich dabei als neuer Typ (und zwar TForm1) eingeführt, der auf dem Typ der Standardform (TForm) basiert. Wirklich im Detail verstehen, was genau diese Typdefinition aussagt, werden wir später. Es reicht aber vorerst völlig aus, wenn man sich grob vorstellt, dass dieser neue Variablentyp TForm1 mehrere Untervariablen (Button1, Label1 und Timer1 bisher) zu Verfügung stellt. Das public und private sagt nur aus, ob von außerhalb des Typs darauf zugegriffen werden kann - ist also auch erstmal uninteressant, so lange das gesamte Programm sowieso nur innerhalb der Form1 sein wird. Im var-Bereich wurde nun gerade unsere Form1 als Variable deklariert, vom Typ TForm1. Alle weiteren Informationen wie z.B. die Position und sonstige Eigenschaften der 3 bisher platzierten Steuerelemente werden im initialization-Teil gesetzt. Dort findet man den Compilerbefehl {$I unit1.lrs}. Alle Kommentare in geschweiften Klammern, die mit einem Dollar-Zeichen beginnen ({$ ...}), sind spezielle Anweisungen für den Compiler, irgendetwas bestimmtes an dieser Stelle beim Compilieren zu tun. Die Anweisung $I sagt dabei, hier soll die folgende Datei (temporär) eingefügt werden. In der Datei unit1.lrs befinden sich dabei die Zuweisungen der speziellen Eigenschaften für die Objekte. (Andere, evtl. von Lazarus hinzugefügten Compileranweisungen können im Moment ignoriert werden.)
Wenn wir jetzt bereits einmal zum Test auf Start () drücken, wird das Programm bereits erfolgreich compiliert und auch gestartet. Man wird allerdings feststellen, dass man nicht wirklich etwas sinnvolles tun kann, denn bisher haben wir auch noch keinen Code geschrieben.
Dies holen wir jetzt nach. Nach einem Doppelklick auf den Button erstellt Lazarus automatisch eine Prozedur Button1Click, welche mit dem Klickereignis des Buttons verknüpft ist. Das bedeutet, wenn wir im eigentlichen Programm hinterher auf den Button klicken, wird die Prozedur Button1Click aufgerufen. Lazarus geht außerdem nach dem automatischen Erstellen der Prozedur auch an die richtige Stelle im Code, so dass wir direkt beginnen können, den eigentlichen Code einzugeben (ist das nicht super einfach?).
Zum Test geben wir mal folgende 2 Zeilen ein:
Label1.Top := Label1.Top - 10;
Label1.Caption := IntToStr(Label1.Top);
Nun testen (d.h. starten) wir erneut das Programm. Nach einem Klick auf den Button können wir nun das erstaunliche Ergebnis bestaunen.
Ein wenig was zum Code: Wer aufmerksam war, hat sicherlich bemerkt, dass die Prozedur Button1Click innerhalb des Typs TForm1 deklariert wurde (im interface-Teil) und im implementation-Teil den Namen TForm1.Button1Click bekommen hat. Dies ist eine dem Typ zugeordnete Funktion. Solche Funktionen/Prozeduren bekommen immer im implementation-Teil den vollen Namen, damit keine Namenskonflikte entstehen können. Der Parameter Sender: TObject informiert beim Aufruf über das sendende Objekt, welches in dem Fall immer Form1 ist. Wir können den Parameter also vollkommen ignorieren (erst viel später wird er sinnvoll werden). Im eigentlichen Code ändern wir gleich 2 Eigenschaften des Objektes Label1. Auf dessen Eigenschaften (allgemein auf die Untervariablen eines selbstdefinierten Typs) greift man immer mit einem Punkt zu, also Objektname.Eigenschaft. In dem Fall vermindern wir Label1.Top um 10. Wie man sicherlich beim Testen des Programms festgestellt hat, gibt die Eigenschaft Top (vom Typ Integer) die Y-Position des Labels an. Etwas verwirrend ist, dass die Y-Koordinate scheinbar umgedreht ist (also ganz oben ist 0 und nach unten hin wird es positiv). Dies hat sich im PC-Bereich durchgesetzt - wohl aus dem Grund, da man auch von oben nach unten liest und so der Punkt (0,0) (links oben) dann auch der Startpunkt ist. Die Caption-Eigenschaft (vom Typ String) entspricht der Beschriftung. IntToStr ist eine Funktion, die einen Wert vom Typ Integer (also eine Zahl) in einen Wert vom Typ String (also einen Text) umwandelt. Man beachte (und halte sich immer im Kopf), dass dies wirklich eine echte Umwandlung ist. Dies merkt man schon daran, dass sich nicht jeder beliebige String in einen Integer umwandeln lässt. Für die Strings, für die dies aber doch möglich ist, gibt es die Funktion StrToInt.
Wir wollen nun
das Label auf der anderen Seite aber auch wieder nach unten laufen
lassen. Hierzu benutzen wir am besten unseren Timer. Wir tätigen
mal einen Doppelklick auf diesen, so dass Lazarus automatisch die
Prozedur Timer1Timer erstellt. Diese wird bei jedem Intervall des
Timers, falls er aktiviert ist, ausgeführt. Wir fügen nun
folgenden Code hinzu:
Label1.Top := Label1.Top + 1;
Label1.Caption := IntToStr(Label1.Top);
Das
aktuelle Ergebnis scheint schon ganz lustig zu sein. Etwas störend
ist noch, dass das Label nicht am unteren Rand der Form halt macht,
sondern ewig weiter wandert. Dies ist schnell korrigiert, Timer1Timer
sieht nun folgendermaßen aus:
if Label1.Top + Label1.Height < Form1.Height then
begin
Label1.Top := Label1.Top + 1;
Label1.Caption := IntToStr(Label1.Top);
end;
Es
wäre ja nun noch ganz lustig, wenn sich dieser Fall nach unten des
Labels noch beschleunigt, so wie ein richtiger Fall im realen Leben
auch sein würde. Hierfür muss das Label eine aktuelle
Geschwindigkeit haben, die sich im Fall immer weiter
vergrößert und von der die Positionsänderung
abhängt. Für die Geschwindigkeit des Labels fügen wir
also eine neue Variable ein, die wir innerhalb der TForm1-Deklaration
platzieren (innerhalb die private-Sektion, denn für solche Sachen ist diese da):
Label1Vel: Integer;
Nun
passen wir Timer1Timer erneut an. Wir wollen, dass wenn das Label unten
angekommen ist, die Geschwindigkeit gerade negiert wird und auch etwas
Energie verloren geht, so dass die Geschwindigkeit auch halbiert werden
soll. Weil man mit einem Integer keine normale Division
durchführen kann (denn dies geht nur mit Kommazahlen problemlos),
müssen wir die Ganzzahldivision div benutzen. Dies resultiert im folgenden Code:
if Label1.Top + Label1Vel + Label1.Height > Form1.Height then
Label1Vel := -Label1Vel div 2
else
begin
Label1.Top := Label1.Top + Label1Vel;
Label1.Caption := IntToStr(Label1.Top);
Label1Vel := Label1Vel + 1;
end;
Wirklich
lehrreich für dich wird das ganze, wenn du aber jetzt selbst mal
ein wenig rumspielst und eigene Varianten testest. Eine kleine
Erweiterung aber trotzdem vielleicht noch: Wähle das Label aus und
setze die Eigenschaft DragMode auf dmAutomatic. Wähle nun im
Objektinspektor die Form1 aus und statt den Eigenschaften klicke hier
auf den Tab Ereignisse. Es werden die Ereignisse der Form1 aufgelistet.
Man mache einen Doppelklick auf das Feld neben OnDragOver. Eine
Funktion FormDragOver wird erstellt, in welche man folgenden Code
hinzufügen mag:
Label1.Left := X; Label1.Top := Y;
Man bestaune erneut das Ergebnis...
Mal ein paar Gedanken zu unseren bisherigen Beispielen in Lazarus: Als erstes haben wir damit begonnen unser späteres Fenster zu entwerfen im Editor, indem wir die entsprechenden Steuerelemente auf der Form platziert haben. Dann haben wir Code geschrieben. Um konkreter zu sein: Wir haben den Code geschrieben, der bei bestimmten Ereignissen der Steuerelemente ausgeführt wird (wie z.B. das Klickereignis von einem Button, d.h. das Ereignis, wenn man auf den Button draufklickt). Diese Form der Programmierung nennt man die Ereignisgesteuerte Programmierung. Das heißt einfach nur, dass all der Code den man schreibt gewissen Ereignissen zugeordnet ist und dann bei den entsprechenden Ereignissen ausgeführt wird. Z.B. wird dieProzedur Button1Click im vorherigen Beispiel genau dann ausgeführt, wenn man auf den Button klickt. Das heißt übrigens auch, dass in einem Ereignisgesteuerten Programm gar kein Code ausgeführt wird, so lange kein Ereignis eintritt. In diesem Zustand "schläft" das Programm sozusagen und wartet halt auf neue Ereignisse, bei denen es dann irgendetwas ausführen kann.
Es gibt eine ganze Reihe von Ereignissen, die alle bei den verschiedenen Steuerelementen eintreten können. Bei dem Button ist dies z.B. das Klickereignis, aber das Klickereignis kann natürlich für grundsätzlich alle anderen Steuerelemente auch eintreten. Bei einem Textfeld (für die Texteingabe) gibt es z.B. das KeyPress-Ereignis (wenn man eine Taste drückt) oder auch das Change-Ereignis (wenn der Text geändert wird). Bei gewissen Ereignissen bekommt man ausserdem auch noch ein paar Informationen als Parameter mit. Z.B. bekommt man bei dem KeyPress-Ereignis natürlich auch mit, welche Taste denn nun gedrückt wurde. Bei dem MouseMove-Ereignis bekommt man z.B. die genaue Position der Maus mit.
Sehen wir uns einmal in Lazarus an, welche Ereignisse wir so zu
Verfügung haben. Nach dem Starten von Lazarus setzen wir also ein Textfeld () auf unsere Form. Danach wählen wir im Objektinspektor die Ereignisliste (das Tab Ereignisse, direkt rechts neben Eigenschaften)
aus. Wir sehen nun eine Liste aller für dieses Steuerelement (das
Textfeld) verfügbaren Ereignisse. Machen wir ein Doppelklick auf ein
Ereignis, erstellt Lazarus uns automatisch eine Funktion für das
entsprechende Ereignis. Dies habe ich hier einmal für das Change-Ereignis gemacht. Lazarus hat dann automatisch die Funktion TForm1.Edit1Change erstellt. (Edit1 ist der Name von dem Textfeld gewesen, da ich das nicht weiter geändert hatte.)
Zum Test setzen wir mal direkt unter das Textfeld noch ein Label (). Nun schreiben wir in die Funktion TForm1.Edit1Change folgenden Code:
Label1.Caption := Edit1.Text;
Das Ergebnis ist wie erwartet, dass bei jeder Textänderung in dem Textfeld der angezeigte Text im Label (die Caption) aktuallisiert wird.
Hier fehlen noch weitere Beispiele und eine Einführung in die am häufigsten verwendeten Steuerelemente, sowie eine Einführung in prinzipielle Programmiertechniken.
Nachdem du bereits nun hoffentlich selbst etwas rumgespielt hast mit Lazarus, was notwendig ist, um ein gewisses Verständnis davon zu bekommen, wollen wir auf Basis eines kleines Spiels zu weiteren Erfahrungen gelangen. Hier kannst du zwar hinterher nicht behaupten, alles selbst gemacht zu haben, allerdings wirst du schnell den gesamten Code verstehen und ihn selbst soweit du Lust hast erweitern können. Interessant und praktisch ist, dass das Erweitern des Codes sogar bereits möglich sein wird, bevor du überhaupt alles verstanden bzw. überhaupt angeguckt hast. Der Vorteil an dieser Art des praktischen Lernens ist, dass du viel schneller und effektiver nette Sachen machen kannst. Immer wenn ich eine neue Programmiersprache wirklich für die praktische Anwendung lernen will, nehme ich mir ein einfaches Open Source Programm in der entsprechenden Sprache vor, lese mich ein, versuche nur erstmal so grob den Aufbau zu verstehen und beginne dann damit an der ein und anderen Stelle mal dies und das abzuändern oder zu erweitern. Das Entscheidende ist dann immer, das Ergebnis nach der Änderung zu betrachten und so einmal das Verständnis zu erlangen und gleichzeitig praktische Erfahrungen zu sammeln. Beides, das wirst du bald merken, sind ungemein schöne Gefühle.
An dieser Stelle eignet sich wohl am besten das kleine Spiel Robot, geschrieben mit Lazarus. Dieses habe ich ursprünglich programmiert, um zu demonstrieren, was man bereits mit minimalen Programmiertechniken bereits erschaffen kann. Ich habe dafür ein neues Projekt in Lazarus erstellt und in dieser einen Form das gesamte Spiel untergebracht.
Lade dir ersteinmal am besten den
Programmcode (am besten der Version 1.7, damit wir eine einheitliche
Basis hier zum besprechen haben) herunter und öffne ihn dann mit
Lazarus:
Robot-Projektseite mit Download
Screenshot von Robot nach dem Start:
In Lazarus sieht das Projekt so aus:
Man
sieht bereits, dass zur Entwicklungszeit (also in Lazarus) die Form
noch sehr im Rohformat aussieht, also erst während der Laufzeit
(nach dem Start) alles gezeichnet wird. Werfen wir nun mal einen Blick
auf den Code dieser Form:unit uMainForm;
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, LResources, Forms, Controls, Graphics, Dialogs, ExtCtrls,
Buttons, GraphType, Crt, StrUtils, StdCtrls, ComCtrls, Menus
{$IFDEF win32}
,MMSystem
{$ENDIF}
;
const
PICTURE_SIZE = // picture cache size
{$IFDEF win32}
65; // windows needs more
{$ELSE}
30; // i think, it's a good value...
{$ENDIF}
BACKGROUND_PIC = 'hinter.bmp'; // used for resetting
PLAYER_PICS: array[1..3] of string
= ('figur.bmp', 'robot*.bmp', 'konig.bmp');
ERROR_PIC = 'error.bmp'; // used for error-displaying
WORLD_WIDTH = 5; // room count
WORLD_HEIGHT = 4;
ROOM_WIDTH = 20; // place count in a room
ROOM_HEIGHT = 20;
KNAPSACK_WIDTH = 10; // place count in the knapsack
KNAPSACK_HEIGHT = 5;
KNAPSACK_MAX = 27; // compatibility with Robot1 (9*3)
COMPUTERCONTROL_INTERVAL = 750; // timer-interval for computer player control
type
TRoomNum = record // world coord
X: 1..WORLD_WIDTH;
Y: 1..WORLD_HEIGHT;
end;
TPlaceNum = record // room coord
X: 1..ROOM_WIDTH;
Y: 1..ROOM_HEIGHT;
end;
TPlaceAbsNum = 1..(ROOM_WIDTH*ROOM_HEIGHT); // abs room-index
TRoomAbsNum = 1..(WORLD_WIDTH*WORLD_HEIGHT); // abs place-index
TKnapsackAbsNum = 1..(KNAPSACK_WIDTH*KNAPSACK_HEIGHT); // abs knapsack-index
TPlace = record
PicIndex: Integer; // index of TPictureCache
end;
TRoom = array[TPlaceAbsNum] of TPlace; // a hole room
TWorld = array[TRoomAbsNum] of TRoom; // a hole world
TPlayer = record
Pos: TPlaceNum;
PicIndex: Integer; // index of TPictureCache
end;
TPlayerList = array of TPlayer; // dyn array of players in the room
TWorldPlayers = array[TRoomAbsNum] of TPlayerList; // all players in the world
TKnapsack = array[TKnapsackAbsNum] of TPlace; // a knapsack
TPictureCacheItem = record
FileName: string;
Picture: TBitmap; // picture cache
ResizedPicture: TBitmap; // resized picture cache
end;
TPictureCache = array of TPictureCacheItem;
TMoveDirection = (mdLeft, mdRight, mdUp, mdDown);
TFocus = (fcRoom, fcKnapsack);
TDiamondSet = record
DiamondNr: Integer
end;
{ TMainForm }
TMainForm = class(TForm)
GamePanel: TPanel;
KnapsackPanel: TPanel;
{ ... viele weitere Objekte auf der Form ... }
ComputerPlayer: TTimer;
// event handlers
procedure ComputerPlayerTimer(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
procedure FormPaint(Sender: TObject);
procedure FormResize(Sender: TObject);
procedure GamePanelClick(Sender: TObject);
procedure GamePanelMouseDown(Sender: TOBject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
procedure GamePanelMouseMove(Sender: TObject; Shift: TShiftState; X,
Y: Integer);
procedure KnapsackPanelClick(Sender: TObject);
procedure KnapsackPanelMouseDown(Sender: TOBject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
procedure mnuEditorLoadClick(Sender: TObject);
procedure mnuEditorModeClick(Sender: TObject);
procedure mnuEditorSaveClick(Sender: TObject);
procedure mnuGameEndClick(Sender: TObject);
procedure mnuGameLoadClick(Sender: TObject);
procedure mnuGameNewClick(Sender: TObject);
procedure mnuHelpAboutClick(Sender: TObject);
procedure mnuHelpControlClick(Sender: TObject);
procedure mnuHelpDescriptionClick(Sender: TObject);
procedure mnuOptionsPauseClick(Sender: TObject);
procedure mnuOptionsSoundClick(Sender: TObject);
private
{ private declarations }
public
// gameplay
// goto next room; return true, if succ
function MoveToRoom(dir: TMoveDirection): boolean;
function MoveToRoom(rnum: TRoomNum): boolean; // goto another room
procedure MoveToPlace(dir: TMoveDirection); // move player
// searchs the player; returns -1, if not found
function GetMainPlayerIndex(): Integer;
procedure KillRobots(); // kill all robots in act room
procedure UseKnapsackSelection();
// make 'intelligent' movements of all robots and the king
procedure ControlComputerPlayers();
// background stuff
procedure InitGame();
procedure RestartGame();
procedure UnInitGame();
procedure ResetRoomPic();
procedure ResetKnapsackPic();
procedure ResetWorld();
procedure ResetKnapsack();
procedure DrawRoom(); // updates MyRoomPic and GamePanel
procedure DrawKnapsack(); // updates MyKnapsackPic and KnapsackPanel
procedure DrawInfo(); // updates InfoPanel
procedure ShowMsg(msg: string); // printed on MessageBar
procedure ShowMsg(msgs: array of string); // like ShowMsg; select
// randomly a msg
procedure LoadWorld(fname: string); // loads a hole world (sce-file)
procedure SaveWorld(fname: string); // saves the hole world
procedure LoadGame(fname: string); // loads a saved game (included world)
procedure SaveGame(fname: string); // saves a game
function ShowLoadGameDialog(): boolean; // returns true, if succ
function ShowSaveGameDialog(): boolean; // returns true, if succ
function GetPicture(fname: string): TBitmap; // load picture from cache/disk
function GetPicture(index: Integer): TBitmap;
function GetPictureName(index: Integer): string; // returns filename
function GetPictureCacheIndex(fname: string): Integer;
procedure ResetPictureResizedCache();
procedure PlaySound(fname: string); // plays wave-file
// get viewed place (with players)
function GetPlace(room: TRoomAbsNum; pos: TPlaceNum): TPlace;
// get viewed place (with players)
function GetPlace(pos: TPlaceNum): TPlace;
// returns picture filename
function GetPlacePicName(pos: TPlaceNum): string;
procedure SetPlace(pos: TPlaceNum; p: TPlace); // set room place
// sets picture filename
procedure SetPlacePicName(pos: TPlaceNum; pname: string);
procedure ResetPlace(pos: TPlaceNum);
function AddPlayer(room: TRoomAbsNum; pos: TPlaceNum;
picindex: Integer): Integer; // returns index
function AddPlayer(room: TRoomAbsNum; pos: TPlaceNum;
picname: string): Integer; // returns index
procedure RemovePlayer(room: TRoomAbsNum; index: Integer);
procedure RemovePlayer(room: TRoomAbsNum; pos: TPlaceNum);
function MovePlayer(oldroom: TRoomAbsNum; oldindex: Integer;
newroom: TRoomAbsNum; newpos: TPlaceNum): Integer; // returns new index
function IsPlayerInRoom(picname: string): boolean;
procedure ResetPlayerList();
function IsPosInsideRoom(x,y: Integer): boolean;
function AddToKnapsack(picindex: Integer): boolean; // returns true, if succ
function AddToKnapsack(picname: string): boolean; // returns true, if succ
function IsInKnapsack(picname: string): boolean;
procedure ChangeKnapsackSelection(dir: TMoveDirection);
procedure AddScores(num: Integer);
procedure AddLife();
function RemoveLife(): boolean; // returns true, if still alive
procedure SetFocus(f: TFocus);
procedure ChangeFocus();
procedure SetPauseState(s: boolean);
Procedure CopyRect(DstCanvas: TCanvas; const Dest: TRect;
SrcCanvas: TCanvas; const Source: TRect);
MyWorld: TWorld; // the world
MyWorldPlayers: TWorldPlayers; // all players
MyRoomNum: TRoomNum; // selected room of my world
MyRoomPic: record // user view
Room: TRoom; // room actually viewed
Picture: TBitmap; // paint cache
end;
MyKnapsack: TKnapsack; // the knapsack
MyEditorKnapsack: TKnapsack; // the knapsack used in editor mode
MyKnapsackPic: record // user view
Knapsack: TKnapsack; // knapsack actually viewed
Selection: TKnapsackAbsNum; // selection act viewed
Picture: TBitmap; // paint cache
end;
MyKnapsackSelection: TKnapsackAbsNum; // selected item in the knapsack
MyFocus: TFocus;
MyPictureCache: TPictureCache; // cache of all graphics in the game
MyLife: Integer; // lifes
MyScores: Integer; // scores
MyDiamonds: array of TDiamondSet; // set diamoonds
MyPauseState: boolean; // true -> pause
MySoundState: boolean; // false -> mute
MyEditorMode: boolean; // true -> editmodus on
end;
function RoomNum(X,Y: Integer): TRoomNum;
function PlaceNum(X,Y: Integer): TPlaceNum;
function Place(picindex: Integer): TPlace;
function Player(picindex: Integer; pos: TPlaceNum): TPlayer;
function GetAbs(rnum: TRoomNum): TRoomAbsNum; // coord -> abs index
function GetAbs(pnum: TPlaceNum): TPlaceAbsNum; // coord -> abs index
function GetNumR(absnum: TRoomAbsNum): TRoomNum; // abs index -> coord
function GetNumP(absnum: TPlaceAbsNum): TPlaceNum; // abs index -> coord
var
MainForm: TMainForm;
implementation
function RoomNum(X,Y: Integer): TRoomNum;
begin
RoomNum.X := X;
RoomNum.Y := Y;
end;
function PlaceNum(X,Y: Integer): TPlaceNum;
begin
PlaceNum.X := X;
PlaceNum.Y := Y;
end;
{ ... und die ganze restliche, eigentlich wichtige und entscheidende Implementierung ... }
initialization
{$I umainform.lrs}
end.
Was ist nun wirklich neu zu unserem bisherigen Wissen? Gehen wir diese Code-Übersicht einmal Schrittweise durch.
Im uses-Teil befinden sich noch ein paar mehr Units als sonst. Diese wurden automatisch dort im Code hinzugefügt, als ich entsprechende Steuerelemente auf der Form platziert hatte. Dann ist da noch dieses {$IFDEF win32} ... {$ENDIF}. Dies ist, wie wir bereits gesagt hatten, eine Compileranweisung. IFDEF ist dabei die Abfrage, ob ein spezieller Wert des Compilers definiert ist oder nicht. win32 ist dabei genau dann definiert, wenn der Code für ein normales aktuelles Windows kompiliert wird. In dem Fall wird also noch die Unit MMSystem hinzugefügt. Diese stellt unter Windows spezielle Funktionen zum Soundabspielen bereit (mehr dazu später an entsprechender Stelle).
Im const-Teil werden nun für den eigentlichen Code einige Konstanten definiert. Die konkreten Konstanten zu erklären ist vermutlich aber erst sinnvoller, wenn sie im entsprechenden Kontext (im späteren implementation-Teil) auch wirklich verwendet werden, wobei dann die Bedeutung sofort klar wird.
Nun im type-Teil ist die erste wirkliche Neuerung und im Prinzip auch die einzige wirkliche Neuerung im gesamten Code (ausgenommen sind jetzt mal konkrete Funktionen oder ähnliches, denn das ist eigentlich nichts wirklich Neues, sondern etwas, was man halt, wenn man es braucht, in einem Buch oder im Internet sich raussucht). Es wurden in diesem Teil für das Spiel einige neue Typen definiert, so z.B. TRoomNum, TPlaceAbsNum, TRoom und weitere.
Was ist ein record?unit uRecordBeispiel;
interface
type
TMeinNeuerTyp = record
Caption: string;
Top, Left: integer;
Width, Height: integer;
Value: real;
MeineForm: TForm1;
{ ... }
end;
{ ... }
implementation
procedure TuWasMitNeuemTyp();
var
stueck: TMeinNeuerTyp;
wert: real;
begin
wert := 42;
stueck.Caption := 'Wilfried';
stueck.Top := 0;
stueck.Value := wert;
// wir gehen davon aus, dass Form1 eine Variable vom Typ TForm1 ist
stueck.MeineForm := Form1;
stueck.MeineForm.Caption := 'Form von Wilfried';
stueck.MeineForm.Top := stueck.Top;
// wir gehen davon aus, dass TForm1 ein Label1 hat
stueck.MeineForm.Label1.Caption := 'Hallo Welt';
end;
{ ... }
end.
Ein record ist im Prinzip nichts anderes als eine Zusammenfassung mehrerer einzelner anderer Variablen eines vorhandenen Variablentyps.
Die nächste Neuerung ist ein Variablentyp in der Form a..b. Dies ist nichts weiter als eine eingeschränkte Integer-Typ, der nur Zahlen von a bis b zulässt (beide eingeschlossen). Manchmal ist es sinnvoll den Zahlenbereich im Vorhinein einzuschränken um spätere Fehler frühzeitig zu entdecken. Auch ist es sinnvoll, um dem Leser des Codes einen Hinweis zu geben, was in dieser Variable gespeichert werden soll. Man behalte sich immer im Hinterkopf, dass man den Code nicht nur programmiert, damit er funktioniert, sondern auch, dass er später auch gut gelesen werden kann. Denn vielleicht wollen wir später unseren längst vergessenen alten Code noch einmal ansehen, wiederverwenden und vielleicht erweitern. Dies ist aber nur möglich, wenn wir dann auch verstehen, wieso unser alter Code eigentlich so ist wie er ist.
Ansonsten ist da noch der bisher unbekannte Typ TBitmap. Hierbei handelt es sich wieder um einen Typ der bereits von Lazarus bereitgestellt wird, ähnlich wie TForm. TBitmap enthält alle Daten für ein komplettes Bitmap (also ein Bild) im Speicher. Wir werden in Variablen von diesem Typ letztendlich die Bilder von den Spielobjekten speichern.
Als letzte Typdefinition haben wir die Definition von TMainForm als class(TForm). Diese Definition in der Form wurde komplett von Lazarus selbst vorgenommen, d.h. im Prinzip müssen wir uns hierum nicht weiter kümmern. (Zum Vergleich: das leere neue Lazarus-Projekt.) Wie man aus dem Code erkennen kann, scheint diese Typdefinition wohl ähnlich wie ein record zu sein, wobei es zusätzlich noch Funktionen und Methoden besitzt. Dies nennt man eine Klasse. Im Endeffekt ist es der Typ unseres späteren Fensters, welches selbst auch einfach eine Variable ist (man nennt Variablen vom Typ einer Klasse auch Klasseninstanzen oder auch Objekte). An dieser Stelle ist es unwichtig die genaueren Details zu verstehen (das wird später nachgeholt). Es reicht aus sich TMainForm als ein besonderes record vorzustellen. Ausserdem basiert es auf dem Typ TForm (das wurde in der Klammer angegeben). Das heißt, dass TMainForm so wie TForm ist, nur durch ein paar Sachen erweitert wurde. Auch das ist an dieser Stelle aber nicht weiter von Bedeutung für das eigentliche Verständnis des Spiels. Fast der gesamte Code von der Typdefinition von TMainForm wurde automatisch von Lazarus erstellt, während ich die einzelnen Elemente wie Labels, Picture-Boxen (Boxen für Bildern), Panels (einfach nur Boxen für irgendwas) und Timer auf das Fenster gesetzt habe. Die ganzen Prozeduren wie z.B. GamePanelClick wurden auch alle von Lazarus automatisch angelegt, als ich die entsprechenden Ereignisse bearbeitete.
Der Code in der Typdefinition von TMainForm ab "gameplay" ist der, den ich selbst hinzugefügt habe. Es sind weitere eigene Prozeduren für das eigentliche Spiel. Nach den Prozeduren kommen noch ein paar Variablen für das eigentliche Spiel. Z.B. enthält die Variable MyWorld die komplette Spielwelt.
Ganz am Ende bei den Typdefinitionen, also wieder ausserhalb von TMainForm, sind noch ein paar weitere Funktionen. Diese habe ich angelegt, um leichter bestimmte Variablen umzuwandeln und umzurechnen.
Im var-Teil befinden sich globale Variablen für das gesamte Programm. Hier ist nur die Variable für unser Fenster (vom Typ TMainForm). Alle weiteren Daten hatten wir ja innerhalb von TMainForm gespeichert, d.h. sie sind in der Variable MainForm gespeichert.
Im implementation-Teil befindet sich dann die genauere Deklaration aller vorher definierten Funktionen und Prozeduren.
Der initialisation-Teil ist auch für uns ersteinmal uninteressant (er wurde von Lazarus automatisch angelegt).
Ein Screenshot haben wir bereits gesehen. Den Programmcode haben wir nun auch auf unserer Festplatte entpackt. Nach der bereits groben Einführung in den Code, wo wir ein paar gröbere Unklarheiten (vor allem wegen neuen Variablentypen) geklärt haben, wollen wir nun auf ein paar Stellen mal einen genaueren Blick werfen.
Sehen wir uns mal die Prozedur TMainForm.FormKeyDown im implementation-Teil an. Dies ist die Prozedur der Hauptform, die bei dem KeyDown-Ereignis der Hauptform ausgeführt wird, d.h. genau dann, wenn der Spieler eine Taste herunterdrückt.procedure TMainForm.FormKeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
begin
if Key = Ord('P') then
SetPauseState(not MyPauseState)
else
SetPauseState(false);
if not (ssCtrl in Shift) then // TODO: change to: nothing in Shift
case Key of
37: // left
begin
if MyFocus = fcRoom then MoveToPlace(mdLeft);
if MyFocus = fcKnapsack then ChangeKnapsackSelection(mdLeft);
end;
39: // right
begin
if MyFocus = fcRoom then MoveToPlace(mdRight);
if MyFocus = fcKnapsack then ChangeKnapsackSelection(mdRight);
end;
38: // up
begin
if MyFocus = fcRoom then MoveToPlace(mdUp);
if MyFocus = fcKnapsack then ChangeKnapsackSelection(mdUp);
end;
40: // down
begin
if MyFocus = fcRoom then MoveToPlace(mdDown);
if MyFocus = fcKnapsack then ChangeKnapsackSelection(mdDown);
end;
Ord(' '), 9: // space, tab
begin
ChangeFocus();
DrawRoom();
DrawKnapsack();
end;
13: // enter
begin
UseKnapsackSelection();
SetFocus(fcRoom);
end;
// 8, 46: // backspace, del
// begin
// MyKnapsack[MyKnapsackSelection].PicIndex := GetPictureCacheIndex(BACKGROUND_PIC);
// DrawKnapsack();
// end;
else
// WriteLn('pressed key: ' + IntToStr(Key));
end;
// only allow the following in editor mode
if MyEditorMode then
begin
if ssCtrl in Shift then
case Key of
37: // left
MoveToRoom(mdLeft);
39: // right
MoveToRoom(mdRight);
38: // up
MoveToRoom(mdUp);
40: // down
MoveToRoom(mdDown);
end;
end;
end;
Vorzeitiges Ende ...
Author: Albert Zeyer
Mail: ich AT admin DOT de
andere Projekte