Sandini Bib
Programmieren lernen für Teens
Sandini Bib
Sandini Bib
Bernd Brügmann
Programmieren lernen für Teens Mit C
ADDISON-WESLEY An imprint of Pearson Education München • Boston • San Francisco • Harlow, England Don Mills, Ontario • Sydney • Mexico City Madrid • Amsterdam
Sandini Bib Die Deutsche Bibliothek – CIP-Einheitsaufnahme Ein Titelsatz für diese Publikation ist bei Der Deutschen Bibliothek erhältlich
Die Informationen in diesem Buch werden ohne Rücksicht auf einen eventuellen Patentschutz veröffentlicht. Warennamen werden ohne Gewährleistung der freien Verwendbarkeit benutzt. Bei der Zusammenstellung von Texten und Abbildungen wurde mit größter Sorgfalt vorgegangen. Trotzdem können Fehler nicht vollständig ausgeschlossen werden. Verlag, Herausgeber und Autoren können jedoch für fehlerhafte Angaben und deren Folgen weder eine juristische Verantwortung noch irgendeine Haftung übernehmen. Für Verbesserungsvorschläge und Hinweise sind Verlag und Herausgeber dankbar. Alle Rechte vorbehalten, auch die der fotomechanischen Wiedergabe und Speicherung in elektronischen Medien. Die gewerbliche Nutzung der in diesem Produkt gezeigten Modelle und Arbeiten ist nicht zulässig. Fast alle Hardware- und Softwarebezeichnungen, die in diesem Buch erwähnt werden, sind gleichzeitig eingetragene Warenzeichen oder sollten als solche betrachtet werden. Umwelthinweis: Dieses Buch wurde auf chlorfrei gebleichtem Papier gedruckt. Die Einschrumpffolie – zum Schutz vor Verschmutzung – ist aus umweltverträglichem und recyclingfähigem PE-Material.
10
9
04
03
8
7 02
6
5
4
3
2
1
01
ISBN 3-8273-1802-5 c 2001 Addison-Wesley Verlag, ein Imprint der Pearson Education Deutschland GmbH Martin-Kollar-Straße 10–12, D-81829 München/Germany Alle Rechte vorbehalten Lektorat: Christina Gibbs,
[email protected] Korrektorat: Simone Burst, Großberghofen Produktion: Anna Plenk,
[email protected] Satz: Hilmar Schlegel, Berlin – gesetzt in Linotype Aldus/Palatino, Berthold Poppl College Umschlaggestaltung: Barbara Thoben, Köln Druck und Verarbeitung: Kösel, Kempten Printed in Germany
Sandini Bib
Für Veronika, Daniel und Gisela.
Sandini Bib
Sandini Bib
V Vorwort V.0 V.1 V.2 V.3
Programmieren geht über Studieren Was du zum Programmierenlernen mit diesem Buch brauchst Allgemeine Hinweise für den Leser, Eltern und Erwachsene Webseite
VIII IX X XI
Sandini Bib
V.0
Programmieren geht über Studieren
Programmieren ist kein Kinderspiel. Oder etwa doch? Was ist eigentlich Programmieren? Ich nehme einmal an, dass du, der Leser, mit der Verwendung von Computern vertraut bist. Wenn du einen Computer anschaltest, laufen ein oder mehrere Programme ab. Wenn du mit der Maus klickst, verarbeitet irgendein Programm dieses Ereignis, z. B. um eine Datei zu löschen oder dein Lieblingsspiel zu starten. Du kannst am Computer Rennwagen steuern, Monster kloppen oder Schach spielen, im Internet surfen oder Briefe schreiben. All das bewerkstelligt der Computer mit Programmen. Ein Computerprogramm ist eine Liste von Anweisungen, die der Computer ausführen kann. Die Anweisungen werden in einer besonderen Sprache verfasst, einer Programmiersprache. In diesem Buch verwenden wir C (gesprochen wie Zeh). C ist eine einfache und überschaubare Sprache mit nur sehr wenigen Wörtern, die recht leicht zu lernen ist und trotzdem ungeheuer vielseitig ist. Man kann Rennwagen-, Monster- und Schachprogramme damit schreiben! Welche Programmiersprache man als erste lernt, ist gar nicht so wichtig. Viel wichtiger ist es zu lernen, wie man eine Aufgabe wie Schach spielen in computergerechte Häppchen zerlegt. Es gibt keine Programmiersprache, die den Befehl ›spiele Schach mit mir‹ enthält. Das Geniale an Programmiersprachen und Computern ist jedoch, dass du mit simplen Befehlen komplizierteste Aufgaben lösen kannst – wenn du gelernt hast, computergerecht zu denken und die Aufgabe in kleine Schritte zu zerlegen. Ist Programmieren ein Kinderspiel? Ich würde sagen, ja und nein. Ja, weil eine Sprache wie C aus einfachen Befehlen besteht, die jeder lernen kann, auch wenn C sich zunächst wie eine Geheimsprache liest. Und nein, weil man sich nichts vormachen darf: Komplizierte Probleme wie Schach oder aufwändige Grafiksimulationen erfordern auch aufwändige Programme und Algorithmen. Sowas würde auf jeden Fall den Rahmen dieses Buches sprengen. Es lohnt sich aber auf alle Fälle, zumindest etwas Programmierluft geschnuppert zu haben, und dieses Buch möchte dir zeigen, wie viel du spielerisch lernen kannst. Vorneweg noch ein paar Bemerkungen: Programmieren geht über Studieren. Natürlich darfst und sollst du dieses Buch gründlich lesen, am besten von vorne bis hinten. Aber das Wichtigste sind die vielen Aufforderungen zum Selbermachen und zum Ausprobieren! Ich muss mich für das deutsch-englische Kauderwelsch entschuldigen. C verwendet englische Worte und überhaupt kommen die meisten Computerfachwörter aus dem Englischen (z. B. Computer, Rechner). Ziel des Buches ist es nicht, dich in einen 3D-Spieleprogrammierguru oder etwas ähnlich Fortgeschrittenes zu verwandeln. VIII
Vorwort
Sandini Bib
Ziel des Buches ist es, dir eine spielerische Einführung in die Grundlagen der C-Programmierung zu geben. Denn wenn du erst einmal einige Grundlagen gelernt hast, eröffnen sich dir viele Wege in die fantastische Welt der Programmierung. Jetzt gibt es nur noch eins: Computer anschalten, und los geht es mit unserem ersten Beispiel. Programmieren kannst du nur lernen, indem du Programme schreibst.
V.1 Was du zum Programmierenlernen mit diesem Buch brauchst Um die Beispiele in diesem Buch ausprobieren zu können, brauchst du einen Computer, Microsoft Windows und einen C-Compiler. Für alle großen Betriebssysteme gibt es C-Compiler. Das liegt nicht zuletzt daran, dass Windows und Unix zum größten Teil in C programmiert wurden. Die Beispiele zur Windowsprogrammierung laufen nur unter Windows, aber die Grundlagen zu C folgen dem ANSI-Standard und funktionieren mit jedem ANSI-C-Compiler. Dem Buch liegt eine CD mit der Vollversion des Borland C++Builder Standard (Version 1.0) von 1997 bei. Zwar ist die aktuelle Version des Borland C++Builders bei Version 5 angekommen (siehe www.borland.com), aber für die Grundlagen der Programmierung leistet selbst Version 1.0 noch ausgezeichnete Dienste. Alle Bedienungsanweisungen im Buch zum Compiler und Windows beziehen sich daher auf den beiliegenden Borland C++Builder Standard 1.0, kurz BCB. Die wesentlichen Voraussetzungen für die Verwendung von BCB sind: Windows 95 oder neuer Ungefähr 100 MB Platz auf der Festplatte Und natürlich brauchst du ein CD-Laufwerk, eine Maus, einen Stromanschluss und so weiter. Die Beispiele des Buches findest du ebenfalls auf der CD. Die Programmtextbeispiele sind direkt aus den Programmdateien in den Buchtext eingebunden worden, also hoffe ich, dass sich keine Tippfehler eingeschlichen haben. Korrekturen und Verbesserungsvorschläge sind immer willkommen.
V.1 Was du zum Programmierenlernen mit diesem Buch brauchst
IX
Sandini Bib
V.2 Allgemeine Hinweise für den Leser, Eltern und Erwachsene Nur zu, jeder darf diese Anmerkungen lesen, also auch du. Zur Erhöhung der Seriosität schalte ich vorübergehend um auf ›Sie‹. Dieses Buch richtet sich an Kinder und Jugendliche ab ungefähr 15 Jahren ohne Vorkenntnisse im Programmieren, die die Motivation zum selbstständigen Lernen mitbringen. Wesentliche Teile des Buches sind aber sicher ab 12 Jahren zugänglich, wenn ab und zu jemand Hilfestellung leisten kann. Begonnen hat dieses Projekt mit Programmbeispielen für meinen damals 9-jährigen Sohn und der Beobachtung, wie viel Spaß elementare Programmierung bei der richtigen Hilfestellung machen kann. Dazu einige allgemeine Bemerkungen: Programme können beliebig kompliziert sein, aber in ihrem Kern bestehen sie aus verblüffend wenigen, grundlegenden Bausteinen: Zahlen, Variablen, Bedingungen, Schleifen, Funktionen, . . . – genau diese Themen finden Sie in den Kapitelüberschriften wieder. Was uns allen von kommerziellen Computerprogrammen vertraut ist, die grafische Oberfläche in Windows, Textverarbeitung, Internetzugang oder Computerspiele, wurde Schritt für Schritt aus einer kleinen Zahl simpler Anweisungen zusammengesetzt. Was die Welt der Programme im Innersten zusammenhält, sind Programmanweisungen wie setze Variable gleich 1, wiederhole bis Zahl gleich 10 usw., und genau um diese Grundlagen geht es in diesem Buch. Verwendet wird die Programmiersprache C. C ist eine kleine, aber gleichzeitig sehr mächtige und flexible Programmiersprache. C kann als Vorstufe zu C++ gelernt werden, denn C++ enthält C als Untermenge. C++ ist heutzutage die wichtigste Programmiersprache im professionellen Bereich. Viele C++Programmierer verwenden zu 90 % C. Grafik und Farbe sind ein großer Motivationsfaktor. Die Kapitel 4, 8 und 11 widme ich deshalb einigen elementaren Elementen der Windowsprogrammierung, auch wenn die Windowsprogrammierung an und für sich nichts mit C zu tun hat. Wir lernen ein Fenster aufzumachen, darin zu malen und die Maus zu verwenden. Richtige Windowsprogrammierung bedeutet, mit Knöpfen, Menüs und Scrolleisten zu programmieren. Das wird in BCB sehr schön mit einer grafischen Oberfläche unterstützt, würde aber den Rahmen dieses Buches sprengen. Ich habe versucht, das Buch zu schreiben, das ich mit 15 oder so gerne über das Programmieren gelesen hätte: Kurz gefasst, aber nicht trivialisierend. Auf das Wesentliche ausgerichtet, aber ohne große Lücken. Jugendgerecht (also keine Beispiele zur Steuererklärung), aber nicht Spaß haben um jeden Preis in dem Sinne, dass nur anspruchslose Beispiele besprochen werden. Selbststudium ist so eine Sache. Was tun, wenn es nicht mehr weiter geht? Wenn die Eltern, ein Lehrer oder irgendein anderer Experte helfen kann, ist
X
Vorwort
Sandini Bib
das natürlich ideal. Nehmen Sie sich Zeit für Ihr Kind, lernen Sie mit. Vielleicht sollten Sie ihm auch einen anständigen Computer spendieren, aber die Hardware ist eigentlich nie das Problem. Ganz entscheidend für die erfolgreiche Verwendung dieses Buches ist es, sich ernsthaft mit den vielen, vielen Beispielen auseinander zu setzen. Wenn die Beispiele irgendwann zu kompliziert aussehen, liegt das vielleicht daran, dass die ersten Kapitel zu schnell abgehakt wurden. Dabei ist es nicht nötig, gleich beim ersten Lesen alle Beispiele eines Kapitels vollständig zu verstehen, aber: Programmieren geht über Studieren. Der Text regt unzählige Male dazu an, das Besprochene selber auszuprobieren. Gerade beim Programmieren lernen kommt es ganz entscheidend darauf an, selber aktiv zu werden. Denn selbst eine überschaubare Programmiersprache wie C steckt voller überraschender Möglichkeiten – Möglichkeiten, Fehler zu machen, aber auch Möglichkeiten, Aufgaben auf verschiedene Weisen zu lösen. Das muss man erlebt haben, um es nachvollziehen zu können. Ein paar Stunden nach einem selbst gemachten subtilen Fehler zu suchen kann wertvoller sein, als 100 Seiten gut gemeinte pädagogische, aber graue Theorie zu studieren. Also, ermuntern Sie den Leser dieses Buches zum Ausprobieren, Selbermachen, wieder Ausprobieren, Experimentieren.
V.3 Webseite Im Internet findest du unter www.proglernen.de
aktuelle Hinweise zum Buch, schau mal vorbei.
V.3 Webseite
XI
Sandini Bib
Sandini Bib
Inhaltsverzeichnis V Vorwort V.0 Programmieren geht über Studieren V.1 Was du zum Programmierenlernen mit diesem Buch brauchst V.2 Allgemeine Hinweise für den Leser, Eltern und Erwachsene V.3 Webseite 0
Die 0.0 0.1 0.2 0.3 0.4 0.5 0.6
1
2
Welt und das Hallo Hallo, Welt? Vom Programmtext zum ausführbaren Programm Installation von Borland C++Builder Ein neues Projekt Programmtexteingabe mit dem Editor Ausführen des Programms Beispiele auf CD-ROM
VII VIII IX X XI 1 2 2 3 4 4 7 8
0.7
10
0.8
11
Hallo, Welt! 1.0 Der printf-Befehl 1.1 Zeichenketten 1.2 Die Funktion main 1.3 Der Befehlsblock 1.4 Ein Befehl nach dem anderen 1.5 #include 1.6 Kürze ohne Würze? 1.7 Eine Stilfrage
13 14 15 16 17 18 19 20 21
1.8
22
1.9
23
Zahlen, Variable, Rechnen 2.0 1 + 2 zum Aufwärmen 2.1 Definition von Variablen 2.2 Nenn das Kind beim Namen
25 26 29 30
Sandini Bib
2.3 2.4 2.5 2.6 2.7 2.8 2.9 2.10 2.11 2.12
3
4
XIV
Wertzuweisung mit = Ausdruck und Befehl Initialisierung von Variablen Zahleneingabe mit der Tastatur Die Grundrechenarten für ganze Zahlen Die Grundrechenarten für Kommazahlen Zahlen im Überfluss Wer wem den Vortritt lässt Zuweisungsoperatoren Inkrement- und Dekrement-Operatoren
30 31 32 33 34 36 37 38 39 40
2.13
41
2.14
42
Felder und Zeichenketten 3.0 Felder für Zahlenlisten 3.1 Felder initialisieren 3.2 Mehrdimensionale Felder 3.3 Zeichen 3.4 Zeichenketten und Stringvariablen 3.5 Eine erste Unterhaltung mit deinem Computer 3.6 Ganze Zeilen lesen und schreiben 3.7 Kommentare 3.8 Geschichtenerzähler
43 44 46 47 47 49 51 53 54
3.9
58
3.10
59
Grafik 4.0 Hallo, Welt im Grafikfenster 4.1 Ein Hallo wie gemalt 4.2 Fensterkoordinaten 4.3 Pixel und Farbe 4.4 Linien 4.5 Rechtecke und Kreise 4.6 Fonts 4.7 Hilfe zu BCB und Windows SDK 4.8 Geradlinige Verschiebung
61 62 64 65 68 69 73 76 78
4.9
81
Inhaltsverzeichnis
Bunte Ellipsen
56
79
Sandini Bib
4.10
5
6
TextOut mit Format
83
4.11
85
4.12
87
Bedingungen 5.0 Bedingungen mit if 5.1 Vergleichsoperatoren, Wahr und Falsch 5.2 Logische Verknüpfungen 5.3 Bedingungen mit mehreren ifs 5.4 Bedingungen mit if und else 5.5 Bedingungen mit else if
89 90 92 95 97 98 101
5.6
Entscheidungen für den Bauch
102
5.7
Zufallszahlen
103
5.8
Ein Held des Zufalls
108
5.9
Bisektion
111
5.10
114
5.11
115
Schleifen 6.0 Zählerschleife mit while 6.1 Schleife im Geschwindigkeitsrausch 6.2 Endlosschleifen 6.3 Schleifenkontrolle mit break und continue 6.4 Schleifen mit while, for und do
117 119 120 121 121 124
6.5
Summe ganzer Zahlen
128
6.6
Schleifen und Felder
128
6.7
#define und Felder
130
6.8
Eingabeschleife
131
6.9
Das doppelte Schleifchen
134
6.10
Primzahlen
135
6.11
Zeit in Sekunden
139 Inhaltsverzeichnis
XV
Sandini Bib
7
8
XVI
6.12
Bisektion mit Schleife
141
6.13
Grafik – die hundertste Wiederholung
146
6.14
Linien aus Pixeln
148
6.15
Schachbrett
150
6.16
Histogramme
152
6.17
157
6.18
158
Funktionen 7.0 Funktionen 7.1 Funktion mit einem Argument 7.2 Funktion mit Rückgabewert 7.3 Funktion mit Argumenten und Rückgabewert 7.4 Prototypen von Funktionen 7.5 Headerdateien und Programmaufbau 7.6 Lokale Variable 7.7 Externe Variable 7.8 Statische und automatische Variablen 7.9 Statische Funktionen und externe Variablen 7.10 Zufallszahlen selbst gemacht
161 162 165 165 167 169 170 173 175 177 179 180
7.11
Rekursion
181
7.12
Rekursion mit Grafik
184
7.13
Labyrinth
187
7.14
202
7.15
203
Fensternachrichten 8.0 Achtung, Nachricht: Bitte malen! 8.1 Nachrichtenwarteschlage und Nachrichtenschleife 8.2 WM PAINT 8.3 Fenster auf, zu, groß, klein
205 206 210 211 212
8.4
215
Inhaltsverzeichnis
Klick mich links, klick mich rechts
Sandini Bib
9
8.5
Na, wo läuft sie denn, die Maus?
218
8.6
Tastatur
221
8.7
Die Uhr macht tick
227
8.8
233
8.9
234
Datentypen 9.0 Von Bits und Bytes 9.1 Bit für Bit 9.2 Datentypen 9.3 Datentypen klein und groß 9.4 Mein Typ, dein Typ: die Typenumwandlung 9.5 Konstante Variable mit const 9.6 Neue Namen für alte Typen: typedef
235 236 238 239 240 243 247 247
9.7
Mathematische Funktionen
248
9.8
Die Sinusfunktion
249
9.9
Kreis und Rotation
256
9.10
Farbtiefe
264
9.11
Mandelbrot-Menge
265
9.12
274
9.13
274
10 Zeiger und Strukturen 10.0 Zeiger 10.1 Zeiger und malloc 10.2 Zeiger, Felder und wie man mit Adressen rechnet 10.3 Zeiger, Variablen, Funktionen 10.4 Strukturen 10.5 Zeiger auf Strukturen 10.6 10.7
277 279 282 285 286 290 292
Bibliotheksfunktionen für Zeichenketten und Felder
296
Zeichenketten kopieren
297 Inhaltsverzeichnis
XVII
Sandini Bib
10.8
Felder aus Zeigern und main mit Argumenten
299
10.9
Dateien lesen und schreiben
301
10.10
WinMain
304
10.11
309
10.12
310
11 Bitmaps 11.0 Bitmaps erzeugen und laden 11.1 BitBlt 11.2 Bitmapfilmchen 11.3 Tonausgabe mit PlaySound 11.4
Bitmappuffer
326
11.5
Gekachelte Landkarte
334
11.6
Mit Pfeiltasten über Land
340
11.7
Bitmaps mit transparenter Hintergrundfarbe
344
Ein Held auf weiter Flur
347
11.8
XVIII
311 312 316 317 324
11.9
348
11.10
349
A Anhang A.0 Rangordnung von Operatoren A.1 Der Preprocessor A.2 Die Schlüsselwörter von C A.3 Buch gelesen, was nun?
351 352 352 355 357
Stichwortverzeichnis
359
Inhaltsverzeichnis
Sandini Bib
0 Die Welt und das Hallo 0.0 0.1 0.2 0.3 0.4 0.5 0.6
Hallo, Welt? Vom Programmtext zum ausführbaren Programm Installation von Borland C++Builder Ein neues Projekt Programmtexteingabe mit dem Editor Ausführen des Programms Beispiele auf CD-ROM
2 2 3 4 4 7 8
0.7
10
0.8
11
Sandini Bib
0.0
Hallo, Welt?
Unser erstes Programmbeispiel ist ein absoluter Klassiker. Wir können gar nicht anders anfangen, weil die Erfinder von C in ihrem ersten Buch so angefangen haben. Das Programm soll Hallo, Welt!
ausgeben. Hier ist das C-Programm dazu: Hallo.c
#include <stdio.h> int main() { printf("Hallo, Welt!\n"); getchar(); return 0; }
So sieht also ein Programmtext (›the code›) aus, viele sonderbare Zeichen. Aber lass dich nicht abschrecken, am besten liest du noch mal langsam jeden Buchstaben und jedes Zeichen. Mittendrin kannst du die Zeichen Hallo, Welt! entdecken, das Drumherum werde ich im nächsten Kapitel erklären. Zuvor müssen wir aber eine große Hürde nehmen. Wie verwandeln wir einen solchen Text in etwas, das der Computer ausführen kann? Wie lassen wir unser selbst gemachtes Programm ablaufen? Auf welche Art wird Hallo, Welt! am Bildschirm ausgegeben? Diese Probleme müssen erst einmal gelöst werden, bevor wir fortfahren können.
0.1 Vom Programmtext zum ausführbaren Programm Die einfachste Lösung ist: Wir verwenden eine Integrierte Entwicklungsumgebung, also ein spezielles Programm, mit dem wir alle nötigen Schritte durchführen können. Wen wundert’s, natürlich verwendet man Programme um Programme zu schreiben. (Wie war das gleich noch mit der Henne und dem Ei, was war zuerst da?) Das Programm Borland C++Builder für Microsoft Windows ist eine gute Wahl und als Vollversion 1.0 auf der CD des Buches enthalten, siehe Kapitel V.1. Ich werde oft von dem ›Compiler‹ sprechen, und damit ist der Borland C++Builder Standard (Version 1.0) gemeint. Eine typische Entwicklungsumgebung hat mehrere Teile, insbesondere eine Projektverwaltung, einen Editor, einen Compiler, einen Linker und einen Debugger: 1. Mit Projektverwaltung ist die Verwaltung verschiedener Programmierpro-
jekte gemeint. So halten wir Ordnung, denn jedes Projekt verwendet mehrere Dateien. 2
Kapitel 0
Die Welt und das Hallo
Sandini Bib
2. Der Editor dient zur Eingabe und Speicherung des Programmtextes. 3. Der Compiler übersetzt einzelne Dateien mit Programmtext in Maschinen-
sprache, die der Mikroprozessor deines Computers ausführen kann. Für jede Datei wird das Ergebnis in einer ›Objektdatei‹ gespeichert. 4. Der Linker erzeugt das ausführbare Programm (das Executable). Dazu kom-
biniert er die Objekte, die du hergestellt hast, mit Objekten aus der Bibliothek des Compilers. Viele oft benötigte Funktionen, z. B. für mathematische Funktionen oder für die Textausgabe, kannst du in mitgelieferten Bibliotheken finden. 5. Der Debugger hilft bei der Fehlersuche im Programm. Welche Fehler? Na
ja, jeder macht Fehler, selbst Profis, und mit dem Debugger kannst du ein Programm Zeile für Zeile und Anweisung für Anweisung ablaufen lassen und untersuchen, was genau vor sich geht. Und hier sind noch ein paar Erläuterungen zu englischen Begriffen. Die Schritte 3 und 4 werden unter dem Befehl ›make‹ (machen) oder ›build‹ (bauen) zusammengefasst. Oft nennt man den gesamten Prozess einfach Kompilieren. Übrigens heißt ›compile‹ zusammenstellen, ›link‹ verketten oder verknüpfen, Objekte sind ›objects‹ und Bibliothek heißt ›library‹. Ein ›bug‹ ist ein lästiges Insekt, und so heißen bei Programmierern die Programmierfehler. Fehler entfernen nennt man dann ›debug‹, also ›entwanzen‹.
0.2 Installation von Borland C++Builder Im Folgenden beschreibe ich kurz die nötigen Schritte, unser Hallo-Programm zum Laufen zu bringen. Borland C++Builder (kurz BCB) erledigt viele Dinge automatisch. Mehr Details findest du in dem Begleitmaterial von BCB. Die Installation und Bedienung von BCB ist einfach und logisch, aber wenn du erst wenig Erfahrung mit der Bedienung von Windowsprogrammen hast, bittest du am besten jemanden um Hilfe. Hier ist der erste Schritt: Installiere BCB. Lege die Buch-CD ins Laufwerk. In Windows öffne unter ›Arbeitsplatz‹ mit einem Doppelmausklick das Verzeichnis mit den Daten auf der CD. Doppelklicke auf das Verzeichnis Cbuilder. Die Installation startest du mit einem Doppelklick auf Setup.exe im Verzeichnis Cbuilder. Insbesondere solltest du bei ›Setup-Typ‹ die vollständige Installation wählen, damit alle Hilfedateien kopiert werden. Die vollständige Installation benötigt ungefähr 100 Megabyte Festplattenplatz. Also immer schön auf ›Weiter‹ und ›Ja‹ klicken, bis das Fenster ›Installation abgeschlossen‹ erscheint. Die angebotene Hilfe zum Borland C++Builder kannst du jetzt oder später lesen. (Das dort erwähnte Verzeichnis SAMS gibt es leider in dieser Version nicht.)
0.2
Installation von Borland C++Builder
3
Sandini Bib
0.3 Ein neues Projekt Als Erstes will ich dir zeigen, wie du mit BCB ein neues Projekt von Anfang bis Ende selber machst. So beginnen wir ein neues Programmprojekt: Starte BCB. Wenn du die Voreinstellungen nicht geändert hast, müsstest du BCB unter ›Start‹, ›Programme‹, ›Borland C++Builder‹ finden. Auf ›C++Builder‹ klicken. Mehrere Fenster erscheinen. Beim ersten Start fragt BCB, ob gewisse Standarddateien erstellt werden sollen. Klicke ›Ja‹. Starte ein neues Projekt. Oben links in der Menüleiste von BCB auf ›Datei‹ klicken, dann auf ›Neu‹. Ein Fenster mit Überschrift ›Neue Einträge‹ erscheint. Hier ist es wichtig, die ›Textbildschirm-Anwendung‹ auszuwählen (anklicken, dann ›OK‹ klicken). Speichere das Projekt mit neuem Namen ab. In der Menüleiste von BCB ›Datei‹, dann ›Projekt speichern unter‹ wählen. Ein Dateidialog erscheint. Wie du siehst, bietet dir BCB an, dein Projekt mit Dateiname Project1.mak im Verzeichnis C:\Programme\Borland\ CBuilder\Projects abzuspeichern. Tippe unter ›Dateiname‹ HalloWelt .mak ein. Klicke auf ›Speichern‹. Du kannst aber auch ein neues Verzeichnis erzeugen, dieses auswählen und dann das Projekt im neuen Verzeichnis abspeichern. Starte BCB neu, probehalber. Beende BCB (unter ›Datei‹, oder klicke das Kreuz rechts oben). Starte BCB neu und suche unter ›Datei‹, ›Projekt öffnen‹ oder ›Neu öffnen‹ nach deinem Projekt HalloWelt.mak. Anklicken. Hat es geklappt? Entweder war dir alles sonnenklar und zu ausführlich, weil du schon ein Windowskenner bist, oder meine schönen Erläuterungen haben dich zum Erfolg geführt, oder es ging daneben. Falls es nicht geklappt hat, nicht aufgeben. Auf deinem Rechner kann es kleine Unterschiede geben, z. B. bei den Namen der Verzeichnisse. Normalerweise wird die linke Maustaste verwendet. Manchmal muss man mit der Maus doppelklicken, um eine Aktion auszulösen, manchmal reicht ein Einfachklick. Bis zu diesem Punkt war alles eine Übung in Windowsbedienung und du findest sicher jemanden, der dir weiterhelfen kann. Das gilt auch für den nächsten Schritt, denn jetzt wollen wir den Text unseres HalloProgramms eingeben.
0.4 Programmtexteingabe mit dem Editor Wenn du unser Projekt HalloWelt.mak neu geöffnet hast, siehst du drei Fenster. Oben findest du die große Menüleiste vom BCB mit den vielen Knöpfen. Links ist ein Fenster ›Objektinspektor‹. Das brauchen wir nicht, du kannst es mit dem Kreuz rechts oben verschwinden lassen. Und dann gibt es noch ein Fenster für eine Datei namens 4
Kapitel 0
Die Welt und das Hallo
Sandini Bib HalloWelt.cpp
Dies ist das Editorfenster, das auf deine Texteingaben und Textänderungen wartet. Die Endung .cpp steht für C++-Dateien. C++ ist eine Erweiterung der Programmiersprache C. Überhaupt kann Borland C++Builder mehr als nur reines C oder C++, z. B. enthält BCB alles, um grafische Programme mit Fenstern und Knöpfen zu schreiben. Im Prinzip könnten wir die Datei HalloWelt.cpp für unseren Programmtext verwenden, und das kannst du später einmal ausprobieren. Beim Kompilieren erzeugt BCB für jedes Projekt aber um die 5 MB zusätzliche Dateien auf der Festplatte. Weil wir zu jedem Projekt typischerweise mehrere Beispiele besprechen wollen, werden wir BCB die Datei HalloWelt.cpp zur Projektverwaltung überlassen und unseren Programmtext in zusätzlichen Dateien speichern. Die verschiedenen Dateien mit dem Programmtext können wir dann je nach Bedarf in ein und demselben Projekt verwenden. Wir werden deshalb eine neue Datei Hallo.c
für unser C-Programm erzeugen und die Datei HalloWelt.cpp so ändern, dass BCB unser C-Programm kompilieren kann: Vereinfache HalloWelt.cpp. Lösche alle Zeilen in HalloWelt.cpp bis auf die vier Zeilen Project1.cpp
//−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− #include
#pragma hdrstop //−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
Falls du noch nie mit einem Editor gearbeitet hast, gibt es hier ein paar Tipps: Per Mausklick oder mit den Pfeiltasten kannst du den Textcursor im Text bewegen. Der Textcursor markiert die Stelle, an der Text eingegeben und gelöscht werden kann. Einzelne Buchstaben kannst du mit den Tasten Entf und der Zurücktaste löschen. Ganze Zeilen verschwinden problemlos, wenn du mit der Maus oder den Pfeiltasten erst Text markierst und dann die Tastenkombination Strg + X betätigst. Die Maus markiert Text, wenn du sie bei gedrückter linker Maustaste im Text bewegst. Die Pfeiltasten markieren Text, wenn du gleichzeitig die Taste drückst (wie für große Buchstaben). Praktisch: Mit Strg + Z kannst du die letzte Eingabe oder Löschung rückgängig machen! Also, ran an die Arbeit, HalloWelt.cpp bekommt einen Zeilenschnitt. Lösche alle bis auf die vier gezeigten Zeilen. (Wenn du mit der Maus ganz links in den Text klickst, erscheint ein rotes Stoppschild für den Debugger. Klicke auf das Stoppschild, um es zu entfernen.) 0.4 Programmtexteingabe mit dem Editor
5
Sandini Bib
Speichere ab. Mit ›Datei‹, ›Speichern‹ oder mit Strg + S kannst du die neue Version von HalloWelt.cpp abspeichern. Öfter mal abspeichern. Stell dir vor, du tippst fleißig eine Stunde lang, dann stürzt der Rechner ab und alles ist weg. Erzeuge eine leere Datei Hallo.c Dazu klickst du unter ›Datei‹ auf ›Neue Unit‹. ›Unit‹ heißt Einheit, in diesem Fall bekommen wir eine Datei namens Unit1.cpp. Der Editor hat jetzt am oberen Rand zwei Auswahlflächen, die du anklicken kannst, um zwischen verschiedenen Dateien umzuschalten. BCB versucht wieder, uns mit Text in Unit1.cpp zu helfen. Lösche bitte den gesamten Text, alle Zeilen. Speichere die leere Datei mit ›Datei‹, ›Speichern unter‹ unter dem Namen Hallo.c. An der Endung .c erkennt BCB, dass es sich um C-Code handelt. Gebe den Text des Hallo-Programms ein. Hier ist der Text für die Datei Hallo.c: Hallo.c
#include <stdio.h> int main() { printf("Hallo, Welt!\n"); getchar(); return 0; }
Achtung, wichtige Durchsage: Jedes Zeichen muss stimmen! Du musst auf der Tastatur die richtigen Zeichen finden, insbesondere # { } \ ;. Auf meiner Tastatur muss ich die 7 zusammen mit der AltGr Taste rechts von der Leertaste drücken, um { zu erhalten. Der Editor versucht dir zu helfen, indem er zur besseren Übersicht manche Wörter farbig oder fett darstellt. Speichere ab. Der Compiler bezieht sich auf die abgespeicherte Version. Im Menü unter ›Datei‹ auf ›Speichern‹ klicken (oder Strg + S drücken). Wenn der Text im Editor nicht mehr mit der gespeicherten Datei übereinstimmt, steht in der linken unteren Ecke des Editors ›Geändert‹. Damit ist dein Projekt vollständig. Soweit die Theorie, wir schreiten zur Praxis.
6
Kapitel 0
Die Welt und das Hallo
Sandini Bib
0.5 Ausführen des Programms Jetzt wird es spannend, denn als Nächstes wird sich herausstellen, ob der Compiler von BCB mit unserem Programm etwas anfangen kann. Kompiliere das Programm. Im BCB-Menü unter ›Projekt‹ findest du den Eintrag ›Projekt neu compilieren‹. Nur zu! BCB wird versuchen, unseren Programmtext in Hallo.c in ein ausführbares Programm zu übersetzen. Es erscheint ein Fenster mit der Überschrift ›Compilieren‹. Wenn es funktioniert hat, werden 0 Warnungen und 0 Fehler angegeben. Wenn es Probleme gibt, werden die Warnungen und Fehler gezählt. Quit-
tiere diese Meldung mit ›OK‹ und gehe auf Fehlersuche. Den Programmtext bitte genau überprüfen, korrigieren, abspeichern und erneut kom›warning‹) ausgegeben pilieren! Es könnte auch eine Warnung ( werden. Auch diese solltest du in unserem Beispiel durch kleine Korrekturen im Code zum Verschwinden bringen. Am unteren Rand des Editorfensters gibt BCB die Liste der Fehlermeldungen aus. Diese sind nur mit etwas Übung richtig zu verstehen. Wenn du auf eine Meldung mit [C++ Error] doppelklickst, springt der Textcursor in die Nähe des Pro›Error‹ heißt blems, manchmal springt er aber in die falsche Zeile. Fehler. Nicht lockerlassen, wir haben es fast geschafft. Kompiliere das Programm und lass es laufen: F9 . ›exWenn keine Fehler gemeldet werden, hast du ein ausführbares ( ecutable‹) Programm erzeugt. Das Programm steht als HalloWelt.exe im Projektverzeichnis. Du kannst es durch Doppelklicken in Windows ausführen – oder in BCB einfach F9 drücken. Diese Abkürzung findest du im BCB-Menü unter ›Start‹ und dann nochmals ›Start‹. F9 überprüft erst, ob eine Änderung des Programms gespeichert wurde, kompiliert neu, falls nötig, und führt dann das Programm aus. Mit Strg + F9 werden nur die zuletzt geänderten Dateien neu kompiliert. Wir haben mit der Kompilierung unter Menüpunkt ›Projekt‹ angefangen, damit du auf jeden Fall die Erfolgsmeldung siehst. Drücke F9 ! Was macht das Programm, wenn es aufgerufen wird? Es sollte ein Fenster erscheinen mit der triumphalen Ansage Hallo, Welt!
0.5 Ausführen des Programms
7
Sandini Bib
Und weil es so schön ist – so sieht das Fenster bei mir aus:
Drücke die Eingabetaste ↵ (die Enter- oder Neuezeiletaste), und das Fenster verschwindet. Da kann ich nur sagen: Herzlichen Glückwunsch! Es kann sein, dass bei dir kein Fenster erscheint und stattdessen das HalloProgramm den ganzen Bildschirm einnimmt. In Windows kannst du einstellen, ob eine Textbildschirmanwendung wie unsere in einem Fenster oder als Vollbild abläuft. Das Textfenster wird unter Windows 95 mit C:\Windows\System\ Conagent.exe erzeugt. Wenn du mit der rechten Maustaste auf diese Datei klickst, kannst du unter ›Eigenschaften‹, ›Bildschirm‹ zwischen ›Vollbild‹ und ›Normal‹ wählen. Mit der Tastenkombination Alt + ↵ kannst du bei laufendem Programm zwischen Vollbild und Fenster umschalten, wenn dies nicht unter ›Eigenschaften‹ von Conagent.exe abgestellt wurde. Wenn ein Projekt erst einmal angelegt ist, wirst du immer wieder Änderungen am Programmtext vornehmen, dein Programm laufen lassen, Text ändern, wieder ausprobieren, usw. BCB kann vor dem Kompilieren automatisch alle Änderungen abspeichern. Diese Option solltest du unbedingt im Menü unter ›Optionen‹, ›Umgebung‹, ›Vorgaben‹ anschalten, indem du Autospeichern für Editordateien und Desktop per Mausklick abhakst.
0.6
Beispiele auf CD-ROM
Jetzt weißt du, wie du mit BCB ein neues Projekt für eine ›TextbildschirmAnwendung‹ anlegen kannst. Bevor ich unsere kleine Einführung in BCB beende, möchte ich dir noch einige allgemeine Hinweise zur Verwendung von BCB mit diesem Buch geben. Auf der CD des Buches findest du im Verzeichnis ProgLernen
8
Kapitel 0 Die Welt und das Hallo
Sandini Bib
den Programmtext für die meisten Beispiele. Die Namen der dazugehörigen Dateien auf der CD werden im Buch rechts oben vom Programmtext angezeigt, z.B. Hallo.c. Als Erstes solltest du die Datei ProgLernen\LiesMich.txt lesen, um herauszufinden, wie die Dateien in ProgLernen organisiert sind. Unter ›Arbeitsplatz‹ in Windows findest du dein CD-Laufwerk. Mit Doppelmausklick kannst du dir die Dateien auf der CD anzeigen lassen. Mit Doppelklick auf LiesMich.txt wird der Text in dieser Datei angezeigt. Am besten kopierst du die Programmbeispiele auf deine Festplatte. Doppelklicke auf Setup.bat im Verzeichnis ProgLernen, um dieses Verzeichnis nach C:\ProgLernen
zu kopieren. Die CD wird dann nur noch benötigt, falls eine Datei ungewollt abgeändert wurde. Du kannst das Verzeichnis auch per Hand mit der Maus kopieren, aber die Dateien von der CD bleiben dann schreibgeschützt. Die Beispiele habe ich auf der CD in drei Gruppen eingeteilt, und zwar in Beispiele ohne Grafik im Textfenster wie das Hallo-Welt-Beispiel, und in einfache und kompliziertere Beispiele mit Grafik. Dazu gibt es dreierlei vorgefertigte Projektdateien, und zwar jeweils eine .mak-Datei und eine .cpp-Datei mit demselben Namen. Die meisten Beispiele benötigen außer den .mak- und .cpp-Dateien noch genau eine zusätzliche .c-Datei, bevor sie kompiliert werden können. In der Menüleiste unter ›Projekt‹ kannst du Dateien zum Projekt hinzufügen oder aus dem Projekt entfernen. Im Dateidialog zur Auswahl einer neuen Datei werden nur die Dateien angezeigt, die du unter ›Dateityp‹ ausgewählt hast. Um eine C-Datei gegen eine andere innerhalb desselben Projekts auszutauschen, entfernst du die eine und fügst die andere hinzu. Mit F9 kannst du das neue Beispiel dann testen. Wenn du ein fertiges Projekt als Vorlage für eigene Projekte verwenden möchtest, musst du normalerweise nur die .mak-, .cpp- und .c-Dateien in ein neues Verzeichnis kopieren. Es kann aber vorkommen, dass dabei falsche Verzeichnisangaben übernommen werden. Im BCB-Menü findest du unter ›Datei‹ die Option ›Projekt speichern unter‹. Dabei werden die .mak- und die .cpp-Datei kopiert. Zusätzliche .c-Dateien werden aber nicht kopiert, sondern bleiben mit der alten Verzeichnisangabe im Projekt stehen. Mit ›Datei‹, ›Speichern unter‹ kannst du diese Dateien dann in das neue Verzeichnis kopieren. Für Textbeispiele habe ich dir gezeigt, wie du mit BCB ein neues Projekt erzeugst, ohne irgendwelche Dateien aus ProgLernen zu benötigen. Falls du aber aus irgendeinem Grund stecken bleibst, kannst du es mit dem TextFenster-Projekt auf der CD versuchen (TextFenster.mak und TextFenster.cpp). Bei den Grafikbeispielen werden wir auf die entsprechenden Projekte auf der CD zurückgreifen, weil die Projekte dann etwas komplizierter sind, siehe Kapitel 4. Auch wenn du die Projektdateien von der CD verwendest, möchte ich dich doch ermuntern, einige der Programmtexte für die C-Dateien selber einzutippen. 0.6
Beispiele auf CD-ROM
9
Sandini Bib
Beim Eintippen bekommst du Übung für deine eigenen Programme und du liest jede Zeile sehr genau. Wenn du ohne nachzudenken jeden Programmtext von der CD in deine Projekte kopierst, übersiehst du wahrscheinlich einige wichtige Einzelheiten. Bei langen oder komplizierten Beispielen brauchst du dich aber auch nicht unnötig mit Tippübungen aufhalten. Hole dir den Programmtext von der CD und verwende das fertige Beispiel als Ausgangspunkt für eigene Experimente.
0.7 Hoffentlich ging alles glatt. Wenn nicht, solltest du wie gesagt einfach jemanden um Hilfe bitten. Falls du dich mit der Bedienung von Windows (Programminstallation, Verzeichnisse, Dateien usw.) und Windowsprogrammen wie BCB noch nicht gut auskennst, kann dir sicher jemand helfen. Als Nächstes wollen wir mit dem eigentlichen Programmieren anfangen und endlich erklären, warum unser Programm tut, was es tut! Hier noch mal das Wichtigste dieses Kapitels in Stichpunkten: Immer alles ganz genau lesen! In C hat jedes Zeichen seine eigene Bedeutung, z. B. dürfen \ und /, oder , . : ; nicht verwechselt oder weggelassen werden. C-Programme werden als Text in eine Datei eingegeben (mit Endung .c), der dann von einem Compiler in ein ausführbares Programm übersetzt wird. Selbst die Standardversion 1.0 vom Borland C++Builder kann viel mehr, als wir in diesem Buch besprechen können oder wollen, denn es geht uns schließlich um die Grundlagen von C. Lass dich nicht von anfänglichen Schwierigkeiten bei der Bedienung des Editors oder des Compilers abschrecken. Bald wird dir das einfach und logisch vorkommen.
10
Kapitel 0 Die Welt und das Hallo
Sandini Bib
0.8 1. Im BCB-Menü findest du unter ›Optionen‹, ›Umgebung‹, einige nützliche
und lustige Sachen. Unter ›Editor‹ habe ich die Tabstops auf ›3 5‹ gesetzt, was zwei Leerzeichen pro Tab bedeutet (die Tabulator-Taste findest du links vom Q). Die Voreinstellung ist vier. Unter ›Anzeigen‹ kannst du die Schriftart und Schriftgröße im Editor ändern und unter ›Farben‹ die, na ja, das kannst du raten. Jedes Optionsfenster hat seine eigene Hilfefunktion rechts unten. 2. Werfe von Windows aus einen Blick in das Verzeichnis, in dem du dein Projekt gespeichert hast, z. B. C:\Programme\Borland\CBuilder\Projects. Dort wirst du jede Menge zusätzlicher Dateien finden. Mit ˜ markiert BCB Si-
cherheitskopien von deinen Dateien. So findest du die vorhergehende Version, falls es Probleme mit der neuesten Version gibt. Per Doppelklick auf HalloWelt.exe kannst du das Programm unabhängig von BCB starten. Mit Doppelklick auf HalloWelt.mak kannst du das Hallo-Projekt in BCB öffnen. 3. Im BCB-Menü kannst du dir mit ›Ansicht‹, ›Projektverwaltung‹ ein prakti-
sches Fensterchen aufmachen lassen, in dem du mit dem Symbol Plus Dateien hinzufügen und mit Minus Dateien entfernen kannst. Zum Entfernen musst du als Erstes die Datei per Mausklick anwählen. Ein Doppelklick auf eine Datei in der Projektverwaltung holt diese in den Editor. Nach ›Projekt speichern unter‹ kann es sein, dass die Projektverwaltung nicht die neuen Namen anzeigt. In diesem Fall BCB neu starten. 4. Eine Änderung der C-Dateien eines Projekts werden von BCB automatisch mit USEUNIT in der .cpp-Datei notiert. Wenn du per Hand die Zeile mit USEUNIT entfernst oder den Dateinamen in USEUNIT änderst, wird das in der Projektverwaltung richtig angezeigt. Das kannst du mit HalloWelt.cpp aus-
probieren. Diese Art und Weise, Projekte zu verwalten, ist eine Eigenart von BCB und hat mit C nichts zu tun.
0.8
11
Sandini Bib
Sandini Bib
1 Hallo, Welt! 1.0 1.1 1.2 1.3 1.4 1.5 1.6 1.7
Der printf-Befehl Zeichenketten Die Funktion main Der Befehlsblock Ein Befehl nach dem anderen #include Kürze ohne Würze? Eine Stilfrage
14 15 16 17 18 19 20 21
1.8
22
1.9
23
Sandini Bib
In diesem Kapitel nehmen wir unser erstes Programmbeispiel Buchstabe für Buchstabe und Zeichen für Zeichen auseinander, um herauszufinden, wie es funktioniert.
1.0
Der printf-Befehl
Schauen wir uns das Hallo-Programm einmal genau an: Hallo0.c
#include <stdio.h> int main() { printf("Hallo, Welt!\n"); getchar(); return 0; }
Die Zeile printf("Hallo, Welt!\n");
ist für die Ausgabe verantwortlich. ›print‹ heißt drucken, und ›Hallo, Welt!‹ ist auch zu sehen, also leuchtet es ein, dass dies ein C-Befehl ist, der ›Hallo, Welt!‹ auf dem Bildschirm ›ausdruckt›. Dieser Befehl setzt sich wie folgt zusammen: Du ), zwischen den Klammern siehst das Wort printf, zwei runde Klammern, ( den Text "Hallo, Welt!\n" und am Ende der Zeile einen Strichpunkt ; . Machen wir doch gleich ein Experiment. Starte BCB und lade dein Hallo-Projekt aus Kapitel 0. Gehe ins Editorfenster von Hallo.c und ändere ›Welt‹ in ›Pizza‹. Drücke F9 . Dann ändere dieses Wort wieder in ›Welt‹. Drücke F9 . In der Tat, dies ist der Text, der ausgegeben wird. Als Nächstes entferne den Strichpunkt am Ende der Zeile mit printf. Weg damit. F9 drücken. Was sagt der Compiler dazu? Bei mir meldet er [C++ Error] Hallo.c(6): Call of nonfunction.
So etwas passiert öfter, man bekommt eine mehr oder weniger verständliche Fehlermeldung. Error bedeutet, dass ein Fehler vorliegt. Der Strichpunkt wird benötigt, um Befehle zu trennen. Deshalb kommt BCB durcheinander. In C müssen Befehlszeilen wie diese immer mit einem Strichpunkt beendet werden. Der Compiler nimmt das sehr genau. Also füge den Strichpunkt wieder ein, kompiliere und überzeuge dich davon, dass das Programm wieder läuft.
14
Kapitel 1 Hallo, Welt!
Sandini Bib
1.1 Zeichenketten Jetzt nehmen wir den Ausdruck "Hallo, Welt!\n"
unter die Lupe. C kennt verschiedene Datenformate. Hier haben wir es mit einer ›string‹ (Faden, Schnur) genannt, zu tun. Die AnfühZeichenkette, auch rungszeichen dienen dazu, mehrere Buchstaben und Zeichen ( ›characters‹) zu einem String zusammenzufassen. Lass das Programm noch einmal laufen, die Anführungszeichen erscheinen nicht in der Ausgabe. Aber was ist mit dem \n geschehen? Mit dem Schrägstrich \ ( ›backslash‹, Gegenschrägstrich) werden spezielle Sonderzeichen und Steuerzeichen eingegeben. \n ist das Neu›newline‹). Kannst du raten, wie ezeilezeichen ( "\nHallo,\nWelt!\n"
ausgegeben wird? Gleich ausprobieren. \n zählt als genau ein Zeichen. ›print‹ heißt drucken. printf ist der Name einer Funktion, Wie gesagt, die in unserem Beispiel auf dem Bildschirm druckt. Das f in printf steht für formatiertes Drucken. Sie wird aufgerufen, indem das, was gedruckt werden soll, zwischen runde Klammern geschrieben wird. Der Begriff ›Funktion‹ wird in C und anderen Programmiersprachen verwendet, wenn verschiedene Operationen unter einem Namen zusammengefasst sind. In C sieht ein Funktionsaufruf im Allgemeinen so aus: Name (Argumente)
In unserem Fall ist der Name der Funktion printf. Das Argument der Funktion, also das, was an die Funktion zur Bearbeitung übergeben wird, ist der String "Hallo, Welt!\n". Somit kannst du jetzt die Befehlszeile printf("Hallo, Welt!\n");
lesen. Sie ruft die Funktion printf auf, um einen String zu drucken, das Sonderzeichen \n sorgt für den Zeilenumbruch und der Strichpunkt ; beendet die Befehlszeile.
1.1 Zeichenketten
15
Sandini Bib
1.2 Die Funktion main Unser Programm besteht aber aus mehr als einer Zeile: Hallo0.c
#include <stdio.h> int main() { printf("Hallo, Welt!\n"); getchar(); return 0; }
Betrachte die Zeile mit main()
Sieht das nicht verdächtig nach einer Funktion aus, einer ohne Argumente? Rich›main‹ bedeutet ›wichtigst‹ und ist der Name der wichtigsten Funktion tig! in jedem C-Programm. Wenn du dein kompiliertes Programm startest, geschehen verschiedene Dinge, z. B. wird von BCB dafür gesorgt, dass Windows ein Fenster aufmacht. Aber dann kommt der Punkt, an dem die Kontrolle an dein Programm übergeben wird: Es wird die Funktion main aufgerufen. Dazu muss main definiert sein und du, der Programmierer, musst die Funktion main erfinden. Sie ist die wichtigste Funktion überhaupt, weil ohne sie der Compiler nicht weiß, wo er mit dem Ausführen des Programmes anfangen soll. Natürlich hätte man dieses Problem auch anders lösen können (z. B. könnte man immer in der ersten Zeile anfangen), aber in C fängt das Programm immer mit einem Aufruf von main an. Klarer Fall, das muss ausprobiert werden. Verwandle den Namen main in hallo. Bei mir beschwert sich der Compiler mit [Linker Error] Undefined symbol _main
Der Linker, der das ausführbare Programm erzeugen soll, teilt uns mit, dass ihm die Funktion main fehlt. Was passiert, wenn du den Namen Main statt main verwendest? Du wirst feststellen, dass C große und kleine Buchstaben unterscheidet. Z.B. gibt es auch Probleme mit Printf oder pRiNtF. In unserem Beispiel wird die Funktion main nach folgendem Schema definiert: int main() {
Befehle }
Die runden Klammern umklammern die Argumente, in unserem Fall wird main ohne Argumente aufgerufen. Danach folgen die Befehle in geschweiften Klammern. Allermeistens gilt: Klammer auf braucht Klammer zu. Klammern sind 16
Kapitel 1 Hallo, Welt!
Sandini Bib
schließlich dazu da, Objekte zu Gruppen zusammenzufassen. Wir kennen jetzt schon drei Beispiele, { ... }, ( ... ) und " ... ".
1.3 Der Befehlsblock Man nennt {
Befehle }
den ›Körper‹ oder ›body‹ der Funktion oder auch den Befehlsblock der Funktion. Eine Funktion ausführen bedeutet, die Befehle in ihrem Befehlsblock einen nach dem anderen abzuarbeiten. In unserem Beispiel gibt es drei Befehle. Das printf haben wir schon besprochen, der zweite Befehl ist getchar();
Brauchen wir den? Ich schlage vor, du entfernst diese Zeile einmal. Wenn du jetzt mit F9 das Programm ausführst, siehst du kurz ein Fenster aufblitzen und wie›get chader verschwinden. Der Befehl getchar bedeutet ›hole Zeichen‹ ( racter‹). Diese Funktion rufen wir ohne Argumente auf. Es werden Zeichen von der Tastatur angenommen, bis du auf die Eingabetaste drückst. Ausprobieren, du kannst ins Programmfenster hineintippen, bis du die Eingabetaste drückst. In anderen Worten, damit unser Programm nach der Textausgabe nicht gleich wieder das Fenster zumacht, lassen wir es mit getchar auf die Eingabetaste warten. Der dritte und letzte Befehl ist return 0;
Funktionen können so definiert werden, dass sie nach Beendigung ihrer Arbeit ›return‹ heißt hier ›zurückgeben‹ und die Zahl ist 0. eine Zahl abliefern. Im Moment spielt der Wert keine Rolle. Aber dem Compiler muss im Voraus gesagt werden, welche Art von Ergebnis main berechnet. Das Wörtchen int in int main()
bedeutet, dass diese Funktion eine ganze ( ›integer‹) Zahl berechnet. Eine eingehende Diskussion von Funktionen vertagen wir auf Kapitel 7. Somit ist klar, was die Zeilen int main() { printf("Hallo, Welt!\n"); getchar(); return 0; }
1.3 Der Befehlsblock
17
Sandini Bib
bedeuten. Sie enthalten die Definition der Funktion main. Wird main aufgerufen, wird der Befehl printf("Hallo, Welt!\n"); ausgeführt. Dann wird der Befehl getchar(); ausgeführt, der auf die Eingabetaste wartet. Der Befehl return 0; beendet die Funktion main und damit das Programm. Wie raffiniert! Eine Funktion kann also andere Funktionen wie z. B. printf aufrufen. Und wer weiß, wie viele untergeordnete Funktionen printf enthält? Egal, irgendwann ist die Arbeit getan, die Pixel für das große Hallo sind auf den Bildschirm gemalt und die Funktion printf hat ihre Arbeit erledigt. Mit return wird die Funktion main beendet und die Kontrolle wird wieder dem Betriebssystem (oder BCB) übergeben.
1.4 Ein Befehl nach dem anderen Jetzt wollen wir unser Programm Schritt für Schritt mit dem Debugger ablaufen lassen. So geht’s: Statt F9 drücke F8 . Ein leeres Textfenster erscheint und im Editorfenster wird die Zeile int main() eingefärbt und durch einen Pfeil markiert. Drücke F8 noch mal. Der Pfeil springt in die Zeile mit dem printf-Befehl! Du musst darauf achten, dass das Editorfenster das aktive Windowsfenster ist und nicht etwa das Textfenster (weil du gerade wie ich das Textfenster mit der Maus verschoben hast). Klicke das Editorfenster an, damit F8 auch an dieses Fenster weitergereicht wird. Drücke F8 noch einmal. Der Pfeil springt in die nächste Zeile. Im Textfenster erscheint ›Hallo, Welt!‹. Die Überschrift der BCB-Menüleiste zeigt an, dass das Programm ›Angehalten‹ hat. Drücke F8 noch mal. Hoppla, die Pfeilmarkierung ist weg. Das Programm ist in die Funktion getchar hineingesprungen und die wartet auf die Eingabetaste. Jetzt steht in der Überschrift ›Läuft‹. Klicke mit der Maus das Textfenster an und drücke die Eingabetaste. Die Pfeilmarkierung erscheint vor dem return-Befehl. Drücke F8 zweimal hintereinander. Das Textfenster verschwindet und unser Programm ist beendet. Wenn du das Programm mit ›Start‹ laufen lässt, werden die Befehle genauso Schritt für Schritt ausgeführt. Nur ist der Befehlstakt nicht nur ein Befehl pro Tastendruck, sondern eben viele Millionen oder Milliarden Befehle pro Sekunde, je nachdem, was der Mikroprozessor deines Rechners so leistet. Im BCB-Menü unter ›Start‹ findest du noch verschiedene andere Anweisungen für die Programmausführung. Wenn du als Erstes F7 drückst, erscheint nicht nur das Textfenster, sondern auch ein Fenster mit Überschrift CPU. Hier siehst 18
Kapitel 1 Hallo, Welt!
Sandini Bib
du die Maschinensprache, in die das Programm übersetzt wurde. Schließe das Fenster mit dem Kreuz rechts oben. Die Überschrift der BCB-Menüleiste zeigt an, ob das Programm ›Angehalten‹ wurde oder gerade ›Läuft‹. Mit Strg + F2 (siehe auch Menü ›Start‹) kannst du das Programm zurücksetzen und somit seine Ausführung beenden. Drücke Strg + F2 , falls du mit F7 das Programm erneut gestartet hattest.
1.5 #include Alles klar bis auf die Zeile #include <stdio.h>
die in unserem Programm auftaucht. Das Wort ›include‹ bedeutet hier ›hineinnehmen‹. Diese Zeile ist kein C-Befehl. Bevor der Compiler den Code Preprocessor (›Vornach C-Befehlen durchsucht, ruft er den so genannten prozessor‹) auf. Preprocessorbefehle beginnen mit #. Die #include-Anweisung führt dazu, dass der Preprocessor diese Zeile durch den Inhalt der Datei stdio.h ersetzt. ›standard input outUnd was soll das Ganze? ›stdio‹ kannst du dir als put‹ merken, also ›Standard Eingabe Ausgabe‹. Im Moment ist nur wichtig, dass der Compiler die Information in stdio.h braucht, um die Funktionen printf und getchar zu erkennen. Diese Funktionen werden freundlicherweise mit C mitgeliefert (du musst sie nicht selber programmieren, sie stehen in der stdioBibliothek), aber sie werden nicht in jedem Fall vom C-Compiler automatisch erkannt. Testen wir das doch gleich einmal. Lösche die Zeile mit der #include-Anweisung in dem Hallo-Programm, und kompiliere mit ›Projekt‹, ›Projekt neu kompilieren‹. Du bekommst zwei Warnungen, z. B. [C++ Warning] Hallo.c(5): Call to function ’printf’ with no prototype.
Das heißt ›Aufruf von Funktion printf ohne Prototyp‹. Solche Prototypen lernen wir in Kapitel 7 kennen. BCB gibt nur Warnungen aus, das Programm läuft trotzdem mit F9 . Manche Compiler verweigern in diesem Fall die Arbeit und melden einen Error. Selbst wenn das Programm läuft, sollte man Warnungen immer restlos beseitigen, sonst übersieht man irgendwann vor lauter ›harmloser‹ Warnungen ein schwerwiegendes Problem.
1.5 #include
19
Sandini Bib
1.6 Kürze ohne Würze? Schade, dass unser Programm so kurz ist. Was fehlt, sind mehr Befehle. Wie wäre es mit Hallo1.c
#include <stdio.h> int main() { printf("Hallo, Welt!\n"); printf("Schoenes Wetter heute.\n"); printf("Lass uns baden gehen.\n"); getchar(); return 0; }
Beachte, wie mehrere Befehlszeilen durch Strichpunkt getrennt untereinander gesetzt werden können. Kannst du raten, was das folgende Programm bewirkt? Hallo2.c
#include <stdio.h> int main() { printf("\n"); printf("Hallo, "); printf("Welt!"); printf("\n"); getchar(); return 0; }
Genau lesen, denken(!), dann erst eintippen. Ohne Neuezeilezeichen macht der zweite printf-Befehl auf der Zeile weiter, wo der erste aufgehört hat. Lass das letzte Programm auch einmal Schritt für Schritt laufen, dann kannst du genau sehen, wann die Ausgabe zur neuen Zeile springt. Wie dem auch sei, vielleicht gefallen dir kurze Programme noch besser als lange? Eine Funktion macht auch ohne Befehle (Un-)Sinn. Das Folgende ist ein vollwertiges C-Programm: Hallo3.c
main() { }
20
Kapitel 1 Hallo, Welt!
Sandini Bib
Der Compiler gibt eine Warnung aus, weil wir int und return weggelassen ›Function should return a value‹ heißt ›Funktion sollte einen Wert haben. liefern‹. Funktionieren sollte es aber trotzdem. Das kürzeste C-Programm ist Hallo4.c
main(){}
1.7 Eine Stilfrage Übrigens ist es im Wesentlichen eine Frage des persönlichen Stils, wie man den Programmtext formatiert. Nach einer Stunde Achterbahnfahren schreibe ich wahrscheinlich so: Hallo5.c
#include <stdio.h> int main ( ) { ( "Hallo, Welt!\n" )
0
printf ;
getchar();return ; }
Und auch so kompiliert und läuft unser Hallo-Beispiel einwandfrei. Unbedingt ausprobieren, dem Compiler ist es ziemlich egal, wo man Leerzeichen oder Leerzeilen einfügt. Wichtigste Ausnahme ist, dass Namen wie main keine Leerstellen enthalten dürfen. Der Compiler liest dann zwei verschiedene Namen nebeneinander, was nicht erlaubt ist. Hauptsache, deine Programme sind gut lesbar und ordentlich aufgeschrieben, denn das erleichtert die Fehlersuche enorm.
1.7 Eine Stilfrage
21
Sandini Bib
1.8 Wer hätte gedacht, dass es in diesem Kapitel mit dem simplen ›Hallo, Welt!‹ so viel zu erzählen gibt. Wir haben einige Funktionen von C kennen gelernt und einige Elemente der ›Syntax‹ von C besprochen. Syntax nennt man die Regeln, nach denen aus verschiedenen Zeichen und Sonderzeichen legaler C-Code zusammengesetzt wird. Hier ist noch mal das Wichtigste: Jedes C-Programm fängt mit einem Aufruf der Funktion main an. Im Programmtext sieht die Funktion main z. B. so aus: int main() {
Befehle }
Die Befehle stehen im Befehlsblock zwischen geschweiften Klammern und werden mit Strichpunkt voneinander getrennt. C unterscheidet kleine und große Buchstaben: main und Main sind verschiedene Namen. Strings sind Zeichenketten wie z. B. "Hallo, Welt!\n". Sie können Sonderzeichen wie \n (›newline‹) enthalten. Leerzeichen und Zeilenumbrüche spielen im C-Code oft keine Rolle. Mit printf kann man Strings auf dem Bildschirm ausgeben. Mit getchar kann man auf das Betätigen der Eingabetaste warten. Diese Funktionen benötigen die Preprocessoranweisung #include <stdio.h>, bevor sie im Programm aufgerufen werden können. Mit F9 startet man in BCB die Kompilierung und die Ausführung des Programms. Mit F8 wird das Programm Schritt für Schritt ausgeführt. Falls BCB auf F9 nicht reagiert, kann es sein, dass gerade ein Programm läuft (siehe Titelzeile von BCB). Mit Strg + F2 kann es beendet werden.
22
Kapitel 1 Hallo, Welt!
Sandini Bib
1.9 Übung macht den Meister, und nur selber denken macht schlau. Die folgenden Progrämmlein kannst du jetzt schreiben: 1. Schreibe ein Programm, das ›Ach, wie schön!‹ ausgibt. Zu einfach? Na, dann alles in Hallo.c löschen und es ohne Buch versuchen. 2. Schreibe ein Programm, das ›Hallo, Welt!‹ ausgibt, aber jeden Buchstaben auf
eine neue Zeile schreibt. 3. Schreibe ein Programm, das eine Schlangenlinie aus Sternchen (*) von links
oben nach rechts unten ausgibt. Ein Tipp für Leute mit wenig Editorerfahrung: Wie schon erwähnt kannst du Text mit der Maus oder den Pfeiltasten markieren. Mit Strg + X und Strg + C kannst du den markierten Text in einem Zwischenspeicher ablegen. Siehe auch das BCB-Menü ›Bearbeiten‹. Strg + X (Auschneiden) entfernt den markierten Text aus dem Editorfenster, Strg + C (Kopieren) kopiert den Text in die Zwischenablage, ohne ihn aus dem Editor zu entfernen. Mit Strg + V (Einfügen) lässt du den Text aus der Zwischenablage am momentanen Ort des Textcursors wieder erscheinen. Also: Schreibe eine Zeile printf("*\n");. Markiere diese Zeile. Kopiere sie mit Strg + C . Drücke zwanzigmal Strg + V . Jetzt hast du zwanzig Ausgabebefehle, in die du nur noch Leerzeichen einfügen musst.
1.9
23
Sandini Bib
Sandini Bib
2 Zahlen, Variable, Rechnen 2.0 2.1 2.2 2.3 2.4 2.5 2.6 2.7 2.8 2.9 2.10 2.11 2.12
1 + 2 zum Aufwärmen Definition von Variablen Nenn das Kind beim Namen Wertzuweisung mit = Ausdruck und Befehl Initialisierung von Variablen Zahleneingabe mit der Tastatur Die Grundrechenarten für ganze Zahlen Die Grundrechenarten für Kommazahlen Zahlen im Überfluss Wer wem den Vortritt lässt Zuweisungsoperatoren Inkrement- und Dekrement-Operatoren
26 29 30 30 31 32 33 34 36 37 38 39 40
2.13
41
2.14
42
Sandini Bib
Kapitel 0 und 1 haben uns die ersten wichtigen Schritte beim Programmieren vorgeführt. Wir haben besprochen, wie wir aus einem Programmtext ein ausführbares Programm machen können (mit BCB). Mit dem Hallo-Programm habe ich dir vorgeführt, wie ein Programmtext in C typischerweise aufgebaut ist. Mit Hilfe der Funktion printf hat unser Programm mit uns am Bildschirm Kontakt aufgenommen. In diesem Kapitel wollen wir uns mit Zahlen beschäftigen, wie man sie in Variablen speichern kann und wie man mit Zahlen und Variablen rechnen kann. Computer heißt Rechner. Obwohl wir nicht vorhaben, große Rechnungen durchzuführen, werden wir kleine Rechnereien sehr oft benötigen. Das liegt daran, dass einfach alles im Computer mit Zahlen zu tun hat. Die wichtigsten Teile in deinem Computer sind der Prozessor, der Befehle ausführt, und der Speicher, in dem Befehle und Daten gespeichert werden. Der Speicher besteht aus einzelnen Speicherzellen, in denen Zahlen als Bits und Bytes gespeichert werden. Der Prozessor macht nichts anderes, als je nach Zusammenhang die vielen Bits und Bytes als Befehl oder Zahl zu verarbeiten. Auch Buchstaben werden als Zahlen gespeichert, die dann erst bei der Ausgabe am Bildschirm als Buchstaben dargestellt werden. Und auch Grafik und Musik sind ›digitalisiert‹, stehen also als Zahlenkolonnen im Speicher. Wie es sich für eine Programmiersprache gehört, gibt es in C viele Befehle und Funktionen, die all diese Zahlen auf komfortable Weise als Text (z. B. printf!) oder Grafik verarbeiten. Was ein angehender Programmierer von Bits und Bytes wissen sollte, werden wir in Kapitel 9 genauer besprechen. Als Erstes wollen wir aber vorführen, dass in C mit Zahlen und Variablen auf einfache und natürliche Weise gerechnet werden kann. Also schön der Reihe nach, was ist 1 + 2?
2.0
1 + 2 zum Aufwärmen
Unser erstes Beispiel zeigt, dass wir in einer Variablen Zahlen speichern können: Zahlen0.c
#include <stdio.h> int main() { int i; i = 1; printf("i ist %d", i); getchar(); return 0; }
26
Kapitel 2 Zahlen, Variable, Rechnen
Sandini Bib
i ist 1
Starte wie in Kapitel 0 beschrieben ein neues Projekt. Tippe den Programmtext ein, oder hole ihn dir von der Buch-CD. Lass das Programm mit F9 laufen. Die Zeilen i = 1; printf("i ist %d", i);
bewirken Setze Variable i gleich 1. Gib den Wert der Variablen i am Bildschirm aus.
Bevor wir ins Detail gehen, gleich noch ein Beispiel, in dem mit Variablen gerechnet wird. Das folgende Programm berechnet 1 + 2 und teilt uns das Ergebnis mit: Zahlen1.c
#include <stdio.h> int main() { int i; int j; int summe; i = 1; j = 2; summe = i + j; printf("\n %d + %d ist gleich %d \n\n", i, j, summe); getchar(); return 0; }
1 + 2 ist gleich 3
Die Zeilen i = 1; j = 2; summe = i + j;
kannst du wie folgt lesen: Setze Variable i gleich 1. Setze Variable j gleich 2. Setze Variable summe gleich i+j, also gleich 1 + 2, was 3 ergibt.
2.0
1 + 2 zum Aufwärmen
27
Sandini Bib
Wie gewöhnlich wird jede Anweisung mit ; beendet. Lass mich kurz die printf-Anweisungen erklären. Schau genau hin, das Neue an printf("i ist %d", i);
im ersten Beispiel ist, dass die Funktion printf mit zwei Argumenten aufgerufen wird statt wie bisher mit einem. Das erste Argument ist der String "i ist %d", dann kommt ein Komma, dann als zweites Argument die Variable i. Des Rätsels Lösung hat mit dem f im Namen printf zu tun, was auf ›formatiertes‹ Drucken hindeutet. Im Allgemeinen wird printf als printf(String, Argumente)
aufgerufen. Der String kann normalen Text enthalten, aber auch Formatierungsanweisungen, die mit % beginnen. Das % gefolgt von d bedeutet, dass das nächste Argument von printf als Zahl ausgegeben werden soll. In unserem Beispiel erscheint deshalb statt %d der Wert der Variable i als Zahl am Bildschirm. Wenn du statt i eine 5 als zweites Argument schreibst, erscheint die 5 am Bildschirm. In printf("\n %d + %d ist gleich %d \n\n", i, j, summe);
verwenden wir %d gleich dreimal. Für jedes %d greift sich printf das nächste Argument und setzt die entsprechende Zahl in den ausgegebenen Text ein. In den Beispielen haben wir drei verschiedene Variablen i, j und summe verwendet. Variablen sind dir sicher aus der Schule geläufig. Eine Variable ist eine ›Veränderliche‹ und man nennt Variablen auch ›Platzhalter‹. Zahlen, die wie 1 oder 2 im Programm stehen, heißen Konstanten. Eine Variable wird dann eingesetzt, wenn man von einer bestimmten Zahl reden will, ohne sich auf ihren Wert festzulegen. Das ist der Fall, wenn du mathematische Formeln wie summe = i + j
verwendest. Hier soll die Zahl summe durch das Summieren der Zahlen i und j berechnet werden. Das Praktische ist, dass diese Formel für beliebige Werte der Variablen i und j angewendet werden kann. Falls i gleich 1 ist und j gleich 2 ist, sagt diese Formel, dass summe gleich 3 sein soll. Für 3 und 7 erhält man 10 und so weiter. Auch in einer Programmiersprache wie C werden Formeln zum Rechnen verwendet und meistens ist die Übersetzung der mathematischen Schreibweise nach C ganz logisch. Allerdings gibt es einige wichtige Unterschiede zur Mathe mit Papier und Bleistift. Damit du wirklich verstehst, wie die Beispiele funktionieren, wollen wir jetzt die Einzelheiten besprechen.
28
Kapitel 2 Zahlen, Variable, Rechnen
Sandini Bib
2.1 Definition von Variablen Jede Variable muss ausdrücklich mit dem Compiler vereinbart werden, bevor wir sie zum Rechnen verwenden können. Die Definition der Variablen in unserem zweiten Beispiel geschieht in den Zeilen int i; int j; int summe;
Dies sind drei Anweisungen, die wie üblich mit einem Strichpunkt ; beendet werden. Tatsächlich bewirkt eine Anweisung wie int summe; dreierlei: Wir vereinbaren einen Datentyp. int bedeutet, dass summe eine ganze Zahl (›integer‹) bezeichnen soll. C unterscheidet ganze Zahlen von Kommazahlen, die wir später besprechen werden. Wir vereinbaren einen Namen. Beim Programmieren verwendet man oft ganze Wörter wie summe statt einzelner Buchstaben als Namen für Variable, damit sich Formeln einfacher lesen lassen. Wir vereinbaren, dass bei Programmablauf Platz im Speicher für eine ganze Zahl reserviert werden soll. In anderen Worten: Nach int summe; steht uns Speicherplatz für eine ganze Zahl zur Verfügung, auf den wir mit dem Namen summe zugreifen können. Das ist der entscheidende Unterschied zur abstrakten Mathematik. Eine Variable in C ist immer an einen bestimmten Speicherplatz, d.h. eine gewisse Anzahl von Bytes irgendwo im Speicher deines Computers, gebunden. Speicherplatz und Bytes besprechen wir in Kapitel 9. Übrigens kannst du mehrere Definitionen vom selben Datentyp in eine Zeile schreiben, z. B. int i, j, summe;
Die Variablen werden durch Komma getrennt, und die Definition mit einem Strichpunkt beendet. Die Definition einer Variablen muss immer vor ihrer ersten Verwendung in einer Rechnung stattfinden. Zudem müssen alle Definitionen am Anfang eines Befehlsblocks stehen. Bisher haben wir nur den Befehlsblock kennen gelernt, der den Körper der Funktion main ausmacht, also alle Befehle zwischen { und }. Wenn du zurückblätterst, wirst du feststellen, dass in allen unseren Beispielen die Definitionen in den ersten Zeilen auftauchen und niemals im Programm verstreut sind. Versuche einmal int summe; im obigen Rechenbeispiel weiter nach unten zu verschieben. Auf jeden Fall gilt: Eine Variable muss definiert werden, bevor sie verwendet werden kann. 2.1 Definition von Variablen
29
Sandini Bib
2.2 Nenn das Kind beim Namen Variablen und auch Funktionen dürfen nicht beliebig genannt werden. Das ist völlig einleuchtend. Einerseits sind gewisse Namen schon vergeben, wie z. B. int und return, die zu den reservierten Schlüsselwörtern von C gehören, siehe Anhang A.2. Ein weiteres Beispiel ist printf, also der Name einer Funktion aus einer der C-Bibliotheken, der durch eine #include-Anweisung dem Programm bekannt gemacht wurde. Andererseits dürfen nicht beliebige Zeichen in Namen verwendet werden, weil sie ganz bestimmte Aufgaben in C erfüllen. Z.B. benötigen wir runde Klammern für Funktionen und das Pluszeichen zum Rechnen. Namen bestehen aus Buchstaben und Ziffern, wobei das erste Zeichen ein Buchstabe sein muss. summe1 ist erlaubt, 1summe nicht. Das Zeichen _ zählt als Buch›underscore‹, Unterstreichung). Es kann dazu verwendet werden, stabe ( lange Namen besser lesbar zu machen. Andere Zeichen sind nicht erlaubt. Z.B. kann man eine Variable langer_name nennen, aber langer+name wird als Plusoperation zwischen zwei Variablen interpretiert. Große und kleine Buchstaben werden unterschieden. Umlaute dürfen in BCB nicht in Namen verwendet werden. Wenn dein Computer nicht auf deutsche Sonderzeichen eingestellt ist, kann es auch in Zeichenketten zu Schwierigkeiten mit Umlauten kommen. Deshalb schreibe ich in allen Programmbeispielen ae, oe, ue und ss. Je nach Compiler gibt es noch die Einschränkung, dass nur Namen einer bestimmten Länge zugelassen sind. Mindestens 31 Zeichen sind jedoch erlaubt.
2.3 Wertzuweisung mit = Wollen wir auf eine Variable zugreifen, nennen wir sie einfach beim Namen. Mit dem Gleichheitszeichen = kann man einer Variablen einen Wert zuweisen, wie in i = 1;
Die Variable steht links und eine Zahl steht rechts. Nach Ausführung dieser Zeile ist ›i = 1‹, weil der Wert rechts vom Gleichheitszeichen in den Speicherplatz links vom Gleichheitszeichen kopiert wurde. Andersherum geht es nicht, es wird immer von rechts nach links kopiert. Ersetze einmal j = 2 im letzten Beispiel durch j = i;
Hier wird erst der Wert von i aus dem Speicher geholt und dann in den Speicherplatz von j kopiert. Also ist j nach dieser Anweisung 1. 30
Kapitel 2 Zahlen, Variable, Rechnen
Sandini Bib
In summe = i + j;
geschieht Folgendes. Erst werden alle Operationen rechts vom Gleichheitszeichen ausgewertet, dann das Endergebnis in summe gespeichert. Wir können in die Definition von Variablen gleich noch die erste Wertzuweisung einbauen, wie in Zahlen2.c
#include <stdio.h> int main() { int i = 1, j = 2; int summe = i + j; printf("\n %d + %d ist gleich %d \n\n", i, j, summe); getchar(); return 0; }
Entscheidend ist, dass zum Zeitpunkt einer Zuweisung alle benötigten Variablen schon definiert wurden.
2.4 Ausdruck und Befehl Mit Ausdruck werden wir ganz allgemein eine Verkettung von Operationen in C bezeichnen. Das kann eine einzelne Zahl sein oder eine Rechenoperation wie i + j oder eine Zuweisungsoperation wie in i = 1. Auch ein Funktionsaufruf wie printf(...) ist ein gültiger Ausdruck. Der Strichpunkt macht aus einem Ausdruck eine Befehlszeile, z. B. i = 1;. Die Syntax von C erlaubt es, Ausdrücke an vielen Stellen einzusetzen, an denen ein einziger Wert verlangt wird. C berechnet den Ausdruck, bis das Endergebnis verfügbar ist. Ersetze einmal den printf-Befehl im letzten Beispiel durch printf("\n %d + %d ist gleich %d \n\n", i, j, i + j);
Wie zu erwarten erhalten wir das richtige Ergebnis, denn C wertet auch in Funktionsargumenten erst Rechenoperationen aus, bevor es die Funktion aufruft. Beachte, dass wir keinen Strichpunkt hinter i + j geschrieben haben. An dieser Stelle benötigen wir einen Ausdruck, und nicht eine Befehlszeile. Ausprobieren, der Compiler meldet einen Syntaxerror.
2.4 Ausdruck und Befehl
31
Sandini Bib
2.5 Initialisierung von Variablen Welchen Wert hat eigentlich die Variable i nach der Zeile int i;? Für einen Test brauchst du nur in unserem allerersten Beispiel eine Zeile zu löschen: Zahlen3.c
#include <stdio.h> int main() { int i; printf("i ist %d\n", i); getchar(); return 0; }
i ist 134517884
Vielleicht ergibt dieses Programm bei dir, dass i gleich 0 ist oder 1 oder etwas ähnlich Harmloses. Aber im Allgemeinen gilt: Nach int i; zu Beginn des Befehlsblocks enthält i einfach nur Müll! In anderen Worten, int i; definiert Typ, Name und Speicherplatz einer neuen Variablen, der Wert von i ist aber noch undefiniert. Das heißt, in den Bytes dieser Variable stehen irgendwelche Zahlen, die zufälligerweise von der letzten Verwendung dieser Bytes übrig sind. Die Wortwahl ist hier nicht ganz gelungen, weil man bei ›Definition‹ vielleicht schon an Wertzuweisung denkt. Es werden aber nur Typ, Name und Speicherplatz definiert. Eine Variable enthält im Allgemeinen erst dann einen sinnvollen Wert, nachdem ihr ein Wert ausdrücklich und offiziell zugewiesen wurde. Diese erste Wertzuweisung einer Variablen nennt man Initialisierung. Der Compiler gibt normalerweise eine Warnung aus, wenn der Wert der Variable vor der Initialisierung ausgelesen wird. Bei mir meldet BCB Possible use of ’i’ before definition.
Das bedeutet: Mögliche Verwendung von ›i‹ vor der Definition.
32
Kapitel 2 Zahlen, Variable, Rechnen
Sandini Bib
2.6 Zahleneingabe mit der Tastatur Variablen kannst du nicht nur direkt im Programmtext auf einen bestimmten Wert setzen, du kannst auch Zahlen von der Tastatur einlesen: Zahlen4.c
#include <stdio.h> int main() { int i; printf("\nSag mir eine ganze Zahl:
");
scanf("%d", &i); printf("%d", i); printf(", was fuer eine schoene Zahl!\n"); getchar(); getchar(); return 0; }
Sag mir eine ganze Zahl: 5 5, was fuer eine schoene Zahl!
In scanf("%d", &i);
wird offensichtlich eine ganze Zahl in die Variable i eingelesen. ›scan‹ heißt genau prüfen oder abtasten. scanf ist das Gegenstück zu printf. Die Formatregeln entsprechen sich sinngemäß, nur dass scanf Zeichen von der Tastatur einliest, statt sie am Bildschirm auszugeben. Wenn scanf aufgerufen wird, wartet das Programm so lange auf Zeichen, die mit der Tastatur eingegeben werden, bis ein \n-Zeichen mit der Enter- oder Eingabetaste eingegeben wird. Der Unterschied zu getchar ist, dass die Zeichen wegen der Formatanweisung %d in eine ganze Zahl umgewandelt werden. Diese Zahl kann mehrere Ziffern haben (ohne Leerstellen). Anders als bei der Ausgabe ganzer Zahlen mit printf müssen wir &i
schreiben, damit die eingelesene Zahl in der Variable i gespeichert wird. Den Grund für das Zeichen & werden wir in Kapitel 10 erklären. Ein kleines Problem mit scanf ist, dass zwar eine Zahl von der Tastatur gelesen wird, dass aber das Neuezeilezeichen nicht gelesen wird, mit dem wir die Eingabe beendet haben. Kein Zeichen, das mit der Tastatur eingegeben wird, geht 2.6 Zahleneingabe mit der Tastatur
33
Sandini Bib
verloren. Es steht in einer Warteschlange (dem Tastaturpuffer), bis es z. B. mit getchar abgeholt wird. Deshalb müssen wir getchar zweimal aufrufen. Beim ersten Mal holt getchar das Neuezeilezeichen von der Zahleneingabe und erst das zweite getchar wartet dann auf ein zweites Neuezeilezeichen. Erst nach dem zweiten Neuezeilezeichen beenden wir das Programm.
2.7 Die Grundrechenarten für ganze Zahlen Ganze Zahlen ( ›integers‹) haben wir schon mehrfach verwendet. Sie erhalten den Typennamen int. Ganze Zahlen sind die positiven Zahlen 1, 2, 3, . . .,
die 0, und auch die negativen Zahlen −1, −2, −3, . . ..
Zwei ganze Zahlen können durch die Operatoren + − * / %
verknüpft werden. Plus, Minus und Malnehmen funktioniert, wie du es gewöhnt bist. Das Minuszeichen kann auch einzeln einem Ausdruck vorausgestellt werden, um sein Vorzeichen zu ändern. Nach i = 5; j = −i;
ist j gleich −5. Manchmal schreiben wir +1, um deutlich zu machen, dass wir nicht etwa −1 meinen. In der Mathematik schreibt man beim Rechnen mit Variablen oft das Malzeichen nicht, aber in C darf das Malzeichen nicht fehlen: 2j + 1 wird in C zu 2 * j + 1.
Die Division wird mit / geschrieben. Bei ganzen Zahlen ergibt die Division eine ganze Zahl plus Rest. Z.B. ist 9/4 eigentlich 2.25, der Operator / ignoriert aber die Stellen hinter dem Komma, damit das Ergebnis wieder eine ganze Zahl ist: 9 / 4 ist 2.
Den Divisionsrest erhält man mit dem Operator %: 9 % 4 ist 1.
Das ist, als ob du 9 Bonbons an 4 Freunde verteilst. Jeder erhält 2, aber eins bleibt übrig. Kennst du das Kartenspiel Skat? Es gibt 32 Karten und 3 Spieler. Jeder bekommt 32/3 = 10 Karten und 32 % 3 = 2 bleiben übrig. Und ein letztes Minibeispiel: 50 % 10 ergibt 0, weil 50 durch 10 teilbar ist. 34
Kapitel 2 Zahlen, Variable, Rechnen
Sandini Bib
Am besten probierst du das folgende Programm mit verschiedenen Eingaben aus: Zahlen5.c
#include <stdio.h> int main() { int i, j; printf("\nSag mir eine Zahl: scanf("%d", &i); printf("Sag mir noch eine Zahl: scanf("%d", &j); printf("%d + %d printf("%d − %d printf("%d * %d printf("%d / %d printf("%d %% %d printf("\n");
= = = = =
");
");
%d\n", i, j, i + j); %d\n", i, j, i − j); %d\n", i, j, i * j); %d\n", i, j, i / j); %d\n", i, j, i % j);
getchar(); getchar(); return 0; }
Ich habe mich in der Zeile printf("%d %% %d
=
%d\n", i, j, i % j);
nicht etwa vertippt, wir brauchen wirklich das doppelte %%, obwohl nur ein % ausgegeben werden soll. Der Grund ist, dass % als Kennzeichen für Formatanweisungen wie %d verwendet wird, selbst aber unsichtbar bleibt. Wie machen wir es sichtbar? Siehe oben. Das Programm bittet dich um Zahleneingaben mit der Tastatur, die du mit der Eingabetaste beenden musst. Wenn ich 5 und 7 eingebe, erhalte ich Sag Sag 5 + 5 − 5 * 5 / 5 %
mir eine Zahl: mir noch eine Zahl: 7 = 12 7 = −2 7 = 35 7 = 0 7 = 5
5 7
Mit der Formatanweisung %3d oder %5d erreichst du, dass printf 3 oder 5 Zeichen Platz für eine ganze Zahl reserviert. Probier das mal aus. Auf diese Weise kannst du im Beispiel die Spalten für die Zahlen gleich breit machen und erreichen, dass in jeder Spalte die Zahlen ganz nach rechts gerückt werden. 2.7 Die Grundrechenarten für ganze Zahlen
35
Sandini Bib
Bei der Division mit / oder % darf nicht durch Null geteilt werden. Was passierst, wenn du alle Warnungen in den Wind schlägst und versuchst durch Null zu teilen? Ausprobieren.
2.8 Die Grundrechenarten für Kommazahlen Wenn ganze Kerle mit ganzen Zahlen rechnen, mit welchen Zahlen rechnen halbe Kerle? Eine halbe 1 ist 0.5, also eine Kommazahl. Statt von Kommazahlen spricht man auch von Fließkommazahlen oder reellen Zahlen. ›floating point numbers‹) werden mit dem Datentyp Fließkommazahlen ( float definiert. Für höhere Genauigkeit kannst du den Datentyp double verwenden (mehr als doppelt so viele Stellen hinter dem Komma). Kommazahlen sind Zahlen wie 1.55 −0.00432
Hoppla, aufgepasst. Auf Deutsch sagen wir Komma, auf Englisch ›Point‹ (Punkt), und tatsächlich müssen wir in C einen Punkt statt einem Komma machen! Das Rechenprogramm lässt sich für Kommazahlen umschreiben: Zahlen6.c
#include <stdio.h> int main() { float x, y; printf("\nSag mir eine Zahl: scanf("%f", &x);
");
printf("Sag mir noch eine Zahl: scanf("%f", &y); printf("%f printf("%f printf("%f printf("%f
+ − * /
%f %f %f %f
= = = =
%f\n", %f\n", %f\n", %f\n",
x, x, x, x,
y, y, y, y,
getchar(); getchar(); return 0; }
36
Kapitel 2 Zahlen, Variable, Rechnen
");
x x x x
+ − * /
y); y); y); y);
Sandini Bib
Sag mir eine Zahl: Sag mir noch eine Zahl: 5.000000 + 7.000000 = 5.000000 − 7.000000 = 5.000000 * 7.000000 = 5.000000 / 7.000000 =
5 7 12.000000 −2.000000 35.000000 0.714286
Hier verwende ich float statt int und %f statt %d. Mit %.3f kannst du die Anzahl der Stellen hinter dem Punkt auf 3 setzen. Mit %10.3f kannst du für die Kommazahl inklusive der Stellen hinter dem Punkt 10 Zeichen Platz reservieren. Dass ich die Namen der Variablen geändert habe, ist eine reine Stilfrage. Der Rest-Operator % funktioniert für Floats nicht. Wie du siehst, liest scanf eine ganze Zahl wie 5 als Kommazahl 5.0, wenn das Format %f ist. Du kannst natürlich auch Kommazahlen wie 1.55 und −0.00432 eingeben. Ein eher unerwartetes Ergebnis liefert float x = 1/2;
Nach dieser Zuweisung ist x gleich 0, denn 1 und 2 sind ganze Zahlen, und deshalb wird mit der ganzzahligen Division gerechnet. Wenn man mit Kommazahlen rechnen will, muss man 1 und 2 wie in float x = 1.0/2.0;
als Kommazahlen ins Programm schreiben und erhält x = 0.5. Wir werden in Kapitel 9.4 die Umwandlung von Datentypen besprechen.
2.9 Zahlen im Überfluss In Kapitel 9 werden wir der Sache auf den Grund gehen, aber an dieser Stelle erst mal eine Warnung. In Integervariablen kannst du nicht beliebig große Zahlen speichern! Dasselbe gilt für Kommazahlen und ist compiler- und computerabhängig. Typischerweise kannst du in Integervariablen problemlos Zahlen von −2 Milliarden bis +2 Milliarden speichern. Aber bei 3 Milliarden kann es zu folgendem Blödsinn kommen:
2.9 Zahlen im Überfluss
37
Sandini Bib Zahlen7.c
#include <stdio.h> int main() { int i = 2000000000; int j = 3000000000; printf("i = %d, j = %d\n", i, j); getchar(); return 0; }
i = 2000000000, j = −1294967296
Autsch. Du solltest also aufpassen, dass du beim Rumprobieren nicht einfach so eine 1 mit beliebig vielen Nullen eingibst. Der Hintergrund ist, dass pro Variable nur eine bestimmte Anzahl von Bytes im Speicher verwendet werden, und wenn eine Zahl zu groß ist, passt sie einfach nicht mehr ’rein. Man spricht von ›Overflow‹ (Überfließen), siehe Kapitel 9.
2.10 Wer wem den Vortritt lässt Aus den Rechenoperationen, die wir besprochen haben, lassen sich längliche Ausdrücke wie i i i i
+ + * *
j j j j
+ * + −
k k k 2*k + 100/i
bilden. Dabei spielt es oft eine Rolle, in welcher Reihenfolge gerechnet wird. Dafür gibt es Regeln wie ›Punkt vor Strich‹, wobei mit ›Punkt‹ Malnehmen und Teilen gemeint ist, und mit ›Strich‹ Summe und Differenz. Zum Beispiel: 1 + 3 * 4 ist gleich 13,
weil erst malgenommen, dann addiert wird. In anderen Worten, das * bindet stärker als das +. Was in runden Klammern steht, wird zuerst ausgerechnet. Um erst zu addieren und dann zu multiplizieren schreiben wir (i + j) * k
38
Kapitel 2 Zahlen, Variable, Rechnen
Sandini Bib
Ein Beispiel mit Zahlen: (1 + 3) * 4 ist gleich 16,
weil erst addiert, dann malgenommen wird. Klammern braucht man aus demselben Grund auch, wenn man Brüche aus der mathematischen Schreibweise übersetzen will: i+j k
als (i + j)/k eingeben, denn i + j/k ist i + kj .
Beim Punktrechnen wird von links nach rechts vorgegangen: 40 40 40 40
/ / / /
4 * 2 (4 * 2) 4 / 2 (4 / 2)
ist gleich 20, ist gleich 5, ist gleich 5, ist gleich 20.
In Anhang A.0 findest du eine Tabelle, der du entnehmen kannst, in welcher Reihenfolge verschiedene Operatoren in C abgearbeitet werden (auch für solche, die wir erst später besprechen werden).
2.11 Zuweisungsoperatoren Oft möchte man mit einer Variable rechnen, und das Ergebnis gleich wieder in der Variable abspeichern. Wie wird i = i + 1
ausgewertet? Der Ausdruck auf der rechten Seite wird ausgerechnet und das Ergebnis in i gespeichert. Das heißt, erst wird der Wert von i ausgelesen, dann wird gerechnet, dann der neue Wert nach i geschrieben. Das Ergebnis von i = 5; i = i + 1;
ist, dass i gleich 6 ist. Ausprobieren! Neben = gibt es noch weitere Zuweisungsoperatoren, die eine Variable verändern können. Elementare Rechenoperationen kann man wie folgt mit einer Zuweisung kombinieren: i += j
entspricht
i = i + j
i −= j
i = i − j
i *= j
i = i * j
i /= j
i = i / j
Falls i auf 5 und j auf 7 gesetzt sind, führt der Ausdruck i *= j dazu, dass i gleich 35 ist. Die Variable j bleibt auf jeden Fall unverändert. Für ganze Zahlen gibt es auch %=. 2.11 Zuweisungsoperatoren
39
Sandini Bib
2.12 Inkrement- und Dekrement-Operatoren Eine sehr häufig vorkommende Operation ist, dass man zu einer Variable 1 dazu›increment‹) oder von einer Variablen 1 abziehen möchte zählen möchte ( ( ›decrement‹). Dazu kann man die Operatoren ++ und −− verwenden. Dies sind spezielle Zuweisungsoperatoren, die den Wert von i ändern, obwohl kein = in Sicht ist. Man kann sowohl ++i als auch i++ schreiben. Achtung, aufgewacht! Womöglich fandest du die letzten Erläuterungen mehr oder weniger offensichtlich. Hier kommt eine kleine Feinheit: Zahlen8.c
#include <stdio.h> int main() { int i, j; i = 5; j = ++i; printf("i ist %d, j ist %d\n", i, j); i = 5; j = i++; printf("i ist %d, j ist %d\n", i, j); getchar(); return 0; }
i ist 6, j ist 6 i ist 6, j ist 5
Was ist hier los? i wird um eins hochgesetzt, wie zu erwarten. Jedoch wird in j = ++i die Addition vor der Zuweisung durchgeführt, während in j = i++ die Addition nach der Zuweisung erfolgt. In einem Fall steht ++ vor i, im anderen Fall kommt ++ nach i.
40
Kapitel 2 Zahlen, Variable, Rechnen
Sandini Bib
2.13 Wie du siehst, kann man in C auf natürliche Weise mit Zahlen und Variablen umgehen. Obwohl wir nicht vorhaben, große Rechnungen durchzuführen, ist das erfreulich. Denn kleine Rechnereien gehören zum Programmieren wie das Brötchen zum Hamburger. In Kürze: In C gibt es verschiedene Datentypen. Wichtige Datentypen für Zahlen sind int für ganze Zahlen und float und double für Kommazahlen. Zahlkonstanten wie 1 oder −20 erhalten den Typ int. Kommazahlen benötigen einen Punkt wie in 2.5 und erhalten den Typ double. Man muss 1.0/2.0 schreiben, wenn das Ergebnis 0.5 sein soll. Eine Variable kann Zahlen unter einem Namen speichern. Jede Variable muss ›definiert‹ werden, bevor ihr im Programm ein Wert zugewiesen werden kann. Beispiele: int i; oder float x;. Einer Variablen i kann mit z. B. i = 1; ein Wert zugewiesen werden. Wenn eine Variable noch nicht ›initialisiert‹ wurde, dann steht in ihr womöglich Müll. (Es gibt Ausnahmen, siehe Kapitel 7.) C kennt die Grundrechenarten + − * / %. Bei den Inkrement- und Dekrement-Operatoren muss man z. B. zwischen ++i und i++ unterscheiden. Operatoren werden in einer bestimmten Reihenfolge ausgeführt, siehe Anhang A.0.
2.13
41
Sandini Bib
2.14 1. Frage nach der Anzahl der Karten in einem Kartenspiel und nach der Anzahl
der Mitspieler. Gib das Ergebnis einer ganzzahligen Division mit Rest aus. 2. Schreibe ein Programm, das eine ganze Zahl i abfragt und dann das Quadrat i*i und die dritte Potenz i*i*i berechnet. Und i*i*i*i, und i*i*i*i*i.
Diese Zahlen werden leicht sehr groß. 3. Welche Formeln aus der Geometrie fallen dir ein? Frage nach den Kantenlän-
gen eines Rechtecks und berechne die Fläche. Wie steht es mit der Oberfläche oder dem Volumen von Würfeln oder Kugeln? 4. Denke dir eine Formel selber aus, z. B. a
=
x−y x+y+z (x
− y)(x − z)
xyz
,
und schreibe ein Programm dazu. 5. In Kapitel 2.4 habe ich behauptet, dass i = 1 ein Ausdruck ist. Teste die fol-
genden Befehle: j = (i = 1); j = i = 1; k = (j += i);
6. Experimentiere mit ++. Was ist (i++) + (++i) − (i++)? Benötigst du die
Klammern?
42
Kapitel 2 Zahlen, Variable, Rechnen
Sandini Bib
3 Felder und Zeichenketten 3.0 3.1 3.2 3.3 3.4 3.5 3.6 3.7 3.8
Felder für Zahlenlisten Felder initialisieren Mehrdimensionale Felder Zeichen Zeichenketten und Stringvariablen Eine erste Unterhaltung mit deinem Computer Ganze Zeilen lesen und schreiben Kommentare Geschichtenerzähler
44 46 47 47 49 51 53 54 56
3.9
58
3.10
59
Sandini Bib
Bisher haben wir in jeder Variable genau eine Zahl gespeichert. In diesem Kapitel möchte ich dir zeigen, wie du in C mehrere Zahlen in so genannten Feldern ›arrays‹) speichern kannst. Der Variablenname bezeichnet dann das ganze ( Feld. Insbesondere lassen sich so ganze Reihen oder Listen von Zahlen abspeichern. Einen Spezialfall stellen die Zeichenketten dar, die wir schon in Kapitel 1.1 kurz besprochen haben. Eine Zeichenkette kann in C wie eine Liste von Zahlen behandelt werden, denn Buchstaben werden intern als Zahlen verarbeitet. Felder machen es möglich, Variablennamen für veränderliche Zeichenketten zu verwenden. Damit können wir dann auch die Tastatureingabe von Text besprechen. Im Laufe des Kapitels wird klar werden, dass nicht jede Eingebung zu einer genialen Ausgebung führt, aber unterhaltsam ist es trotzdem.
3.0
Felder für Zahlenlisten
In einem Feld werden mehrere Objekte desselben Datentyps hintereinander in einem zusammenhängenden Speicherbereich gespeichert. Hier ist ein Beispiel: Felder0.c
#include <stdio.h> int main() { int a[5]; int i; a[0] a[1] a[2] a[3] a[4]
= = = = =
0; 2; 4; 6; 8;
i = 0; printf("i i++; printf("i i++; printf("i i++; printf("i i++; printf("i
= %d
a[%d] = %d\n", i, i, a[i]);
= %d
a[%d] = %d\n", i, i, a[i]);
= %d
a[%d] = %d\n", i, i, a[i]);
= %d
a[%d] = %d\n", i, i, a[i]);
= %d
a[%d] = %d\n", i, i, a[i]);
getchar(); return 0; }
44
Kapitel 3 Felder und Zeichenketten
Sandini Bib
i i i i i
= = = = =
0 1 2 3 4
a[0] a[1] a[2] a[3] a[4]
= = = = =
0 2 4 6 8
Als Erstes nehmen wir die Definition der Variablen a unter die Lupe. Mit int a[5];
erhalten wir Speicherplatz für 5 ganze Zahlen. Diese stehen hintereinander im Speicher, aber im Moment kann es uns egal sein, wie die Zahlen im Speicher stehen. Entscheidend ist, wie wir auf diese Zahlen zugreifen. In der Definition haben wir den Variablennamen a für unser Feld eingeführt. Bei einer Feldvariablen darfst du nicht einfach a = 0 schreiben (auch wenn es vielleicht logisch wäre, auf diese Weise alle Zahlen im Feld a auf Null zu setzen). Richtig ist, wie in a[0] a[1] a[2] a[3] a[4]
= = = = =
0; 2; 4; 6; 8;
jeder Zahl im Feld a einzeln einen Wert zuzuweisen. Der Name a gibt an, welches Feld gemeint ist, und die Zahl zwischen eckigen Klammern bestimmt, die wievielte Zahl in der Liste gemeint ist. Die Zahlen in einem Feld nennt man auch die Elemente des Feldes. In Ausdrücken wie a[2] = 4 bestimmt die Zahl zwischen den eckigen Klammern die Nummer oder den ›Index‹ des Elements, während in int a[5]; die Größe des Feldes auf 5 gesetzt wird. Die Reihenfolge der Zuweisungen spielt in unserem Beispiel keine Rolle. Wie bei einer Kommode mit Schubladen kannst du die Zahlen in einer beliebigen Reihenfolge einsortieren und dann mit dem richtigen Index gezielt darauf zugreifen. Ein anderes Bild, das du dir vom Feld a machen kannst, ist eine Reihe von 5 Kästchen auf einem Blatt Karopapier, die mit 0, 1, 2, 3, 4 durchnummeriert sind. Achtung: Lustigerweise haben die Erfinder von C beschlossen, beim Zählen mit 0 anzufangen! Unser Beispiel zeigt, dass das 1. Element den Index 0 hat, das 2. Element den Index 1 und das letzte Element hat den Index 4. Macht genau 5 Elemente. Ein Element a[5] gibt es nicht, denn dann hätten wir ja 6 Zahlen, aber unser Feld wurde mit int a[5]; nur für 5 ausgelegt. Daran musst du dich sicher erst einmal gewöhnen. Warum wir beim Zählen mit 0 anfangen, wird uns erst in Kapitel 10.2 klar werden. Übrigens, welche Nummer hat das erste Kapitel dieses Buches? Wenn du nach int a[5]; über das Ende des Feldes hinausliest oder gar hinausschreibst (z. B. mit a[10] = 0;), machst du einen schrecklichen Fehler: 3.0
Felder für Zahlenlisten
45
Sandini Bib
Du darfst niemals über das letzte Element eines Feldes hinaus Werte lesen oder Werte zuweisen! Du kannst dabei Speicherplatz überschreiben, der wahrscheinlich für andere Zwecke reserviert ist. Dein Programm kann mit einer unerfreulichen Fehlermeldung von BCB oder Windows abstürzen. Leider kontrolliert dein C-Programm nicht, ob Felderindizes innerhalb des Feldes bleiben. Auf Kosten der Geschwindigkeit könnte im Prinzip intern jeder Index auf Korrektheit geprüft werden, aber das geschieht nicht. Mehr dazu in Kapitel 10. Unser Beispiel zeigt auch, dass du eine ganzzahligen Variable wie i als Index verwenden kannst. Die Zeilen mit den printf-Befehlen sind alle gleich, aber weil mit i++ die Indexvariable zwischen den printf-Befehlen hochgezählt wird, ergibt die Ausgabe von a[i]
jeweils das nächste Element im Feld a. Nach der Definition kann man einen Ausdruck wie a[i] überall dort verwenden, wo auch eine einzelne Variable stehen darf. In der Definition von Feldern sind Variablen wie i allerdings nicht erlaubt. Hier muss auf jeden Fall eine Konstante stehen.
3.1 Felder initialisieren Einer Integervariablen kannst du bekanntlich schon bei der Definition einen Wert zuweisen, int i = 1;. So ähnlich geht das auch bei Zahlenlisten. Mit int a[5] = {0, 2, 4, 6, 8};
erhältst du genau das gleiche Ergebnis wie im letzten Beispiel, probiere es aus. Der Strichpunkt darf nicht fehlen. Diese Art der Initialisierung ist praktisch, allerdings darf eine solche Kommaliste nur in der Definition, nicht aber an beliebigem Ort im Programm stehen. Bei int a[] = {0, 1, 2, 3, 4};
habe ich zwischen den eckigen Klammern die Größe des Feldes weggelassen. Weil aber eine Liste mit 5 Anfangswerten für das Feld angegeben ist, erhält a automatisch die Größe 5. Wenn du in int a[5] = {...}; weniger als 5 Zahlen in die Liste schreibst, wird von Element 0 an das Feld beschrieben, die übrigen Plätze im Feld werden nicht initialisiert.
46
Kapitel 3 Felder und Zeichenketten
Sandini Bib
3.2 Mehrdimensionale Felder Eine Definition wie int a[10]; erzeugt ein ›eindimensionales‹ Feld. Vielleicht weißt du aus der Geometrie, dass ein Punkt nulldimensional ist, eine Gerade eindimensional (weil sie Ausdehnung nur in einer Richtung besitzt), eine Ebene zweidimensional und ein Volumen dreidimensional. Auf einer Geraden muss man eine Zahl (eine Koordinate) angeben, um einen Ort festzulegen, genau wie wir einen Index für ein eindimensionales Feld brauchen. In der Ebene braucht man zwei Koordinaten, ähnlich wie man bei einer Tischplatte Länge und Breite angibt. Wenn man dann die Tiefe hinzufügt, ist man in drei Dimensionen. Wenn man jetzt noch die Zeit hinzufügt, will man wahrscheinlich Einsteins Relativitätstheorie diskutieren. Felder können auch mehrdimensional sein und oft hat das nichts mit räumlichen Dimensionen zu tun: int int int int int
einmaleins[10][10]; grosseseinmaleins[10][20]; vektor[3]; matrize[3][3]; raum[500][300][200];
Das Feld einmaleins ist sowas wie ein Rechteck auf einem Blatt Karopapier mit 10 mal 10 Kästchen. Wie bei einem Rechteck die Kantenlängen musst du hier die Zahlen in der Definition in [ ] miteinander malnehmen, um die Anzahl der Elemente zu erhalten. Eindimensionale Felder könnten wir genauso gut Vektoren oder Listen nennen, aber der Name Feld ist gebräuchlich, weil Felder eine unterschiedliche Anzahl von Dimensionen haben können. In Kapitel 7.13 werden wir soweit sein, ein interessantes Beispiel mit mehrdimensionalen Feldern durchführen zu können. Jetzt wollen wir erst einmal den wichtigsten Spezialfall von eindimensionalen Feldern besprechen, nämlich Zeichenketten. Dazu erklären wir als Erstes, wie einzelne Zeichen in C gehandhabt werden.
3.3 Zeichen Die Datentypen int und float kennst du schon. Sie werden für ganze und reelle Zahlen verwendet. In Variablen des Datentyps char kannst du einzelne Zeichen ›characters‹) speichern. Das geht z. B. so: ( char c; c = ’a’;
Aufgepasst, einzelne Zeichen werden mit dem hochgestellten Komma ’ eingegeben, dem Apostroph, und nicht mit ". Die Anführungszeichen oder Gänsefüßchen " sind für Zeichenketten reserviert. Spezielle Zeichen können mit \ angegeben werden: 3.2
Mehrdimensionale Felder
47
Sandini Bib \a \b \f \n \r \t \v
Klingelzeichen Zeichen zurück Neue Seite Neue Zeile Wagenrücklauf Tabulator Vertikaltabulator
\" \’ \? \\ \0
Anführungszeichen Apostroph Fragezeichen Schrägstrich Zahlencode Null
Deutlich hörst du hier einen alten elektrisch-mechanischen Drucker aus der Urzeit der Computertechnik rattern. Wie wir wissen, zählt z. B. \n als ein Zeichen. In Kapitel 9 werden wir besprechen, dass Buchstaben intern als Zahlencode mit ganzen Zahlen gespeichert werden und dass sich der Datentyp char im Wesentlichen nur in der Anzahl der im Speicher belegten Bytes von Datentyp int unterscheidet. Weil wir dauernd schon die Funktion getchar verwenden, ist ein kleines Beispiel angebracht: Felder1.c
#include <stdio.h> int main() { int c; printf("Gib mir ein Zeichen: "); c = getchar(); printf("c = %d oder %c\n", c, c); getchar(); getchar(); return 0; }
›Gib mir ein Zeichen‹ heißt nicht, dass du dem Computer zuwinken sollst, sondern dass er auf einen Tastendruck gefolgt von ↵ wartet, z. B. Gib mir ein Zeichen: c = 97 oder a
a
Wie du siehst, ist getchar eine Funktion, die wie eine Funktion in der Mathematik einen Wert liefert, den wir einer Variablen zuweisen können: c = getchar();
Diesen Wert haben wir bisher nur immer ignoriert. Wir können ihn in einer Integervariablen c speichern. Die ganze Zahl in c ist der Code für das Zeichen, das getchar gelesen hat. Der printf-Befehl gibt den Wert der Variablen c einmal als ganze Zahl mit %d und dann als Buchstaben bzw. Zeichen mit %c aus 48
Kapitel 3 Felder und Zeichenketten
Sandini Bib
(c wie ›character‹). Wie nach scanf rufen wir dann getchar noch zweimal auf, damit das Fenster nicht einfach zuklappt. Diese zeichenweise Eingabe ist unpraktisch, aber wie wir gleich sehen werden, kannst du mit scanf ganze Zeichenketten einlesen. Also, wie kommen wir von Zeichen zu Zeichenketten?
3.4 Zeichenketten und Stringvariablen Eine Variable für Zeichenketten (eine Stringvariable) ist nichts weiter als ein eindimensionales Feld für den Datentyp char, z. B. char s[100];
Auf diese Weise erhalten wir eine ›Stringvariable‹ namens s mit Speicherplatz für 100 Zeichen. Wenn wir wollten, könnten wir dieser Variablen gleich einen Text zuweisen, indem wir genau wie bei Integervariablen jedes Zeichen einzeln in eine Liste schreiben, z. B. char s[] = {’H’, ’a’, ’l’, ’l’, ’o’, ’\n’, 0};
Achtung, ein richtiger String benötigt in C die Null als letztes Zeichen! Die Stringvariable s ist ein Feld mit genau sieben Zeichen, denn die letzte 0 zählt mit. Die Zahl 0 kannst du der Einheitlichkeit halber auch als ’\0’ schreiben. Weil du jetzt sicher denkst, oh Graus, wie umständlich, will ich dir gleich zeigen, wie es viel einfacher geht. Eine Stringvariable wirst du normalerweise nie mit einzelnen Zeichen, sondern immer wie in char s[] = "Hallo\n";
initialisieren. Die Zeichenkette "Hallo\n"
ist eine Stringkonstante. Frage: Was ist ’a’, was ist "a"? Das eine ist ein einzelnes Zeichen, das andere ist eine Zeichenkette aus zwei Zeichen, ’a’ und ’\0’. Lange Rede, kurzer String, hier ist endlich ein Programmbeispiel: Felder2.c
#include <stdio.h> int main() { char meldung[100] = "Hallo, Welt!\n"; printf("%s", meldung); getchar(); return 0; }
3.4 Zeichenketten und Stringvariablen
49
Sandini Bib
Hallo, Welt!
In der Zeile char meldung[100] = "Hallo, Welt!\n";
definieren wir die Variable meldung. In printf("%s", meldung);
verwenden wir %s zur Ausgabe des Strings. Wie du siehst, erwartet printf als zweites Argument den Namen der Stringvariablen ohne [ ]! Kannst du dir denken, was das Folgende bewirkt? Felder3.c
#include <stdio.h> int main() { char gruesse[100] = "Hallo"; char wendenn[100] = "Welt"; printf("\nAlle zusammen: %s %s %s !\n", gruesse, wendenn, gruesse); getchar(); return 0; }
Pass auf, dass du \n an der richtigen Stelle eingibst. Dieses Programm schreibt Alle zusammen:
Hallo Welt Hallo !
printf gibt den Text in "\nAlle zusammen:
%s %s %s !\n"
Zeichen für Zeichen aus. Als Erstes kommt ein Neuezeilezeichen, \n
Dann wird der Text Alle zusammen:
gefolgt von zwei Leerzeichen ausgegeben. Jetzt trifft printf auf das erste %s. Diese Formatanweisung besagt, dass an dieser Stelle der Inhalt der ersten Stringvariablen, nämlich der Variablen gruesse, in den Ausgabetext hineingeflickt werden soll. Also wird 50
Kapitel 3 Felder und Zeichenketten
Sandini Bib Hallo
ausgegeben. Dann wird ein Leerzeichen ausgegeben. Dann wird an der Stelle des zweiten %s der Inhalt der zweiten Stringvariablen ausgegeben. Und so weiter. Wie bei der Zahlenausgabe mit %d greift sich printf für jedes %s das nächste Argument. Du kannst Zahlen und Strings auch mischen, Hauptsache die Reihenfolge der Argumente stimmt mit der Reihenfolge der Formatanweisungen überein.
3.5 Eine erste Unterhaltung mit deinem Computer Hier ist ein kleines Frage-und-Antwort-Spiel: Felder4.c
#include <stdio.h> int main() { char frage[100] = "Wie heisst du?"; char antwort[100] = "Bernd"; printf("\n%s\n", frage); printf("%s\n", antwort); printf("\nHallo, %s!\n", antwort); getchar(); return 0; }
Bitte eintippen und ausprobieren, ausgegeben wird: Wie heisst du? Bernd Hallo, Bernd!
Natürlich stehen meine Chancen schlecht, dass ich deinen Namen richtig geraten habe. Also ändere das Programm wie folgt:
3.5 Eine erste Unterhaltung mit deinem Computer
51
Sandini Bib Felder5.c
#include <stdio.h> int main() { char frage[100] = "Wie heisst du?"; char antwort[100]; printf("\n%s\n", frage); scanf("%99s", antwort); printf("\nHallo, %s!\n", antwort); getchar(); getchar(); return 0; }
Die entscheidende Neuerung ist, dass printf("%s", antwort);
durch scanf("%99s", antwort);
ersetzt wurde. Im Gegensatz zu scanf("%d", &i);
im letzten Kapitel wird kein & geschrieben. Das hat damit zu tun, dass i eine Integervariable ist, während antwort eine Feldvariable ist (siehe Kapitel 10). Wenn scanf aufgerufen wird, wartet das Programm. Es werden so lange Zeichen von der Tastatur im Tastaturpuffer gespeichert, bis ein \n-Zeichen eingegeben wird (mit der Enter- oder Eingabetaste). Der Unterschied zu getchar ist, dass nach \n die Zeichen nicht einzeln abgeliefert werden, sondern mehrere Zeichen auf einmal in einen String geschrieben werden können. Nachdem die Eingabe mit ↵ beendet wurde, verarbeitet scanf den Formatstring. Für jedes %s schreibt scanf ein Wort aus der Eingabe in die dazugehörige Stringvariable. Wörter sind Zeichenfolgen, die durch Leerzeichen getrennt sind, aber auch \n und \t zählen als Leerzeichen. Gegebenenfalls liest scanf mehrere Zeilen, um Wörter für mehrere %s zu finden. Beachte, dass wir die Variable antwort mit char antwort[100]; eingeführt haben. Damit nicht mehr als 100 Zeichen in antwort gespeichert werden, geben wir in der Formatanweisung mit %99s an, dass höchstens 99 Zeichen gelesen werden sollen (eine 0 für das Ende des Strings kommt noch hinzu).
52
Kapitel 3 Felder und Zeichenketten
Sandini Bib
Das Ergebnis sieht dann z. B. so aus: Wie heisst du? Rumpelstilzchen Hallo, Rumpelstilzchen!
Ich freue mich immer wieder, wenn ich sehe, wer heutzutage so alles programmieren lernt.
3.6 Ganze Zeilen lesen und schreiben Ein kleines Problem ist, dass das Programm durcheinander kommt, wenn mehr als ein Wort pro Zeile eingegeben wird. Gib einmal deinen vollständigen Namen ein, z. B. Bernd Brügmann. Das Fenster klappt trotz der zwei Aufrufe von getchar sofort zu, denn jetzt kehrt getchar mit dem Leerzeichen zurück, das den Vornamen vom Nachnamen trennt. Das ist unschön. Das gewünschte Ergebnis liefert Felder6.c
#include <stdio.h> int main() { char frage[100] = "\nWie heisst du?"; char antwort[10000]; puts(frage); gets(antwort); printf("\nHallo, %s!\n", antwort); getchar(); return 0; }
Ausprobieren. Die Funktion puts (›put string‹) gibt eine Zeichenkette plus ein zusätzliches Neuezeilezeichen aus. Die Funktion gets (›get string‹) liest Zeichen aus dem Tastaturpuffer, nachdem die Eingabetaste gedrückt wurde. Im Gegensatz zu getchar holt gets nicht nur ein, sondern alle Zeichen inklusive dem Neuezeilezeichen aus dem Tastaturpuffer und speichert alle Zeichen bis auf das Neuezeilezeichen (aber plus einer 0 am Ende) in einer Zeichenkette. Kurz gesagt, gets liest eine komplette Eingabezeile minus \n von der Tastatur, puts gibt eine komplette Zeile plus \n im Textfenster aus. gets und puts sind wesentlich weniger flexibel als scanf und printf, weil sie mit nur einem Argument und ohne Formatanweisungen arbeiten. 3.6
Ganze Zeilen lesen und schreiben
53
Sandini Bib
Ein ernstes Problem von gets ist, dass diese Funktion nicht darauf achtet, wie viele Zeichen gelesen werden. In einem richtigen Programm solltest du niemals Zeichenketten abspeichern, ohne sicherzustellen, dass genügend Speicherplatz reserviert wurde. Falls du gets für kleine Experimente verwenden möchtest, solltest du das Feld für die Zeichenkette länger als die längste mögliche Eingabezeile machen. Bei mir sind das ungefähr 256, aber wer weiß, ob und wann sich das ändert. Den Puffer von gets überlaufen zu lassen ist eine beliebte Hackermethode. In Kapitel 10.9 lernen wir die Funktion fgets kennen, die nicht mehr als eine gegebene Anzahl von Zeichen liest.
3.7 Kommentare Bei dieser Gelegenheit besprechen wir, wie man Kommentare in seinen Programmtext einbaut. Das folgende Programm enthält genau dieselben Befehle wie das vorletzte Beispiel, aber auch viele nützliche Bemerkungen: Kommentare.c
/* Wie heisst du? Beispiel fuer Texteingabe mit scanf BB 23.8.2000 */ #include <stdio.h>
// Headerdatei fuer printf, scanf, getchar
/* Funktion main Hier geht es los! */ int main() { char frage[100] = "Wie heisst du?"; char antwort[100];
// Text fuer Frage // Speicherplatz fuer Antwort
printf("\n%s\n", frage); // gebe die Frage aus scanf("%99s", antwort); // lese die Antwort ein printf("\nHallo, %s! \n\n", antwort); // gebe die Antwort aus getchar(); getchar(); return 0;
// das erste Newline kommt von der Namenseingabe // warte auf ein zweites Newline // fertig!
}
Alles, was zwischen /*
...
*/
steht, wird vom Compiler ignoriert, d.h. übersprungen, auch über mehrere Zeilen hinweg. An dieser Stelle kannst du Briefe schreiben oder Blödsinn eintip54
Kapitel 3 Felder und Zeichenketten
Sandini Bib
pen oder auch nützliche Bemerkungen zum Programmtext unterbringen. Diesen Text schaut sich der Compiler nicht an. Auch mit // ...
kannst du Kommentare eingeben. In diesem Fall gilt die Regel, dass alles bis zum Ende der Zeile zum Kommentar wird. Genau genommen gehört // zu C++, aber die meisten C-Compiler kennen diese Variante. Wenn du mal schnell testen willst, was ohne eine bestimmte Zeile Programmtext passiert, musst du sie nicht löschen. Du kannst die Zeile mit // abschalten. Der Editor von BCB zeigt Kommentare in einer besonderen Farbe an. Ich verwende gerne Rot, damit meine Augen die wichtigen Stellen im Programm leichter finden. Kommentare sind sehr, sehr nützlich. Sie helfen anderen, deine Programme zu lesen und zu verstehen. Aber das gilt auch für dich selbst, wenn du nach längerer Zeit eines deiner eigenen Programm wieder lesen willst. Wahrscheinlich kannst du dich dann nicht mehr genau daran erinnern, was in deinem Programm los war. Ein nützlicher Hinweis hier und da kann Wunder der Verständlichkeit wirken. Bei manchen Leuten findest du mehr Kommentar als Code! Man sollte aber versuchen, nur wirklich nützliche Kommentare anzubringen. Kommentare wie getchar(); // rufe getchar auf
führen nur dazu, dass man irgendwann den Wald vor lauter Bäumen nicht mehr sieht. Je komplizierter unsere Programme werden, desto mehr werden wir Kommentare benutzen.
3.7 Kommentare
55
Sandini Bib
3.8
Geschichtenerzähler
Es folgt unser bisher längstes Programm: Geschichte.c
/* Erzaehle mir eine Geschichte BB 23.8.00 */ #include <stdio.h> int main() { // diese Strings benoetigen wir char lebewesen[100], farbe[100], teil[100], verb[100], name[100]; // bitte den Nutzer um einige Eingaben printf("\n"); printf("Sag mir eine Farbe: "); scanf("%99s", farbe); printf("Sag mir ein Lebewesen: scanf("%99s", lebewesen); printf("Sag mir ein Verb: scanf("%99s", verb);
der ");
");
printf("Sag mir einen Koerperteil: der "); scanf("%99s", teil); printf("Sag mir einen Namen: scanf("%99s", name);
der ");
// gib die Geschichte aus printf("\n"); printf("Es war einmal ein %s.\n", lebewesen); printf("Er hatte einen %sen %s.\n", farbe, teil); printf("Auf dem Weg zur Schule sah er %s.\n", name); printf("Da musste der %s vor Freude so %s,\n", lebewesen, verb); printf("dass sein %s wackelte und %s auch ganz %s wurde.\n", teil, name, farbe); printf("\n"); getchar(); getchar(); return 0; }
Kannst du beim Lesen erkennen, was das Programm tun soll? Als Erstes stellen wir mit
56
Kapitel 3 Felder und Zeichenketten
Sandini Bib char lebewesen[100], farbe[100], teil[100], verb[100], name[100];
Speicherplatz für einige Strings bereit. Wir haben dazu ein char angegeben und dann eine Liste mit Kommas, hätten aber genauso gut char char char char char
lebewesen[100]; farbe[100]; teil[100]; verb[100]; name[100];
schreiben können. Diese Zeilen sind dann durch Strichpunkt zu trennen. Danach fragen wir nacheinander nach verschiedenen Worten, die wir in den Stringvariablen speichern. Schließlich rühren wir die Antworten zu einer Geschichte zusammen, die am Bildschirm ausgegeben wird. Also, ich weiß nicht. Ist das nun Prosa oder nicht? Mein erster Versuch ergab das Folgende am Bildschirm: Sag Sag Sag Sag Sag
mir mir mir mir mir
eine Farbe: blau ein Lebewesen: der Hund ein Verb: schwimmen einen Koerperteil: der Bauch einen Namen: der Peter
Es war einmal ein Hund. Er hatte einen blauen Bauch. Auf dem Weg zur Schule sah er Peter. Da musste der Hund vor Freude so schwimmen, dass sein Bauch wackelte und Peter auch ganz blau wurde.
Oder gefällt dir das vielleicht besser? Sag Sag Sag Sag Sag
mir mir mir mir mir
eine Farbe: affig ein Lebewesen: der Affe ein Verb: Computerspielen einen Koerperteil: der Nasenspitze einen Namen: der Apfelmus
Es war einmal ein Affe. Er hatte einen affigen Nasenspitze. Auf dem Weg zur Schule sah er Apfelmus. Da musste der Affe vor Freude so Computerspielen, dass sein Nasenspitze wackelte und Apfelmus auch ganz affig wurde.
Aus Versehen hatte ich hier ›Computer spielen‹ nicht getrennt geschrieben, wie es sich gehört. Wenn du ›Computer spielen‹ korrekt als zwei Wörter eingibst, kommt das Programm wegen scanf wie besprochen durcheinander: 3.8
Geschichtenerzähler
57
Sandini Bib
Sag Sag Sag Sag
mir mir mir mir
eine Farbe: affig ein Lebewesen: der Affe ein Verb: Computer spielen einen Koerperteil: der Sag mir einen Namen:
der Nasenspitze
Es war einmal ein Affe. Er hatte einen affigen spielen. Auf dem Weg zur Schule sah er Nasenspitze. Da musste der Affe vor Freude so Computer, dass sein spielen wackelte und Nasenspitze auch ganz affig wurde.
Als Programmierer(in) musst du immer versuchen, narrensichere Programme zu schreiben. Stell dir vor, ein Affe benutzt dein Programm. Dass dann die Grammatik nicht richtig stimmt, mag ja noch angehen, aber das Durcheinander mit der Worttrennung kann so nicht bleiben. Deshalb solltest du scanf durch gets ersetzen.
3.9 Zeichenketten werden Zeichen für Zeichen in Feldern gespeichert. Mit scanf und printf können wir uns nett mit dem Computer unterhalten, auch wenn man behaupten könnte, dass die künstliche Intelligenz unseres Geschichtenerzählers noch unter Null liegt. Kurz und gut: Felder werden z. B. mit int a[5]; definiert, was Speicherplatz für genau fünf ganze Zahlen reserviert. Die Elemente eines Feldes sind von 0 an durchnummeriert, d.h. int a[5]; ergibt ein Feld mit den fünf Elementen a[0], a[1], a[2], a[3], a[4]. a[i] kann überall dort stehen, wo eine einzelne Integervariable verwendet werden kann, z. B. a[i] = 2;.
Stringvariable sind Felder für eine bestimmte Anzahl von Zeichen, z. B. char meldung[100] = "Hallo\n";. Mit printf("Hallo %s", name); kann man Zeichenketten ausgeben. In diesem Beispiel wird die Formatanweisung %s durch den Text in der Stringvariablen name ersetzt. Mit scanf("%99s", name); kann man Zeichenketten von der Tastatur einlesen. In diesem Beispiel wird ein Wort in der Stringvariable name gespeichert. Der Compiler ignoriert alles zwischen /* und */ und er ignoriert alles zwischen // und dem Zeilenende. Schreibe lieber zu viel als zu wenig Kommentare in deinen Programmtext.
58
Kapitel 3 Felder und Zeichenketten
Sandini Bib
3.10 1. Schreibe ein Programm, das alle Zeichen in "abc 123" einzeln mit %c und mit %d ausgibt. 2. Schreibe ein Programm, das Wörter mit fünf Buchstaben rückwärts ausgibt.
Dazu definierst du eine Zeichenkette mit fünf Buchstaben plus Null, z. B. char name[6] = "string";, und vertauschst die Elemente des Feldes paarweise. 3. Erfinde deine eigene Geschichte. Probiere sie mit Freunden aus. 4. Nachdem ihr eine Weile deinen Geschichtenmacher ausprobiert habt, setze
deinen Freund oder deine Freundin vor ein Programm, das Fragen stellt, auf jede Frage irgendwie antwortet, in Wirklichkeit aber die Antworten völlig ignoriert. Das Ergebnis könnte so aussehen: Hallo, wie heisst du? Grete Schoen dich zu sehen. Wie geht’s? prima Das ist schlimm. Hast du Probleme? nein Ging mir letztes Jahr auch so. Was ist deine Lieblingsfarbe? gruen Na denn, tschuess, Hugo!
3.10
59
Sandini Bib
Sandini Bib
4 Grafik 4.0 4.1 4.2 4.3 4.4 4.5 4.6 4.7
Hallo, Welt im Grafikfenster Ein Hallo wie gemalt Fensterkoordinaten Pixel und Farbe Linien Rechtecke und Kreise Fonts Hilfe zu BCB und Windows SDK
62 64 65 68 69 73 76 78
4.8
Geradlinige Verschiebung
79
4.9
Bunte Ellipsen
81
4.10
TextOut mit Format
83
4.11
85
4.12
87
Sandini Bib
Alles, was auf dem Computerbildschirm erscheint, sind einzelne bunte Punkte, so genannte Pixel. Bilder setzen sich aus vielen Pixeln zusammen, aber auch Linien und Buchstaben setzen sich aus einzelnen Pixeln zusammen. Bisher haben wir nur sehr indirekt mit Pixeln zu tun gehabt. Unsere Programme waren Textbildschirmanwendungen, die von einem speziellen Windowsprogramm, der DOS-Konsole, in einem Textfenster angezeigt werden. In diesem Fall geben wir Text mit der Funktion printf aus. printf ist eine Funktion aus der Standard Ein-/Ausgabe-Bibliothek von C. Aber wie erhalten wir Zugriff auf all diese schönen, bunten Pixel auf deinem Bildschirm? Grafik ist nicht Teil der Programmiersprache C und es gibt auch keine Standardbibliothek für Grafik in C. Aber für Windowsrechner steht uns Microsofts Windows SDK, das Windows Software Development Kit (Entwicklungspaket), zur Verfügung. Windows ist im Wesentlichen ein riesiges C-Programm. Und mit dem Borland C++Builder haben wir Zugang zu allen Funktionen des Windows SDK. Bei der Installation von BCB hast du auch die Hilfe für das ›Win32 SDK‹ installiert. Hier findest du eine ausführliche und klare Beschreibung des Windows SDK, allerdings auf Englisch. Im Win32 SDK finden wir eine große Auswahl an Funktionen für Grafik, aber auch für Sound, und all diesen Windowskleinkram wie Fenster, Knöpfe, Menüs, Scrolleisten und so weiter. Wir wollen uns in diesem Kapitel auf das Einmaleins der Grafikausgabe konzentrieren. Weil Windows so umfangreich ist, lässt es sich nicht vermeiden, dass unsere Programme einige umständliche Details enthalten. Ich werde nicht alle diese Details auf der Stelle vollständig erklären, sondern dich bitten, zunächst wie bei einem Kochrezept gewisse Zutaten einfach so zu verwenden. Aber am Ende des Buches wirst du mir sicher zustimmen, dass der Kuchen trotzdem schmeckt und dass sich der Aufwand auf jeden Fall gelohnt hat. Besonders im Vergleich zu manchen Textbeispielen ist Grafik einfach cool.
4.0
Hallo, Welt im Grafikfenster
Auf der CD des Buches findest du im Verzeichnis ProgLernen\ WindowsMalen alles, was wir für unsere ersten Grafikexperimente brauchen. Das Projekt heißt WinHallo.mak. Öffne dieses Projekt mit BCB und füge mit ›Projekt‹, ›Zum Projekt hinzufügen‹ die Datei WinHallo0.c dem Projekt hinzu. Speichere das Projekt in einem neuen Verzeichnis ab und speichere auch die Datei WinHallo0.c in diesem Verzeichnis ab (siehe BCBMenü ›Datei‹). Du kannst aber auch im Verzeichnis ProgLernen\WindowsMalen arbeiten, wenn du es von der Buch-CD auf die Festplatte kopiert hast. Vielleicht solltest du an dieser Stelle nochmals Kapitel 0.6 und die Datei ProgLernen\ LiesMich.txt lesen.
62
Kapitel 4 Grafik
Sandini Bib
Kompiliere das Programm und lass es laufen ( F9 ). Ein Fenster mit Überschrift ›WinHallo‹ geht auf:
Hallo, Welt! Nach dem Öffnen des Projekts zeigt das Editorfenster die Datei WinHallo.cpp. Im Gegensatz zu unseren Textfensteranwendungen enthält diese Datei nicht nur die Anweisungen, die BCB für C-Programme braucht. Weil ich unseren C-Code nicht mit den recht verwickelten Details der Windowsverwaltung belasten wollte, habe ich die Funktionen für das Fenster in WinHallo.cpp versteckt. Halt, nicht anschauen! Zu spät, schon wieder einen Leser verloren. Spaß beiseite, so schrecklich ist die Datei nun auch wieder nicht. Natürlich darfst du dir WinHallo.cpp anschauen, auch wenn dir dabei vieles unverständlich bleiben wird. Geduld. Im Moment genügt es uns zu wissen, dass die Programmausführung in Windows mit der Funktion WinMain
beginnt. Hier erzeugen wir ein Fenster. Ich habe es so eingerichtet, dass, wann immer das Fenster neu gemalt werden soll, eine Funktion namens malen aufgerufen wird. Die Funktion malen ist keine Standardfunktion von C oder Windows. Ich definiere malen in der Datei WinHallo0.c. Wie wir Funktionen selber machen, besprechen wir in Kapitel 7. Die Funktion malen habe ich mir ausgedacht, um dir die ersten Gehversuche mit Grafikfenstern zu erleichtern. Alles, was ich so weit vorne in diesem Buch noch nicht erklären kann, steht in WinHallo.cpp, aber die einfachen Sachen in WinHallo0.c werden wir jetzt besprechen. Alle unsere Experimente werden wir mit WinHallo.cpp plus einer zusätzlichen C-Datei für die Funktion malen durchführen. 4.0
Hallo, Welt im Grafikfenster
63
Sandini Bib
4.1 Ein Hallo wie gemalt Öffne die Datei WinHallo0.c: WinHallo0.c WinHallo.cpp
#include <windows.h> /* Male im Fenster */ void malen(HDC hdc) { TextOut(hdc, 50, 50, "Hallo, Welt!", 12); }
Bei Programmbeispielen mit Grafik gebe ich nicht nur den Namen der .c-Datei, sondern auch den der .cpp-Datei an. Als Erstes fällt dir sicher auf, dass wir statt der Headerdatei stdio.h die Headerdatei windows.h verwenden. Klarer Fall, schließlich wollen wir ja die Windowsfunktionen verwenden. Es folgt die Definition einer Funktion namens malen: void malen(HDC hdc) { TextOut(hdc, 50, 50, "Hallo, Welt!", 12); }
›void‹ heißt nichts oder leer. Die Funktion malen liefert kein Ergebnis und benötigt auch kein return 0;. Andererseits soll malen mit einer Variablen hdc vom Typ HDC aufgerufen werden. Dieser Datentyp wird in windows.h definiert. Solche Definitionen, die nicht direkt Teil von C sind, erhalten zur besseren Kennzeichnung einen Namen aus lauter Großbuchstaben. Handle heißt Griff und das Verb dazu Das ›h‹ bzw. ›H‹ steht für ›handle‹. ist handhaben. ›Handle‹ werden uns noch oft begegnen. In diesem Fall geht es Device Context (›Geräteumfeld‹). Die Variable hdc um einen DC, einen ist alles, was gewisse Grafikfunktionen für die Ausgabe in einem bestimmten Fenster benötigen. hdc wird uns in die Funktion hereingereicht und wir reichen es an die Funktion TextOut weiter. Wir sind also in der Zeile TextOut(hdc, 50, 50, "Hallo, Welt!", 12);
angekommen. Vergleiche einmal mit printf("Hallo, Welt!");
64
Kapitel 4 Grafik
Sandini Bib
Die Funktion TextOut ( ›Text Raus‹) benötigt neben dem String, der ausgegeben werden soll, noch vier zusätzliche Parameter. Das Schema sieht so aus: TextOut(hdc, x-Koordinate, y-Koordinate, String, Anzahl Zeichen);
Mit hdc legen wir das Ausgabefenster fest. Für Textfensteranwendungen gibt es keine Wahl, alle Ausgaben von printf werden in das Textfenster geschickt. Seltsamerweise benötigt TextOut im Gegensatz zu printf die Anzahl der Zeichen (die Länge des Strings). Die Koordinatenangaben bestimmen, bei genau welchem Pixel auf dem Bildschirm die Textausgabe beginnt. Bei printf wird ein Zeichen neben dem anderen ausgegeben, und unsere Kontrolle beschränkt sich darauf, den Beginn neuer Zeilen mit \n festzulegen. TextOut erkennt \n nicht (aber siehe DrawText in der Hilfe zum Windows SDK, für die du Zeiger kennen musst, Kapitel 10). Alles in allem ist die Textausgabe in einem Grafikfenster also auch nicht viel schwieriger als bei einer Textfensteranwendung. Weil wir mehr Freiheiten haben, müssen wir auch mehr Angaben machen. Wir bestimmen das Fenster, den Ort im Fenster, den String und die Anzahl der auszugebenen Zeichen.
4.2 Fensterkoordinaten Koordinaten? Vielleicht erinnerst du dich nur vage daran, was du in der Schule über Koordinaten gehört hast. Definieren wir also erst einmal, welches Koordinatensystem wir für Fenster verwenden. Auf dem Bildschirm sind Fenster rechteckige Felder, die sich aus einzelnen Pixeln (farbigen Punkten) zusammensetzen. Jedes Pixel hat zwei Koordinaten, sagen wir, x und y. Hier ist ein Bild: x=0 y=0
x=9 y=0 x-Achse
x=0 y=7
y-Achse
x=9 y=7
Die obere linke Fensterecke hat die Koordinaten x = 0 und y = 0. Dies ist der Ursprung des Koordinatensystems. Angenommen, jemand gibt uns zwei ganze Zahlen x und y als Koordinaten. x gibt an, wie viele Pixel wir vom Ursprung aus nach rechts gehen sollen, y gibt an, wie weit wir nach unten gehen sollen. In der Mathematik zeigt die y-Achse üblicherweise nach oben! 4.2
Fensterkoordinaten
65
Sandini Bib
Gleich ausprobieren. Experimentiere mit TextOut(hdc, 0, 0, "Hallo, Welt!", 12); TextOut(hdc, 50, 50, "Hallo, Welt!", 12); TextOut(hdc, 50, 70, "Hallo, Welt!", 12);
und anderen Koordinatenangaben. In Windows muss man darauf achten, welcher Ursprung gerade gemeint ist: Die linke obere Bildschirmecke. Die linke obere Ecke eines Fensters. Die linke obere Ecke im Anzeigebereich (
›Client Area‹) des Fensters.
In unserem TextOut-Beispiel beziehen sich die Koordinaten auf den Anzeigebereich innerhalb des Fensters. Das ist der Normalfall und wird in fast allen unseren Beispielen so sein. In Windows darf nicht ohne weiteres irgendwo auf dem Bildschirm herumgemalt werden, jedes Programm verwendet seine eigenen Fenster. Zudem ist es in den allermeisten Fällen verboten, in die Umrandung eines Fensters, d.h. in den Rahmen oder die Titelleiste, hineinzumalen. Wenn du das Fenster verschiebst, bleibt die Position des Texts in Bezug auf die linke obere Ecke unverändert. Dasselbe gilt, wenn du das Fenster maximierst (zweiter Knopf rechts oben im Fenster). Probiere mal TextOut(hdc, −50, 50, "Hallo, Welt!", 12); TextOut(hdc, 400, 400, "Hallo, Welt!", 12);
Bei Programmstart ist die erste Nachricht abgeschnitten, die zweite ist gar nicht sichtbar. Maximiere das Fenster, und die zweite Nachricht wird sichtbar. Mache die Maximierung des Fensters rückgängig. Ändere die Größe des Fensters, indem du den Rand anklickst und verschiebst. So kannst du die zweite Nachricht teilweise verdecken. Jedoch führen solche Änderungen nicht dazu, dass negative x-Koordinaten sichtbar werden, und die erste Nachricht bleibt abgeschnitten. Die Ausgabe außerhalb des erlaubten Bereichs abzuschneiden, nennt man ›Clipping‹. Clipping ist sehr nützlich, weil es automatisch verhindert, dass irgendwelche Programme, zum Beispiel deine, irgendwo auf dem Bildschirm rumkritzeln und die Anzeige anderer Programme durcheinander bringen. Ansonsten steht es uns frei, unseren Text an beliebiger Stelle im Fester auszugeben! Weißt du, was das heißt?
66
Kapitel 4 Grafik
Sandini Bib
Wir sind den Zwängen der Textfensterausgabe entkommen! Das Programm dazu: WinHallo1.c WinHallo.cpp
#include <windows.h> void malen(HDC hdc) { char s1[50] = "Ich bin frei?"; char s2[50] = "Ich bin frei!"; char s3[50] = "FREI ! ! !"; char s4[50] = "UUUHahahaaa !"; TextOut(hdc, 20, 20, TextOut(hdc, 50, 50, TextOut(hdc, 50, 100, TextOut(hdc, 110, 105, TextOut(hdc, 170, 110, TextOut(hdc, 50, 180, TextOut(hdc, 10, 200, TextOut(hdc, 900, 600, TextOut(hdc,9000,6000,
s1, s2, s3, s3, s3, s4, s4, s3, s3,
strlen(s1)); strlen(s1)); strlen(s3)); strlen(s3)); strlen(s3)); strlen(s4)); strlen(s4)); strlen(s3)); strlen(s3));
}
Wir definieren vier Stringvariable, s1 bis s4. Damit wir nicht für TextOut die Anzahl der Zeichen zählen müssen, verwenden wir die Funktion strlen (›string length‹), die mit einem String als Argument aufgerufen wird und dessen Länge liefert. Falls du strlen in Textanwendungen ohne windows.h verwenden möchtest, benötigst du #include <string.h>.
4.2
Fensterkoordinaten
67
Sandini Bib
4.3 Pixel und Farbe Einzelne Pixel kannst du mit der Funktion SetPixel setzen: Grafik0.c WinHallo.cpp
#include <windows.h> void malen(HDC hdc) { SetPixel(hdc, 100, 100, 0); }
Schreibe den Programmtext in die Datei WinHallo0.c (oder verwende die Datei Grafik0.c) und lass das Programm laufen. Ziemlich mickrig, so ein Pixel. Kannst du es nahe x = 100 und y = 100 erkennen?
So sieht das bei mir aus. Ich zeige mit der Maus auf das Pixel. Der Schnappschuss rechts zeigt eine Vergrößerung des Pixels. In der Vergrößerung ist das Pixel deutlich zu sehen und auch, dass der Mauszeiger wie alles andere aus Pixeln aufgebaut ist. Das Schema von SetPixel ist SetPixel(hdc, x, y, farbe)
Als Farbe kannst du eine ganze Zahl angeben. In dieser Zahl wird auf spezielle Weise ein Farbwert codiert, und dazu verwendet man RGB: RGB(rot, gruen, blau)
Die Werte für rot, gruen und blau sind ganze Zahlen von 0 bis 255, die die Intensität für die jeweilige Farbe angeben. Eine Intensität von 255 bedeutet volle Intensität, 0 bedeutet kein Beitrag in dieser Farbe: 68
Kapitel 4 Grafik
Sandini Bib RGB( 0, 0, 0) RGB(255, 0, 0) RGB( 0, 255, 0) RGB( 0, 0, 255) RGB(255, 255, 255)
schwarz rot grün blau weiß
RGB(0,0,0) ist gleich der Zahl 0, und darum erhalten wir im Beispiel mit SetPixel einen schwarzen Punkt. Experimentiere mit verschiedenen Farben
und Intensitäten, z. B. SetPixel(hdc, SetPixel(hdc, SetPixel(hdc, SetPixel(hdc,
100, 105, 100, 105,
100, 100, 105, 105,
RGB(200, 0, 0)); RGB( 0, 200, 200)); RGB(200, 0, 200)); RGB(200, 200, 0));
Was, du bist immer noch nicht beeindruckt? Du sagst, ein mickriges Pixel in Farbe ist immer noch mickrig? Wer das Pixel nicht ehrt, ist die Grafik nicht wert, wie schon mein Großvater so ähnlich zu sagen pflegte. Aber etwas besser wird es noch in diesem Kapitel.
4.4 Linien Unter den Windowsfunktionen für Grafik gibt es auch die folgenden Funktionen für elementare Liniengrafik. Das Programm Grafik1.c WinHallo.cpp
#include <windows.h> void malen(HDC hdc) { // x MoveToEx(hdc, 50, LineTo(hdc, 200,
y 50, 0); 50);
// x y MoveToEx(hdc, 50, 100, 0); LineTo(hdc, 150, 100); // x y MoveToEx(hdc, 50, 150, 0); LineTo(hdc, 100, 150); }
zeichnet drei waagrechte Linien wie folgt: LineTo(hdc, x, y) bewegt den Stift von der momentanen Position zum Punkt (x, y) und zeichnet dabei eine gerade Linie. MoveToEx(hdc, x, y, 0) bewegt den Stift zum Punkt (x, y), ohne dabei
eine Linie zu zeichnen. 4.4 Linien
69
Sandini Bib
Wie ist das zu verstehen? Die Linien werden mit einem Zeichenstift ( ›Pencil‹) gezeichnet. Dieser Stift besitzt eine Position (x, y) und er kann mit LineTo und MoveToEx geradlinig verschoben werden. Bei LineTo wird der Stift auf dem Blatt (Bildschirm) bewegt und zieht eine gerade Linie, bei MoveToEx wird der Stift durch die Luft bewegt und zieht keine Linie. Das vierte Argument von MoveToEx verwenden wir nicht und setzen es auf 0. Beachte, dass bei waagrechten Linien die y-Koordinate konstant ist (siehe Beispiel). Bei senkrechte Linien ist die x-Koordinate konstant. Kannst du dir denken, was passiert, wenn du jedes MoveToEx in unserem Beispiel durch ein entsprechendes LineTo ersetzt?
Das erste Bild entspricht sicher deiner Erwartung. Wir sehen, wie der Stift links oben anfängt und dann einen sichtbaren Strich hinterlässt, wenn er zum Anfang der waagrechten Linien geht. 70
Kapitel 4 Grafik
Sandini Bib
Was aber ist im zweiten Bild geschehen? Dieses Bild erhältst du, wenn du das Fenster einmal mit der Maus vergrößerst oder verkleinerst. In diesem Fall verlangt Windows, dass der Inhalt des Fensters neu gezeichnet wird, und unsere Funktion malen wird erneut aufgerufen! Und beim zweiten Malen fängt der Stift da wieder an, wo wir beim ersten Malen aufgehört hatten. Bei Linien können wir nicht nur die Farbe wählen, sondern auch die Breite in Pixeln vorgeben. Wenn die Breite 1 ist, haben wir auch noch die Möglichkeit, gestrichelte Linien zu zeichnen. Hier ist ein Beispiel: Grafik2.c WinHallo.cpp
#include <windows.h> void malen(HDC hdc) { COLORREF rosa, gelb, hellblau; HPEN stift1, stift2, stift3; // Farben rosa = RGB(255, 0, 255); gelb = RGB(255, 255, 0); hellblau = RGB( 0, 255, 255); // Stifte stift1 = CreatePen(PS_SOLID, 5, rosa); stift2 = CreatePen(PS_SOLID, 30, gelb); stift3 = CreatePen(PS_DOT, 1, hellblau); // Bewege Stift zum Ausgangspunkt MoveToEx(hdc, 130, 50, 0); // Zeichne drei Linien mit verschiedenen Stiften SelectObject(hdc, stift1); LineTo(hdc, 200, 200); SelectObject(hdc, stift2); LineTo(hdc, 20, 200); SelectObject(hdc, stift3); LineTo(hdc, 130, 50); }
4.4 Linien
71
Sandini Bib
Am Bild erkennst du, in welcher Reihenfolge die Linien gezeichnet werden. Damit sich ein geschlossenes Dreieck bildet, beenden wir das letzte LineTo mit den Koordinaten, bei denen wir durch das MoveToEx angefangen haben. Um das Programm übersichtlicher zu machen, speichern wir den Zahlencode, den RGB liefert, in Variablen vom Typ COLORREF mit den treffenden Namen rosa, gelb, hellblau. Diese Variablen werden wie immer am Anfang des Befehlsblocks definiert. Wir verwenden diese Variablen statt einer RGB-Anweisung, wann immer wir einen bestimmten Farbenwert benötigen. Die Funktion CreatePen (erzeuge Stift) wird als CreatePen(strichart, strichbreite, farbe)
aufgerufen. Die Strichart ( ›pencil style‹) wird mit Namen angegeben, die in windows.h definiert sind, z. B. PS_SOLID, PS_DASH, PS_DOT, PS_DASHDOT, PS_DASHDOTDOT.
›solid‹ heißt solide (durchgezogen), ›dash‹ heißt Strich, ›dot‹ heißt Punkt. Wenn die Strichbreite größer als 1 ist, zeichnet der Stift unabhängig von der Strichart eine durchgezogene Linie. Das Ergebnis von CreatePen ist ein Handle zu einem Stift mit Datentyp HPEN. Wir erzeugen drei verschiedene Stifte und speichern die Handles in stift1, stift2, stift3. Diese Stifte drücken wir dem Zeichner in die Hand, indem wir den jeweiligen Stift mit der Funktion SelectObject(hdc, handle)
für den Device Context hdc auswählen.
72
Kapitel 4 Grafik
Sandini Bib
4.5 Rechtecke und Kreise Punkt, Punkt, Komma, Strich, fertig ist das Mondgesicht: Grafik3.c WinHallo.cpp
#include <windows.h> void malen(HDC hdc) { Rectangle(hdc, 10, 10, 100, 100); TextOut(hdc, 15, 45, "Hallo, Welt!", 12); Ellipse(hdc, 100, 100, 200, 200); Arc(hdc, 120, 120, 180, 180, 100, 200, 200, 200); SetPixel(hdc, 130, 140, 0); SetPixel(hdc, 170, 140, 0); MoveToEx(hdc, 150, 100, 0); LineTo(hdc, 145, 90); LineTo(hdc, 155, 80); LineTo(hdc, 150, 75); }
Die folgenden Funktionen ermöglichen das Zeichnen von Rechtecken, Kreisen und Kreisteilen: Rectangle(hdc, links, oben, rechts, unten)
Zeichne Rechteck mit waagrechten und senkrechten Kanten. Ellipse(hdc, links, oben, rechts, unten)
Zeichne eine Ellipse, die in das angegebene Rechteck passt. Einen Kreis erhält man durch Angabe eines Quadrats. 4.5 Rechtecke und Kreise
73
Sandini Bib
Arc(hdc, links, oben, rechts, unten, x1, y1, x2, y2)
Zeichne den Teil einer Ellipse. Die zwei Punkte (x1, y1) und (x2, y2) definieren zwei Linien vom Zentrum der Ellipse aus, die den Ellipsenbogen begrenzen. In diesen Funktionen definieren wir mit vier Zahlen links, oben, rechts und unten ein Rechteck. Moment, im Allgemeinen hat ein Viereck vier Ecken (warum?). Für jede Ecke brauchen wir 2 Zahlen, also insgesamt 8. Ist dir schon aufgefallen, dass in Windows alle Fenster waagrechte Rechtecke sind, die nicht gekippt oder gedreht sind? Die Kanten dieser Rechtecke sind parallel zu den x und y-Achsen. Dafür brauchen wir nur 4 Zahlen: left
right
x-Achse top
bottom
y-Achse
Die x-Koordinate der linken Kante nennen wir links (left) und die x-Koordinate der rechten Kante plus 1 rechts (right). Die y-Koordinate der oberen Kante nennen wir oben (top) und die y-Koordinate der unteren Kante plus 1 unten (bottom). Weil wir 1 dazuzählen, ist z. B. die Breite des Rechtecks genau right−left Pixel. Solche Rechtecke können ohne Überlappung nebeneinander gelegt werden, wenn left des zweiten Rechtecks gleich right des ersten Rechtecks ist usw. Gleich ausprobieren. Lege zwei Rechtecke so nebeneinander, dass z. B. die xKoordinaten des ersten von 0 bis 100 und des zweiten von 100 bis 200 reichen. Oder von 99 bis 200, oder von 101 bis 200. Hier ist noch ein Experiment. Vertausche einmal die Zeilen mit TextOut und Rectangle. Wenn du erst den Text und dann das Rechteck zeichnest, bleibt der Text unsichtbar! Das liegt daran, dass bei Rechtecken und übrigens auch bei Ellipsen das Innere weiß ausgemalt wird, wenn wir das nicht ändern. In dem folgenden Beispiel kannst du die verschiedenen Funktionen in Farbe ausprobieren: 74
Kapitel 4 Grafik
Sandini Bib Grafik4.c WinHallo.cpp
#include <windows.h> void malen(HDC hdc) { HPEN hpblau; HBRUSH hbrot, hbgruen; // Stift hpblau = CreatePen(PS_SOLID, 1, RGB(0,0,255)); SelectObject(hdc, hpblau); // Pinsel hbrot = CreateSolidBrush(RGB(255,0,0)); hbgruen = CreateSolidBrush(RGB(0,255,0)); // Rechteck und Text SelectObject(hdc, hbrot); Rectangle(hdc, 10, 10, 100, 100); TextOut(hdc, 50, 50, "Hallo, Welt!", 12); // Mondgesicht SelectObject(hdc, hbgruen); Ellipse(hdc, 100, 100, 200, 200); Arc(hdc, 120, 120, 180, 180, 100, 200, 200, 200); SetPixel(hdc, 130, 140, 0); SetPixel(hdc, 170, 140, 0); }
Wie du siehst, werden Rechtecke und Ellipsen genau wie Linien mit dem momentanen Stift gezeichnet. Rechtecke und Ellipsen können farbig ausgefüllt wer-
4.5 Rechtecke und Kreise
75
Sandini Bib
den. Dazu wird der momentan gewählte Pinsel ( ›brush‹) verwendet. Einen deckend malenden Pinsel, Datentyp HBRUSH, bekommst du mit CreateSolidBrush(farbe);
und genau wie den Stift musst du diesen Pinsel mit z. B. SelectObject(hdc, hbrot);
im Device Context auswählen. Die Voreinstellung für die Stiftfarbe ist schwarz, die Voreinstellung für den Pinsel weiß. Vertausche auch in diesem Beispiel TextOut und Rectangle. Der Text wird verdeckt. Aber mit der Anweisung SelectObject(hdc, GetStockObject(NULL_BRUSH));
kannst du den Pinsel auf ›durchsichtig‹ umschalten. GetStockObject liefert vor›stock objects‹) von Windows, in diesem Fall definierte Standardobjekte ( einen Handle zu einem Pinsel, der das Ausmalen von Rechtecken und Kreisen verhindert. Ausprobieren, der Text wird wieder sichtbar.
4.6 Fonts Auffallend ist, dass im letzten Beispiel der Schriftzug Hallo, Welt! einen weißen Hintergrund behält. Text wird in einem Rechteck mit eigener Farbe ausgegeben. Auch wird die Farbe von Text nicht vom momentanen Stift bestimmt. Nützliche Funktionen für TextOut sind: SetTextColor(hdc, farbe) setzt die Farbe von Text. SetBkColor(hdc, farbe) setzt die Farbe des Texthintergrunds. SetBkMode(hdc, TRANSPARENT) macht den Texthintergrund durchsichtig, während mit SetBkMode(hdc, OPAQUE) der Texthintergrund sichtbar wird.
Das probieren wir im nächsten Beispiel gleich aus. Hier verändern wir auch die Textart, den so genannten Font:
76
Kapitel 4 Grafik
Sandini Bib Grafik5.c WinHallo.cpp
#include <windows.h> void malen(HDC hdc) { HFONT hfont; hfont = CreateFont( 80, // Hoehe 0, // Breite 300, // Winkel in 1/10 Grad 0, // Orientierung in 1/10 Grad FW_BOLD, // Gewicht: 0 bis 900, oder FW_NORMAL, FW_MEDIUM, FW_BOLD, ... 0, // Kursiv (0,1) 1, // Unterstreichung (0,1) 0, // Durchgestrichen (0,1) 0, // Character Set 0, 0, 0, // Praezision 0, // Familie "Times" // Fontname, z.B. "Times", "Courier", "Symbol" oder 0 ); SelectObject(hdc, hfont); SetTextColor(hdc, RGB(255,0,0)); SetBkColor(hdc, RGB(200,200,200)); TextOut(hdc, 20, 120, " Hallo ", 7); }
Fonts sind dir sicher schon begegnet. Viele Programme, BCB und auch Windows selbst, bieten dir an, die verwendete Schriftart zu ändern. Das kannst du auch mit der Funktion CreateFont erreichen, die als Ergebnis einen Handle vom Typ HFONT liefert. Auch in diesem Fall verwenden wir SelectObject, um die neue Schriftart zu aktivieren. Die Funktion CreateFont wird mit 14 Argumenten aufgerufen. Eine 0 bedeutet für die meisten Argumente, dass die Voreinstellung verwendet werden soll. Nur zu, mach das Hallo BILDSCHIRMFÜLLEND riesig, indem du die Fonthöhe auf 700 oder so setzt. Für die Fontbreite verwendest du 4.6
Fonts
77
Sandini Bib
am besten die Voreinstellung durch Angabe der 0, es sei denn, du willst den Text stauchen. Den Effekt des Winkels zeigt das Beispiel. Mach den Text fett oder dünn, indem du das Fontgewicht änderst. Eine weitere wichtige Unterscheidung ist, ob der Text pro Buchstabe denselben Platz verwendet oder jeder Buchstabe nur so viel Platz verwendet wie nötig. Letzteres ist in Buchtexten normal, weil sich das besser lesen lässt. In Programmtexten verwenden wir ›fixed width fonts‹, damit sich saubere Spalten schreiben lassen. Schau dir den Text in diesem Buch einmal genau an. In den Programmbeispielen stehen die Buchstaben tatsächlich in Spalten übereinander, aber im normalen Text wie in diesem Abschnitt sind die Buchstaben unterschiedlich breit und stehen nicht in Spalten. Auch bei Textfensteranwendungen wird ein Font mit konstanter Breite verwendet. So weiß man im Voraus, dass in jede Zeile genau 80 Zeichen passen oder was immer man eingestellt hat. Bei Textausgabe im Grafikfenster hat man die Wahl.
4.7 Hilfe zu BCB und Windows SDK Mehr Informationen zu den Funktionen des Windows SDK findest du unter ›Start‹, ›Programme‹, ›Borland C++Builder‹, ›MS Help‹, ›Win32 SDK‹. Im BCBEditor kannst du auch jederzeit die Hilfe zu dem Wort am Textcursor aufrufen, indem du F1 drückst. Nach der Installation funktioniert das jedoch nur für die Borland Hilfedateien. Die Microsoft-Hilfe kannst du wie folgt aktivieren: Du findest unter dem Eintrag ›Borland C++Builder‹ das Programm OpenHelp. Starte OpenHelp. Unter ›Suchbereiche‹ wähle ›All Help Files‹, klicke auf ›Auswählen‹, dann auf ›OK‹. Das Aufrufen der Microsoft-Hilfe mit F1 hat bei mir trotzdem nicht immer funktioniert. Bei Bedarf kannst du mit ›Start‹, ›Programme‹ usw. ein extra Fenster für die Hilfe zum Win32 SDK aufmachen und unter ›Index‹ den Namen der Funktion eintippen. Teste die Hilfe gleich für main, SetPixel, SetBkMode, TextOut und Rectangle. Mit etwas geduldigem Lesen kann diese Hilfe sehr nützlich sein. Z.B. bietet dir das Hilfefenster rechts oben unter ›Group‹ eine Liste aller verwandten Funktionen an. So findest du heraus, dass es auch eine Funktion GetPixel gibt. Stöbere ruhig in dieser Hilfe. Je weiter wir in dem Buch fortschreiten, desto mehr kannst du davon verstehen. Hilfe zur Selbsthilfe ist immer gut. Alle Funktionen, die wir in diesem Buch verwenden, werde ich aber auch an Ort und Stelle erklären.
78
Kapitel 4 Grafik
Sandini Bib
4.8
Geradlinige Verschiebung
Das folgende Windowsbeispiel (Projekt WinHallo.mak laden!) zeigt einen Kreis an verschiedenen Orten: Grafik6.c WinHallo.cpp
#include <windows.h> void malen(HDC hdc) { int x, dx, y, dy, r; x y dx dy r
= 100; = 100; = 20; = 0; = 2;
Ellipse(hdc, x−r, y−r, x+r, y+r); x += dx; y += dy; Ellipse(hdc, x−r, y−r, x+r, y+r); x += dx; y += dy; Ellipse(hdc, x−r, y−r, x+r, y+r); x += dx; y += dy; Ellipse(hdc, x−r, y−r, x+r, y+r); x += dx; y += dy; Ellipse(hdc, x−r, y−r, x+r, y+r); }
4.8
Geradlinige Verschiebung
79
Sandini Bib
In int x, dx, y, dy, r;
definieren wir fünf Variable für ganze Zahlen, die wir dann in x y dx dy r
= 100; = 100; = 20; = 0; = 2;
initialisieren. Mit x und y bestimmen wir den Mittelpunkt eines Kreises, der den Radius r haben soll. Einen solchen Kreis malen wir mit Ellipse(hdc, x−r, y−r, x+r, y+r);
Wie in Kapitel 4.5 besprochen, werden Kreise und Ellipsen durch die Ausmaße eines Rechtecks bestimmt. Wenn (x, y) der Mittelpunkt eines Kreises sein soll, dann ist die linke Kante des Rechtecks bei x−r, die rechte Kante bei x+r, die obere Kante bei y−r und die untere Kante bei y+r. Schon bei der Ausgabe nur eines Kreises hat sich die Verwendung von Variablen gelohnt. Ohne die Variable r müssten wir vier Zahlen im Ellipse-Befehl ändern, wenn wir den Radius des Kreises ändern wollen. Das Gleiche gilt, wenn wir die Position des Kreises ändern möchten. So müssen wir nur die Initialisierung der Variablen ändern und das Programm berechnet anhand der Variablen alle benötigten Koordinaten. Nach der Initialisierung und der Ausgabe des ersten Kreises wiederholen wir viermal die folgenden Befehle: x += dx; y += dy; Ellipse(hdc, x−r, y−r, x+r, y+r);
Schau dir das Programm genau an, die Befehle sind identisch. Indem wir die xKoordinate des Mittelpunkts vor jeder Ausgabe um immer dieselbe Differenz dx erhöhen, verschieben wir den Kreis um konstante Schrittweiten in einer geraden Linien. Gleichzeitig können wir eine Verschiebung in der y-Richtung um die Differenz dy durchführen. Die drei Schnappschüsse habe ich mit den folgenden Parametern erzeugt: 1. r =
2; dx = 20; dy =
2. r = 10; dx =
0;
0; dy = −20;
3. r = 30; dx = 20; dy =
21;
// horizontal nach rechts // vertikal nach oben // schraeg nach rechts unten
Dieses Beispiel schreit geradezu nach einer Verbesserung. Es muss doch möglich sein, identische Programmteile wie die Verschiebebefehle plus Ausgabe beliebig 80
Kapitel 4 Grafik
Sandini Bib
oft zu wiederholen. Zwar können wir die Befehle einfach kopieren, aber dann ist die Anzahl der Wiederholungen von vornherein festgelegt. Wir würden z. B. gerne n Wiederholungen anfordern, wobei n gleich 5 oder gleich 1000 sein kann. Solche Wiederholungen macht man mit so genannten Schleifen, siehe Kapitel 6.
4.9
Bunte Ellipsen
Hier ist ein nettes Beispiel, in dem Ellipsen verschiedener Größe durch die Wiederholung desselben Befehls, aber mit unterschiedlichen Werten der Variablen ausgegeben werden: Grafik7.c WinHallo.cpp
#include <windows.h> void malen(HDC hdc) { int x, y, rx, ry, drx, dry; COLORREF gelb, rot, schwarz; HBRUSH hbgelb, hbrot, hbschwarz, hbalt; // Farben gelb = RGB(255, 255, rot = RGB(255, 0, schwarz = RGB( 0, 0,
0); 0); 0);
// Pinsel hbschwarz = CreateSolidBrush(schwarz); hbgelb = CreateSolidBrush(gelb); hbrot = CreateSolidBrush(rot); // Waehle neuen Pinsel, aber merke dir den alten hbalt = SelectObject(hdc, hbschwarz); // Hintergrund Rectangle(hdc, 0, 0, 1600, 1200); // Malen x = 120; y = 110; rx = 100; ry = 80; drx = −20; dry = 0;
4.9
Bunte Ellipsen
81
Sandini Bib Grafik7.c WinHallo.cpp
SelectObject(hdc, hbrot); Ellipse(hdc, x−rx, y−ry, x+rx, rx += drx; ry += dry; SelectObject(hdc, hbgelb); Ellipse(hdc, x−rx, y−ry, x+rx, rx += drx; ry += dry; SelectObject(hdc, hbrot); Ellipse(hdc, x−rx, y−ry, x+rx, rx += drx; ry += dry; SelectObject(hdc, hbgelb); Ellipse(hdc, x−rx, y−ry, x+rx, rx += drx; ry += dry; SelectObject(hdc, hbrot); Ellipse(hdc, x−rx, y−ry, x+rx, rx += drx; ry += dry; SelectObject(hdc, hbgelb); Ellipse(hdc, x−rx, y−ry, x+rx,
y+ry);
y+ry);
y+ry);
y+ry);
y+ry);
y+ry);
// Aufraeumen SelectObject(hdc, hbalt); DeleteObject(hbschwarz); DeleteObject(hbgelb); DeleteObject(hbrot); }
In hbalt = SelectObject(hdc, hbschwarz);
82
Kapitel 4 Grafik
Sandini Bib
zeigen wir, dass SelectObject als Ergebnis einen Handle liefert. Dieser Handle gibt an, welches Objekt ›aus der Hand gelegt‹ wurde, als das neue Objekt ausgewählt wurde. In hbalt merken wir uns den vorhergehenden Pinsel, damit wir ihn vor dem Verlassen von malen mit SelectObject(hdc, hbalt);
wieder aktivieren können. Eine weitere gute Angewohnheit ist, mit DeleteObject(hbschwarz);
und so weiter die Handle wieder freizugegeben und zu löschen ( ›delete‹), die wir erzeugt hatten. Jedem ›create‹ sollte im Programm ein ›delete‹ folgen, sonst muss Windows irgendwann zu viele Pinsel und so weiter verwalten. Zur Abwechslung malen wir diesmal auf einen schwarzen Hintergrund, den wir mit Rectangle(hdc, 0, 0, 1600, 1200);
erzeugen. Die Größe des Rechtecks derart zu fixieren ist ungeschickt. In Kapitel 8 besprechen wir eine Möglichkeit, die Größe des Fensters zu erhalten. Beim Malen verändern wir diesmal nicht die Position, sondern den Radius in der x- und y-Richtung der Ellipsen. Außerdem ändern wir zwischen den Malbefehlen die Farbe. Die beiden Bilder erhältst du mit 1. rx = 100; ry = 2. rx =
80; drx = −20; dry =
20; ry = 100; drx =
0;
20; dry = −20;
4.10
TextOut mit Format
Bei der Funktion printf können wir in den Text, der ausgegeben werden soll, auch den Wert von Variablen einsetzen: mit %d ganze Zahlen, mit %f Fließkommazahlen und mit %s Strings. TextOut ist nicht so vielseitig, aber betrachte einmal das folgende Beispiel:
4.10
TextOut mit Format
83
Sandini Bib Grafik8.c WinHallo.cpp
#include <windows.h> #include <stdio.h> void malen(HDC hdc) { HFONT hfalt, hfneu; char text[100]; int x, y, dy, yfont; yfont = 20; dy = yfont + yfont/5; x = 20; y = 50; hfneu = CreateFont(yfont, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, "Courier"); hfalt = SelectObject(hdc, hfneu); SetTextColor(hdc, RGB(255,0,0)); SetBkColor(hdc, RGB(200,200,200)); sprintf(text, "Fontgroesse: %d", yfont); TextOut(hdc, x, y, text, strlen(text)); y += dy; sprintf(text, "Neue Zeile: +%d", dy); TextOut(hdc, x, y, text, strlen(text)); SelectObject(hdc, hfalt); DeleteObject(hfneu); }
Das erste Bild zeigt Ausgabe mit dem Font "Courier" und das zweite Bild den voreingestellten Font, den du mit 0 statt "Courier" erhältst. "Courier" benutzt gleich viele Pixel pro Zeichen. Wie in Beispiel 4.9 verwenden wir Variablen, hier 84
Kapitel 4 Grafik
Sandini Bib
hfontalt und hfontneu vom Typ HFONT, um die Handles zu speichern. Vor dem Verlassen der Funktion malen stellen wir mit SelectObject(hdc, hfontalt)
den ursprünglichen Font wieder her. Dieses Beispiel führt dir die Funktion sprintf vor. sprintf funktioniert genau wie printf, außer dass der Text nicht am Textbildschirm ausgegeben, sondern in einen String geschrieben wird. Das Schema ist printf(Formatstring, Argumente . . .) sprintf(Stringvariable, Formatstring, Argumente . . .)
Beide Funktion kommen aus der stdio, deshalb #include <stdio.h>. Dort findest du auch die Funktion sscanf, die nicht wie scanf von der Tastatur, sondern aus einem String liest. Ohne Textfenster ist printf nutzlos. Und weil TextOut keine Formatstrings kennt, können wir mit TextOut nicht ohne weiteres Variablen ausgeben. Aber wie das Beispiel zeigt, können wir den Ausgabetext mit sprintf im String text zusammenbasteln und dann mit TextOut ausgeben. Weil TextOut keinen Zeilenvorschub durchführt, machen wir das mit den Zeilen dy = yfont + yfont/5; y += dy;
selbst. Für die neue Zeile erhöhen wir y um dy. Bei dy = yfont; sitzen die Zeilen genau untereinander, bei dy = yfont − 1; überlappen sie sich. Probier das mal aus. Wir könnten dy = yfont + 2; oder Ähnliches verwenden, aber wenn sich dann die Fontgröße ändert, bleibt der Zwischenraum konstant 2. In unserem Beispiel berechnen wir den Zwischenraum mit yfont/5, so dass der Zwischenraum für beliebige Fontgrößen 20% beträgt.
4.11 In diesem Kapitel haben wir wenig über C, aber dafür umso mehr über die einfachsten Grafikfunktionen des Windows SDK gelernt. Punkte, Linien, Kreise, verschiedene Fonts und verschiedene Farben: alles kein Problem. Jedes einzelne Pixel können wir per Koordinatenangabe auswählen und dadurch die verschiedenen Grafikelemente an einem beliebigen Ort im Fenster positionieren. Davon werden wir in den nachfolgenden Kapiteln ausgiebig Gebrauch machen. Genau genommen will sich niemand die Mühe machen, für ein Rechteck jede Koordinate durch Ausprobieren hinzufummeln. Wenn die Aufgabe lediglich darin 4.11
85
Sandini Bib
besteht, ein einzelnes Rechteck zu zeichnen, ist man mit einem fertigen Malprogramm besser bedient. Der Witz ist natürlich, dass mit jedem Sprachelement von C, das wir dazulernen, auch unsere Möglichkeiten anwachsen, kompliziertere Grafiken zu berechnen und zu automatisieren – das heißt Grafik zu programmieren! In Kürze: Wir verwenden das Projekt WinHallo.mak, welches in WinHallo.cpp für uns die Fenstererzeugung in Windows erledigt. Wir experimentieren mit der Funktion malen in WinHallo0.c und anderen Beispielen, weil uns noch einige Sprachelemente von C fehlen, um Fenster selber zu verwalten. Die folgenden Windowsfunktionen fürs Zeichnen und für Text haben wir kennen gelernt: SetPixel zeichnet ein Pixel. LineTo zeichnet eine Linie. MoveToEx bewegt den Zeichenstift, ohne eine Linie zu zeichnen. Rectangle zeichnet ein Rechteck. Ellipse zeichnet Ellipsen und Kreise. Arc zeichnet den Teil einer Ellipse. TextOut schreibt Text mit verschiedenen Fonts.
Zum Zeichnen und für die Textausgabe mit TextOut benötigen wir den Handle für einen Device Context, Typ HDC. Dieses ›Geräteumfeld‹ verändern wir mit SelectObject(hdc, objekt)
Neue Objekte für den Device Context erzeugen wir mit: CreatePen für Zeichenstifte für Linien CreateSolidBrush für Pinsel zum Ausmalen CreateFont für Fonts
Farbangaben machen wir mit RGB(rot, gruen, blau),
wobei jede der drei Zahlen die Intensität für die jeweilige Farbe angibt. Die Ausgabe von Text beeinflussen wir mit SetTextColor, SetBkColor und SetBkMode.
86
Kapitel 4 Grafik
Sandini Bib
4.12 1. Komponiere deine eigene Grafik. 2. Spiele in den Beispielen mit allen Koordinaten, um ein Gefühl für Koordina-
ten zu bekommen. 3. Male vier ineinander gesetzte Rechtecke, deren rote Farbe nach innen immer
heller wird. Das größte Rechteck soll den ganzen Bildschirm ausfüllen. Wie erhältst du mehrere konzentrische Kreise (Kreise mit demselben Mittelpunkt, aber unterschiedlichen Radien)? 4. Suche nach Windowsanwendungen, die es erlauben, den Font einzustellen (z. B. BCB). Versuche, mit CreateFont diesen Font nachzubilden. 5. In Beispiel 4.8 wird eine Figur in gleich großen Schritten versetzt. Das heißt dx und dy bleiben konstant. Ändere das Programm derart, dass bei jedem Schritt dx und dy größer (oder kleiner) werden. 6. Der Punkt in der Mitte einer Linie von (x1, y1) nach (x2, y2) ist gegeben
durch xmitte =
x1+x2 2
,
ymitte =
y1+y2 2
.
Übersetze diese Formeln nach C. Schreibe ein Testprogramm, das mehrere Linien zeichnet und jeden Mittelpunkt durch einen kleinen Kreis mit Radius 2 Pixel oder so markiert. Weil du für Pixelkoordinaten mit ganzen Zahlen rechnest, kann es sein, dass die Markierung die Linie nicht genau trifft, denn halbe Pixel gibt es nicht. Du könntest dann auch die Mittelpunkte durch Linien verbinden. Was erhältst du, wenn du mit einem Quadrat beginnst, dann die Mittelpunkte verbindest und anschließend die Mittelpunkte der neuen Linien verbindest?
4.12
87
Sandini Bib
Sandini Bib
5 Bedingungen 5.0 5.1 5.2 5.3 5.4 5.5
Bedingungen mit if Vergleichsoperatoren, Wahr und Falsch Logische Verknüpfungen Bedingungen mit mehreren ifs Bedingungen mit if und else Bedingungen mit else if
90 92 95 97 98 101
5.6
Entscheidungen für den Bauch
102
5.7
Zufallszahlen
103
5.8
Ein Held des Zufalls
108
5.9
Bisektion
111
5.10
114
5.11
115
Sandini Bib
In allen Beispielen, die wir bisher betrachtet haben, wurden die Anweisungen Zeile für Zeile schön der Reihe nach ausgeführt. In diesem Kapitel besprechen if und else (›wenn‹ und ›sonst‹), mit denen du wir die C-Anweisungen die Ausführung von Befehlen an Bedingungen knüpfen kannst. Das heißt, wir wollen besprechen, wie man Befehle überspringt oder ausführt, je nachdem ob eine Bedingung zutrifft oder nicht. Mit dem nächsten Beispiel wird klar werden, was ich damit meine.
5.0
Bedingungen mit if
Wenn du weißt, dass raten, was hier los ist:
›if‹ auf Deutsch ›wenn‹ oder ›falls‹ heißt, kannst du If0.c
#include <stdio.h> int main() { int i = 10; if (i > 0) printf("%d ist groesser als 0.\n", i); getchar(); return 0; }
Dies ist ein Beispiel ohne Grafik und wird als Textfensteranwendung eingegeben. Die Zeilen if (i > 0) printf("%d ist groesser als 0.\n", i);
bedeuten: wenn i größer als 0 ist, teile das der Welt mit.
Wir erhalten 10 ist groesser als 0.
Wenn du i = −10 statt i = 10 einsetzt, wird der printf-Befehl übersprungen und der Textbildschirm bleibt leer:
Im Allgemeinen verwendet man if so: 90
Kapitel 5 Bedingungen
Sandini Bib if (Ausdruck)
Befehl
Wenn Ausdruck wahr ist, wird Befehl ausgeführt. Und wenn Ausdruck falsch ist, wird Befehl übersprungen. Lass das Beispiel Schritt für Schritt im Debugger laufen! Tatsächlich wird je nach Wert von i der printf-Befehl ausgeführt oder übersprungen. In anderen Worten: Mit if können wir die Ausführung von Befehlen von der Erfüllung einer Bedingung abhängig machen. Manche Programmiersprachen verwenden ›if‹ und ›then‹, um ausdrücklich wenn/dann zu sagen. In C wird kein ›then‹ geschrieben. Praktischerweise kannst du mehrere Befehle mit geschweiften Klammern zu einem Befehl zusammenfassen. Ein solcher Befehlsblock darf in die ifKonstruktion eingesetzt werden: If1.c
#include <stdio.h> int main() { int i; printf("Sag mir eine positive ganze Zahl: scanf("%d", &i);
");
if (i > 0) { printf("%d ist groesser als 0.\n", i); printf("Vielen Dank.\n"); } getchar(); getchar(); return 0; }
Bitte merken: hinter einem Block kein ; schreiben. Je nachdem, ob die eingegebene Zahl positiv ist (i > 0) oder nicht, werden entweder alle Befehle zwischen den Klammern ausgeführt oder der ganze Befehlsblock wird übersprungen. Zum Beispiel Sag mir eine positive ganze Zahl: 3 ist groesser als 0. Vielen Dank.
3
Sag mir eine positive ganze Zahl:
0
Ist dir aufgefallen, dass ich alle Befehle, die vom if abhängen, nach rechts verschoben eingetippt habe? Das ist eine gute Angewohnheit, denn so kann man 5.0
Bedingungen mit if
91
Sandini Bib
auf einen Blick feststellen, wo die Ausführung des Programms nach dem if weitergeht. Was wird geschehen, wenn du in if (i > 0) { printf("%d ist groesser als 0.\n", i); printf("Vielen Dank.\n"); }
die geschweiften Klammern weglässt, also if (i > 0) printf("%d ist groesser als 0.\n", i); printf("Vielen Dank.\n");
/* falsch eingerueckt! */
schreibst? Dass wir den Befehl printf("Vielen Dank.\n"); so schön nach rechts gerückt haben, ist dem Compiler völlig egal: Sag mir eine positive ganze Zahl: Vielen Dank.
0
Denn if bezieht sich auf genau einen Befehl oder Befehlsblock, daher wird printf("Vielen Dank.\n"); ausgeführt, egal welche Zahl eingegeben wurde. Einrücken mit Leerzeichen ist dem Compiler egal. Es ist auf jeden Fall eine gute Idee, Befehle, die im selben Block stehen, gleich weit einzurücken. Man muss es nur richtig machen. Das Beispiel schreiben wir nach unserer Einrückregel korrekt als if (i > 0) printf("%d ist groesser als 0.\n", i); printf("Vielen Dank.\n");
Du kannst zur Verdeutlichung auch if (i > 0) { printf("%d ist groesser als 0.\n", i); } printf("Vielen Dank.\n");
verwenden. Eine kompaktere Schreibweise ist if (i > 0) printf("%d ist groesser als 0.\n", i); printf("Vielen Dank.\n");
if bezieht sich immer auf genau einen nachfolgenden Befehl oder Befehlsblock.
5.1 Vergleichsoperatoren, Wahr und Falsch In unseren ersten Beispielen für if haben wir das Größerzeichen > verwendet, um die Aussage i > 0 zu formulieren. Zahlen können mit den folgenden Operatoren verglichen werden: 92
Kapitel 5 Bedingungen
Sandini Bib ==
gleich
!=
ungleich
>
größer
<=
kleiner oder gleich
>=
größer oder gleich
<
kleiner
Diese Vergleichsoperatoren verändern den Wert von Variablen nicht. Insbesondere musst du = und == unterscheiden: i == j i = j
›vergleiche‹: teste, ob i gleich j ist ›setze gleich‹: kopiere Wert von j nach i
Für zwei Variablen i und j ist eine Aussage wie ›i ist gleich j ‹ entweder wahr oder falsch. Mit i == j können wir das testen. Manche Programmiersprachen definieren einen neuen Datentyp, der nur die zwei Werte wahr und falsch annehmen kann. Man spricht von Booleschen Variablen, und wenn man mit solchen Variablen rechnet, von der Booleschen Algebra C ist sparsam: Die Zahl 0 hat (nach dem Mathematiker George Boole). auch die Bedeutung falsch und jede Zahl ungleich 0 hat die Bedeutung wahr. Also bedeuten 1, −1 oder 99 wahr. Tatsächlich funktioniert i == j in C wie eine Rechenoperation, die zwei Zahlen verarbeitet und als Ergebnis eine neue Zahl liefert! Falls wahr, ergibt eine Vergleichsoperation den Wert 1, falls falsch den Wert 0. Hier ist dazu ein Testprogramm für ganze Zahlen, die Vergleichsoperatoren sind aber auch für Kommazahlen verwendbar: If2.c
#include <stdio.h> int main() { int i, j; printf("\nSag mir eine Zahl: scanf("%d", &i);
");
printf("Sag mir noch eine Zahl: scanf("%d", &j); printf("%d printf("%d printf("%d printf("%d printf("%d printf("%d
== != > <= < >=
%d %d %d %d %d %d
ist ist ist ist ist ist
%d\n", %d\n", %d\n", %d\n", %d\n", %d\n",
");
i, i, i, i, i, i,
j, j, j, j, j, j,
i i i i i i
== != > <= < >=
j); j); j); j); j); j);
getchar(); getchar(); return 0; }
5.1 Vergleichsoperatoren, Wahr und Falsch
93
Sandini Bib
Sag mir eine Zahl: Sag mir noch eine Zahl: 5 == 7 ist 0 5 != 7 ist 1 5 > 7 ist 0 5 <= 7 ist 1 5 < 7 ist 1 5 >= 7 ist 0
5 7
Sag mir eine Zahl: Sag mir noch eine Zahl: 10 == 10 ist 1 10 != 10 ist 0 10 > 10 ist 0 10 <= 10 ist 1 10 < 10 ist 0 10 >= 10 ist 1
10 10
Weil das Ergebnis jeder Vergleichsoperation die ganze Zahl 0 oder 1 ist, verwenden wir das printf-Format %d. Schau dir das Ergebnis genau an. Das Ergebnis 1 bedeutet wahr, das Ergebnis 0 bedeutet falsch. In der Tabelle am Anfang dieses Unterkapitels steht in der zweiten Spalte das logische Gegenteil von der ersten. Wenn die Aussage ›i ist gleich j ‹ wahr ist, ist die Aussage ›i ist ungleich j ‹ falsch. Beachte, dass das Gegenteil von > nicht etwa < ist. Denn wenn z. B. eine Zahl i nicht größer als j ist, muss sie nicht notwendigerweise kleiner als j sein. Die Zahl i könnte auch gleich j sein. Überprüfe in unserem Beispiel, dass das logische Gegenteil von 0 die 1 ist, und von 1 die 0. Da wir jetzt wissen, dass Nicht-Null und Null die Rolle von wahr und falsch spielen, können wir die if-Anweisung besser verstehen. if (Ausdruck) kannst du auf verschiedene Weisen lesen: wenn (Ausdruck != 0), wenn (Aussage wahr), wenn (Bedingung erfüllt).
Was aber tatsächlich geschieht, ist, dass die Ausführung von Befehlen nach if nur davon abhängt, was für eine Zahl Ausdruck liefert. Insbesondere wird der Befehl in if (−99)
Befehl
immer ausgeführt, der Befehl in if (0)
Befehl
wird niemals ausgeführt. 94
Kapitel 5 Bedingungen
Sandini Bib
5.2 Logische Verknüpfungen Viele Entscheidungen beruhen auf mehr als einer Bedingung: Wenn ich Zeit habe und wenn ich Geld übrig habe, gehe ich ins Kino. Wenn ich Zeit habe und wenn mich jemand einlädt, gehe ich ins Kino. Wenn ich Zeit habe und wenn ich Geld übrig habe oder wenn mich jemand einlädt, gehe ich ins Kino.
An der Kinokasse gibt es Gruppenrabatt: Wenn es weniger als 10 Leute sind, kostet es 8 Euro. Wenn es 10 oder mehr Leute sind, kostet es 5 Euro. Wenn es 2 Leute sind, kostet es auch 5 Euro.
In der automatisierten Kasse läuft vielleicht das folgende Programm: If3.c
#include <stdio.h> int main() { int n; printf("Wie viele Leute seid ihr? scanf("%d", &n);
");
if (n <= 0) printf("Naechster bitte.\n"); if (n == 2 || n >= 10) printf("Das macht 5 Euro pro Nase.\n"); if (n == 1 || n >= 3 && n <= 9) printf("Das macht 8 Euro pro Nase.\n"); getchar(); getchar(); return 0; }
Wie viele Leute seid ihr? 2 Das macht 5 Euro pro Nase.
Hier möchte ich dir zeigen, wie sich zwei Aussagen mit dem logischen Oder- und dem logischen Und-Operator verknüpfen lassen: || &&
logisches Oder logisches Und
5.2
Logische Verknüpfungen
95
Sandini Bib
Diese funktionieren wie folgt: Aussage1 || Aussage2 ist wahr, falls mindestens eine der beiden Aussagen wahr ist, sonst falsch. Aussage1 && Aussage2 ist wahr, falls beide Aussagen wahr sind, sonst falsch.
Im Beispiel ist n == 2 || n >= 10
wahr, wenn n gleich 2 ist oder wenn n größer gleich 10 ist. Dann gilt das Sonderangebot. Das Sonderangebot gilt nicht, wenn man alleine ist, n == 1
oder wenn 3 bis 9 Freunde zusammen ins Kino gehen, n >= 3 && n <= 9
d.h. mindestens 3 und höchstens 9. So muss man einen Zahlenbereich in C eingeben, denn 3 <= n <= 9 funktioniert nicht. Im Beispiel habe ich diese Bedingungen als n == 1 || n >= 3 && n <= 9
verknüpft. Genau wie beim Rechnen mit Punkt und Strich stellt sich die Frage, in welcher Reihenfolge Aussagen verknüpft werden. Bei den logischen Operatoren gilt Und vor Oder und Klammern werden wie immer zuerst ausgerechnet: i || j && k entspricht i || (j && k) i || j && k ist nicht dasselbe wie (i || j) && k
Aussagen lassen sich auch ins Gegenteil umkehren. Der Nicht-Operator ! macht aus wahr falsch und aus falsch wahr. Man sprich von Verneinung oder Negation. Wie oft habe ich mir einen solchen Zauberstab schon gewünscht! Genauer gesagt wird aus 0 eine 1 und aus jeder Zahl, die nicht 0 ist, eine 0. Zum Beispiel können wir statt mit if (n <= 0) mit if (!(n > 0)) printf("Naechster bitte.\n");
den Fall behandeln, dass 0 oder eine negative Zahl eingegeben wird. Das kannst du als wenn nicht n größer als 0 ist
lesen. Beachte, dass ! stärker als > und die anderen Vergleichsoperatoren bindet, deshalb schreiben wir !(n > 0). Die Operatoren Und (&&), Oder (||) und Nicht (!) sind wichtige Operationen in der Booleschen Algebra. Mit !, || und && kann man jede denkbare Verknüpfung von Aussagen formulieren, z. B. auch das Entweder-Oder, für das es in C keinen eigenen Aussagenoperator gibt, siehe 5.11. 96
Kapitel 5 Bedingungen
Sandini Bib
5.3 Bedingungen mit mehreren ifs Je nachdem, was uns klarer vorkommt, können wir die Operatoren || und && in vielen Fällen umgehen: If4.c
#include <stdio.h> int main() { int n; printf("Wie viele Leute seid ihr? scanf("%d", &n);
");
if (n <= 0) printf("Naechster bitte.\n"); if (n == 1) printf("Das macht 8 Euro pro Nase.\n"); if (n == 2) printf("Das macht 5 Euro pro Nase.\n"); if (n >= 10) printf("Das macht 5 Euro pro Nase.\n"); if (n >= 3) if (n <= 9) printf("Das macht 8 Euro pro Nase.\n"); getchar(); getchar(); return 0; }
Statt mit if (n == 2 || n >= 10) printf("Das macht 5 Euro pro Nase.\n");
das Oder in ›wenn n gleich 2 oder n größer gleich 10‹ zu bewirken, können wir einfach zwei if-Anweisungen hintereinander schreiben. Allerdings müssen wir dann den printf-Befehl wiederholen. Das Und in n >= 3 && n <= 9 können wir als if (n >= 3) if (n <= 9) printf("Das macht 8 Euro pro Nase.\n");
schreiben. Was geht hier vor? Bei 5.3 Bedingungen mit mehreren ifs
97
Sandini Bib if (n <= 9) printf("Das macht 8 Euro pro Nase.\n");
handelt es sich um einen Befehl, der nur ausgeführt wird, wenn das erste if (n >= 3) erfüllt ist. Oder andersherum, wir können Bedingungen verschärfen, indem wir mehrere ifs ineinander setzen. Um das noch deutlicher zu machen, kannst du if (n >= 3) { if (n <= 9) { printf("Das macht 8 Euro pro Nase.\n"); } }
schreiben. Im Befehlsblock des ersten ifs können beliebige Befehle stehen, in diesem Fall eben noch ein if.
5.4 Bedingungen mit if und else Wir haben jetzt erste Erfahrungen gesammelt, wie man mit if die Ausführung von Programmteilen von Bedingungen abhängig machen kann. Oft möchten wir je nach Erfüllung der Bedingung die eine oder die andere Aktion ausführen. Das könnte so aussehen: if (i > 0) printf("%d ist groesser als 0.\n", i); if (!(i > 0)) printf("%d ist nicht groesser als 0.\n", i);
Dafür gibt es in C die if-else-Konstruktion. else schreiben wir Alternativen z. B. als
›else‹ heißt ›sonst‹. Mit ifIf5.c
#include <stdio.h> int main() { int i; i = −10; if (i > 0) printf("%d ist groesser als 0.\n", i); else printf("%d ist nicht groesser als 0.\n", i); getchar(); return 0; }
98
Kapitel 5 Bedingungen
Sandini Bib
−10 ist nicht groesser als 0.
Das liest sich so: Wenn i größer als 0 ist, teile das der Welt mit, sonst ist unsere Botschaft, dass i nicht größer als 0 ist. Die Syntax sieht so aus: if (Ausdruck)
Befehl1 else
Befehl2
Der Ausdruck wird ausgewertet. Wenn das Ergebnis wahr ergibt, wird Befehl1 ausgeführt und Befehl2 ignoriert. Wenn hingegen der Ausdruck falsch ergibt, wird Befehl1 ignoriert und Befehl2 ausgeführt. Der Debugger kann dir helfen, den Programmablauf zu verfolgen. Übrigens zählt die gesamte if-else-Konstruktion ebenfalls als ein Befehl. Du kannst also wie in If6.c
#include <stdio.h> int main() { int i; printf("Sag mir eine positive ganze Zahl: scanf("%d", &i);
");
if (i <= 0) { if (i < 0) { printf("Diese Zahl ist negativ!\n"); } else { printf("Knapp daneben ist auch daneben.\n"); } } getchar(); getchar(); return 0; }
eine Bedingung mit aufeinander folgenden ifs verschärfen und gleichzeitig Alternativen mit else behandeln. Dabei ist zu beachten, dass else sich immer auf das if bezieht, das dem else vorausgeht. In unserem Beispiel könnten die geschweiften Klammern auch weggelassen werden (und zwar alle Klammern, probier es mal aus). Gerade weil else sich immer auf das if direkt über sich bezieht, sind im nächsten Beispiel geschweifte Klammern erforderlich:
5.4 Bedingungen mit if und else
99
Sandini Bib If7.c
#include <stdio.h> int main() { int i; printf("Sag mir eine positive ganze Zahl: scanf("%d", &i);
");
if (i <= 0) { printf("Diese Zahl ist nicht > 0.\n"); if (i < 0) printf("Diese Zahl ist negativ!\n"); } else printf("Vielen Dank.\n"); getchar(); getchar(); return 0; }
Ausprobieren! Im Zweifelsfall einfach Klammern setzen. Manchmal benötigt man if-else nur, um sich zwischen zwei Zahlen zu entscheiden. Dafür gibt es auch den ?:-Operator: maximum = (i > j) ? i : j;
weist der Variable maximum den größeren der beiden Werte i und j zu. Das Schema ist Ausdruck0 ? Ausdruck1 : Ausdruck2
was zusammen einen einzigen Ausdruck ergibt. Wenn Ausdruck0 wahr ist, ergibt der gesamte Ausdruck den Wert von Ausdruck1. Wenn Ausdruck0 falsch ist, ergibt der gesamte Ausdruck den Wert von Ausdruck2. Welchen Wert erhält maximum, wenn i gleich j ist? Dann wird Ausdruck2 zugewiesen, also maximum = j;, was wiederum gleich i ist.
100
Kapitel 5 Bedingungen
Sandini Bib
5.5 Bedingungen mit else if Eine Verkettung von if-else wie in If8.c
#include <stdio.h> int main() { int i; printf("Wieviele Hamburger moechtest du? scanf("%d", &i);
");
if (i < 0) printf("Quatsch.\n"); else if (i == 0) printf("Na gut.\n"); else if (i == 1) printf("Bitte schoen.\n"); else if (i == 2 || i == 3) printf("Na ausnahmsweise.\n"); else printf("Prost Bauchweh.\n"); getchar(); getchar(); return 0; }
ist nützlich, wenn man einen von mehreren möglichen Fällen behandeln möchte. else if ist kein neuer Befehl, sondern ein else mit einem Befehl, der wieder mit if anfängt. In diesem Fall sieht man vom Einrücken nachgeordneter Blöcke ab, damit man den Code besser lesen kann. Wie würde unser Beispiel aussehen, wenn jedes if und jedes else seine eigene Zeile bekäme und um zwei weiter eingerückt würde? Die Bedingungen werden eine nach der anderen getestet, bis eine erfüllt ist, und nur deren Befehl wird ausgeführt. Das letzte else stellt sicher, dass auch dann etwas Sinnvolles geschieht, wenn keine einzige der Bedingungen zutrifft. In einer Fallunterscheidung nennt man so etwas auch den ›Default‹. Das ist sozusagen die Voreinstellung, die bestimmt, was geschieht, wenn sonst nichts unternommen wird. Auch bei Variablen bezeichnet man oft den Wert, den eine Variable als Voreinstellung erhält, als ihren Default. In C kann man Fallunterscheidungen statt mit if und else if auch mit den Schlüsselwörtern switch und case konstruieren (falls in den Bedingungen nur mit Konstanten verglichen wird). Falls dir das irgendwo begegnet, kannst du in Anhang A.2 darüber nachlesen.
5.5 Bedingungen mit else if
101
Sandini Bib
5.6
Entscheidungen für den Bauch
Hier ist eine noch realistischere Computersimulation eines Restaurantbesuchs: If9.c
#include <stdio.h> int main() { int trinken, essen; // bestelle Getraenke printf("\nWillkommen in unserem Restaurant.\n\n"); printf("Was moechtest du trinken?\n"); printf("1 Wasser\n"); printf("2 Brause\n"); printf("3 Kaffee\n"); printf("Bitte waehle eine Nummer: "); scanf("%d", &trinken); printf("\n"); // und was sagt die Bedienung dazu? if (trinken < 1 || trinken > 3) printf("Haben wir nicht, auch egal."); else printf("Gut."); // bestelle das Essen printf(" Was moechtest du essen?\n"); printf("1 Pommes\n"); printf("2 Schnitzel mit Pommes\n"); printf("3 Pommes mit Schnitzel\n"); printf("4 Schnommes mit Pitzel\n"); printf("Bitte waehle eine Nummer: "); scanf("%d", &essen); printf("\n"); // und was sagt die Bedienung dazu? if (essen < 1 || essen > 4) { if (trinken < 1 || trinken > 3) { printf("Sag mal, willst du mich veraeppeln? Raus mit dir!\n"); } else { printf("Haben wir nicht. Ich bringe erst mal die Getraenke.\n"); } } else { printf("Gut. Kommt sofort!\n"); } getchar(); getchar(); return 0; }
102
Kapitel 5 Bedingungen
Sandini Bib
Dies ist ein Beispiel für eine Reihe von Entscheidungen, mit denen überprüft wird, ob gewisse Eingaben Sinn machen. Es werden zwei Fragen gestellt, die sinnig oder unsinnig beantwortet werden können. Jede der vier Möglichkeiten ergibt eine unterschiedliche Reaktion. Alles klar? Im Zweifelsfall mit dem Debugger das Programm durchlaufen. Gib auch mal −1 und 99999 ein. Ein möglicher Ablauf ist Willkommen in unserem Restaurant. Was moechtest du trinken? 1 Wasser 2 Brause 3 Kaffee Bitte waehle eine Nummer: 2 Gut. Was moechtest du essen? 1 Pommes 2 Schnitzel mit Pommes 3 Pommes mit Schnitzel 4 Schnommes mit Pitzel Bitte waehle eine Nummer: 0 Haben wir nicht. Ich bringe erst mal die Getraenke.
5.7
Zufallszahlen
In diesem Beispiel stelle ich dir Zufallszahlen vor. Das hat zunächst überhaupt nichts mit Bedingungen zu tun, aber im nächsten Beispiel kann ich dann vorführen, wie ein Programm mit if und else auf den Zufall reagieren kann. Zudem tut es gut, die strenge Logik dieses Kapitels mit dem chaotischen Zufall zu konfrontieren. Vielleicht denkst du bei dem Wort Zufall als Erstes an Spiele. Jedes Würfelspiel verwendet Würfel als ›Zufallsgenerator‹. Du würfelst und das Ergebnis ist 1, 2, 3, 4, 5 oder 6. Aber welche Zahl gewürfelt wird, ist dem Zufall überlassen. Du kannst diese Zahl nicht vorhersagen, aber du weißt, welche Zahlen möglich sind und dass bei häufigem Würfeln jede Zahl ungefähr gleich oft erscheint. Aber natürlich ist das ganze Leben voller Zufälle. Du hattest sicher schon Glücksträhnen und Phasen rabenschwarzen Pechs. Nichts ist sicher, und wenn der Zufall überhand nimmt, herrscht Chaos. Denke nur an den tägliche Wetterbericht. Viele Spiele und Computerspiele leben vom Zufall, darum wollen wir dieses Thema ausführlich diskutieren.
5.7
Zufallszahlen
103
Sandini Bib
Der Computer ist geradezu ein Vorbild an Zuverlässigkeit. Seine ganze Konstruktion ist darauf angelegt, Programme unter allen Umständen identisch auszuführen. Auch beim abermillionsten Mal muss 1 plus 1 gleich 2 sein. Ab und zu ›stürzt der Computer unerwartet ab‹, d.h. ein Programm benimmt sich nicht wie erwartet. Nur in den seltensten Fällen liegt das daran, dass zufälligerweise irgendein Elektron in den Schaltkreisen des Computers einen Anfall von Chaos hatte. Nein, entweder ist der Computer defekt – oder es war alles Deine Schuld, ich meine, die Schuld des Programmierers oder der Programmiererin. Das heißt, es liegt ein subtiler, gut versteckter Programmierfehler vor. Du solltest immer davon ausgehen, dass der Computer 100‚000-prozentig logisch vorgeht. Wie kann dann der Computer Zufallszahlen berechnen? Die kurze Antwort ist: Er kann es nicht. Aber es gibt simple Rechenvorschriften, mit denen man Pseudozufallszahlen generieren kann, also Folgen von Zahlen, die zufällig aussehen, es aber gar nicht sind. Trotzdem erfüllen diese Zahlen in den allermeisten Fällen ihren Zweck. Solange ein Programm nicht auf sehr spezielle Weise versucht, Regelmäßigkeiten zu entdecken, sind diese Zahlen gleichmäßig verteilt wie bei einem Würfel und auch in langen Zahlenfolgen gibt es keine Wiederholungen. Lange Rede, kurzer Sinn: Obwohl zwar im Prinzip ein Paradox vorliegt, können Computer den Zufall sehr gut simulieren. In C berechnen wir Zufallszahlen mit der Funktion rand: Zufall0.c
#include <stdio.h> #include <stdlib.h> int main() { printf("%d\n", printf("%d\n", printf("%d\n", printf("%d\n", printf("%d\n", getchar(); return 0; }
rand()); rand()); rand()); rand()); rand());
346 130 10982 1090 11656
Der Name der Funktion rand kommt von Der Prototyp dieser Funktion wird mit
104
Kapitel 5 Bedingungen
›random‹, was hier zufällig heißt.
Sandini Bib #include <stdlib.h>
geladen. ›stdlib‹ steht für
›standard library‹ (Standardbibliothek).
Lass das Beispiel jetzt bei dir laufen. Welche Zahlen bekommst du? Wenn du denselben Compiler BCB verwendest wie ich, wirst du rufen: ›Nein, solch ein Zufall, ich bekomme genau dieselben Zahlen!‹ Lass das Programm noch mal laufen und noch mal. Egal, ob du dieselben Zahlen wie ich erhältst oder nicht, die fünf Zahlen werden sich auch bei dir von Mal zu Mal nicht ändern. Wie kann das angehen? Der Grund ist, dass rand Zahlenfolgen berechnet. Jeder Aufruf von rand liefert die nächste Zahl. Diese Zahlenfolge ist für einen gegebenen Startwert immer gleich. Diesen Startwert, der alle anderen Zahlen in der Folge von vornherein ›seed‹) der Zahlenfolge. Die Zufallszahlen festlegt, nennt man den Samen ( sind nur insofern zufällig, als die Zahlen gut gemischt sind! Den Samen für den Zufallszahlengenerator rand kannst du mit der Funktion srand sähen (›seed random number generator‹): Zufall1.c
#include <stdio.h> #include <stdlib.h> int main() { srand(2);
// 1 ist der Default
printf("%d\n", printf("%d\n", printf("%d\n", printf("%d\n", printf("%d\n", getchar(); return 0;
rand()); rand()); rand()); rand()); rand());
}
692 32682 21834 23967 22222
Überzeuge dich davon, dass srand(1) der Default ist und du dieselben Zahlen wie ohne srand erhältst und dass srand(1) und srand(2) verschiedene Zufallsfolgen ergeben. Wenn du srand(1) nach einigen rand() noch mal aufrufst, startet die Reihe wieder von vorne. 5.7
Zufallszahlen
105
Sandini Bib
In der Testphase eines Programmes ist es praktisch, dass man die Zahlenreihen wiederholen kann, indem man denselben Startwert verwendet. Aber drehen wir uns vielleicht im Kreis? Brauchen wir womöglich eine Zufallszahl, um den Generator so zu initialisieren, dass bei jedem Programmablauf eine andere Zufallsfolge verwendet wird? Nein, wir können die Zeitfunktion time wie folgt verwenden (Prototyp in time.h): srand(time(0));
Der Aufruf von time(0) liefert die Zeit seit dem 1. Januar 1970 in Sekunden. Wie seltsam. Aber nützlich, denn selbst nach einer Sekunde sehen die Zufallszahlen schon ganz anders aus. Denn eine Änderung des Seeds um lediglich 1 reicht aus, verschiedene Reihen zu erzeugen (die insbesondere nicht bloß um eine Zahl gegeneinander verschoben sind oder so). Ausprobieren! In der Hilfe von BCB findest du den Hinweis, dass rand() die Werte von 0 bis zu einer Konstanten namens RAND MAX liefert. Bei mir ist RAND MAX gleich 32767. Solche großen Zufallszahlen kannst du leicht z. B. mit rand() % 6
in kleinere verwandeln. Der Rest bei der Division durch 6 ist 0, 1, 2, 3, 4 oder 5. Also zählen wir 1 dazu, 1 + rand() % 6
und fertig ist unser Würfel. Jetzt weißt du also, wie man in C würfelt. An dieser Stelle kann ich es mir nicht verkneifen, ein wenig Unfug mit Grafik anzustellen. Was hältst du von folgendem Programm (du benötigst das WinHallo Beispiel aus Kapitel 4): Zufall2.c WinHallo.cpp
#include <windows.h> #include <stdio.h> void malen(HDC hdc) { char text[100]; sprintf(text, "%d", 1 + rand()%6); TextOut(hdc, 100, 100, text, strlen(text)); }
Lass es laufen. Du bekommst eine einsame, kleine Zufallszahl in einem Fenster. Bei mir ist es die 5. Jetzt könntest du den Trick mit srand anwenden, damit du bei jedem Programmstart eine neue Zahl bekommst. Jetzt halte dich fest. Oder besser, halte die Maus fest. Ich meine, klicke mit der linken Maustaste auf einen Rand des Fensters, halte die linke Maustaste gedrückt 106
Kapitel 5 Bedingungen
Sandini Bib
und bewege die Maus. Du wirst die Größe des Fensters verändern. Und weil nach jeder Größenänderung das Fenster neu gemalt wird (unter Umständen musst du die Maustaste dazu wieder loslassen), wird jedes Mal eine neue Zufallszahl angezeigt. Nanana, so programmiert ein ernsthafter Windowsprogrammierer nicht. Aber das sind wir ja (zum Glück?) nicht. Wie wäre es mit Zufall3.c WinHallo.cpp
#include <windows.h> #include <stdio.h> void malen(HDC hdc) { HFONT hfont, hfalt; char text[100]; hfont = CreateFont(5 + rand()%400, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); hfalt = SelectObject(hdc, hfont); sprintf(text, "%d", 1 + rand()%6); TextOut(hdc, 70, 20, text, strlen(text)); SelectObject(hdc, hfalt); DeleteObject(hfont); }
Hier setzen wir gleich auch noch die Fontgröße zufällig, alles andere ist dir schon aus den Kapiteln 4.6 und 4.10 bekannt. Und wenn wir schon dabei sind, warum nicht Ellipsen mit einer zufälligen Farbe und zufälligen Koordinaten:
5.7
Zufallszahlen
107
Sandini Bib Zufall4.c WinHallo.cpp
#include <windows.h> void malen(HDC hdc) { HBRUSH hbrush, hbalt; int max = 250; hbrush = CreateSolidBrush(RGB(rand()%256, rand()%256, rand()%256)); hbalt = SelectObject(hdc, hbrush); Ellipse(hdc, rand()%max, rand()%max, rand()%max, rand()%max); SelectObject(hdc, hbalt); DeleteObject(hbrush); }
Die Bilder habe ich mit max gleich 500 gemacht, nachdem ich im Editor die 5 Programmzeilen für die Zufallsellipse mehrmals kopiert hatte (markiere die 5 Zeilen, drücke einmal Strg + C , drücke zehnmal Strg + V ).
5.8
Ein Held des Zufalls
Kennst du so genannte Rollenspiele? Du spielst einen Fantasiehelden, dessen Fähigkeiten mit dem Würfel festgelegt werden. Oft benötigt man für einen bestimmten Beruf wie Dieb, Kämpfer oder Zauberer eine gewisse Mindestpunktzahl in bestimmten Fähigkeiten. Hier ist unser Beispiel: 108
Kapitel 5 Bedingungen
Sandini Bib ZufallsHeld.c
#include <stdio.h> #include <stdlib.h> #include int main() { int stae, bewe, inte, ausd, mana; int ok; srand(time(0)); stae = 3 + rand()%6 bewe = 3 + rand()%6 inte = 3 + rand()%6 ausd = 3 + rand()%6 mana = 3 + rand()%6
+ + + + +
rand()%6 rand()%6 rand()%6 rand()%6 rand()%6
+ + + + +
rand()%6; rand()%6; rand()%6; rand()%6; rand()%6;
printf("\nNeuer Held mit %d Punkten: \n", stae + bewe + inte + ausd + mana); printf(" %2d Staerke\n", stae); printf(" %2d Ausdauer\n", ausd); printf(" %2d Beweglichkeit\n", bewe); printf(" %2d Intelligenz\n", inte); printf(" %2d Mana\n", mana); ok = 0; printf("\n−−> "); if (bewe >= 14) { printf(" Dieb"); ok = 1; } if (stae >= 13 && ausd >= 10) { printf(" Kaempfer"); ok = 1; } if (inte >= 8 && mana >= 8 && printf(" Zauberer"); ok = 1; }
inte + mana >= 22) {
if (!ok) printf(" noch mal wuerfeln"); printf("\n"); getchar(); return 0; }
5.8
Ein Held des Zufalls
109
Sandini Bib
Neuer Held mit 61 Punkten: 13 Staerke 12 Ausdauer 13 Beweglichkeit 8 Intelligenz 15 Mana −−>
Zauberer
Für jede Fähigkeit definieren wir eine Variable, in der wir die Punktezahl speichern wollen. Für jede Fähigkeit wird mit drei Sechserwürfeln 1 + rand()%6 gewürfelt, also ist das Ergebnis 3 + rand()%6 + rand()%6 + rand()%6
Nach dem Würfeln geben wir das Ergebnis am Bildschirm aus. Als Nächstes soll entschieden werden, welche Berufe unserem Helden zur Wahl stehen. Beachte, dass die drei if-Bedingungen nicht eine dieser Fallunterscheidungen ist, bei denen sich die Möglichkeiten gegenseitig ausschließen! Unser Held darf gerne zwei Berufe gleichzeitig lernen oder einen von zwei auswählen. Deshalb testen wir nacheinander, ob verschiedene Voraussetzungen für einen Beruf erfüllt sind, und melden das mit printf. Alle drei Bedingungen werden getestet und keine, eine, zwei oder alle drei können erfüllt sein und sich mit printf melden. Dabei taucht ein kleines Problem auf, das wir mit der Variable ok lösen. Wie erzeugen wir eine Meldung, wenn kein Beruf erlaubt ist und noch mal gewürfelt werden soll? Das soll der Default sein, falls kein Beruf erlaubt ist. Kein Beruf ist erlaubt, wenn die Bedingung für Dieb Nicht erfüllt ist Und die Bedingung für Kämpfer Nicht erfüllt ist Und die Bedingung für Zauberer Nicht erfüllt ist. Das ist eine große Bedingung, die wir erhalten, wenn wir die einzelnen Bedingungen kopieren und mit Und und Nicht richtig verknüpfen: if (
!(bewe >= 14) && !(stae >= 13 && ausd >= 10) && !(inte >= 8 && mana >= 8 && inte + mana >= 22)) printf(" nochmal wuerfeln");
Das ist unschön, weil kompliziert, und wenn du z. B. die Anforderungen an den Zauberer ändern willst, musst du daran denken, zwei Stellen im Programm zu ändern. Eine elegantere Lösung ist, die Variabe ok einzuführen. Wie du siehst, initialisieren wir ok mit 0. Dann folgen die drei Bedingungen für die verschiedenen Berufe. Falls ein Beruf erlaubt ist, setzen wir ok auf 1. Falls noch ein Beruf erlaubt ist, schadet es nicht, ok noch mal auf 1 zu setzen. Abschließend müssen wir nur noch mit if (!ok) testen, ob das Würfelergebnis vielleicht nicht ok war, und können dann unsere Defaultmeldung ausgeben.
110
Kapitel 5 Bedingungen
Sandini Bib
5.9
Bisektion
Angenommen, du spielst mit einem Freund oder einer Freundin Zahlenraten. Der eine denkt sich eine der Zahlen 0, 1, 2, 3, 4, 5, 6, 7.
Der andere darf Fragen stellen wie ›Ist die Zahl kleiner als 4?‹, auf die er die Antwort ›richtig‹ oder ›falsch‹ bekommt. Aha, hier wird dir also wieder eine Fallunterscheidung vorgeführt. Eine gut Ratestrategie ist, mit jeder Frage die Anzahl der Möglichkeiten zu halbieren. So etwas nennt man Bisektion (›Zweiteilung‹). Das könnte so ablaufen: 1. Frage: Zahl < 4? Richtig! Möglich sind noch 0, 1, 2, 3. 2. Frage: Zahl < 2? Falsch! Möglich sind noch 2, 3. 3. Frage: Zahl < 3? Falsch! Möglich ist nur noch 3. 4. Du hast dir die 3 gedacht. Richtig!
Hier ist ein Programmbeispiel, zur Abwechslung mit Textausgabe in einem Grafikfenster (siehe Kapitel 4). Die Sache ist viel einfacher, als sie aussieht:
5.9
Bisektion
111
Sandini Bib Bisektion0.c WinHallo.cpp
#include <windows.h> void malen(HDC hdc) { int x = 5, dx = 60; int y = 100, dy = 50; int zz; zz = rand()%8; TextOut(hdc, x, y, "zz < 8 ", 7); if (zz < 4) { TextOut(hdc, x+=dx, y−=dy, "zz < 4 ", 7); dy /= 2; if (zz < 2) { TextOut(hdc, x+=dx, y−=dy, "zz < 2 ", 7); dy /= 2; if (zz < 1) TextOut(hdc, x+=dx, y−=dy, "zz == 0", 7); else TextOut(hdc, x+=dx, y+=dy, "zz == 1", 7); } else { TextOut(hdc, x+=dx, y+=dy, "zz >= 2", 7); dy /= 2; if (zz < 3) TextOut(hdc, x+=dx, y−=dy, "zz == 2", 7); else TextOut(hdc, x+=dx, y+=dy, "zz == 3", 7); } } else { TextOut(hdc, x+=dx, y+=dy, "zz >= 4", 7); dy /= 2; if (zz < 6) { TextOut(hdc, x+=dx, y−=dy, "zz < 6 ", 7); dy /= 2; if (zz < 5) TextOut(hdc, x+=dx, y−=dy, "zz == 4", 7); else TextOut(hdc, x+=dx, y+=dy, "zz == 5", 7); } else { TextOut(hdc, x+=dx, y+=dy, "zz >= 6", 7); dy /= 2; if (zz < 7) TextOut(hdc, x+=dx, y−=dy, "zz == 6", 7); else TextOut(hdc, x+=dx, y+=dy, "zz == 7", 7); } } }
112
Kapitel 5 Bedingungen
Sandini Bib
Der Computer besorgt sich eine Zufallszahl zz kleiner als 8 mit der Funktion rand und bestimmt diese dann durch eine Reihe von ineinander gesetzten, verschachtelten Fallunterscheidungen. Mit TextOut geben wir nach jedem Test der Variable zz den Stand der Dinge aus. Konzentriere dich als Erstes auf den Fall, dass zz gleich 0 ist. Der Ablauf wird wie folgt sein: zz < 4 ist wahr, nächster Befehlsblock zz < 2 ist wahr, nächster Befehlsblock zz < 1 ist wahr, nächster Befehlsblock
also muss die Zahl gleich 0 sein.
Alle anderen Befehlsblöcke werden übersprungen und die Ausgabe ist fertig. Im Fall zz gleich 3: zz < 4 ist wahr, nächster Befehlsblock zz < 2 ist falsch, springe zum Befehlsblock des else zz < 3 ist falsch, springe zum Befehlsblock des else
also muss die Zahl gleich 3 sein.
Was soll die Rechnerei mit den Koordinaten bewirken? Wir wollen die Entscheidungsfindung wie einen Weg mit Abzweigungen von links nach rechts aufmalen. Das sieht dann so aus:
Die erste Textausgabe geschieht für x = 5, bei jeder weiteren Textausgabe erhöhen wir x um dx = 60 mit x += dx. Dadurch wird der neue Text immer weiter nach rechts gerückt. 5.9
Bisektion
113
Sandini Bib
Der Witz ist, dass wir gleichzeitig die Textposition in der y-Richtung nach oben (y −= dy) oder unten (y += dy) verschieben, je nachdem ob die Aussage wahr oder falsch war. Und bei jedem Schritt nach rechts halbieren wir die Schrittgröße dy für die y-Richtung. Wie du an den Beispielfensterchen siehst, findet dadurch jede Zahl ihren eigenen Platz am rechten Fensterrand! Wenn du jetzt mit der Maus den Fensterrand verschiebst (bei gedrückter linker Maustaste), wird das Fenster immer wieder mit einer neuen Zufallszahl neu gezeichnet, und du bekommst den gesamten ›Entscheidungsbaum‹ zu sehen.
5.10 Statt Schritt für Schritt blindlings geradeaus durchs Programm zu stapfen, wissen wir jetzt, wie C je nach den gegebenen Bedingungen einen Programmteil ausführt oder überspringt. Wie schön, dass wir das jetzt können. Was offensichtlich noch fehlt, ist die Möglichkeit, im Programm nicht nur vorwärts, sondern auch rückwärts zu springen. So könnten wir Programmteile wiederholen. Wiederholungen, auch Schleifen genannt, sind das Thema von Kapitel 6. Zur Zusammenfassung: Zahlen kann man mit == != > < >= <= vergleichen. Null wird auch als ›falsch‹, Nicht-Null als ›wahr‹ interpretiert. Wahre und falsche Aussagen kann man mit dem Verneinungsoperator ! (Nicht) umkehren und mit || (Oder) und && (Und) verknüpfen. Der Befehl in if (i < 10)
Befehl
wird nur ausgeführt, wenn i kleiner als 10 ist, ansonsten wird er übersprungen. In if (i < 10)
Befehl1 else
Befehl2
wird genau einer der beiden Befehle ausgeführt, und zwar der erste, wenn i kleiner als 10 ist, und der zweite, wenn i nicht kleiner als 10 ist. Pseudozufallszahlen erhältst du mit der Funktion rand. Mit srand kannst du den Startwert ändern. Diese Zufallszahlen durchlaufen bei gleichem Startwert immer dieselben Werte. Aber mit srand(time(0)) kannst du den Startwert abhängig von der Uhrzeit ändern.
114
Kapitel 5 Bedingungen
Sandini Bib
5.11 1. Schreibe ein Testprogramm für ||, &&, und !. Teste verschiedene Verknüp-
fungen. 2. Wahr oder falsch? Erst denken, dann mit deinem Testprogramm testen. Nimm an, dass i = 1; j = 10; k = 0; ausgeführt wurde: i < 0 (i > 0) && (j < 1) i − k > 0 i − k > 0 && j − i <= 0 i >= 0 && j >= 0 && k >= 0 i && k i || k i && !k i || j || k !(i || j || k) i && !i i || !i
3. Wie erzeugst du einen Entweder-Oder-Operator? Es soll entweder Aussage1
oder Aussage2 wahr sein, aber nicht beide zugleich. Betrachte (a || b) && !(a && b) (a || b) && (!a || !b) a && !b || !a && b
Von der zweiten zur dritten Zeile kommst du, wenn du die Klammern mit && ›ausmultiplizierst‹. 4. Schreibe ein Programm, das dir die Entscheidung, ins Kino zu gehen, ab-
nimmt, siehe Kapitel 5.2. Dazu soll das Programm drei Fragen stellen, hast du Zeit, hast du Geld, darf ich dich einladen. Antworten darf man mit 0 oder 1 (oder einer Zahl ungleich Null). Schau dir die dritte Bedingung genau an. Ist die Umgangssprache mit und/oder eindeutig? 5. Programmiere ein kleines Grafikprogramm, das eine knallige Zufallsfarbe für den Hintergrund auswählt und dich mit einem Zufallstext begrüßt ("Hallo", oder "Schon wieder du", oder "Mein Chip schlaegt nur fuer dich",
oder . . .).
5.11
115
Sandini Bib
Sandini Bib
6 Schleifen 6.0 6.1 6.2 6.3 6.4
Zählerschleife mit while Schleife im Geschwindigkeitsrausch Endlosschleifen Schleifenkontrolle mit break und continue Schleifen mit while, for und do
119 120 121 121 124
6.5
Summe ganzer Zahlen
128
6.6
Schleifen und Felder
128
6.7
#define und Felder
130
6.8
Eingabeschleife
131
6.9
Das doppelte Schleifchen
134
6.10
Primzahlen
135
6.11
Zeit in Sekunden
139
6.12
Bisektion mit Schleife
141
Sandini Bib
6.13
Grafik – die hundertste Wiederholung
146
6.14
Linien aus Pixeln
148
6.15
Schachbrett
150
6.16
Histogramme
152
6.17
157
6.18
158
In allen Beispielen, die wir bisher betrachtet haben, wurden die Anweisungen Zeile für Zeile in der Reihenfolge ausgeführt, in der sie im Programm stehen. Zwar haben wir in Kapitel 5 gelernt, wie man Befehle kontrolliert überspringen kann, aber der Programmablauf ging unerbittlich von oben nach unten. Wenn ein Befehl einmal ausgeführt war, kam er nie wieder an die Reihe. Jetzt geht ›solange‹, es rund. In diesem Kapitel besprechen wir while, do und for ( ›mache‹, ›für‹), mit denen du Befehle beliebig oft wiederholen kannst. Wenn ein Programm mehrmals dieselben Befehle wiederholt, spricht man davon, dass es ›loop‹) ausführt. eine Schleife ( Man könnte sagen, Computer wurden erfunden, damit man Schleifen programmieren kann. Insbesondere die Wiederholung von Befehlen mit hoher Geschwindigkeit macht Computer so nützlich. Ein Algorithmus ist eine Methode zur schrittweisen Lösung einer Aufgabe und Schleifen spielen dabei eine wichtige Rolle. Algorithmen können formuliert werden, ohne dass auf eine Programmiersprache Bezug genommen wird. Andererseits kann man Algorithmen als Methoden bezeichnen, die man niemals ohne Computer verwenden würde, weil die vielen Wiederholungen zu viel Arbeit machen!
118
Kapitel 6 Schleifen
Sandini Bib
6.0 Zählerschleife mit while Hier ist unser erstes Beispiel: While0.c
#include <stdio.h> int main() { int i; i = 10; while (i > 0) { printf("%d\n", i); i = i − 1; } getchar(); return 0; }
Das kannst du wie folgt lesen: Setze i gleich 10. Solange (›while‹) i größer als 0 ist, drucke den Wert von i, dann verkleinere i um 1. Wenn i nicht mehr größer als 0 ist, fahre mit dem ersten Befehl nach dem Befehlsblock fort. Also erhältst du einen tenstart:
›Count-down‹ (Zähl-runter) wie bei einem Rake-
10 9 8 7 6 5 4 3 2 1
Unbedingt ausprobieren! Die Konstruktion einer Schleife mit while erinnert an das if: while (Ausdruck) {
Befehle }
und für einen Befehl geht auch while (Ausdruck)
Befehl
6.0 Zählerschleife mit while
119
Sandini Bib
Ausdruck wird genau wie beim if als Aussage gelesen, die entweder wahr oder falsch ist. Alles, was wir in Kapitel 5 über die Konstruktion von wahren und falschen Aussagen und erfüllten und unerfüllten Bedingungen gelernt haben, gilt auch hier. Der entscheidende Unterschied ist aber, dass nach einem if der Befehl einmal oder keinmal ausgeführt wird und nach einem while wird der Befehl solange ausgeführt, wie der Ausdruck wahr ergibt: keinmal, einmal oder viele Male! Einmal? Setze in userem Beispiel i = 1. Keinmal? Setze i = 0 oder auf irgendeine Zahl kleiner 0.
6.1 Schleife im Geschwindigkeitsrausch Die Zahlen von 10 bis 1 werden so schnell nacheinander ausgegeben, dass es aussieht, als ob sie von Anfang an im Textfenster stehen. Starte den Debugger mit F8 und drücke wiederholt F8 . So kannst du genau verfolgen, wie die Schleife abläuft. Gib die Variable i mit Strg + F5 oder ›Start‹, ›Ausdruck hinzufügen‹ als Ausdruck ein, der überwacht werden soll. Jetzt kannst du sehr schön sehen, wie die Variable i erst ausgegeben und dann heruntergezählt wird. Du siehst auch, wie bei i gleich 0 der Befehlsblock übersprungen wird und so die Schleife verlassen wird. Versuche es doch mal mit i = 100000; ohne Debugger ( F9 ). Die Zahlen sollten nach oben aus dem Fenster flitzen. Experimentiere mit dem Startwert, bis der Computer viele Sekunden hart arbeiten muss. Und was für eine Ironie des Schicksals. Das Programm zählt womöglich einen Count-down bei einer Million angefangen in Richtung 0 – doch der Zähler bleibt bei 1 stehen, die Rakete hebt nicht ab. Das kannst du leicht korrigieren, indem du die Schleifenbedingung in while (i >= 0)
umänderst. Womöglich hast du es übertrieben und es sieht so aus, als wolle das Programm nun stundenlang zählen? Normalerweise müsste es dir gelingen, mit der Tastenkombination Strg + C das Programm zu beenden. Notfalls kannst du ein Programm auch beenden, indem du das Fenster mit rechts oben schließt. Vermutlich ist dir schon aufgefallen, dass es Knöpfe unterhalb der BCB-Menüleiste gibt, die wie bei einem CD-Spieler aussehen. Der grüne Pfeil ist gleichbedeutend mit Start ( F9 ) und gleich rechts davon ist die Pausentaste. Während die Zahlen flitzen, kannst du das Programm mit der Pausentaste anhalten. Das 120
Kapitel 6 Schleifen
Sandini Bib
CPU-Fenster, welches dann erscheint, hilft uns nicht viel, aber jetzt kannst du die Variable i untersuchen.
6.2 Endlosschleifen Der springende Punkt an jeder Schleife ist, dass die Schleifenbedingung irgendwann nicht mehr erfüllt ist. Sonst erhältst du eine Endlosschleife. Versuche einmal While1.c
#include <stdio.h> int main() { int i; i = 10; while (i > 0) { printf("%d\n", i); } getchar(); return 0; }
Hier ändert sich der Wert von i nie, die Bedingung ist immer erfüllt, das Programm läuft und läuft und läuft. Eine endlose Pause kann das Programm auch mit while (1);
(eine Befehlszeile mit Strichpunkt) einlegen! Wie gesagt, mit kommst du dieser Falle.
Strg + C
ent-
6.3 Schleifenkontrolle mit break und continue Oft ist es nützlich, aus einer Schleife unabhängig von der Schleifenbedingung ausbrechen zu können. Dies kann mit dem break-Befehl geschehen:
6.2
Endlosschleifen
121
Sandini Bib While2.c
#include <stdio.h> int main() { int i; i = 10; while (1) { if (i < 0) break; printf("%d\n", i); i = i − 1; } getchar(); return 0; }
Dies ist äquivalent zu unserem Beispiel, in dem bis 0 gezählt wurde. In der Schleife wird als Erstes getestet, ob i < 0 ist. Falls ja, wird die break-Anweisung ausgeführt und die Programmausführung springt zum ersten Befehl nach dem while-Block. break kann an beliebiger Stelle in der Schleife stehen und darf auch mehrmals auftreten. Statt die Schleife mit break zu beenden, gibt es auch die Möglichkeit, mit continue zum Anfang der Schleife zurückzukehren, siehe While3.c
#include <stdio.h> int main() { int i; i = 100; while (i >= 0) { if (i % 10 != 0) { i = i − 1; continue; } printf("%d\n", i); i = i − 1; } getchar(); return 0; }
Als Erstes wird getestet, ob i durch 10 teilbar ist. Wenn das nicht der Fall ist (also wenn der Divisionsrest, den wir mit % erhalten, nicht gleich 0 ist), wird der Block 122
Kapitel 6 Schleifen
Sandini Bib
hinter dem if ausgeführt. Würden wir in diesem Block nicht i = i − 1 ausführen, würde sich das Programm bei i gleich 99 in einer Endlosschleife verlaufen! (Versuch das mal.) Durch das continue; wird in die Zeile mit dem while gesprungen. Weil wir wie zuvor rückwärts zählen, aber nur jedes zehnte Mal printf aufrufen, erhalten wir 100 90 80 70 60 50 40 30 20 10 0
Das könnten wir auch wie folgt programmieren: While4.c
#include <stdio.h> int main() { int i; i = 100; while (i >= 0) { if (i % 10 == 0) printf("%d\n", i); i−−; } getchar(); return 0; }
Zur Abwechslung habe ich hier den Operator −− verwendet, um den Inhalt der Zählervariablen um 1 zu erniedrigen. Den Effekt von continue (und übrigens auch den von break) kann man normalerweise auch anders erhalten. Verwende die Version, die dir klarer vorkommt.
6.3 Schleifenkontrolle mit break und continue
123
Sandini Bib
Unser Beispiel lässt sich noch weiter vereinfachen: While5.c
#include <stdio.h> int main() { int i; i = 100; while (i >= 0) { printf("%d\n", i); i −= 10; } getchar(); return 0; }
6.4 Schleifen mit while, for und do Das letzte Beispiel können wir auch mit Hilfe der Anweisung for schreiben: For0.c
#include <stdio.h> int main() { int i; for (i = 100; i >= 0; i −= 10) printf("%d\n", i); getchar(); return 0; }
In der Tat ist for (Ausdruck1; Ausdruck2; Ausdruck3)
Befehl
gleichbedeutend mit Ausdruck1; while (Ausdruck2) { Befehl Ausdruck3; }
124
Kapitel 6 Schleifen
Sandini Bib
Schau dir unser Beispiel für for genau an. Für Zählerschleifen kannst du dir die for-Schleife auch so merken: for (Initialisierung; Bedingung; Weiterzählen)
Befehl
was du wie folgt in eine while-Schleife zerlegen kannst: Initialisierung; while (Bedingung) { Befehl Weiterzählen; }
Besonders in Zählschleifen ist for praktisch, weil alle Schleifenanweisungen übersichtlich in der ersten Zeile versammelt sind, wie z. B. auch in For1.c
#include <stdio.h> int main() { int i, n; printf("Zaehle bis: scanf("%d", &n);
");
for (i = 1; i <= n; i++) printf("%d\n", i); getchar(); getchar(); return 0; }
Manchmal möchte man zwei Zählervariablen haben, vielleicht wie in
6.4 Schleifen mit while, for und do
125
Sandini Bib For2.c
#include <stdio.h> int main() { int i, j, n; printf("Zaehle bis: scanf("%d", &n);
");
j = 2; for (i = 1; i <= n; i++) { printf("%d %d\n", i, j); j += 2; } getchar(); getchar(); return 0; }
Zaehle bis: 1 2 2 4 3 6 4 8 5 10 6 12 7 14 8 16 9 18 10 20
10
Natürlich könntest du auch in der Schleife j = 2*i schreiben oder gleich 2*i als Argument in printf einsetzen. Um die zwei Zahlen ordentlich in Spalten zu stellen, kannst du printf("%10d %10d\n", i, j);
verwenden. Dies reserviert 10 Zeichen Platz für die Ausgabe von i und j. Die Anweisungen für j kannst du auch in die Zeile mit for hineinschreiben:
126
Kapitel 6 Schleifen
Sandini Bib For3.c
#include <stdio.h> int main() { int i, j, n; printf("Zaehle bis: scanf("%d", &n);
");
for (i = 1, j = 2; i <= n; i++, j += 2) printf("%d %d\n", i, j); getchar(); getchar(); return 0; }
Die Regel ist, dass einfache Anweisungen wie diese durch Komma getrennt zwischen die Strichpunkte geschrieben werden dürfen. Diese Befehlslisten mit Kommas sind auch an anderen Stellen erlaubt. Weil darunter aber die Übersichtlichkeit leiden kann, empfehle ich dir, das höchstens bei for-Schleifen zu nutzen. Übrigens kann man die Anweisungen auch teilweise oder ganz bei for weglassen, z. B. for (; i < 10; i++) for (; i < 10;)
Man schreibt nur die Anweisungen, die man braucht. for (;;) ergibt eine Endlosschleife. Eine weitere Möglichkeit für Schleifen ist mit do gegeben. Bei while und for wird erst getestet, und nur wenn die Bedingung erfüllt ist, wird der Schleifenblock durchlaufen. Mit do {
Befehle } while (Ausdruck);
wird der Schleifenblock garantiert einmal ausgeführt, bevor die Bedingung getestet wird. Bleibt nur noch anzumerken, dass break und continue nicht nur für while, sondern genauso für for und do funktionieren. Weil Schleifen mit while, for und do derart wichtig, nützlich und unterhaltsam sind, folgen viele Beispiele.
6.4 Schleifen mit while, for und do
127
Sandini Bib
6.5
Summe ganzer Zahlen
Hier ist ein Programm, das alle ganzen Zahlen von 1 bis 10 aufaddiert: For4.c
#include <stdio.h> int main() { int i, n, summe; n = 10; summe = 0; for (i = 1; i <= n; i++) summe = summe + i; printf("Die Summe aller ganzen Zahlen von 1 bis %d ist %d\n", n, summe); getchar(); return 0; }
Die Summe aller ganzen Zahlen von 1 bis 10 ist 55
Beachte, dass wir erst summe auf 0 setzen müssen, bevor wir mit dem Dazuzählen anfangen können. Die Summation kann auch mit summe += i;
durchgeführt werden.
6.6
Schleifen und Felder
In Kapitel 3.0 haben wir ganzzahlige Felder kennen gelernt. Die Elemente der Felder haben wir eins nach dem anderen gesetzt, z. B. a[0] = 0, a[1] = 2, a[2] = 4 und so weiter. Klarer Fall, dieses ›und so weiter‹ verlangt nach einer Schleife:
128
Kapitel 6 Schleifen
Sandini Bib For5.c
#include <stdio.h> int main() { int i; int a[10]; int summe, min, max; for (i = 0; i < 10; i++) a[i] = 1 + rand()%6; summe = 0; for (i = 0; i < 10; i++) summe += a[i]; min = max = a[0]; for (i = 1; i < 10; i++) { if (a[i] < min) min = a[i]; if (a[i] > max) max = a[i]; } printf("\n %d−mal gewuerfelt, min = %d, max = %d, summe = %d\n", 10, min, max, summe); getchar(); return 0; }
10−mal gewuerfelt, min = 1, max = 6, summe = 29
Mit int a[10]; definieren wir ein Feld aus zehn ganzen Zahlen, auf die wir mit Indizes 0, 1, 2, 3, 4, 5, 6, 7, 8 und 9 zugreifen können. Mit for (i = 0; i < 10; i++) a[i] = 1 + rand()%6;
setzen wir die zehn Elemente des Feldes a auf eine Zufallszahl von 1 bis 6, als ob wir gewürfelt hätten. Die Schleifenbedingung i < 10
ist genau, was wir brauchen, um jeden der zehn Indizes von 0 bis 9 zu erzeugen. Insbesondere wäre i <= 10 falsch, denn dann würden wir um genau ein Element über das Ende des Feldes a hinausgehen. Die Schleife in summe = 0; for (i = 0; i < 10; i++) summe += a[i];
durchlaufen wir ebenfalls zehnmal, um alle Zahlen im Feld a aufzuaddieren. In der letzten Schleife 6.6
Schleifen und Felder
129
Sandini Bib min = max = a[0]; for (i = 1; i < 10; i++) { if (a[i] < min) min = a[i]; if (a[i] > max) max = a[i]; }
suchen wir nach der kleinsten und der größten gewürfelten Zahl. Dazu verwenden wir zwei Variablen, min für das Minimum und max für das Maximum. Jede der Zahlen a[i] ist ein Kandidat, also können wir beide Variablen zunächst gleich a[0] setzen. Dann durchsuchen wir das ganze Feld von Index 1 bis 9 nach Zahlen, die das Minimum unterbieten, a[i] < min, oder das Maximum überbieten, a[i] > max.
Falls wir eine solche Zahl gefunden haben, merken wir uns ihren Wert als die bisher kleinste beziehungsweise größte Zahl.
6.7
#define und Felder
Eigentlich ist es ziemlich unpraktisch, dass wir die Zahl 10 im letzten Beispiel an mehreren Stellen im Programm stehen haben. Das ist lästig, wenn du ihren Wert ändern möchtest. Eigentlich ist das ein Fall für eine Variable, z. B. int n = 10;, aber hier macht uns die Definition des Feldes int a[10]; einen Strich durch die Rechnung. Wie in 3.0 schon erwähnt, darf in dieser Definition nur eine Konstante stehen! Es gibt verschiedene Auswege. In Kapitel 10.1 besprechen wir, wie man in C Felder definiert, deren Größe durch eine Variable gegeben ist. Wenn du auf einfachere Weise eine Variable n in den Schleifen verwenden willst, könntest du das Feld ausreichend groß machen, sagen wir int a[100000], und aufpassen, dass n niemals größer als diese 100 000 wird. Außerdem kann dir der Preprocessor mit der Anweisung #define helfen:
130
Kapitel 6 Schleifen
Sandini Bib For6.c
#include <stdio.h> #define N 1000 int main() { int i; int a[N]; int summe, min, max; for (i = 0; i < N; i++) a[i] = 1 + rand()%6; summe = 0; for (i = 0; i < N; i++) summe += a[i]; min = max = a[0]; for (i = 1; i < N; i++) { if (a[i] < min) min = a[i]; if (a[i] > max) max = a[i]; } printf("\n %d−mal gewuerfelt, min = %d, max = %d, summe = %d\n", N, min, max, summe); getchar(); return 0; }
10−mal gewuerfelt, min = 1, max = 6, summe = 29
In der Zeile #define N 1000
definieren wir eine so genannte symbolische Konstante. Wichtig: Solche Preprocessoranweisungen werden nicht mit einem Strichpunkt beendet. Bevor der Compiler den Programmtext zu sehen bekommt, geht der Preprocessor an die Arbeit und ersetzt im Programmtext überall dort, wo N wie eine Variable verwendet wird, dieses N durch die Zeichen 1000. Der Compiler bekommt also statt N die Konstante 1000 zu sehen. Deshalb ist int a[N]; in diesem Fall erlaubt. Es ist üblich, symbolische Konstanten mit Großbuchstaben zu schreiben. Mehr dazu in Anhang A.1.
6.8
Eingabeschleife
Eine sehr häufige Anwendung von Schleifen ist die Eingabeschleife. Das Programm wartet auf eine Eingabe (Text oder auch einen Mausklick). Wenn eine 6.8
Eingabeschleife
131
Sandini Bib
Eingabe erfolgt ist, wird diese verarbeitet. Nach getaner Arbeit kehrt das Programm zum Beginn der Schleife zurück und wartet auf die nächste Eingabe. Das kann so aussehen: Zoo.c
#include <stdio.h> int main() { int i; // begruesst wird nur einmal printf("\nWillkommen im Zoo!\n\n"); // Eingabeschleife, wird mit break verlassen while (1) { // Eingabemenue printf("Was moechtest du sehen?\n"); printf(" 1: Affe\n"); printf(" 2: Affenfloh\n"); printf(" 0: keine Lust mehr\n"); printf("\nGib die Nummer ein: "); scanf("%d", &i); // Bedingung fuer den Ausstieg if (i == 0) break; // die Tiere else if (i == 1) { printf("\n\n"); printf(" xxx \n"); printf(" doxob b \n"); printf(" xxx b \n"); printf(" xxxxx b \n"); printf(" xxxxx xx b \n"); printf(" x xxxx \n"); printf(" x xx xx \n"); printf("\n\n"); } else if (i == 2) { printf("\n\n"); printf(" . \n"); printf("\n\n"); } // der Default else printf("Wie bitte? Dieses Tier kenne ich nicht.\n\n"); } // fertig return 0; }
132
Kapitel 6
Schleifen
Sandini Bib
Willkommen im Zoo! Was moechtest du sehen? 1: Affe 2: Affenfloh 0: keine Lust mehr Gib die Nummer ein:
1
xxx doxob b xxx b xxxxx b xxxxx xx b x xxxx x xx xx
Was moechtest du sehen? 1: Affe 2: Affenfloh 0: keine Lust mehr Gib die Nummer ein:
0
Das Willkommen steht außerhalb der Schleife, damit es nur einmal zu Beginn des Programms angezeigt wird. Die Schleifenbedingung ist immer erfüllt, while (1)
aber die Schleife kann durch eine bestimmte Eingabe verlassen werden. Die Fallunterscheidung mit if und else kennt vier Fälle: 0 für Ausstieg mit break, 1 oder 2 für die Anzeige eines Tieres, und alle anderen Eingaben ergeben eine entsprechende Meldung als Default. Wenn es dir nicht gefällt, dass das Textfenster bei 0 einfach zuklappt, kannst du ja noch ein ›Auf Wiedersehen!‹ und zwei getchar vor dem return, aber nach dem Ende des Schleifenblocks einfügen.
6.8
Eingabeschleife
133
Sandini Bib
6.9
Das doppelte Schleifchen
In den Beispielen für Zählerschleifen haben wir die Zahlen ordentlich eine unter der anderen ausgegeben. Nebeneinander geht natürlich auch, wir müssen nur das Neuezeilezeichen weglassen. Eine Tabelle mit Reihen und Spalten erhalten wir mit ForFor0.c
#include <stdio.h> int main() { int i, j; for (j = 0; j < 10; j++) { for (i = 0; i < 10; i++) { printf(" %d*%d", j, i); } printf("\n"); } getchar(); return 0; }
0*0 1*0 2*0 3*0 4*0 5*0 6*0 7*0 8*0 9*0
0*1 1*1 2*1 3*1 4*1 5*1 6*1 7*1 8*1 9*1
0*2 1*2 2*2 3*2 4*2 5*2 6*2 7*2 8*2 9*2
0*3 1*3 2*3 3*3 4*3 5*3 6*3 7*3 8*3 9*3
0*4 1*4 2*4 3*4 4*4 5*4 6*4 7*4 8*4 9*4
0*5 1*5 2*5 3*5 4*5 5*5 6*5 7*5 8*5 9*5
0*6 1*6 2*6 3*6 4*6 5*6 6*6 7*6 8*6 9*6
0*7 1*7 2*7 3*7 4*7 5*7 6*7 7*7 8*7 9*7
0*8 1*8 2*8 3*8 4*8 5*8 6*8 7*8 8*8 9*8
0*9 1*9 2*9 3*9 4*9 5*9 6*9 7*9 8*9 9*9
Hier steht eine Schleife im Befehlsblock einer anderen. Für jeden Wert von j werden alle Werte von i durchlaufen. Es ist deutlich zu erkennen, dass i die Spalten zählt und j die Reihen. In jeder Spalte ist i konstant, in jeder Reihe ist j konstant. Für jeden Wert von j durchläuft i einmal die Zahlen von 0 bis 9. Entscheidend dafür ist, dass die innere Schleife jedes Mal von neuem mit i = 0 initialisiert wird. Mit for ist es schwer, diese Initialisierung zu vergessen, bei while kann das schon eher passieren. Was liefert das folgende doppelte Schleifchen?
134
Kapitel 6 Schleifen
Sandini Bib ForFor1.c
#include <stdio.h> int main() { int i, j; for (j = 0; j < 10; j++) { for (i = 0; i <= j; i++) { printf(" %d*%d", j, i); } printf("\n"); } getchar(); return 0; }
Erst denken, dann ausprobieren. Mit F8 und i und j unter Beobachtung wird es noch klarer, was hier vor sich geht. Wie verhalten sich break und continue in doppelten Schleifen? Ein break in der inneren Schleife beendet nur die innere Schleife, nicht aber die äußere. Du kannst mit break die momentane Schleife beenden, nicht aber aus mehreren verschachtelten Schleifen gleichzeitig ausbrechen. continue bezieht sich ebenfalls nur auf die nächstäußere Schleife. Ein Beispiel für break in einer doppelten Schleife begegnet uns in Kapitel 6.10.
6.10
Primzahlen
Eine Primzahl ist eine positive ganze Zahl, die nur durch 1 und sich selbst ohne Rest teilbar ist. Diese Definition enthält schon alles, was wir für einen Algorithmus brauchen. Wir müssen nur alle diese Zahlen durchprobieren. Genauer gesagt, gegeben eine Zahl zahl, probiere alle Zahlen i von 2 bis zahl − 1 durch. Wenn sich eine Zahl i findet, für die bei der Division kein Rest bleibt, wissen wir, dass es keine Primzahl sein kann, und wir brauchen die anderen Zahlen nicht mehr zu testen. Wenn sich kein solcher Teiler findet, handelt es sich bei zahl um eine Primzahl, denn wir haben ja die trivialen Teiler 1 und zahl von vornherein ausgeschlossen. Gesagt, getan:
6.10
Primzahlen
135
Sandini Bib Primzahlen0.c
#include <stdio.h> int main() { int i, zahl; for (zahl = 2; zahl < 500; zahl++) { for (i = 2; i < zahl; i++) if (zahl % i == 0) break; if (i == zahl) printf("%4d", zahl); } printf("\n"); getchar(); return 0; }
2 3 5 7 11 13 17 73 79 83 89 97 101 103 179 181 191 193 197 199 211 283 293 307 311 313 317 331 419 421 431 433 439 443 449
19 107 223 337 457
23 109 227 347 461
29 113 229 349 463
31 127 233 353 467
37 131 239 359 479
41 137 241 367 487
43 139 251 373 491
47 53 59 61 67 71 149 151 157 163 167 173 257 263 269 271 277 281 379 383 389 397 401 409 499
Und weil es so schön funktioniert, testen wir gleich alle Zahlen von 2 bis 499. Das ist die äußere Schleife in zahl. Die innere Schleife kann auf zweierlei Weise zu Ende gehen, durch die Schleifenbedingung i < zahl (kein Teiler gefunden) oder durch ein break wegen zahl % i == 0 (Teiler gefunden). Lass das Programm Schritt für Schritt laufen und versuche den nächsten Schritt zu erahnen! Wie kommt es, dass wir nach Beendigung der Schleife mit i == zahl diese beiden Fälle unterscheiden können? Weil die erste Zahl i, für die die Schleifenbedingung falsch wird, zahl ist. Wenn mir bei einer Schleife nicht ganz klar ist, was passiert, nehme ich mir einfach ein konkretes Beispiel her. Angenommen zahl ist 11. Kurz vor Schluss testen wir 9 < 11, kein Teiler, dann 10 < 11, kein Teiler, dann 11 < 11, stopp. Dies ist eine typische Situation für for-Schleifen. Nach Beendigung der Schleife ist die Zählervariable um 1 größer als beim letzten Durchgang im Schleifenblock. Die Variable i enthält nicht den letzten gültigen Wert, sondern den ersten ungültigen. Wie sollte es auch anders sein. Für 10 wird noch im Block gearbeitet, dann wird 10 auf 11 erhöht, dann führt 11 < 11 zum Ende der Schleife, und mit diesem Wert kommen wir unten an. Es ist immer angebracht, beide Endpunkte einer Schleife sorgfältig zu prüfen. Und Doppelschleifen müssen auch noch auf wechselseitige Beeinflussung überprüft werden. In der Tat, was geschieht für zahl gleich 2? Die innere Schleife 136
Kapitel 6 Schleifen
Sandini Bib
wird kein einziges Mal durchlaufen, aber weil die Initialisierung von i auf 2 trotzdem stattfindet, wird 2 korrekt als Primzahl ausgegeben. Denke einmal scharf nach, wie musst du die Schleifen ändern, damit 1 als Primzahl angegeben wird? zahl = 1 in der äußeren Schleife? Primzahlen spielen eine wichtige Rolle in der Verschlüsselungstechnik. Dabei wird ausgenutzt, dass es enorm aufwändig ist, große Zahlen in ihre Primfaktoren zu zerlegen. Die Primfaktorenzerlegung einer Zahl ist eindeutig, wenn man von der Anordnung der Faktoren absieht. Es kann nicht vorkommen, dass z. B. 3 bei einer Möglichkeit der Zerlegung auftritt und bei einer anderen nicht. (Warum? Die Antwort erfordert etwas Nachdenken.) Wie dem auch sei, hier ist ein Beispiel mit drei verschachtelten Schleifen: die innere Schleife führt den Primzahltest durch wie im letzten Beispiel und teilt zahl durch jeden neu gefundenen Teiler, so dass zahl immer kleiner wird, die mittlere Schleife wiederholt die innere Schleife, bis kein nicht trivialer Teiler mehr möglich ist wegen zahl gleich 1, und die äußere Schleife ist die mittlerweile vertraute Eingabeschleife.
6.10
Primzahlen
137
Sandini Bib Primzahlen1.c
#include <stdio.h> int main() { int i, zahl; while (1) { printf("Gib mir eine Zahl > 1: scanf("%d", &zahl); if (zahl <= 1) { printf("Na denn nicht.\n"); break; }
");
while (zahl > 1) { for (i = 2; i <= zahl; i++) { if (zahl % i == 0) { printf("%d", i); zahl = zahl / i; break; } } if (zahl > 1) printf(" * "); } printf("\n"); } getchar(); getchar(); return 0; }
Das Ergebnis sieht dann z. B. so aus: Gib mir 2 Gib mir 2 * 2 * Gib mir 17 * 71 Gib mir 1657 Gib mir Na denn
eine Zahl > 1:
2
eine Zahl 2 * 2 * 2 eine Zahl * 1657 eine Zahl
1657
> 1: 256 * 2 * 2 * 2 > 1: 1999999 > 1:
eine Zahl > 1: nicht.
1
Ich überlasse es dir, herauszufinden, wie die Schleifen im Detail funktionieren. Was passiert für die Eingabe 2 oder 11 oder 10? Per Hand durchrechnen und dann auch im Debugger ausprobieren! Auch die Ausgabe des * ist nicht völlig trivial. Wir könnten zwar mit jedem gefundenen Teiler gleich ein * ausgeben, 138
Kapitel 6 Schleifen
Sandini Bib
hätten am Ende dann aber ein * zu viel! Schleifenbinden will gelernt sein, aber keine Sorge, dass war beileibe nicht die letzte Schleife, die uns begegnen wird.
6.11
Zeit in Sekunden
Nachdem das letzte Beispiel dir vielleicht recht schwierig vorkam, folgt ein kleines Beispiel zur Zeitausgabe. Wir besprechen die Funktionen time und clock, siehe auch 8.7. Hier ist unser erstes Beispiel: Zeit0.c
#include <stdio.h> #include int main() { while (1) printf("%d\n", time(0)); return 0; }
Lass das Programm laufen und beende die Endlosschleife mit ein Schnappschuss von meinem Bildschirm:
Strg + C .
Hier ist
977133334 977133334 977133334 977133334 977133334 977133334 977133334 977133335 977133335 977133335 977133335 977133335
Die Zahlen sausen nach oben, nur ab und zu ändern sich die letzten Ziffern und die Zahl wird um eins größer. Der Funktionsaufruf time(0) liefert die Anzahl der Sekunden seit dem 1.1.1970, 0 Uhr, 0 Minuten, 0 Sekunden, mittlere Greenwich-Zeit. Klarer Fall, wir beobachten, wie der Computer die Sekunden zählt. Wenn du wolltest, könntest du jetzt auf die Sekunde genau ausrechnen, wann ich die Textausgabe für das Buch produziert habe. (Beziehungsweise, was die Uhr in meinem Computer für die momentane Zeit hielt.) Dann habe ich mich zurückgelehnt, zum Fenster rausgeguckt, nachgedacht und etwas später das Programm für die folgende Ausgabe laufen lassen: 6.11
Zeit in Sekunden
139
Sandini Bib
977133959 977133960 977133961 977133962 977133963 977133964 977133965 977133966
Wie habe ich es angestellt, dass das Programm mit der Ausgabe wartet, bis der Sekundenzähler auf die nächste Zahl gesprungen ist? Hier ist der Code: Zeit1.c
#include <stdio.h> #include int main() { int t = 0; while (1) { if (t != time(0)) { t = time(0); printf("%d\n", t); } } return 0; }
Erst wenn t != time(0) ist, also erst wenn die Variable t eine andere Zeit gespeichert hat, als time(0) liefert, wird die neue Zeit in t gespeichert und ausgegeben. Um die Sekunden seit Programmstart anzuzeigen, können wir die Startzeit speichern und von der momentanen Sekundenzahl abziehen:
140
Kapitel 6 Schleifen
Sandini Bib Zeit2.c
#include <stdio.h> #include int main() { int tstart = time(0); int t = 0; while (1) { if (t != time(0)) { t = time(0); printf("%d\n", t − tstart); } } return 0; }
Eine weitere nützliche Funktion ist clock. Der Funktionsaufruf clock() liefert die seit Programmstart verbrauchte CPU-Zeit. Das ist die Zeit, die der Mikroprozessor zur Ausführung dieses Programms gebraucht hat. Ignoriert wird die Zeit, die andere Programme, die vielleicht gleichzeitig liefen, benötigt haben. Um die Zeit in Sekunden zu erhalten, muss der Wert von clock durch CLK_TCK geteilt werden, z. B. t = clock() / CLK_TCK;
Die symbolische Konstante CLK_TCK wird in time.h definiert. Bei mir steht in CBuilder\Include\Time.h die Zeile #define CLK_TCK
1000.0
Der Name bedeutet ›clock ticks per second‹, also Uhrsignale pro Sekunde. Obwohl in BCB die Zahl CLK_TCK gleich 1000 ist, bedeutet das nicht, dass clock die Zeit auf eine tausendstel Sekunde (gleich eine Millisekunde) genau zählt. Der Hardwaretimer, auf dem diese Zeitmessung beruht, tickt gewöhnlich nur 18.2-mal pro Sekunde. Für Windowsanwendungen gibt es die Funktion GetTickCount, mit der man die Anzahl der Millisekunden seit dem Start von Windows abfragen kann.
6.12
Bisektion mit Schleife
Lass uns Zahlenraten spielen, diesmal mit einer Eingabeschleife. Als Erstes darfst du eine Zahl raten, die sich der Computer ausgedacht hat, später darf der Computer raten. Zur Abwechslung habe ich die Erklärung des Programms in die Kommentare verlegt: 6.12
Bisektion mit Schleife
141
Sandini Bib Zahlenraten0.c
/* Zahlenraten Demonstriert typische Fallunterscheidung mit if−else und eine while−Schleife BB 18.12.00 */ /* fuer den Preprocessor */ #include <stdio.h> #include <stdlib.h> #include /* hier geht es los */ int main() { int klein = 0; int gross = 100; int zahl; int meinezahl;
// // // //
untere Schranke obere Schranke die vermutete Zahl die zu ratende Zahl
/* Zahl zufaellig auswaehlen */ srand(time(0)); meinezahl = 1 + rand()%99; // meinezahl = 1, 2, ..., 99 /* lass den Nutzer wissen, worum es geht */ printf("Hallo, ich habe mir eine ganze Zahl > %d und < %d ausgedacht.\n", klein, gross); /* die Rateschleife, wird mit break verlassen */ while (1) { /* lass den Nutzer eine Zahl eingeben */ printf("Rate mal: "); scanf("%d", &zahl); /* Eingabe auswerten */ if (zahl == meinezahl) break; else if (zahl < meinezahl) printf("Zu klein! "); else if (zahl > meinezahl) printf("Zu gross! "); else printf("Unmoeglich.");
// Zahl geraten, verlasse while−Schleife
// diese Zeile wird nie erreicht
} /* die Zahl wurde geraten */ printf("Richtig!\n"); getchar(); getchar(); return 0; }
142
Kapitel 6
Schleifen
Sandini Bib
Das Programm enthält eine Endlosschleife zur Wiederholung des Spieles, die bei einer bestimmten Eingabe mit break verlassen wird. Der Computer denkt sich eine Zahl, der Spieler rät und wir entscheiden mit if-else, ob der Computer mit zu groß, zu klein und so weiter antwortet. Ausprobieren: Hallo, ich Rate mal: Zu klein! Zu klein! Zu gross! Zu gross! Zu gross! Zu gross! Richtig!
habe 5 Rate Rate Rate Rate Rate Rate
mir eine ganze Zahl > 0 und < 100 ausgedacht. mal: mal: mal: mal: mal: mal:
7 50 30 20 10 9
Wenn wir den Spieß umdrehen und den Computer eine Zahl des Spielers raten lassen, müssen wir dem Programm so etwas wie künstliche Intelligenz verleihen. Die einfachste Lösung wäre, den Computer jede Zahl einzeln probieren zu lassen. Ist es die 1? Größer. Ist es die 2? Größer. Ist es die 3? Größer. Ist es die 4? Größer. Ist es die 5? Größer. Ist es die 6? Größer. Ist es die 7? Größer. Ist es die 8? Größer. Ist es die 9? Größer. Ist es die 10? Größer. Ist es die 11? Größer. Ist es die 12? Größer. Ist es die 13? Größer. Ist es die 14? Größer. Ist es die 15? Größer. Meine Güte, mir ist schon beim Tippen langweilig geworden. Im Prinzip könntest du diese Strategie aber leicht mit einer Schleife programmieren. Wie bist du denn vorgegangen, als der Computer dich mit dem ersten Programm raten ließ? Hast du dich an Kapitel 5.9 und die Bisektion erinnert? Die nötigen Fallunterscheidungen in Kapitel 5.9 sahen recht verwickelt aus, und für Zahlen bis 100 ist es sicher keine gute Idee, den ganzen Entscheidungsbaum auszuschreiben. Aber weil wir immer wieder dieselbe Art von Frage stellen (›Ist die Zahl kleiner oder größer . . .‹), können wir unseren Algorithmus sehr elegant als Schleife schreiben. Und weil es so schön ist, spielen wir mit Zahlen bis 1000! Also los:
6.12
Bisektion mit Schleife
143
Sandini Bib Zahlenraten1.c
/* Rate eine Zahl mit Bisektion Netter Algorithmus Zeigt typische Fallunterscheidung mit if−else und zwei while−Schleifen BB 23.8.00 */ #include <stdio.h>
/* hier geht es los */ int main() { int klein = 0; int gross = 1000; int zahl; int c;
// // // //
untere Schranke obere Schranke die vermutete Zahl die eingetippte Antwort
/* lass den Nutzer wissen, worum es geht */ printf("\nDenk dir eine ganze Zahl > %d und < %d.\n", klein, gross); printf("Lass mich raten.\n"); printf("Bitte antworte mit\n"); printf(" > wenn deine Zahl groesser ist,\n"); printf(" < wenn deine Zahl kleiner ist,\n"); printf(" = wenn ich deine Zahl geraten habe.\n"); /* die Rateschleife, wird mit break verlassen */ while (1) { /* die naechste Vermutung soll genau zwischen den Schranken liegen */ zahl = (klein + gross)/2; /* der Nutzer soll uns sagen, wie wir mit unserer Zahl liegen */ printf("Zwischen %4d und %4d ... ist es %4d? ", klein, gross, zahl); c = getchar(); while (getchar() != ’\n’); // leere Eingabepuffer /* Eingabe auswerten */ if (c == ’=’) break; else if (c == ’<’) gross = zahl; else if (c == ’>’) klein = zahl; else printf("Wie bitte?\n");
// Zahl geraten, verlasse while−Schleife // kleiner? dann muss die obere Schranke runter // groesser? dann muss die untere Schranke hoch // wenn c nicht den Wert = oder < oder > hat
} /* wir haben die Zahl geraten */ printf("AHA! Deine Zahl ist %d.\n", zahl); getchar(); return 0; }
144
Kapitel 6
Schleifen
Sandini Bib
Denk dir eine ganze Zahl > 0 und < 1000. Lass mich raten. Bitte antworte mit > wenn deine Zahl groesser ist, < wenn deine Zahl kleiner ist, = wenn ich deine Zahl geraten habe. Zwischen 0 und 1000 ... ist es 500? Zwischen 0 und 500 ... ist es 250? Zwischen 250 und 500 ... ist es 375? Zwischen 375 und 500 ... ist es 437? Zwischen 375 und 437 ... ist es 406? Zwischen 375 und 406 ... ist es 390? Zwischen 390 und 406 ... ist es 398? Zwischen 398 und 406 ... ist es 402? Zwischen 398 und 402 ... ist es 400? AHA! Deine Zahl ist 400.
< > > < < > > < =
Der Computer rät eine Zahl in der Mitte zwischen einer unteren und oberen Schranke (anfangs 0 und 1000): zahl = (klein + gross)/2;
Falls klein + gross nicht gerade ist, ergibt die ganzzahlige Division durch 2 den auf die nächste ganze Zahl abgerundeten Mittelwert. Der Computer bekommt dann von uns gesagt, ob die Zahl in der unteren oder oberen Hälfte liegt. Dazu lesen wir ein einzelnes Zeichen von der Tastatur. Erinnerst du dich, was getchar genau macht? Jeder Aufruf von getchar liefert ein Zeichen aus dem Tastaturpuffer, aber wenn der Tastaturpuffer leer ist, wartet das Programm, bis die Eingabe von neuen Zeichen mit einem Neuezeilezeichen beendet wurde, und diese Zeichen werden dann wiederum einzeln von getchar abgeliefert. Im Beispiel verwenden wir c = getchar();
um das erste Zeichen im Tastaturpuffer in c zu speichern. Dann leeren wir den Tastaturpuffer, indem wir mit while (getchar() != ’\n’);
so lange Zeichen lesen, bis das Neuezeilezeichen angibt, dass die Eingabe mit \n beendet wurde. Auf diese Weise stellen wir sicher, dass das nächste getchar wieder beim ersten Zeichen der nächsten Zeile anfängt. Ausprobieren, wenn du mehr als ein Zeichen eingibst, spielt nur das erste eine Rolle. Weil am Ende des Programms jetzt keine überflüssigen \n im Tastaturpuffer stehen, benötigen wir deshalb auch nur noch ein einziges getchar, um das Textfenster offen zu halten. Unter der Überschrift ›Eingabe auswerten‹ findest du eine typische Fallunterscheidung. Überzeuge dich davon, dass alles ganz logisch ist. Bei = ist die Zahl 6.12
Bisektion mit Schleife
145
Sandini Bib
erraten und die Eingabeschleife wird mit break beendet. Falls ein falsches Zeichen eingeben wurde, sagt der Computer ›Wie bitte?‹ und wartet auf eine neue Eingabe. Entscheidend sind die Fälle c == ’<’ und c == ’>’. Wenn die Zahl zahl, die der Computer geraten hat, zu klein ist, setzt er die obere Schranke auf diese Zahl runter. Die tatsächliche Zahl liegt dann nach wie vor im Intervall zwischen klein und gross. Aber dieses Intervall ist um die Hälfte kleiner geworden! Dadurch, dass der Computer immer eine Zahl in der Mitte zwischen klein und gross rät, teilt er die möglichen Antworten in zwei Hälften, und die Angabe kleiner oder größer erlaubt es ihm, die Hälfte der Möglichkeiten auszuschließen. Dieser Algorithmus ist verblüffend effizient. Für Zahlen zwischen 0 und 1000 werden maximal zehn Schritte benötigt, denn wenn man 1024 zehnmal durch 2 teilt, erhält man 1. Nach zehn Schritten ist also nur noch eine Möglichkeit übrig, wenn die Zahl nicht schon vorher geraten wurde. Vielleicht kennst du die Geschichte vom Reiskorn und dem Schachbrett? Ein Mann, der seinem König einen großen Dienst erwiesen hatte, durfte sich als Lohn dafür etwas wünschen. Und so wünschte er sich auf dem ersten Feld eines Schachbretts ein Korn, auf dem zweiten zwei Körner, dann vier, immer das Doppelte bis zum 64. Feld. Die Zahl wächst ›exponentiell‹ mit der Anzahl der Felder, so viele Körner gibt es auf der ganzen Welt nicht! Das war frech und soll ihn den Kopf gekostet haben. Die Bisektion lässt dieses Spielchen rückwärts ablaufen. Statt eine Zahl zu verdoppeln, halbieren wir bei jedem Schritt unsere Möglichkeiten. Das geht genauso rasant, aber in die andere Richtung. Implosion statt Explosion.
6.13
Grafik – die hundertste Wiederholung
Wie war das gleich noch mit den Zufallsellipsen in Kapitel 5.7? Aus einem einzigen, vereinsamten Rechteck machen wir mit einer Schleife mir nichts dir nichts satte 100:
146
Kapitel 6 Schleifen
Sandini Bib ZufallsRechtecke.c WinHallo.cpp
#include <windows.h> void malen(HDC hdc) { HBRUSH hbrush, hbalt; int max = 250; int i; for (i = 0; i < 100; i++) { hbrush = CreateSolidBrush(RGB(rand()%256, rand()%256, rand()%256)); hbalt = SelectObject(hdc, hbrush); Rectangle(hdc, rand()%max, rand()%max, rand()%max, rand()%max); SelectObject(hdc, hbalt); DeleteObject(hbrush); } }
Oder wie wäre es mit einer Million bunten Punkten? Hier habe ich meine Bildschirmdaten eingetragen, damit bei maximiertem Fenster alles vollgepixelt wird: ZufallsPixel.c WinHallo.cpp
#include <windows.h> void malen(HDC hdc) { int xmax = 1024, ymax = 768; int i; for (i = 0; i < 1000000; i++) SetPixel(hdc, rand()%xmax, rand()%ymax, RGB(rand()%256, rand()%256, rand()%256)); }
Beides finde ich hübsch:
Grafik – die hundertste Wiederholung
147
Sandini Bib
6.14
Linien aus Pixeln
Mit einer Schleife und SetPixel kannst du Geraden zeichnen: PixelLinie0.c WinHallo.cpp
#include <windows.h> void malen(HDC hdc) { int x; for (x = 0; x < 200; x++) SetPixel(hdc, x, 100, 0); }
Die x-Koordinate durchläuft Werte von 0 bis 199, während die y-Koordinate bei 100 konstant bleibt. Das ergibt eine waagrechte (horizontale) gerade Linie:
Wie habe ich die gepunktete Linie im rechten Bild erzeugt? Einfach x++ durch x += 5 ersetzen. Auch das folgende Programm erzeugt eine gepunktete gerade Linie:
148
Kapitel 6 Schleifen
Sandini Bib PixelLinie1.c WinHallo.cpp
#include <windows.h> void malen(HDC hdc) { int x, y; for (x = 0; x < 200; x++) { y = 3*x; SetPixel(hdc, x, y, 0); } }
Hier wird x mit x++ in Einerschritten hochgezählt und die Punkte sind in xRichtung um jeweils ein Pixel versetzt. Aber wenn sich x um eins ändert, ändert sich y um drei. Wie du siehst, ist es gar nicht so einfach, durchgezogene Linien in beliebigem Winkel mit einzelnen Pixeln zu zeichnen. Eine durchgezogene Linie für y = 3*x;
erhältst du, wenn du x = y/3;
zeichnest, indem du y als Schleifenvariable mit y++ verwendest und x aus y berechnest. Durch die ganzzahlige Division bekommst du horizontale Linienstückchen aus 3 Pixeln, weil z. B. 3/3, 4/3 und 5/3 alle gleich 1 sind. Ausprobieren. Auf jeden Fall ist es einfacher, die Grafikfunktion LineTo des Windows SDK zu verwenden. Zudem kannst du davon ausgehen, dass eine solche Bibliotheksfunktion wesentlich schneller ist, weil sie in Maschinensprache optimiert wurde. Womöglich werden Linien sogar von deiner Grafikhardware beschleunigt. 6.14
Linien aus Pixeln
149
Sandini Bib
6.15
Schachbrett
Ein typisches Beispiel für eine doppelte Schleife ist das Schachbrettmuster: ForFor2.c WinHallo.cpp
#include <windows.h> void malen(HDC hdc) { HBRUSH hbrot, hbgruen, hbalt; int i, j; int x, x0 = 16, dx = 26; int y, y0 = 8, dy = 26; hbrot = CreateSolidBrush(RGB(255,0,0)); hbgruen = CreateSolidBrush(RGB(0,255,0)); hbalt = SelectObject(hdc, hbrot); for (i = 0; i < 8; i++) { for (j = 0; j < 8; j++) { if ((i+j)%2) SelectObject(hdc, hbrot); else SelectObject(hdc, hbgruen); x = i*dx + x0; y = j*dy + y0; Rectangle(hdc, x, y, x+dx, y+dy); } } SelectObject(hdc, hbalt); DeleteObject(hbrot); DeleteObject(hbgruen); }
150
Kapitel 6 Schleifen
Sandini Bib
Wie du an Rectangle(hdc, x, y, x+dx, y+dy);
siehst, malen wir Rechtecke, deren rechte obere Ecke die Koordinaten x und y haben. Die Breite der Rechtecke ist dx, die Höhe ist dy Um ein Schachbrett zu erhalten, lassen wir i von 0 bis 7 laufen, und für jedes i lassen wir j ebenfalls von 0 bis 7 laufen. Denke ein wenig darüber nach, warum (i+j)%2
die Bedingung ist, die uns für jede Reihe und Spalte die richtige Farben für das Schachbrettmuster auswählt. Die Koordinaten für die Rechtecke berechnen wir mit x = i*dx + x0; y = j*dy + y0;
Die Variablen x0 und y0 geben an, um wie viel das gesamte Schachbrett verschoben werden soll. Ausprobieren. Mit dem Debugger kannst du dir ansehen, wie ein Quadrat nach dem anderen gemalt wird. (Setze den Textcursor im Editor auf die Zeile mit dem RectangleBefehl und halte F4 gedrückt). Dazu solltest du auch i, j, x und y mit Strg + F5 in das Beobachtungsfenster setzen. Vergiss nicht, vor weiteren Experimenten das Programm mit Strg + F2 zu verlassen. Unbedingt ausprobieren! Bleibt noch anzumerken, dass bei mir auf dem Bildschirm die Trennungslinien zwischen den roten und grünen Quadraten verschieden dick sind. Das liegt daran, dass grüne und rote Pixel auf einem typischen Farbbildschirm ein kleines bisschen gegeneinander versetzt sind. Farben werden ja sowieso im Allgemeinen aus roten, grünen und blauen Pixeln zusammengemischt, und weil die Pixel dieser Grundfarben gegeneinander verschoben sind, ist immer eine gewisse Unschärfe vorhanden. Wenn du im Programmbeispiel statt Grün, RGB(0,255,0), 6.15
Schachbrett
151
Sandini Bib
ein dunkles Rot verwendest, RGB(150,0,0), sehen die Zwischenräume gleichmäßig breit aus. Es werden nur noch die roten Pixel verwendet, die gleichmäßig verteilt sind, und das Schachbrettmuster kommt durch die unterschiedliche Helligkeit der Pixel zustande.
6.16
Histogramme
In diesem Beispiel wollen wir mehrere Schleifen kombinieren, um die Verteilung von Zufallszahlen grafisch sichtbar zu machen. Betrachte als Erstes Wuerfeln0.c WinHallo.cpp
#include <windows.h> void malen(HDC hdc) { int x, y, i, zz; int dx = 40; int n[6]; for (i = 0; i < 6; i++) n[i] = 0; for (i = 0; i < 1000; i++) { zz = rand()%6; n[zz] += 1; } for (i = 0; i < 6; i++) { x = i*dx; y = n[i]; Rectangle(hdc, x, 0, x+dx+1, y); } }
Wie in 3.0 besprochen, definiert int n[6]; ein Feld aus 6 ganzen Zahlen, auf die wir mit Indizes 0, 1, 2, 3, 4, 5 zugreifen können. Kein Problem mit Schleifen. Mit for (i = 0; i < 6; i++) n[i] = 0;
setzen wir alle 6 Elemente des Feldes n auf 0. Die Schleife in for (i = 0; i < 1000; i++) { zz = rand()%6; n[zz] += 1; }
152
Kapitel 6
Schleifen
Sandini Bib
durchlaufen wir 1000-mal. Wir berechnen 1000-mal eine Zufallszahl zz, die die Werte 0, 1, 2, 3, 4, 5 annehmen kann. Und jetzt kommt der entscheidende Schritt des Programms. Mit n[zz] += 1;
zählen wir 1 zum Element n[zz] dazu. Das ist, als ob du eine Strichliste beim Würfeln führst. Wenn du eine 0 gewürfelt hast, erhöhst du n[0] um eins (ein Strich kommt dazu). Wenn du eine 1 gewürfelt hast, erhöhst du n[1] um 1. Und so weiter. Um bei 0 mit dem Zählen anzufangen, haben wir als Erstes alle Elemente von n auf 0 gesetzt. Nachdem 1000-mal gewürfelt und gezählt wurde, geben wir das Ergebnis grafisch aus. Dazu malen wir sechs Rechtecke mit for (i = 0; i < 6; i++) { x = i*dx; y = n[i]; Rectangle(hdc, x, 0, x+dx+1, y); }
Die Rechtecke sind alle gleich breit, nämlich dx Pixel. Ausprobieren: Was geschieht, wenn du im Rectangle-Befehl statt x+dx+1 z. B. x+dx−5 oder nur x+dx verwendest? Die y-Koordinaten der Rechtecke gehen von 0 bis n[i], das heißt ihre Länge nach unten gibt an, wie oft eine bestimmte Zahl gewürfelt wurde. Das sieht dann z. B. so aus:
Wenn jede Zahl genau gleich oft vorkäme, müsste jedes Rechteck gerade 1000/6mal vorkommen, also ziemlich genau 133-mal. Die Rechtecke wären dann genau 133 Pixel lang. Aber weil beim Würfeln die Ergebnisse schwanken, ändert sich auch die Länge der Rechtecke von Mal zu Mal. Wenn du die Fenstergröße änderst, wird neu gewürfelt und du bekommst einen Eindruck davon, wie sehr das Ergebnis selbst bei 1000-mal würfeln schwankt. Ein solches Diagramm aus Rechtecken nennt man auch Balkendiagramm oder Histogramm. Lass uns das Programm noch verbessern: 6.16
Histogramme
153
Sandini Bib Wuerfeln1.c WinHallo.cpp
#include <windows.h> #include <stdio.h> #define WUERFEL 2 #define AUGEN 6 #define MAXWURF (WUERFEL*AUGEN) void malen(HDC hdc) { char text[1000]; int x, y, i, j, zz; int dx = 40; int n[MAXWURF+1]; int nmal = 100000; for (i = 0; i <= MAXWURF; i++) n[i] = 0; for (i = 0; i < nmal; i++) { zz = 0; for (j = 0; j < WUERFEL; j++) zz += 1 + rand()%AUGEN; n[zz] += 1; } for (i = WUERFEL; i <= MAXWURF; i++) { x = (i−WUERFEL)*dx; y = 1000*n[i]/nmal; y = 200−y; Rectangle(hdc, x, 200, x+dx+1, y); sprintf(text, "%d", i); TextOut(hdc, x+dx/3, 200, text, strlen(text)); } if (WUERFEL == 1) sprintf(text, "%d mal mit einem Wuerfel mit %d Augen gewuerfelt", nmal, AUGEN); else sprintf(text, "%d mal mit %d Wuerfeln mit %d Augen gewuerfelt", nmal, WUERFEL, AUGEN); TextOut(hdc, 5, 5, text, strlen(text)); }
Erst wollen wir das Programm besprechen und dann ein paar Experimente mit verschiedenen Würfeln machen. Zu Beginn des Programms definieren wir eine symbolische Konstante WUERFEL für die Anzahl der Würfel, und eine Konstante AUGEN für die Anzahl der Augen pro Würfel (ein Sechser-Würfel hat sechs Augen). Diese Konstanten dürfen wir in der Definition einer Konstanten MAXWURF verwenden, die wir später in der Größenangabe des Feldes n verwenden wollen. 154
Kapitel 6 Schleifen
Sandini Bib
Wir haben auch eine gewöhnliche Variable nmal eingeführt, die angibt, wie oft mit WUERFEL Würfeln gewürfelt wird. Schöner Zungenbrecher. Findest du die doppelte Schleife, in der dies getan wird? Hier verwenden wir jetzt 1 + rand()%AUGEN, so dass, falls AUGEN = 6, Zufallszahlen von 1 bis 6 erzeugt werden. Damit die Rechtecke in der Anzeige nicht zu groß werden, setzen wir nicht wie vorher y = n[i], sondern y = 1000*n[i]/nmal;
Als weiteren Trick setzen wir y = 200 − y, damit die Rechtecke nicht nach unten, sondern nach oben größer werden. Denn −y geht in die entgegengesetzte Richtung von y. Die 200 Pixel geben wir auch im Rectangle-Befehl an, damit von y = 200 nach oben gezeichnet wird. Überleg dir einfach mal, was bei y gleich 0, 50 und 150 passiert. Des Weiteren habe ich die Ausgabe mit etwas Text verschönert. Unter jedem Rechteck geben wir mit TextOut den Index des Feldes aus, der ja zugleich die Summe des Wurfs angibt. Zum Schluss schreiben wir noch eine Zeile, die angibt, wie gewürfelt wurde. Mit if und else unterscheiden wir, ob Würfel in Einzahl oder Mehrzahl geschrieben wird. Der Compiler gibt eine Warnung aus. Weil WUERFEL eine Konstante ist, wird immer nur einer der beiden Fälle zutreffen. Lass die Würfel rollen! Wenn du zwei Sechser-Würfel nimmst, aber nur 120-mal würfelst, ist das Ergebnis z. B.
6.16
Histogramme
155
Sandini Bib
Bei so wenigen Würfen ist noch nicht klar, wie das durchschnittliche Ergebnis aussieht. Aber bei nmal = 100000 sieht das Bild ziemlich genau so aus:
Bei einem Würfel sind alle Zahlen gleich wahrscheinlich, aber bei zwei Würfeln ist das anders. Denn für eine 2 müssen beide Würfel die 1 zeigen, während es für die 3 schon zwei Möglichkeiten gibt, und so weiter: 2 = 1+1 3 = 1+2 = 2+1 4 = 1+3 = 2+2 = 3+1 5 = 1+4 = 2+3 = 3+2 = 4+1 6 = 1+5 = 2+4 = 3+3 = 4+2 = 5+1 7 = 1+6 = 2+5 = 3+4 = 4+3 = 5+2 = 6+1 8 = 2+6 = 3+5 = 4+4 = 5+3 = 6+2 9 = 3+6 = 4+5 = 5+4 = 6+3 10 = 4+6 = 5+5 = 6+4 11 = 5+6 = 6+5 12 = 6+6
Hier habe ich alle Möglichkeiten aufgeschrieben, die Zahlen von 2 bis 12 mit zwei Sechser-Würfeln zu erzielen. Von 2 bis 7 wird die Anzahl der Möglichkeiten jeweils um eins größer, während von 7 bis 12 die Anzahl um jeweils 1 abnimmt. Und wie du siehst, spiegelt unser Würfelprogramm diese ›Dreiecksform‹ wider, wenn wir nur oft genug würfeln! Was passiert bei drei Sechser-Würfeln? Kannst du das Ergebnis mit Papier und Bleistift vorhersagen? Unser Programm malt
156
Kapitel 6 Schleifen
Sandini Bib
Die Rechtecke ergeben keinen dreieckigen Berg, sondern einen abgerundeten Berg. Je mehr Würfel du nimmst, desto runder der Berg, und desto ähnlicher wird unser Balkendiagramm der so genannten ›Glockenkurve‹. Zurzeit kannst du diese Glockenkurve noch auf dem Zehnmarkschein finden. Der Berg wird breiter, grob gesprochen, wenn du statt drei 6er-Würfeln zwei 9er-Würfel oder einen 18er-Würfel nimmst. Ausprobieren. Der Berg wird schmaler, wenn du statt drei 6er-Würfeln sechs 3er-Würfel nimmst oder im Extremfall achtzehn 1er-Würfel. Zum Abschluss zeige ich dir ein Histogramm für zwanzig 2er-Würfel:
Du kennst keine 2er-Würfel? Doch, du kannst eine Münze werfen. Wie dem auch sei, solltest du irgendwann einmal in einem deiner Programme eine interessantere Zufallsverteilung brauchen als die Gleichverteilung, kennst du jetzt einige Alternativen.
6.17 Endlich haben wir die Kontrolle über den Programmablauf übernommen. Wir können anhand von Schleifenbedingungen entscheiden, ob ein Programmteil keinmal, einmal, viele Male oder unendlich oft ausgeführt wird! (Na ja, eben so lange, wie der Computer überhaupt läuft.) Mit Schleifen fängt der Programmierspaß erst so richtig an! Zur Erinnerung: Die folgenden drei Zählerschleifen drucken alle die Ziffern 0, 1, 2: for (i = 0; i < 3; i++) { printf("%d\n", i); } i = 0; while (i < 3) { printf("%d\n", i); i++; } i = 0; do { printf("%d\n", i);
6.17
157
Sandini Bib i++; } while (i < 3);
Wenn in diesen Beispielen die Initialisierung in i = 10 umgeändert wird, dann drucken die for- und while-Schleifen nichts und die do-Schleife einmal die 10. Mit break kann eine Schleife beendet und zum nächsten Befehl nach der Schleife gesprungen werden. Mit continue wird zum Anfang der Schleife gesprungen und der nächste Durchgang begonnen. Gerade bei Schleifen ist die Schritt-für-Schritt-Methode im Debugger sehr hilfreich. Mit Strg + F5 kannst du dir die Werte der Zählervariablen anzeigen lassen. Aber niemand sagt, dass du alles im Kopf oder mit dem Debugger analysieren musst! Mir hilft es, bei verzwickten Fällen ein paar konkrete Beispiele per Hand mit Papier und Bleistift zu überprüfen.
6.18 1. Das Count-down-Beispiel läuft sehr flott. Die meiste Zeit verbringt die Schleife jedoch mit der printf-Anweisung. Stoppe die Zeit für 100 000 Iterationen (Schleifendurchläufe) mit time oder clock (Beispiel 6.11). Dann entferne das \n und stoppe wieder die Zeit. Es kostet bei mir ei-
nige Zeit, den Bildschirm nach oben zu schieben. Schließlich entferne die printf-Anweisung und stoppe die Zeit. Vielleicht musst du jetzt mehr
Schleifendurchläufe einplanen. Wenn du verschiedene Operationen wie Addition und Multiplikation von Ints und Doubles in die Schleife einsetzt, kannst du bestimmen, was mehr und was weniger Zeit benötigt. 2. Erweitere das Beispiel mit der Summe. Frage nach der Zahl n, bis zu der
die Zahlen addiert werden sollen. Vergleiche verschiedene Ergebnisse mit der Formel summe2 = n * (n + 1) / 2;
die du auch am Bildschirm ausgibst. Das müsste dasselbe Ergebnis liefern! Die Moral von der Geschichte: Oft gibt es einfache Algorithmen, die man sofort programmieren kann, wie hier für die Summe. Manchmal gibt es aber auch verblüffende Vereinfachungen. Die Formel ist natürlich viel schneller berechenbar als eine lange Schleife bis 10 000. 3. Erweitere den Zoo. Setze drei Affen untereinander, indem du die printfAnweisungen in den Befehlsblock einer for-Schleife schreibst. Den langen Hals einer Giraffe kannst du auch mit einer for-Schleife programmieren. 4. Schreibe ein Programm, das eine schräge Linie aus Sternchen (*) im Textfen-
ster ausgibt. In der Ausgabe gibst du dazu vor dem Sternchen eine wachsende 158
Kapitel 6 Schleifen
Sandini Bib
Anzahl von Leerzeichen aus (siehe das Beispiel zum doppelten Schleifchen). Im nächsten Schritt soll das Programm eine Zickzacklinie ausgeben, also erst eine Linie nach rechts, dann nach links, und immer so weiter. Du solltest das Sternchen hin- und herflitzen sehen, weil die Zeilen sich ja nach oben bewegen. Wenn das bei dir zu schnell läuft, warte mit getchar nach jedem Sternchen auf die Eingabetaste. Und wenn dir so was Spaß macht, kannst du noch per Zufallszahl bestimmen, wie viele Plätze sich das Sternchen in eine Richtung bewegt, bevor es seine Richtung umkehrt. 5. Fallen dir noch weiter Grafikmotive ein, die Wiederholungen enthalten? An-
regungen findest du z. B. in den Kapiteln 4.8 und 4.9. Male nicht 5, sondern 100 farbige Ellipsen. Male eine Zielscheibe aus 500 bunten konzentrischen Ringen. Ändere dein Zielscheibenprogramm ab, so dass alle Ringe dieselbe Farbe haben, aber mit nach innen abnehmender Helligkeit. Wenn du jetzt den Mittelpunkt von Ring zu Ring etwas verschiebst, sieht das fast wie ein Tunnel aus. Wenn du die Ellipsen mit einem dicken, farbigen Stift malst, das Innere aber durchsichtig lässt, kannst du den Tunnel noch schärfer abbiegen lassen. 6. Schreibe ein Programm, das wie bei einem richtigen Würfel verschieden viele Punkte anzeigt. Du könntest clock oder GetTickCount verwenden, um die
Anzeige eine gewisse Zeit einzufrieren, bevor eine neue Zahl angezeigt wird. Das könnte aussehen, als ob der Würfel erst schnell und dann immer langsamer neue Augen zeigt.
6.18
159
Sandini Bib
Sandini Bib
7 Funktionen 7.0 7.1 7.2 7.3 7.4 7.5 7.6 7.7 7.8 7.9
Funktionen Funktion mit einem Argument Funktion mit Rückgabewert Funktion mit Argumenten und Rückgabewert Prototypen von Funktionen Headerdateien und Programmaufbau Lokale Variable Externe Variable Statische und automatische Variablen Statische Funktionen und externe Variablen
162 165 165 167 169 170 173 175 177 179
7.10
Zufallszahlen selbst gemacht
180
7.11
Rekursion
181
7.12
Rekursion mit Grafik
184
7.13
Labyrinth
187
7.14
202
7.15
203
Sandini Bib
Die meisten Programmieraufgaben lassen sich auf natürliche Weise in Teilaufgaben zerlegen. In C geschieht dies mit Funktionen. Ein C-Programm besteht aus einer (main!) oder mehreren Funktionen. Diese können über mehrere Dateien verteilt sein, die getrennt kompiliert werden. Oft ist es besser, viele kleine statt weniger großer Funktionen zu schreiben. Funktionen sind praktisch, weil sie Programmteile wiederverwendbar machen. Statt dieselben Zeilen Code an verschiedenen Stellen zu wiederholen, verpackst du sie in einer Funktion und rufst diese mehrmals auf. Ein gutes Beispiel sind die Funktionen in den Standardbibliotheken. Funktionen sind auch deshalb praktisch, weil sie verschiedene Aufgaben sauber voneinander trennen können. Im aufrufenden Programm kannst du dich mit Befehlen wie ›print‹ oder ›erzeuge Zufallszahl‹ auf die eigentliche Aufgabe konzentrieren, weil du von einer gut gemachten Funktion nur ihre ›Funktion‹, nicht aber ihr Innenleben kennen musst. In der Funktion selbst kannst du dich dann auf ein eingeschränktes Problem konzentrieren und die Außenwelt ignorieren. Ein wichtiger Punkt dabei ist, wie eine Funktion Daten mit dem aufrufenden Programm austauscht. Dazu gehört auch, dass man externe und lokale Variablen definieren kann. Externe Variablen sind von allen Funktionen aus zugänglich, während auf lokale Variablen nur innerhalb ihres Blocks zugegriffen werden kann. In diesem Kapitel besprechen wir, wie du deine eigenen Funktionen schreiben und verwenden kannst.
7.0
Funktionen
Anhand der Funktion main haben wir schon in Kapitel 1 diskutiert, wie die Definition einer Funktion aussieht. Im Allgemeinen geht das so: Datentyp Funktionsname(Argumente) {
Befehle }
Dabei wird den Befehlen im Block zwischen { } der Name Funktionsname zugewiesen. Im folgenden Beispiel definieren wir eine neue Funktion namens ansage, die wir in main aufrufen, um einen Text auszugeben:
162
Kapitel 7 Funktionen
Sandini Bib Funktion0.c
#include <stdio.h> void ansage(void) { printf("Hier spricht der Captain.\n"); } int main() { ansage(); getchar(); return 0; }
Hier spricht der Captain.
Als Erstes definieren wir die Funktion ansage mit void ansage(void) { printf("Hier spricht der Captain.\n"); }
Wenn eine Funktion keine Argumente annimmt, schreibt man void ( ›leer‹, ›nichts‹) in die Argumentenliste. Und wenn keine Daten zurückgeliefert werden, schreibt man als Datentyp ebenfalls void. Nach der Definition der Funktion ansage folgt die Definition der Funktion main. In main rufen wir mit ansage();
unsere selbst gemachte Funktion auf. Beim Aufruf schreiben wir kein void, sondern schreiben einfach keine Argumente zwischen die Klammern (). So haben wir das auch bisher mit getchar() gemacht. Kannst du im Programmtext verfolgen, wie das Programm abläuft? Lasse das Programm im Debugger laufen. Wenn du wiederholt F8 drückst, wird die Funktion main Zeile für Zeile ausgeführt. Bei ansage(); erscheint der Text und der Cursor springt in die Zeile getchar();. Den Debuggerbefehl ›nächster Befehl‹ gibt es in BCB in zwei Versionen. Mit F8 (Gesamte Routine) werden Funktionen als ein Schritt ausgeführt, aber mit F7 (Einzelne Anweisung) wird in eine Funktion hineingesprungen. Von der Zeile mit dem Funktionsaufruf ansage() springt die Anzeige zur Definition der Funktion, void ansage(void). Nachdem die Befehle in ansage ausgeführt worden sind, springt die Anzeige auf die Zeile nach dem Funktionsaufruf. Das klingt 7.0
Funktionen
163
Sandini Bib
komplizierter, als es ist, einfach ausprobieren. Starte unser Beispiel mit F8 und dann drücke wiederholt F7 . Oder klicke die entsprechenden Knöpfe unterhalb der BCB-Menüleiste. Wenn eine Funktion erst mal definiert ist, können wir sie auch mehrmals aufrufen: Funktion1.c
#include <stdio.h> void ansage(void) { printf("Hier spricht der Captain.\n"); } int main() { ansage(); ansage(); ansage(); getchar(); return 0; }
Hier spricht der Captain. Hier spricht der Captain. Hier spricht der Captain.
Das ist auf jeden Fall kürzer, als dreimal den printf-Befehl zu wiederholen, und wenn du die Ansage ändern möchtest, brauchst du das nur an einer Stelle zu tun.
164
Kapitel 7 Funktionen
Sandini Bib
7.1 Funktion mit einem Argument Wir wissen schon, dass Funktionen mit Argumenten aufgerufen werden können. So definieren wir eine Funktion mit genau einem Argument: Funktion2.c
#include <stdio.h> void warp(int i) { printf("Geschwindigkeit Warp %d.\n", i); } int main() { warp(5); getchar(); return 0; }
Geschwindigkeit Warp 5.
Weil die Funktion warp mit warp(5) aufgerufen wird, wird die 5 in die Variable i kopiert. In der Funktion warp kann dann i wie eine gewöhnliche Variable verwendet werden. Das ist, als ob wir zu Beginn des Befehlsblocks von ansage die Zeile int i; geschrieben hätten, außer dass bei Programmablauf eben diese Variable i gleich mit der Zahl 5 initialisiert wird.
7.2 Funktion mit Rückgabewert Eine Funktion kann mit dem Befehl return Zahlen zurückliefern:
7.1 Funktion mit einem Argument
165
Sandini Bib Funktion3.c
#include <stdio.h> int warpmax(void) { int wmax; int pi = 3; int daumen = 4; wmax = pi * daumen − 5; return wmax; } void warp(int i) { int imax; imax = warpmax(); printf("Geschwindigkeit Warp %d.\n", i); printf("Hoechstgeschwindigkeit ist Warp %d.\n", imax); } int main() { warp(5); getchar(); return 0; }
Geschwindigkeit Warp 5. Hoechstgeschwindigkeit ist Warp 7.
Wir werden immer mutiger und rufen in main die Funktion warp auf, die wiederum die Funktion warpmax aufruft. Die Definition der Funktion warpmax beginnt mit int warpmax(void)
Also wird warpmax ohne Argumente aufgerufen, und das int bedeutet, dass als Ergebnis eine ganze Zahl zurückgegeben wird. Welche Zahl das ist, wird durch return wmax;
bestimmt. Wie können wir den Wert verwenden, den warpmax() berechnet? Zum Beispiel wie in imax = warpmax();
166
Kapitel 7 Funktionen
Sandini Bib
Der Ausdruck rechts vom = wird als Erstes ausgewertet. In diesem Fall steht rechts der Funktionsaufruf warpmax(). Das Ergebnis ist der Rückgabewert der Funktion. Dieser Wert wird nach imax kopiert. Weil in diesem Fall der Funktionsaufruf eine ganze Zahl liefert, kannst du genauso gut printf("Hoechstgeschwindigkeit ist Warp %d.\n", warpmax());
verwenden. Du musst nur aufpassen, dass zu jedem Klammer auf ein Klammer zu an der richtigen Stelle steht. Diese Eigenschaft von Funktionen, dass sie dort geschrieben werden dürfen, wo du ihren Rückgabewert benötigst, haben wir in den vorangegangenen Kapiteln schon fleißig benutzt (z. B. bei rand()). Eine Funktion darf mehrere return-Befehle enthalten. Du kannst Funktionen an beliebiger Stelle mit return verlassen, nicht nur in der letzten Zeile. Falls kein Rückgabewert benötigt wird, verwendet man den Befehl return;
Das erinnert an den break-Befehl, mit dem eine Schleife verlassen werden kann. Während break aber nur die momentane Schleife beendet, kannst du mit return eine Funktion mitten in einer doppelten Schleife oder einem dreifachen if verlassen. Ein einfaches Beispiel findest du in Kapitel 7.11.
7.3 Funktion mit Argumenten und Rückgabewert Im nächsten Beispiel definieren wir eine Funktion differenz, die sinnigerweise zwei Zahlen als Argument annimmt und als Ergebnis eine Zahl liefert:
7.3 Funktion mit Argumenten und Rückgabewert
167
Sandini Bib Funktion4.c
#include <stdio.h> int differenz(int i, int j) { int ergebnis; ergebnis = i − j; return ergebnis; } int main() { int i = 7; printf("Hier spricht der Captain. Warp %d.\n", i); printf("Captain, wir schaffen nur Warp %d.\n", differenz(i, 1)); getchar(); return 0; }
Hier spricht der Captain. Warp 7. Captain, wir schaffen nur Warp 6.
Wie du siehst, können bei der Definition einer Funktion mehrere Argumente in einer Kommaliste angegeben werden. Für return müssen wir das Ergebnis nicht erst in einer Variable zwischenspeichern. Wir können auch int differenz(int i, int j) { return i − j; }
verwenden. Vielleicht fragst du dich jetzt, wie man eine Funktion definiert, die je nach Bedarf eine unterschiedliche Anzahl von Argumenten annimmt. printf ist dafür ein Beispiel. Solche Funktionen werden eher selten verwendet, deshalb verweise ich an dieser Stelle auf die BCB-Hilfe (Stichwort ›va_arg‹). Ein Grund, warum ›Variable Argumentenlisten‹ nur selten tatsächlich benötigt werden, ist, dass in den meisten Fällen Felder, Zeiger und Strukturen für die Übergabe von komplexen Argumenten ausreichen (Kapitel 10). Mit diesen Mitteln lässt sich auch das Problem lösen, wie eine Funktion mehr als genau eine Zahl als Ergebnis liefern kann.
168
Kapitel 7 Funktionen
Sandini Bib
7.4 Prototypen von Funktionen Bevor wir eine Funktion im Programmtext aufrufen können, muss der Prototyp der Funktion bekannt sein. Betrachte das folgende Beispiel: Funktion5.c
#include <stdio.h> void ansage(void); int main() { ansage(); getchar(); return 0; } void ansage(void) { printf("Hier spricht der Captain.\n"); }
Hier spricht der Captain.
Das ähnelt dem Beispiel aus Kapitel 7.0, aber diesmal steht im Programmtext erst die Definition von main und dann die Definition von ansage. Die Zeile void ansage(void);
nennt man den Prototyp der Funktion ansage. Der Prototyp einer Funktion ist alles vor dem Funktionsblock in ihrer Definition, plus einen Strichpunkt. Das ist keine vollständige Definition – der Befehlsblock fehlt! Aber diese Zeile enthält die Vereinbarungen, die der Compiler benötigt, um den Funktionsaufruf von ansage in main handhaben zu können. Tipp das Beispiel ein oder ändere das Beispiel aus Kapitel 7.0, indem du die Reihenfolge der Definitionen vertauschst. Ausprobieren, ohne die Zeile mit dem Prototyp von hallo beschwert sich der Compiler mit sowas wie Call to function ‘hallo’ with no prototype. Type mismatch in redeclaration of ‘hallo’.
Das heißt sinngemäß, dass erstens hallo ohne Prototyp aufgerufen wurde, und zweitens, dass der Typ von hallo nicht mit einer vorausgegangenen Deklaration (›Vereinbarung‹) zusammenpasst. Was geht hier vor? Der Compiler liest den Programmtext von oben nach unten (und von links nach rechts). Wenn ihm der Prototyp oder die Definition einer Funktion begegnet, 7.4 Prototypen von Funktionen
169
Sandini Bib
bevor sie verwendet wird, ist alles in Ordnung. Das ist genau wie bei der Vereinbarung von Variablen. Wenn aber der Funktionsaufruf vor dem Prototypen oder der Definition steht, nimmt der Compiler an, dass alle Datentypen int sind. Weil aber weiter unten ansage mit dem Typ void definiert wird, beschwert sich der Compiler. All das klingt unnötig umständlich, ist aber so. Man sollte bei der Vereinbarung von Funktionen des Typs int nicht versuchen, mit dem Default des Compilers zu tricksen, und immer alle Typen explizit angeben. Das gilt auch für die Vereinbarung der Funktion main. Genau genommen sollten wir int main(void) schreiben, aber main ist ein Sonderfall, denn main kann auch bestimmte Argumente erhalten, siehe 10.8. Im Prototypen kannst du die Variablennamen allerdings auch weglassen und z. B. int differenz(int, int); schreiben. Mit aussagekräftigen Variablennamen sind Prototypen aber viel lesbarer. Übrigens dürfen Funktionen nicht innerhalb von anderen Funktionen definiert werden. Im Programmtext wird eine Funktion nach der anderen definiert, aber jede für sich und nicht etwa innerhalb des Blocks einer anderen Funktion.
7.5 Headerdateien und Programmaufbau In kleinen Programmen kann man oft die Funktionen in der richtigen Reihenfolge anordnen, so dass keine Prototypen gebraucht werden. Oder man sammelt die Prototypen von allen Funktionen, die man verwendet, und schreibt sie an den Anfang des Programmtextes. Und genau das machen wir mit den Prototypen für Funktionen wie printf schon die ganze Zeit! Die Datei stdio.h enthält die Prototypen für alle Funktionen ›head‹ heißt in der Standard-Input-Output-Library. ›.h‹ steht für Header, Kopf, und ein Header ist etwas, das vorausgeschickt wird. Erinnerst du dich an Kapitel 0 und daran, dass ein ausführbares Programm in zwei Schritten erzeugt wird? Der Compiler übersetzt den Programmtext, und dazu benötigt er die Prototypen. Der Linker setzt die verschiedenen kompilierten Programmteile zusammen. Dazu verbindet er deine selbst geschriebenen Programmteile mit kompilierten Objekten aus den Standardbibliotheken. Und der Prototyp von printf in stdio.h stellt sicher, dass alles zusammenpasst. Wenn deine Programme so groß werden, dass eine lange .c-Datei zu unübersichtlich ist, kannst du deine Funktionen auf mehrere .c-Dateien verteilen. Jede Funktion muss dabei komplett in einer Datei stehen, aber es müssen eben nicht alle Funktionen in einer Datei stehen. Hier ist ein Beispiel:
170
Kapitel 7 Funktionen
Sandini Bib AnsageMain0.c
/* AnsageMain0.c */ #include <stdio.h> void ansage(void); int main() { ansage(); getchar(); return 0; }
Ansage0.c
/* Ansage0.c */ #include <stdio.h> void ansage(void) { printf("Hier spricht der Captain.\n"); }
Das Programm ist auf zwei Dateien verteilt. Damit der Compiler weiß, dass eine aufgerufene Funktion in einer anderen Datei definiert ist, schreibt man ihren Prototyp an den Anfang von jeder Datei, in der die Funktion aufgerufen wird. Der Compiler erzeugt für jede Datei ein Objekt. Der Linker durchsucht alle Objekte nach den benötigten Funktionen. Deine Prototypen kannst du in einer selbst gemachten Headerdatei sammeln, die mit #include gelesen wird: Ansage1.h
/* Ansage1.h */ void ansage(void);
AnsageMain1.c
/* AnsageMain1.c */ #include <stdio.h> #include "Ansage1.h" int main() { ansage(); getchar(); return 0;
// Prototyp in Ansage1.h // Prototyp in stdio.h
}
7.5 Headerdateien und Programmaufbau
171
Sandini Bib Ansage1.c
/* Ansage1.c */ #include <stdio.h> #include "Ansage1.h" void ansage(void) { printf("Hier spricht der Captain.\n"); }
// Prototyp in stdio.h
Weil #include "Ansage1.h" einfach nur den Text aus der Datei Ansage1.h in die Datei AnsageMain1.c einsetzt, funktioniert dieses Beispiel genauso wie das erste. In Ansage1.c wird das #include "Ansage1.h" eigentlich nicht benötigt. Es ist aber eine gute Idee, alle Prototypen in einer Headerdatei zu sammeln und diese Headerdatei in allen Programmdateien einzulesen. Solltest du irgendwann die Definition von ansage um ein Argument erweitern, bekommst du dann eine Fehlermeldung, dass die Definition nicht mehr mit dem Prototyp in der Headerdatei übereinstimmt. Jede Funktion darf nur einmal definiert werden (siehe jedoch 7.9), aber ihr Prototyp kann beliebig oft angegeben werden. In Kapitel 0.6 haben wir besprochen, wie man Dateien in Projekten verwaltet. In BCB findest du unter dem Menüpunkt ›Projekt‹ die Einträge ›Zum Projekt hinzufügen‹ und ›Aus dem Projekt entfernen‹. BCB muss wissen, welche C-Dateien kompiliert und gelinkt werden sollen. Unter dem Menüpunkt ›Ansicht‹ findest du ›Projektverwaltung‹, was ein praktisches Fensterchen zur Verwaltung aller deiner Dateien aufmacht.
172
Kapitel 7 Funktionen
Sandini Bib
7.6 Lokale Variable Ein weiteres Thema, das direkt im Zusammenhang mit Funktionen und Programmaufbau steht, ist der Gültigkeitsbereich von Variablen. Schau dir mal das folgende Beispiel an: VarLokal0.c
#include <stdio.h> void hallo(int i) { printf("i ist %d\n", i); } int main() { int n; n = 3; hallo(n); getchar(); return 0; }
i ist 3
Klarer Fall, hallo bekommt eine ganze Zahl übergeben. Diese heißt in main zwar n, aber in hallo erhält sie den Namen i. Versuche einmal, in hallo die Zeile printf("n ist %d\n", n);
zu verwenden. Der Compiler sagt dir, dass er an dieser Stelle keine Variable n kennt. Es hilft auch nicht, die Definition von hallo hinter main zu schreiben (Prototyp nicht vergessen). n bleibt unerkannt. Ausprobieren! Eine Variable ist nur in dem Block sichtbar, in dem sie definiert wurde. Das kann am Anfang eines Funktionsblocks geschehen wie in int n; oder durch die Definition in der Argumentenliste einer Funktion wie in void hallo(int i). Das ist sehr sinnvoll, denn so kommt die Information in verschiedenen Funktionen nicht durcheinander. Solche Variable nennt man lokal, denn sie sind nur lokal innerhalb des jeweiligen Befehlsblocks verwendbar. Wir können sogar den Namen n mehrmals für eine Variable vereinbaren, denn jede Variable hat einen lokalen, wohldefinierten Gültigkeitsbereich:
7.6
Lokale Variable
173
Sandini Bib VarLokal1.c
#include <stdio.h> void hallo(int i) { int n = 0; printf("i ist %d\n", i); printf("n in hallo ist %d\n", n); } int main() { int n; n = 3; hallo(n); printf("n in main ist %d\n", n); getchar(); return 0; }
i ist 3 n in hallo ist 0 n in main ist 3
In diesem Beispiel gibt es zwei verschiedene Variable mit Namen n, aber jede hat ihren eigenen Speicherplatz. Daher ist n in der Funktion main nach dem Aufruf von hallo immer noch gleich 3, obwohl zwischendurch n = 0; in hallo ausgeführt wurde. Die Gültigkeitsbereiche dieser zwei Variablen sind durch die Befehlsblöcke sauber voneinander getrennt. Jede Variable hat ihren eigenen lokalen Gültigkeitsbereich. Der Vollständigkeit halber will ich erwähnen, dass lokale Variable zu Beginn jedes beliebigen Befehlsblocks definiert werden können, also z. B. auch innerhalb des Befehlsblocks einer if-Bedingung. Dabei gelten dieselben Regeln wie bei lokalen Variablen zu Beginn eines Funktionsblocks. Weil Befehlsblöcke innerhalb von Befehlsblöcken vorkommen können, stellt sich die Frage, was passiert, wenn eine Variable n in mehreren verschachtelten Befehlsblöcken definiert wird. Jede Variable n erhält ihren eigenen Speicherplatz, und es ist immer nur die nächstgelegene Definition von n sichtbar. Auf die anderen Variablen n kann nicht zugegriffen werden. Dieselbe Frage der Sichtbarkeit stellt sich auch in Kapitel 7.7.
174
Kapitel 7 Funktionen
Sandini Bib
7.7 Externe Variable Hier besprechen wir eine neue Idee. Eine Variable kann auch außerhalb von Funktionen definiert werden. Solche Variablen nennt man extern oder global, im Gegensatz zu internen oder lokalen Variablen innerhalb eines Blocks. Hier ist ein Beispiel: VarExtern0.c
#include <stdio.h> int n; void hallo(void) { printf("n in hallo ist %d\n", n); n = 0; } int main() { n = 3; hallo(); printf("n in main ist %d\n", n); getchar(); return 0; }
n in hallo ist 3 n in main ist 0
Alle Funktionen, die nach der Zeile int n; definiert werden, haben Zugriff auf n. Im Beispiel muss deshalb n nicht extra an hallo übergeben werden. Zudem kann hallo genauso wie main den Wert von n ändern! Eine Funktion kann mehrere Zahlen berechnen und diese der aufrufenden Funktion in externen Variablen zur Verfügung stellen. Wenn wir in unserem Beispiel trotz externer Variable eine direkte Übergabe vornehmen, erhalten wir
7.7 Externe Variable
175
Sandini Bib VarExtern1.c
#include <stdio.h> int n; void hallo(int n) { printf("n in hallo ist %d\n", n); n = 0; } int main() { n = 3; hallo(n); printf("n in main ist %d\n", n); getchar(); return 0; }
n in hallo ist 3 n in main ist 3
In der Funktion hallo haben wir eine zweite Variable namens n definiert. Beim Funktionsaufruf wurde der Wert des ersten n in den Speicherplatz des zweiten n kopiert. Diese lokale Variable n hat nichts mit anderen Variablen dieses Namens außerhalb der Funktion hallo zu tun. Der Befehl n = 0; ändert die externe Variable n nicht. In anderen Worten, innerhalb des Funktionsblocks von void hallo(int n) überschattet die lokale Variable n die externe Variable n. Das bedeutet auch, dass die externe Variable n innerhalb von hallo nicht sichtbar ist. Auf die externe Variable n kann innerhalb von hallo nicht zugegriffen werden, weil der Variablenname n sich auf die lokale Variable bezieht. Im Prinzip könnte man alle Variablen zu externen Variablen machen. In dem Fall benötigt jede Variable einen eigenen Namen, aber man muss dann nicht über Argumentenlisten nachdenken. Vorsicht, das ist nicht in jedem Fall eine gute Idee! Denn dadurch geht die Trennung von Innenleben und Aufruf einer Funktion verloren. Zum Beispiel müssten wir dann vor dem Aufruf einer Funktion differenz(void) ohne Argumente immer erst die Zahlen in die richtigen Variablen kopieren, was sehr ungeschickt ist. Es könnte auch leicht passieren, dass aus Versehen zwei Funktionen ein und dieselbe Variable i verwenden, obwohl eigentlich zwei unabhängige Variablen gebraucht werden. Was für ein Durcheinander. Externe Variablen sind sehr nützlich, aber du solltest ihre Verwendung auf Fälle beschränken, wo sie tatsächlich nützlich sind. 176
Kapitel 7 Funktionen
Sandini Bib
7.8 Statische und automatische Variablen Eine statische lokale Variable wird eingesetzt, wenn eine Funktion zwischen den Funktionsaufrufen den Wert von lokalen Variablen nicht vergessen soll: VarStatic0.c
#include <stdio.h> void hallo(void) { static int wieoft = 1; printf("Hallo! (jetzt ruft der mich schon zum %d. Mal auf)\n", wieoft++); } int main() { int i; for (i = 0; i < 5; i++) hallo(); getchar(); return 0; }
Hallo! Hallo! Hallo! Hallo! Hallo!
(jetzt (jetzt (jetzt (jetzt (jetzt
ruft ruft ruft ruft ruft
der der der der der
mich mich mich mich mich
schon schon schon schon schon
zum zum zum zum zum
1. 2. 3. 4. 5.
Mal Mal Mal Mal Mal
auf) auf) auf) auf) auf)
Das Schlüsselwort static bewirkt, dass zwischen den wiederholten Aufrufen der Funktion hallo der Speicherplatz samt Inhalt für die Variable wieoft erhalten bleibt, obwohl wieoft außerhalb der Funktion gar nicht sichtbar ist. Unbedingt ausprobieren! Wenn du das static weglässt, wird bei jedem Aufruf der Funktion hallo in int wieoft = 1; die Variable wieoft erneut auf 1 gesetzt. Die Ausgabe ist Hallo! Hallo! Hallo! Hallo! Hallo!
(jetzt (jetzt (jetzt (jetzt (jetzt
ruft ruft ruft ruft ruft
der der der der der
mich mich mich mich mich
schon schon schon schon schon
zum zum zum zum zum
1. 1. 1. 1. 1.
7.8
Mal Mal Mal Mal Mal
auf) auf) auf) auf) auf)
Statische und automatische Variablen
177
Sandini Bib
und das ist nicht, was wir wollen. Der Witz ist, dass bei static int wieoft = 1; die Initialisierung nur beim ersten Mal geschieht und sich die Funktion hallo so den letzten Wert von wieoft von Aufruf zu Aufruf merken kann. Anders ausgedrückt: Lokale Variablen sind normalerweise automatisch. Das ist der Default. Beim Eintritt in die Funktion werden sie angelegt, beim Verlassen verschwinden sie wieder. Das kann man mit static für lokale Variablen verhindern und gleichzeitig diese Variable vor der Außenwelt verstecken. Externe Variable sind auf jeden Fall statisch. Sie werden beim Start des Programms angelegt und sind unabhängig vom Eintritt oder Verlassen irgendwelcher Funktionen verfügbar. Das kannst du mit der folgenden Variante von unserem Beispiel testen, in dem ebenfalls korrekt gezählt wird: VarStatic1.c
#include <stdio.h> int wieoft = 1; void hallo(void) { printf("Hallo! (jetzt ruft der mich schon zum %d. Mal auf)\n", wieoft++); } int main() { int i; for (i = 0; i < 5; i++) hallo(); getchar(); return 0; }
Hier ist wieoft eine externe Variable. Diese erhält einmal vor Beginn des Progamms Speicherplatz zugewiesen, der nur einmal mit 1 initialisiert wird. Wie schon besprochen ist es jedoch normalerweise eine gute Idee, Variablen in Funktionen von der Außenwelt abzuschirmen, und deshalb gibt dir C mit static die Möglichkeit, innerhalb von Funktionen den Wert von Variablen zwischen den Funktionsaufrufen zu erhalten. In Kapitel 2.5 habe ich betont, dass nach der Definition, aber vor der Initialisierung von automatischen Variablen diese Variable Müll enthalten. Externe und statische interne Variable erhalten aber immer den Anfangswert 0. Trotzdem neige ich dazu, ausdrücklich static int wieoft = 0; zu schreiben, wenn ein Programm diesen Anfangswert tatsächlich benötigt. Dann sehe ich auf einen Blick, dass die Initialisierung hier und nicht anderswo im Programm erfolgt. 178
Kapitel 7 Funktionen
Sandini Bib
7.9 Statische Funktionen und externe Variablen Bevor wir uns den Beispielen zuwenden, will ich der Vollständigkeit halber noch eine weitere Verwendung des Schlüsselwortes static besprechen. Verwirrenderweise wird static nicht nur bei statischen und automatischen Variablen verwendet, sondern auch, wenn Variablen und Funktionen über mehrere Dateien verteilt sind (siehe Kapitel 7.5). Funktionen, die in einer anderen Datei definiert werden, werden durch ihren Prototyp sichtbar gemacht. Wenn void hallo(void);
in einer Datei steht, kannst du die Funktion hallo aufrufen, auch wenn hallo in einer anderen Datei definiert wurde. Um auf eine externe Variable in einer anderen Dateien zuzugreifen, schreibst du z. B. extern int wieoft;
Das Schlüsselwort extern gibt an, dass die nachfolgende Variable außerhalb dieser Datei definiert wird. Mit extern kann also zweierlei gemeint sein: außerhalb einer Datei und außerhalb von Funktionen. Die Variable wieoft muss als externe Variable außerhalb von Funktionen in einer der Dateien des Programms definiert sein, damit man sie mit dem Schüsselwort extern in anderen Dateien sichtbar machen kann. extern int wieoft; führt lediglich Namen und Typ der Variable ein, stellt aber keinen Speicherplatz bereit. In diesem Fall spricht man nicht von einer Definition, sondern von der Deklaration der Variablen wieoft. Das entspricht genau dem Zweck eines Prototypen von Funktionen. Wie Prototypen kann man alle extern-Deklarationen in einer Headerdatei sammeln. So erhält man externe Variablen, die im gesamten Programm sichtbar sind.
Wenn eine externe Variable oder eine Funktion nicht in anderen Dateien sichtbar sein soll, benutzt man in ihrer Definition das Schlüsselwort static: static void hallo(void);
teilt dem Compiler mit, dass der Funktionsname hallo nur in dieser Datei gelten soll. Das ist praktisch, wenn man in verschiedenen Programmteilen den Funktionsnamen hallo für verschiedene Aufgaben verwenden möchte. Um verschiedene Versionen einer Variablen wieoft in mehreren Dateien verwenden zu können, definiert man eine externe Variable außerhalb der Funktionen mit static int wieoft;
Bei lokalen Variablen wird static verwendet, um den Inhalt von Variablen zwischen Funktionsaufrufen zu erhalten. Jede externe Variable ist in diesem Sinne statisch, aber für externe Variable gibt es genau wie für Funktionen eine andere Verwendung des Schlüsselwortes static, die die Sichtbarkeit auf die jeweilige Datei beschränkt. 7.9 Statische Funktionen und externe Variablen
179
Sandini Bib
7.10
Zufallszahlen selbst gemacht
In Kapitel 5.7 habe ich die etwas paradoxe Idee diskutiert, dass dein überaus zuverlässiger Computer scheinbar zufällige Zahlenfolgen ausspucken kann. Die Funktion rand haben wir jetzt schon einige Male verwendet, jetzt gehen wir ins technische Detail. Die Funktion rand und ihre Artgenossen berechnen Pseudozufallszahlen mit folgender Formel (oder einer ihrer Varianten): zzneu = (a * zzalt + c) % m
Eine neue Pseudozufallszahl zzneu wird aus der vorhergehenden Zufallszahl zzalt nach immer derselben Vorschrift berechnet. Wir multiplizieren mit a und addieren c. Als Ergebnis verwenden wir den Rest, den die Division durch m ergibt. Du kannst dir sicher vorstellen, dass bei krummen Zahlen das Ergebnis auch krumm ist. Beachte, dass wegen % m die Zahlen immer kleiner als m sind. Eine Eigenschaft der Methode ist, dass sich die Zahlen spätestens nach m Schritten wiederholen, d.h. die Periode ist maximal m. Jede Zahl von 0 bis m−1 kommt dann genau einmal vor. Eine selbst gemachte Funktion für Zufallszahlen ist ein schönes Beispiel für die Verwendung von Funktionen: Zufall5.c
#include <stdio.h> int zz(void) { static int zufallszahl = 0; zufallszahl = (106 * zufallszahl + 1283) % 6075; return zufallszahl; } int main() { printf("%d\n", printf("%d\n", printf("%d\n", printf("%d\n", printf("%d\n", getchar(); return 0; }
180
zz()); zz()); zz()); zz()); zz());
Kapitel 7 Funktionen
Sandini Bib
1283 3631 3444 1847 2665
Das Schöne ist, dass die ganze Rechnerei für die Zufallszahlen in der Funktion zz versteckt und verpackt ist. In einem längeren Programm würde es nur vom Zweck der Zufallszahlen ablenken, wenn du jedes Mal diese Formel einsetzen würdest. Die Funktion zz liefert bei jedem Aufruf eine neue Zahl. Diese Zahl wird in einer statischen Variable zufallszahl lokal gespeichert, damit beim nächsten Aufruf die nächste Zahl daraus berechnet werden kann. Das erledigen wir in einer Zeile: zufallszahl = (106 * zufallszahl + 1283) % 6075;
Also ist hier a = 106, c = 1283 und m = 6075. Erst wird der Wert von zufallszahl aus dem Speicher geholt, dann wird gerechnet, dann das Ergebnis wieder in zufallszahl gespeichert (genau wie bei i = i + 1). Zum Experimentieren kannst du das static entfernen oder auch das % 6075 weglassen. Die schwierige Arbeit ist, gute Werte a, c und m zu finden. Z.B. müssen wir einen Overflow vermeiden. (Ganze Zahlen dürfen nicht beliebig groß sein, Kapitel 2.9.) Wenn du möchtest, kannst du die folgenden Zahlen ausprobieren: a
c
m
106 211 8121 4561
1283 1663 28 411 51 349
6075 7875 134 456 243 000
Oder einfach selbst welche erfinden und die Formel mit kleineren Zahlen testen. Eine Warnung: Ein solcher Generator liefert erst dann richtig gute Ergebnisse, wenn man die berechneten Zufallszahlen noch zusätzlich untereinander vertauscht oder mehrere Generatoren kombiniert. Insbesondere solltest du in unserem Beispiel zz()%6 nicht verwenden, weil diese Zahlen nicht besonders zufällig sind. Deshalb werden wir die Funktion rand aus der Standardbibliothek von C verwenden, die normalerweise bessere Pseudozufallseigenschaften hat.
7.11
Rekursion
Wenn eine Funktion sich selbst (!) aufruft, spricht man von Rekursion. Eine kleine Aufgabe, für die sich das anbietet, ist die Berechnung von Fakultäten. An7.11
Rekursion
181
Sandini Bib
genommen, du fragst dich, wie viele Möglichkeiten es gibt, 5 Geburtstagsgäste auf 5 Stühle zu verteilen. Die Antwort erhältst du, wenn du dir vorstellst, die Gäste einen nach dem anderen auf einen Stuhl zu setzen. Beim ersten Gast hast du 5 Möglichkeiten. Beim zweiten Gast 4, denn ein Stuhl ist ja schon besetzt. Insgesamt macht das 5 × 4 = 20 Möglichkeiten. Der dritte Gast hat noch 3 Möglichkeiten, macht 20 × 3 = 60. Dann rechnest du 60 × 2 = 120 und schließlich 120 × 1 = 120 für den letzten Gast. Du hast also alle Zahlen von 5 bis 1 miteinander malgenommen. Dieses Produkt nennt man 5 Fakultät und schreibt in der Mathematik 5!. Im Allgemeinen gilt also n! = n × (n − 1) × . . . × 1.
Genauso gut kannst du definieren, dass n! = n × (n − 1)! falls n > 1, und 1! = 1.
Dabei wird ausgenutzt, dass man, wenn die Fakultät für die um 1 kleinere Zahl n − 1 schon bekannt ist, nur noch mit n malnehmen muss. Diese Definition nennt man rekursiv, weil man in der Definition das Objekt verwendet, das man eigentlich definieren möchte (nämlich die Fakultät von n − 1). Das wäre Quatsch und würde unendlich immer so weitergehen, wenn die Rekursion nicht durch die Bedingung 1! = 1 beendet würde. Hier ist ein Programm, das die Fakultät für alle Zahlen von 1 bis 13 berechnet (einen Fakultätsoperator ! gibt es in C nicht): Fakultaet0.c
#include <stdio.h> int fakultaet(int n); int main() { int n; printf(" n n!\n"); for (n = 1; n <= 12; n++) printf("%2d %d\n", n, fakultaet(n)); getchar(); return 0; } int fakultaet(int n) { int i, ergebnis = 1; for (i = 1; i <= n; i++) ergebnis *= i; return ergebnis; }
182
Kapitel 7 Funktionen
Sandini Bib
n 1 2 3 4 5 6 7 8 9 10 11 12
n! 1 2 6 24 120 720 5040 40320 362880 3628800 39916800 479001600
Dies ist die nicht-rekursive Version. Die Funktion fakultaet enthält eine Schleife, mit der wir alle Zahlen von 1 bis n miteinander malnehmen. Nichts Besonderes. Rekursiv wird es mit: Fakultaet1.c
int fakultaet(int n) { if (n > 1) return n * fakultaet(n−1); return 1; }
Wenn du diese Funktion anstatt der mit der Schleife verwendest, erhältst du dasselbe Ergebnis. Falls n größer als 1 ist, ruft sich die Funktion fakultaet mit n−1 als Argument selber auf und kehrt mit n * fakultaet(n−1) zurück. Bei jedem Aufruf von fakultaet wird eine neue Variable n angelegt, die eine um 1 kleinere Zahl enthält. Ausprobieren, setze printf("%d\n", n);
als ersten Befehl in die Funktion fakultaet. Die Bedingung if (n > 1) verhindert, dass die Funktion sich unendlich oft selbst aufruft. Sobald n gleich 1 ist, ruft sich die Funktion nicht noch mal selbst auf, sondern kehrt mit 1 zurück. Beachte, dass wir in diesem Beispiel die Funktion fakultaet an zwei verschiedenen Stellen mit return und unterschiedlichen Rückgabewerten verlassen. Wenn wir von vornherein eine Tabelle von 1! bis 12! berechnen wollen, sollten wir das printf in die Funktion fakultaet einbauen. Sonst multiplizieren wir die Zahlen öfter miteinander als nötig (wie kommt das?). Ändere das Programm entsprechend um. Warum hören wir bei 12! auf? Ausprobieren, diese Funktion 7.11
Rekursion
183
Sandini Bib
explodiert schneller als jede Exponentialfunktion. Z.B. wird bei 2n immer wieder mit 2 multipliziert, bei n! kommen mit größeren n immer größere Multiplikatoren hinzu. Und irgendwann kommt es zum Overflow der Integervariablen. Auch bei Rekursionen hilft es oft, ein paar Durchgänge mit dem Debugger durchzuführen, um die einzelnen Schritte genau zu verfolgen. Um zu erreichen, dass ein bestimmtes Zwischenergebnis vom Debugger angezeigt wird, kann man falls nötig einfach eine zusätzliche Variable einführen, wie in Fakultaet2.c
int fakultaet(int n) { int ergebnis; if (n > 1) ergebnis = n * fakultaet(n−1); else ergebnis = 1; return ergebnis; }
Und wieder brauchen wir uns um die mehrfache Verwendung einer Variablen wie ergebnis keine Gedanken zu machen. Bei jedem Aufruf der Funktion wird eine neue lokale Version angelegt und das Programm speichert alle diese Variablen getrennt ab.
7.12
Rekursion mit Grafik
Hier ist ein Beispiel für Rekursion mit Grafik. Die Idee ist, dass wir ein großes Rechteck zeichnen wollen und auf jede Ecke ein kleineres Rechteck und auf jede Ecke der kleinen Rechtecke noch kleinere Rechtecke und so weiter. Nahe einer Ecke kann das Ergebnis so aussehen:
184
Kapitel 7 Funktionen
Sandini Bib
Du siehst ein Rechteck in der Mitte und 4 kleinere Rechtecke, die ich mit ihren Mittelpunkten auf die Ecken des ersten Rechtecks gesetzt habe. Wenn wir das fünfmal so machen, also mit einem großen Rechteck anfangen und dann noch viermal verkleinern, sieht das z. B. so aus:
Schau dir das Bild an und folge der Konstruktion in Gedanken. Fange mit der linken oberen Ecke des größten Rechtecks an, gehe zum nächstkleineren links oben, dann wieder zum nächstkleineren, bis du den kleinen Ausschnitt findest, den ich als Erstes gezeigt habe. Nett, und so schön regelmäßig, und diese Farben ... . Hier ist das Programm:
7.12
Rekursion mit Grafik
185
Sandini Bib RekursionsRechteck.c WinHallo.cpp
#include <windows.h> void rechtecke(HDC hdc, int n, int x, int y, int dx, int dy) { int dxneu, dyneu; HBRUSH hbrush, hbalt; hbrush = CreateSolidBrush(RGB(295−40*n,0,30*n)); hbalt = SelectObject(hdc, hbrush); Rectangle(hdc, x−dx, y−dy, x+dx, y+dy); DeleteObject(SelectObject(hdc, hbalt)); n = n − 1; if (n == 0) return; dxneu = 5*dx/10; dyneu = 5*dy/10; rechtecke(hdc, rechtecke(hdc, rechtecke(hdc, rechtecke(hdc,
n, n, n, n,
x−dx, x+dx, x−dx, x+dx,
y−dy, y−dy, y+dy, y+dy,
dxneu, dxneu, dxneu, dxneu,
dyneu); dyneu); dyneu); dyneu);
} void malen(HDC hdc) { int n = 5; int xmax = 1024; int ymax = 768; rechtecke(hdc, n, xmax/2, ymax/2, xmax/4, ymax/4); }
Wie gewöhnlich beginnt das Zeichnen mit der Funktion malen. Die Variable n gibt die Anzahl der Rekursionen an, die ausgeführt werden sollen. Die Größe meines Fensters habe ich mit xmax und ymax angegeben. Dann geht es los. Wir rufen die selbst gemachte Funktion rechtecke auf, die weiter oben in der Datei steht. Wie du sofort siehst, ruft rechtecke einmal die Zeichenfunktion Rectangle auf und genau viermal sich selbst. Der Aufruf von Rectangle ist Rectangle(hdc, x−dx, y−dy, x+dx, y+dy);
also wird ein Rechteck mit Mittelpunktkoordinaten x und y und Kantenlängen, die das Doppelte von jeweils dx und dy sind, gezeichnet. Diese Variablen wurden direkt an rechtecke übergeben. Das heißt unsere Funktion rechtecke zeichnet als Erstes ein Rechteck. Die Farbe hängt von n ab. Dann wird n um eins erniedrigt. Wenn n gleich 0 ist, war’s das. Ein Rechteck wurde mit Rectangle gezeichnet und die Funktion wird mit return verlassen. 186
Kapitel 7 Funktionen
Sandini Bib
Falls aber n nicht 0 ist, ruft sich rechtecke selber auf, nur diesmal mit den Koordinaten der Ecken des momentanen Rechtecks. Die Kantenlängen machen wir aber kleiner, damit die Rechtecke sich nicht überlappen. Ausprobieren, wie sieht das Bild für eine Verkleinerung um 3/10, 4/10, 6/10, 7/10 aus? Beim zweiten Aufruf von rechtecke muss die Funktion nichts von all dem wissen, was vorher passiert ist. Die Funktion erhält einen neuen Satz von Parametern n, x, y, dx und dy (hdc hat sich nicht geändert) und weiß genau, was zu tun ist. Bei jedem Aufruf ist n um eins kleiner, also wird die Rekursion irgendwann abbrechen.
7.13
Labyrinth
In diesem Kapitel wollen wir ein Programm schreiben, das ein Labyrinth baut. Das Endergebnis wird z. B. so aussehen: ###################################### ##......##..##......................## ##..######..######################..## ##..##..............##..##..##..##..## ##..##############..##..##..##..##..## ##......##..##......##..##......##..## ##..######..##..##..##..######..##..## ##......##..##..##......##..##..##..## ######..##..##########..##..##..##..## ##......##..##..##..........##..##..## ######..##..##..##########..##..##..## ##..##......##......##..##..##..##..## ##..######..######..##..##..##..##..## ##......##......##..##..##..##..##..## ######..######..##..##..##..##..##..## ##......##..........##......##......## ######..##########..######..######..## ##..............................##..## ##################..##########..##..## ##..##..............##......##..##..## ##..######..##############..##..##..## ##..........##........................ ######################################
Oder so, denn wir verwenden den Zufallsgenerator:
7.13
Labyrinth
187
Sandini Bib
###################################### ##..##......##..............##......## ##..######..##############..##..###### ##..##......##......##..........##..## ##..######..######..######..######..## ##..........##..##..................## ##########..##..######..############## ##......##......##..........##......## ##..##########..######..######..###### ##..##..##..##..............##......## ##..##..##..##########..##########..## ##......##..........##......##......## ######..##########..##..##########..## ##..........##..............##......## ##########..##############..######..## ##..##......##..##..##..............## ##..######..##..##..##############..## ##..##..##..........................## ##..##..##########################..## ##..................................## ##..######..##########..##..######..## ##......##..##..........##..##........ ######################################
Die Gänge sind durch . und die Wände durch # dargestellt. Natürlich liegt es nahe, statt der Textdarstellung in einem Grafikfenster die Wände als Rechtecke zu zeichnen, das Ganze mit Monstern und Schätzen zu füllen und einen Abenteurer auf die Reise zu schicken. Aber in diesem Beispiel wollen wir uns nicht auf Grafik, sondern auf das Programmieren mit Funktionen konzentrieren. Das Besondere an unserem Labyrinth ist, dass der gesamte Platz in einem Rechteck ausgenützt wird und es von jedem Raum aus nur genau einen Weg zum Ausgang gibt! Denke einmal darüber nach, wie der Computer das anstellen könnte. Das Problem ist, dass wir nicht einfach Räume und Gänge zufällig anlegen können. Wenn du auf einem Blatt Karopapier einfach einmal zufällig Linien zeichnest, die Gänge sein sollen, werden sich die Gänge schneiden, und Abkürzungen wollen wir ja nicht erlauben. Oder es gibt Gänge, die überhaupt keine Verbindung zum Ausgang haben! Angenommen, ein Abenteurer soll in unserem Labyrinth nach einem Schatz suchen, dann kann die Aufgabe entweder zu leicht oder völlig unmöglich sein. Machen wir uns also an die Arbeit bzw. das Vergnügen, dem Computer das durchdachte Labyrinthebasteln beizubringen.
188
Kapitel 7 Funktionen
Sandini Bib
Als Erstes denken wir uns eine Möglichkeit aus, das Labyrinth zu speichern. Wir entscheiden uns zunächst einmal, rechteckige Labyrinthe zu bauen. Bei Rechteck fällt uns ein, dass diese sehr einfach in zweidimensionalen Feldern gespeichert werden können. Wie wäre es mit int labyrinth[100][100];
Der Platz sollte fürs Erste reichen. Jedes Feld kann dann mit Indizes i und j als labyrinth[j][i] angesprochen werden, wobei j die Zeile und i die Spalte angibt. Zwar könnten wir für jedes Feld vier Wände definieren und diese extra abspeichern, aber der Einfachheit halber beschließen wir, Wände genauso wie Felder zu behandeln. Wir stellen uns vor, dass in einen Berg Gänge gegraben werden und dass die Wände genauso dick wie die Gänge sind. Wie speichern wir Gänge und Wände im Feld labyrinth ab? Lass uns vereinbaren, dass 0 für Gang und 1 für Wand steht. Diese Idee gefällt uns: 1. Wir fangen mit einem rechteckigen Berg ohne Gänge an, d.h. wir schreiben
1 in das Feld. 2. Dann graben wir Gänge irgendwie zufällig, d.h. das Programm macht aus
einer 1 eine 0, wenn es einen Gang gegraben hat. 3. Am Schluss zeigen wir das Labyrinth, und damit wir Gänge besser von Felsen unterscheiden können, geben wir statt 0 und 1 die Zeichen # und . aus, siehe
oben. Guter Plan. Ein solches Programm schreibe ich nicht in einem Rutsch, d.h. ich tippe nicht einfach alle Funktionen und Variablen ein, die ich zu brauchen meine, sondern als Erstes erstelle ich ein ›Skelett‹. Darin lasse ich vieles aus, aber trotzdem soll es ein lauffähiges Programm sein. Wenn du in kleinen Schritten vorgehst, vermeidest du, dass dir zum Schluss dutzende kleine Fehler beim Debuggen das Leben schwer machen. Hier ist ein Anfang:
7.13
Labyrinth
189
Sandini Bib Labyrinth0.c
/* Labyrinth0.c Erzeuge ein Labyrinth durch zufaelliges Graben. Jeder Raum soll nur auf eine Weise mit dem Ausgang verbunden sein. */ /* Header */ #include <stdio.h> #include <stdlib.h> #include /* Prototypen */ int zz(int n); void initlab(void); void baulab(void); void zeiglab(void); int graben(int i, int j); /* Definitionen fuer Feldertypen */ #define GANG 0 #define FELSEN 1 #define AUSSENWAND −1 /* externe Variable */ int labyrinth[100][100]; int imax = 10; int jmax = 12;
// ein Array, Eintraege fuer Gang, Felsen, Wand // Anzahl Raeume in x Richtung // Anzahl Raeume in y Richtung
/* main */ int main() { /* wiederholen solange nur auf Eingabe gedrueckt wird */ while (1) { initlab(); baulab(); zeiglab(); if (getchar() != ’\n’) break; } return 0; }
/* unfertig */ void initlab(void){} void zeiglab(void){} void baulab(void){}
Als Erstes lesen wir alle benötigten Headerdateien, dann geben wir die Prototypen unserer selbst geschriebenen Funktionen an. Die Funktion zz soll Zufallszahlen berechnen, also brauchen wir stdlib.h und time.h. Für 1., 2. und 3. in unserem Plan haben wir jeweils eine Funktion eingeführt: initlab für initialisiere Labyrinth, baulab für baue Labyrinth und zeiglab für zeige Labyrinth. 190
Kapitel 7 Funktionen
Sandini Bib
Damit ich in dieser Beschreibung nicht zu oft sagen muss, ›und jetzt bitte da und dort einfügen‹, taucht hier gleich ein Prototyp graben auf, den wir später benötigen. Gewöhnlich wirst du nach und nach für jede neue Funktion wie bauen oder zz die Prototypen an dieser Stelle eintragen. Dann kommt eine Reihe von Definitionen. Damit wir nicht zwischen 0 und 1 für Gang und Felsen durcheinander kommen, definieren wir zunächst GANG und FELSEN: #define GANG 0 #define FELSEN 1 #define AUSSENWAND −1
Später stellt sich heraus, dass es praktisch ist, zusätzlich einen Felsentyp namens Außenwand zu verwenden. Es folgen die Variablen int labyrinth[100][100]; int imax = 10; int jmax = 12;
Wir verwenden externe Variable, damit alle Funktionen auf labyrinth zugreifen können, ohne dass dieses Feld übergeben werden muss. Wir definieren ein ausreichend großes Feld labyrinth und verwenden die Variablen imax und jmax, um die tatsächliche Größe des Labyrinths anzugeben. Das ist nicht gerade elegant, soll uns aber reichen. Es folgt die Definition von main. In main bauen wir gleich eine einfache Schleife ein, damit wir einfach nur ↵ drücken müssen, um ein neues Labyrinth zu erzeugen. In der Schleife wiederholen wir immer wieder die drei Schritte, in die wir unser Problem zerlegt haben: initlab(); baulab(); zeiglab();
Durch die Verwendung von Funktionen ist main schön übersichtlich geblieben. Es folgt für jede dieser Funktionen ein Dummy, und fertig ist das Skelett für die erste Runde Debuggen. Eintippen (oder von der CD holen) und ausprobieren! Noch passiert natürlich nicht viel beim Ausführen des Programms. Aber du hast jetzt alle Tippfehler beseitigt, und du kannst dich davon überzeugen, dass die Eingabetaste eine neue Zeile gibt und dass › q ‹ › ↵ ‹ das Programm beendet. Als Nächstes beschließen wir, eine erste Version von initlab und zeiglab zu schreiben. Wir erinnern uns an das doppelte Schleifchen in 6.9 und schreiben
7.13
Labyrinth
191
Sandini Bib Labyrinth1a.c
/* Initialisiere Labyrinth: setze alle Felder auf FELSEN */ void initlab(void) { int i, j; /* doppelte Schleife ueber alle Felder */ for (j = 0; j < jmax; j++) for (i = 0; i < imax; i++) labyrinth[j][i] = FELSEN; }
Nichts Besonderes, außer dass wir beschlossen haben, wie in der folgenden Ausgabeschleife in der Funktion zeiglab die Indizes in der Reihenfolge [j][i] zu verwenden: Labyrinth1b.c
/* Zeige das Labyrinth mit printf */ void zeiglab(void) { int i, j; printf("\n"); for (j = 0; j < jmax; j++) { printf(" "); for (i = 0; i < imax; i++) { if (labyrinth[j][i] == FELSEN) printf("#"); else printf("."); } printf("\n"); }
// fuer Zeilen // fuer Spalten // FELSEN // GANG
}
In unser Programm eingebaut (Dummyfunktionen ausfüllen) erhalten wir
192
Kapitel 7 Funktionen
Sandini Bib
########## ########## ########## ########## ########## ########## ########## ########## ########## ########## ########## ##########
Wie schön, das erste Anzeichen von intelligentem Leben in unserem Programm! Weil wir sowieso vorhaben, Zufallszahlen zu verwenden, spendieren wir unserem Programm eine Funktion zz. Und weil wir der Versuchung nicht widerstehen können, statische lokale Variablen auszuprobieren, schreiben wir Labyrinth2a.c
/* Liefere Zufallszahl von 0 bis n−1. Wird beim ersten Aufruf initialisiert. */ int zz(int n) { static int ersteraufruf = 1; /* initialisiere Zufallszahlen */ if (ersteraufruf) { ersteraufruf = 0; srand(time(0)); } /* Zufallszahl von 0 bis n−1 */ return rand() % n; }
Der Trick ist, den Generator beim ersten Durchgang mit srand zu initialisieren und dann mit der statischen lokalen Variable ersteraufruf zu verhindern, dass srand nochmals aufgerufen wird. Das ist hübsch, weil wir dann alle Funktionen des Zufallsgenerators und auch time in zz verpackt haben. Der Rest des Programms muss einzig und allein zz kennen. Oft schreibt man den Aufruf von srand einfach an den Anfang von main, weil das etwas einfacher ist. Versuche einmal, in inilab mit zz(2) statt FELSEN zu initialisieren: labyrinth[j][i] = zz(2);
Damit wählst du zufällig 1 (FELSEN) oder 0 (GANG) aus. Das Endergebnis sieht z. B. so aus: 7.13
Labyrinth
193
Sandini Bib
.#######.# #....#.##. #...#..... .#...###.# ...#.#..#. .#..#.##.# #.##...### ##.#..###. .###.####. ###.##...# ..#...###. #.#.#..###
Nicht schön, aber selten. Wir bemerken wie so oft erst beim Ausprobieren etwas, das uns in unserem groben Plan noch nicht so klar war. Schau dir das Zufallsbild an. Wir müssen uns entscheiden, ob wir schräge und diagonale Gänge zulassen wollen. Einfach so beschließen wir, ein rechtwinkliges Labyrinth ohne diagonale Gänge zu erzeugen, wie zu Beginn dieses Unterkapitels eines gezeigt ist. Jetzt ist es an der Zeit, sich genau zu überlegen, wie die Gänge überhaupt gegraben werden sollen. Nimm ein Blatt Karopapier zur Hilfe. Stell dir vor, wir fangen auf einem Gangfeld an. Wir graben ein Feld weit in eine zufällig gewählte Richtung. Dann graben wir noch mal ein Feld weit in eine zufällig gewählte Richtung. Wenn wir beim zweiten Schritt links oder rechts abbiegen, gibt es schräge Kanten! Das wäre auch nett, aber ich überlasse es dir, diese Variante auszuprobieren. Dann sind wir gezwungen, mindestens zwei Felder geradeaus zu graben. Und der Einfachheit halber beschließen wir, immer in Zweierschritten zu graben. Das Labyrinth sieht dann in jeder Richtung ungefähr so aus: Gang, Wand, Gang, Wand, Gang, Wand, . . ., wobei eine Wand natürlich auch abgegraben sein kann. Im Grunde gibt es also auf jedem zweiten Feld Räume und dazwischen entweder einen Gang oder eine Wand. Wenn wir von einem Punkt aus nur Zweierschritte machen, ergibt sich ein regelmäßiges Gitter. Ändere inilab wie folgt. Statt labyrinth[j][i] = zz(2);
schreiben wir labyrinth[j][i] = (i%2 || j%2) ? FELSEN : GANG;
Überlege mal. Die Bedingung ist wahr, wenn i oder j ungerade sind. Sie ist falsch, wenn sowohl i als auch j gerade sind. Wir müssten alle zwei Schritte einen Raum erhalten. Wir erhalten tatsächlich:
194
Kapitel 7 Funktionen
Sandini Bib
.#.#.#.#.# ########## .#.#.#.#.# ########## .#.#.#.#.# ########## .#.#.#.#.# ########## .#.#.#.#.# ########## .#.#.#.#.# ##########
Das macht alles viel klarer, oder? Wir müssen eigentlich nur noch einige Wände entfernen, und schon haben wir ein schönes Labyrinth. Das ist eine gute Idee, die du auch ausprobieren solltest. Das Problem ist, wie verhindert man, dass sich Gänge kreuzen? Bevor eine Wand durchbrochen wird, musst du überprüfen, dass es nicht schon einen zweiten Weg zu dem Raum auf der anderen Seite der Wand gibt. Wir werden dieses Problem umgehen, indem wir die Räume nicht von Anfang an, sondern einen nach dem anderen aushöhlen. Die Regel ist einfach: 1. Fange mit einem Labyrinth mit nur einem ausgehöhlten Raum an. Dieser
Raum ist unsere Startposition. 2. Grabe zwei Felder geradeaus in eine zufällig gewählte Richtung, aber nur
dann, wenn der Raum auf der anderen Seite des Wandfeldes noch aus Felsen besteht. 3. Wähle einen der vorhandenen ausgehöhlten Räume zufällig aus und fahre
mit 2. fort, bis alle Räume hohl sind. Punkt 2 stellt sicher, dass sich unsere Gänge niemals kreuzen. Vom ersten Raum ausgehend, wuchern sie zufällig in alle Richtungen, aber Kreuzungen kann es wegen 2 nicht geben. Also ist garantiert, dass es zu jedem Punkt nur einen Weg gibt. Wie verhindern wir, dass wir beim Graben die Außenwand des Labyrinths durchbrechen? Wir könnten bei jedem Schritt testen, ob einer der Indizes i und j zu groß oder zu klein wird. Etwas einfacher ist es, den Feldtyp AUSSENWAND zu erfinden. Das soll ein FELSEN sein, der nicht abgegraben werden kann. Hier ist die entgültige Version von inilab:
7.13
Labyrinth
195
Sandini Bib Labyrinth3a.c
/* Initialisiere Labyrinth: setze auessere Felder auf AUSSENWAND innere Felder auf FELSEN setze drei Felder unten rechts auf GANG als Eingang */ void initlab(void) { int i, j; /* doppelte Schleife ueber alle Felder */ for (j = 0; j <= jmax; j++) for (i = 0; i <= imax; i++) /* ein minimaler oder maximaler Index bedeutet Aussenwand */ if (i == 0 || i == imax || j == 0 || j == jmax) labyrinth[j][i] = AUSSENWAND; /* ansonsten sind wir im Inneren */ else labyrinth[j][i] = FELSEN; /* lege einen Zugang an */ for (i = 0; i <= 2; i++) labyrinth[jmax−2][imax−i] = GANG; }
Wir graben nach der Initialisierung mit FELSEN und AUSSENWAND einen Zugang, von dem aus die Gänge sich ausbreiten sollen. Ausgabe mit Labyrinth3b.c
/* Zeige das Labyrinth mit printf. */ void zeiglab(void) { int i, j; printf("\n"); for (j = 0; j <= jmax; j++) { printf(" "); for (i = 0; i <= imax; i++) { if (labyrinth[j][i] == FELSEN) printf("#"); else if (labyrinth[j][i] == GANG) printf("."); else printf("@"); } printf("\n"); } }
196
Kapitel 7 Funktionen
// fuer Zeilen // fuer Spalten // FELSEN // GANG // AUSSENWAND
Sandini Bib
ergibt @@@@@@@@@@@ @#########@ @#########@ @#########@ @#########@ @#########@ @#########@ @#########@ @#########@ @#########@ @#######... @#########@ @@@@@@@@@@@
Wir stehen in den Startlöchern, lasst das Graben beginnen! Der Plan ist, einen ausgehöhlten Raum zufällig auszuwählen und von dort aus zwei Felder weit zu graben. Hier ist die Funktion baulab: Labyrinth4a.c
/* Baue das Labyrinth. Felder mit geraden i und j sind Raeume. Felder mit ungeraden i oder j sind Waende. Es werden so lange Gaenge zufaellig gegraben, bis alle Raeume ausgehoehlt und verbunden sind. */ void baulab(void) { int i, j, n; int ni = imax/2 − 1; int nj = jmax/2 − 1; int nzugraben = ni * nj − 1; /* solange es was zu graben gibt */ for (n = 0; n < nzugraben;) { /* finde einen leeren Raum durch Rumprobieren */ do { i = 2*zz(ni) + 2; j = 2*zz(nj) + 2; } while (labyrinth[j][i] != GANG); /* buddel drauf los, vermerke das Ergebnis */ n += graben(i, j); } }
Wir probieren so lange mit zufällig gewählten Räumen herum, bis wir einen ausgehöhlten finden. Das ist natürlich sehr ineffizient. Schreibe bei Gelegenheit 7.13
Labyrinth
197
Sandini Bib
eine Funktion, die alle unfertigen Räume in einer Liste speichert. Dann kann das Programm aus dieser Liste einen Raum auswählen. Nach dem Graben muss der Raum aus der Liste entfernt werden. Lies die Funktion baulab genau durch. Das Schwierige ist, die Indizes genau so zu setzen, dass die Zweierabstände stimmen. Die innere do Schleife probiert herum. Wenn ein leerer Raum gefunden wurde, buddeln wir drauf los, aber in einer Extrafunktion graben, damit wir die Übersicht behalten. Der Rückgabewert von graben soll 1 sein, wenn tatsächlich ein neuer Gang angelegt und ein Raum ausgehöhlt wurde. Beim Rumprobieren finden wir aber auch Räume, von denen aus nicht mehr gegraben werden darf! In diesem Fall soll graben 0 liefern. Die Variable n zählt, wie viele Räume fertig sind. Die äußere for-Schleife testet mit n < nzugraben, ob alle Räume ausgegraben sind. Im Rechteck gibt es ni*nj Räume, aber wir setzen nzugraben = ni*nj − 1;, weil ein Raum als Startpunkt schon in inilab ausgehöhlt wurde. Diese Indexgymnastik ist nicht ganz einfach. Und es kommt darauf an, sie exakt richtig hinzubekommen. Du kannst das Beispiel mit Papier und Bleistift durchgehen oder den Debugger laufen lassen, wenn es Probleme gibt. Um baulab zu testen, schreiben wir eine vorläufige Funktion graben: Labyrinth4b.c
int graben(int i, int j) { if (labyrinth[j][i−2] == AUSSENWAND) return 0; labyrinth[j][i−2] = GANG; labyrinth[j][i−1] = GANG; return 1; }
So graben wir nach links, denn labyrinth[j][i−2] ist das Feld zwei Schritte nach links. Als Erstes stellen wir sicher, dass wir nicht die Außenwand durchbrechen. Wenn das Feld zwei Positionen weiter die Außenwand ist, kehren wir mit 0 zurück. Das bedeutet, ›nichts gegraben‹. Ansonsten graben wir den Raum mit Indizes [j][i−2] und die dazwischen liegende Wand mit [j][i−1] ab. Das heißt, wir setzen diese Felder auf den Wert GANG. Das Ergebnis ist:
198
Kapitel 7 Funktionen
Sandini Bib
@@@@@@@@@@@ @#########@ @#########@ @#########@ @#########@ @#########@ @#########@ @#########@ @#########@ @#########@ @#......... @#########@ @@@@@@@@@@@
Gut! Jetzt fehlt noch zweierlei. Wir wollen die Richtung, in der wir graben, zufällig auswählen und wir graben nur dann, wenn das Feld zwei Positionen weiter FELSEN ist, also weder GANG noch AUSSENWAND. Unsere endgültige Funktion graben ist
7.13
Labyrinth
199
Sandini Bib Labyrinth4c.c
/* Grabe einen Gang von Raum i,j zu einem Nachbarraum. Es wird in eine zufaellig ausgewaehlte Richtungen gebuddelt. Wichtig: Es wird nur gegraben, wenn der Nachbarraum noch voller Felsen ist, damit keine Querverbindungen entstehen. return: 0 falls kein neuer Gang und neuer Raum gegraben 1 sonst */ int graben(int i, int j) { int ineu, jneu; int richtung; /* return 0 falls keiner der Nachbarr"aume aus FELSEN */ if (labyrinth[j][i+2] != FELSEN && labyrinth[j+2][i] != FELSEN && labyrinth[j][i−2] != FELSEN && labyrinth[j−2][i] != FELSEN) return 0; /* finde eine Richtung mit Felsen durch Rumprobieren das vorhergehende if stellt sicher, dass es die gibt! */ do { richtung = zz(4); if (richtung == 0) { ineu = i + 2; jneu = j; } if (richtung == 1) { ineu = i; jneu = j + 2; } if (richtung == 2) { ineu = i − 2; jneu = j; } if (richtung == 3) { ineu = i; jneu = j − 2; } } while (labyrinth[jneu][ineu] != FELSEN); /* hoehle den neuen Raum aus */ labyrinth[jneu][ineu] = GANG; /* grabe die Wand ab */ ineu = i + (ineu−i)/2; jneu = j + (jneu−j)/2; labyrinth[jneu][ineu] = GANG; /* Erfolg */ return 1; }
200
Kapitel 7 Funktionen
Sandini Bib
Als Erstes stellen wir sicher, dass es überhaupt eine Richtung gibt, in die noch gegraben werden darf. Wenn nicht, return 0. Dann wählen wir so lange Richtungen zufällig aus, bis wir eine der erlaubten Richtungen mit labyrinth[jneu][ineu] == FELSEN gefunden haben. Wir graben den Raum, wir graben die Wand ab und melden den Erfolg mit return 1;. Das Praktische ist, dass wir alle Aufgaben, die mit verschiedenen Richtungen im Labyrinth zu tun haben, in bauen versteckt haben. Na ja, fast alle, denn inilab gräbt den Eingang. Jedenfalls müssen wir uns nur an wenigen Stellen mit der etwas verwirrenden [j+2][i] Rechnerei herumschlagen. Mit dieser Funktion baulab erhalten wir @@@@@@@@@@@ @#########@ @#...#.#.#@ @#.#.#.#.#@ @#.#.#...#@ @###.###.#@ @#.....#.#@ @#####.#.#@ @#.#...#.#@ @#.###.#.#@ @#......... @#########@ @@@@@@@@@@@
Super! Geschafft! Toll! Eine nette Variante ist, die Zeilen zeiglab(); getchar();
direkt vor return 1; einzufügen. So wird das Labyrinth nach jedem Grabeschritt angezeigt. Das ist auch beim Debuggen sehr nützlich. Einfach zusätzliche Ausgabebefehle einbauen, wenn die Schritt-für-Schritt-Methode im Debugger nicht weiterhilft. Als Letztes polieren wir unser Programm noch ein bisschen. Die endgültige Version findest du in Labyrinth.c. Wahrscheinlich sieht die Ausgabe auch bei dir quadratischer aus, wenn du ## und .. für die Ausgabe in zeiglab verwendest. Tu das. Mir gefiel das Ergebnis ohne die Außenwand etwas besser, also habe ich in zeiglab die Schleifen von 1 bis imax−1 bzw. jmax−1 laufen lassen. In unserem Programm gibt es noch zwei versteckte Fehlerquellen, die wir zum Schluss beseitigen wollen. Das Labyrinth muss eine ungerade Anzahl von Feldern pro Kante haben, damit wir, wenn wir in Zweierschritten graben, nicht die Außenwandmarkierung überspringen. Weil die typische Schleife in z. B. inilab von i = 0 bis einschließlich i = imax läuft, müssen imax und jmax gerade Zahlen sein. Füge die Zeilen 7.13
Labyrinth
201
Sandini Bib Labyrinth.c
imax *= 2; jmax *= 2; if (imax >= 100 || jmax >= 100) { printf("imax oder jmax sind zu gross.\n"); return 0; }
am Anfang von main ein. Wir verdoppeln imax und jmax, damit sie gerade sind. Wir initialisieren also mit der Anzahl der Räume und arbeiten aber intern mit der Anzahl der Felder. Und besonders wichtig: Wir testen, dass diese Werte noch in die gegebene Größe von int labyrinth[100][100]; passen. Solche Speicherprobleme muss man auf jeden Fall vermeiden! Jetzt kannst du imax und jmax beliebig initialisieren, ohne dass das Programm überraschend abstürzt oder in eine Endlosschleife gerät. Blicke einmal an den Anfang dieses Unterkapitels zurück. Nach und nach haben wir uns an die endgültige Lösung des Problems herangetastet. Erst haben wir allgemein über Labyrinthe und ein mögliches Datenformat nachgedacht. Dann haben wir ein Skelett mit Dummies für unfertige Funktionen geschrieben. Schritt für Schritt haben wir die fehlenden Programmteile ausgefüllt, ausprobiert, umgeschrieben. Oft gab es Entscheidungen zu treffen, z. B. ob wir rechtwinklige Gänge wollen oder Räume, die größer als ein Feld sind. Beim ersten Durchgang haben wir die einfachste Lösung gewählt, statt gleich den optimalen Algorithmus anzustreben. Und wir hatten Erfolg!
7.14 Die Idee der Funktion macht es möglich, selbst komplizierte Probleme in überschaubare Einheiten zu zerlegen. Dabei werden Befehle zu Gruppen zusammengefasst und gleichzeitig die Sichtbarkeit und Gültigkeitsdauer von Variablen geregelt. Das ist, als ob wir die ganze Zeit mit Bausteinen gespielt haben (printf, getchar, . . .) und plötzlich herausfinden, dass wir aus einfachen Bausteinen neue Bausteine zusammenkleben können, die innen kompliziert sind, aber außen genauso schön einfach wie die alten. Und dann die Sache mit der Rekursion, dass wir einen Baustein in sich selbst hineinsetzen können – nicht nur Zweijährige kommen da ins Staunen. Wir fassen zusammen: Eine Funktion wird im Allgemeinen wie folgt definiert: Datentyp Funktionsname(Datentyp Variablenname, . . .) {
Befehle }
202
Kapitel 7 Funktionen
Sandini Bib
Ihr Prototyp ist Datentyp Funktionsname(Datentyp Variablenname, . . .);
Eine Funktion wird entweder durch ihre vorangehende Definition oder ihren Prototyp bekannt gemacht, bevor sie aufgerufen wird. Die Typbezeichnung void wird verwendet, falls eine Funktion keinen Rückgabewert liefert oder keine Argumente annimmt. Die Definition einer Variable gilt ab der Zeile, in der die Definition erfolgt, und zwar für den Rest der Datei, falls sie außerhalb einer Funktion definiert wird
(externe Variable), für den nachfolgenden Block, falls sie zu Beginn eines Funktionsblocks definiert wurde oder falls sie als Argument an eine Funktion übergeben wird (lokale Variable). Die Definition einer Variablen n kann zeitweise außer Kraft gesetzt werden, indem eine Variable mit gleichem Namen eingeführt wird. Bei jeder Verwendung des Namens n wird auf die momentan gültige Variable zugegriffen. Trotz gleichen Namens wird unterschiedlicher Speicherplatz bereitgestellt. Lokale Variablen sind normalerweise automatisch, d.h. sie werden bei Aufruf der Funktion angelegt und verschwinden bei Beendigung der Funktion wieder. Damit eine lokale Variable nicht verschwindet und ihren Wert behält, definiert man sie als statische lokale Variable, z. B. static int i;. Externe Variablen sind bei Programmablauf immer verfügbar, d.h. in diesem Sinne sind externe Variablen statisch.
7.15 1. Schreibe eine übersichtlichere Version des Zooprogramms in Kapitel 6.8, in-
dem du jedes Tier in seiner eigenen Funktion malst. 2. Schreibe eine übersichtlichere Version der Primzahlprogramme in Kapitel
6.10. Insbesondere kannst du den Primzahltest in einer Funktion verpacken. So manche Mehrfachschleife kann vereinfacht werden, indem eine innere Schleife in eine Funktion ausgelagert wird. 3. Die Headerdateien mit den Prototypen für verschiedene Bibliotheken stehen
bei BCB im Verzeichnis Borland\CBuilder\Include
Bei mir sind das mehr als 300 Dateien! Wenn du den Textcursor auf eine Dateibezeichnung im Editor setzt und auf Strg + ↵ drückst, erscheint die Datei im Editor. Hier kannst du mit Strg + F nach einem Prototypen von getchar oder printf suchen. Diese Headerdateien sind sehr unübersichtlich. 7.15
203
Sandini Bib
4. Wenn du den Prototypen einer Bibliotheksfunktion wissen möchtest, kannst
du die Hilfefunktion aufrufen. Textcursor auf Funktion bewegen, F1 drücken. Noch wirst du nicht alle Prototypen verstehen können, weil wir Zeiger erst in Kapitel 10 besprechen. Zeiger erkennst du in Prototypen am Sternchen *. Schau dir einmal die Hilfe zu rand und srand an. Dort findest du Beispiele, und unter ›Siehe auch‹ weitere Funktionen, die von Interesse sind. 5. Schreibe eine rekursive Version des Flutfüll-Algorithmus (
›flood fill‹). In einem Grafikfenster soll eine Fläche ausgemalt werden, die durch eine bestimmte Farbe begrenzt ist. Gemalt wird mit SetPixel, und mit GetPixel wird überprüft, ob der Rand erreicht wurde. Dazu verwendest du eine Funktion, die mit den Koordinaten eines Pixels aufgerufen wird. Wenn dieses Pixel die Randfarbe hat, kehrt die Funktion sofort zurück. Wenn das Pixel nicht die Randfarbe hat, wird es angemalt und die Funktion ruft sich selbst viermal für die benachbarten vier Pixel auf. Teste die Funktion in einem Fenster, in das du ein Rechteck oder etwas Komplizierteres gemalt hast. Was passiert am Rand?
6. Hier sind ein paar Vorschläge für das Labyrinth-Programm: Statt in baulab rumzuprobieren, bis ein leerer Raum gefunden ist, verwalte
eine Liste mit leeren Räumen. Statt Gänge und Räume zu graben, initialisiere mit ausgehöhlten Räumen und entferne die Wände. Manchmal ist es zu einfach, den Weg vom Eingang in die linke obere Ecke zu finden. Grabe in zwei Stufen. In der ersten Stufe blockierst du das Zentrum (oder sonstwo), indem du einige Felder auf AUSSENWAND setzt. Wenn drumherum alles ausgegraben ist, gebe das Zentrum frei, indem du wieder auf FELSEN umstellst. So erhältst du im Zentrum lauter Sackgassen, die nicht nach links oben führen. Schreibe eine Funktion, die den Weg von rechts unten nach links oben findet. Mit WEG oder so kann er von zeiglab ausgegeben werden. Eine simple Regel ist: 1. Bei Abzweigungen immer rechts halten. 2. Falls Sackgasse, um 180 Grad drehen. Solche Schatzsucher kannst du hereinlegen, indem du nach Erzeugung des Labyrinths ein paar Wände zusätzlich entfernst, so dass die obige Regel im Kreis herumführen kann. Der Schatzsucher muss dann um Brotkrumen bzw. Steinchen erweitert werden, die er fallen lässt, um nicht einen Weg zweimal zu gehen.
204
Kapitel 7 Funktionen
Sandini Bib
8 Fensternachrichten 8.0 8.1 8.2
Achtung, Nachricht: Bitte malen! Nachrichtenwarteschlage und Nachrichtenschleife WM PAINT
206 210 211
8.3
Fenster auf, zu, groß, klein
212
8.4
Klick mich links, klick mich rechts
215
8.5
Na, wo läuft sie denn, die Maus?
218
8.6
Tastatur
221
8.7
Die Uhr macht tick
227
8.8
233
8.9
234
Sandini Bib
Was wohl mit Fensternachrichten gemeint ist? Mit Fenster meine ich hier die Fenster, die von einer typischen Windowsanwendung auf deinem Bildschirm aufgemacht werden. Bisher hat sich unsere Handhabung von solchen Fenstern auf ein absolutes Minimum beschränkt. Das Fenster wird in WinHallo.cpp erzeugt, und nur wenn es was zu malen gibt, wird unsere selbst gemachte Funktion malen aufgerufen. Das soll jetzt anders werden! Wie man Fenster aufmacht, werden wir in Kapitel 10 besprechen, weil man dazu erst die Sprache der Zeiger und Strukturen in C lernen muss. Aber in diesem Kapitel werde ich dir einfache Möglichkeiten zeigen, wie dein Windowsprogramm auf Maus und Tastatur reagieren kann. Die verschiedenen Ereignisse, auf die ein Fenster reagieren kann, werden als ›messages‹) an eine bestimmte Funktion deines Programms Nachrichten ( übergeben, die so genannte Fensterprozedur ( ›windows procedure‹). Klickst du mit der Maus in dein Fenster, wird deine Fensterprozedur mit der Nachricht Maus-geklickt aufgerufen und es werden z. B. auch die Koordinaten des Mauszeigers übermittelt. Taste gedrückt, die Fensterprozedur erhält die Nachricht Tastegedrückt zusammen mit der Information, welche Taste das war. Es gibt auch Nachrichten für Ereignisse, an die du vielleicht nicht im Zusammenhang mit Nachrichten gedacht hast, z. B. wenn sich die Größe deines Fensters ›timer‹) aktivieren, der alle zehnändert. Du kannst auch einen Zeitgeber ( tel Sekunde eine Nachricht verschickt, um zu veranlassen, dass ein Ball 5 Pixel weiter links gezeichnet wird. Aber wie könnte es anders sein, als Erstes kramen wir das gute alte Hallo-Welt-Beispiel hervor, um Fensternachrichten Schritt für Schritt einzuführen.
8.0
Achtung, Nachricht: Bitte malen!
Für die Beispiele mit Nachrichten verwenden wir das BCB-Projekt WinMain.mak. Du findest es im Verzeichnis ProgLernen\Windows auf der Buch-CD. Starte das Projekt WinMain.mak. Füge die Datei Nachricht0.c zum Projekt hinzu. Speichere das Projekt und die C-Datei in einem Verzeichnis deiner Wahl. Drücke F9 , und nach dem erfolgreichen Kompilieren erhältst du
206
Kapitel 8 Fensternachrichten
Sandini Bib
Der Programmtext sieht so aus: Nachricht0.c WinMain.cpp
#include <windows.h> /* Male im Fenster */ void malen(HDC hdc) { TextOut(hdc, 50, 50, "Hallo, Welt!", 12); } /* Fensterprozedur unseres Programms hwnd: Handle des Fensters m: Nummer der Nachricht wParam, lParam: verschiedene Parameter je nach Art der Nachricht */ LRESULT CALLBACK WindowProc(HWND hwnd, UINT m, WPARAM wParam, LPARAM lParam) { HDC hdc; // Malen if (m == WM_PAINT) { hdc = GetDC(hwnd); malen(hdc); ReleaseDC(hwnd, hdc); ValidateRect(hwnd, 0); } // Fenster zu else if (m == WM_DESTROY) PostQuitMessage(0); // Default else return DefWindowProc(hwnd, m, wParam, lParam); // falls wir die Nachricht bearbeitet haben return 0; }
8.0
Achtung, Nachricht: Bitte malen!
207
Sandini Bib
Am Anfang steht die Funktion malen, wie du sie schon aus vielen Beispielen kennst. Aber jetzt lüften wir das Geheimnis, wie malen aufgerufen wird. In LRESULT CALLBACK WindowProc(HWND hwnd, UINT m, WPARAM wParam, LPARAM lParam)
beginnen wir die Definition der Windows Procedure namens WindowProc. Die Wörter in Großbuchstaben werden durch #include <windows.h> definiert. Windows gibt sich sehr oft nicht mit den simplen Datentypen in C zufrieden, sondern definiert alles mögliche für seine eigenen Zwecke. LRESULT steht für eine bestimmte Art von Ergebnis ( ›result‹), das die Funktion WindowProc liefern soll. Es folgt ein zweites Wort, CALLBACK, was ebenfalls mit ›Call back‹ heißt ›rufe zurück‹: Hast dem Typ der Funktion zu tun hat. du eine Nachricht? Ruf mich zurück! Deshalb nennt man WindowProc auch die Rückruffunktion des Fensters. Die Funktion WindowProc wird mit vier Argumenten aufgerufen. HWND steht für einen Fenster-Handle. Auch Fenster haben einen Griff, mit dem sie identifiziert werden können! Das zweite Argument ist die Nachricht, die bearbeitet werden soll. UINT legt nahe, dass es sich dabei einfach um eine ganze Zahl handelt. Es folgen zwei weitere Argumente, jeweils mit eigenem Typ, die Zusatzinformationen zur Nachricht enthalten. Lass dich von den vielen großbuchstabigen Namen und Typen nicht aus der Ruhe bringen. Für deine eigenen Programme musst du meistens gar nicht genau wissen, was dahinter steckt, denn du kannst die entsprechenden Variablen blindlings weiterreichen. Man muss nur wissen, welche Funktion welchen Typ erwartet. So haben wir es schon die ganze Zeit mit HDC und der Variable hdc gemacht. Also begeben wir uns frohen Mutes ins Innere des Funktionsblocks. Schau genau hin, die Funktion besteht im Wesentlichen aus einer Fallunterscheidung, mit der wir verschiedene Nachrichten sortieren und entsprechend reagieren. In HDC hdc; // Malen if (m == WM_PAINT) { hdc = GetDC(hwnd); malen(hdc); ReleaseDC(hwnd, hdc); ValidateRect(hwnd, 0); }
siehst du, wie man mit Nachrichten umgeht. Die Variable m ist die Nummer der ›Windows Nachricht. Mit m == WM_PAINT testen wir, ob wir die Nachricht Message Paint‹ (Fensternachricht Malen) bekommen haben. Das ist gut lesbar, weil Windows für uns eine symbolische Konstante WM_PAINT definiert hat. Es spielt überhaupt keine Rolle, welcher Zahlenwert sich hinter WM_PAINT versteckt, du musst nur den Namen kennen. 208
Kapitel 8 Fensternachrichten
Sandini Bib
Das Malen selbst ist einfach. Mit hdc = GetDC(hwnd);
besorgst du dir den Handle hdc, den du zum Malen brauchst. Dann rufst du unsere Funktion malen auf, die, wie du aus unseren Beispielen weißt, alles mögliche mit hdc anstellen kann. Ich verwende die Funktion malen hier nur, weil wir seit Kapitel 4 mit dieser Funktion gemalt haben. Ausprobieren, du kannst malen entfernen und TextOut gleich in der Zeile nach GetDC aufrufen. Nach dem Malen gibst du den Device Context mit ReleaseDC(hwnd, hdc);
wieder frei. Dann rufst du ValidateRect(hwnd, 0);
auf. ›validate‹ heißt ›gültig machen‹. Wir teilen Windows mit, dass wir das Innere des Fensters auf den neuesten Stand gebracht haben. Falls nur bei der Nachricht WM_PAINT gemalt werden soll, verwendet man gewöhnlich die Funktion BeginPaint, um sich den Device Context zu holen, und EndPaint, um das Malen zu beenden, siehe die Windows SDK-Hilfe. Mit der gezeigten Methode können wir aber jederzeit ins Fenster malen. Die Nachricht WM_PAINT wird immer dann erzeugt, wenn Windows möchte, dass dein Programm den Inhalt seines Fensters ausmalt. Zu bedenken bei Windowsanwendungen ist, dass ein solches WM_PAINT aus ganz verschiedenen Gründen nötig werden kann. Wenn dein Programm den ganzen Bildschirm für sich hätte, könntest du, wann immer du willst, die Pixel auf dem Bildschirm ändern. In Windows gibt es aber viele Fenster. Was, wenn ein anderes Fenster deines verdeckt und später wieder aufdeckt? Was, wenn dein Fenster minimiert oder maximiert wird? Die grundlegende Strategie in Windows ist, dass jedes Programm in der Lage sein sollte, zu einem beliebigen Zeitpunkt den gesamten Inhalt seines Fensters neu zu malen, und zwar immer dann, wenn WM_PAINT verschickt wird. Zurück zum Beispiel. Falls die Nachricht WM_PAINT war, überspringt das Programm nach dem Malen alle anderen Fälle in der Fallunterscheidung und kehrt mit return 0; aus der Funktion WindowProc zurück. Die zweite Nachricht, auf die wir in unserem Beispiel achten, ist WM_DESTROY (Fensternachricht Zerstören): // Fenster zu else if (m == WM_DESTROY) PostQuitMessage(0);
Diese Nachricht wird zum Beispiel erzeugt, wenn du ein Fenster mit dem Kreuz rechts oben schließen möchtest. Die normale Reaktion auf diese Nachricht ist, mit PostQuitMessage selbst eine Nachricht zu verschicken, und zwar WM_QUIT. 8.0
Achtung, Nachricht: Bitte malen!
209
Sandini Bib
Wie nett, du kannst dir selbst Nachrichten schicken! Diese Nachricht wird nicht in WindowProc verarbeitet, führt aber dazu, dass das Fenster zugemacht wird, siehe Kapitel 8.1. Alle Nachrichten, die wir nicht selbst bearbeiten, müssen wir an die Funktion DefWindowProc weitergeben: // Default else return DefWindowProc(hwnd, m, wParam, lParam);
Wir rufen DefWindowProc gleich im return auf, um die Funktion WindowProc mit dem Rückgabewert von DefWindowProc zu verlassen. Das heißt entweder wurde eine Nachricht in der Fallunterscheidung erkannt und WindowProc kehrt mit return 0; zurück, oder keine Nachricht wurde erkannt und wir überlassen die Arbeit DefWindowProc. Du kannst ausprobieren, was passiert, wenn du einen der Fälle auskommentierst. Wenn du WM_PAINT oder WM_DESTROY nicht in der Fallunterscheidung abfängst, gibt es Probleme, weil DefWindowProc nur wenig automatisch macht. Wenn du WM_PAINT nicht abfängst, wird wie zu erwarten nichts gemalt. Wenn du WM_DESTROY nicht abfängst, geht das Fenster beim Klick aufs Kreuzchen zwar zu, aber das Programm läuft weiter. Das siehst du in der Überschrift des BCBFensters. Drücke Strg + F2 , um das Programm zurückzusetzen (oder im Notfall Strg + Alt + Entf , drei Tasten gleichzeitig). Wenn du den Default entfernst, geht das Fenster erst gar nicht auf, aber das Programm läuft trotzdem. Drücke Strg + F2 .
8.1 Nachrichtenwarteschlage und Nachrichtenschleife Erinnerst du dich an unsere Diskussion des Tastaturpuffers bei Textbildschirmanwendungen? Dort warten alle eingegebenen Zeichen, bis sie der Reihe nach abgeholt werden. Ähnlich ist es mit den Nachrichten einer Windowsanwendung. Als Erstes ist anzumerken, dass unsere Programme nicht direkt auf Maus oder Tastatur zugreifen, sondern dass das normalerweise die Aufgabe des Betriebssystems ist, in unserem Fall Windows. Windows verwaltet verschiedene Eingabegeräte und verschiedene Fenster und Anwendungen gleichzeitig. Im Normalfall bekommt ein Fenster nur dann Nachrichten, wenn es das aktive Fenster ist (man sagt auch, wenn es den Fokus hat). Wenn du ein Fenster anklickst, bekommt es den Fokus und erhält z. B. eine andere Farbe im Überschriftsbalken. Alle Nachrichten an eine bestimmte Anwendung werden bei ihrem Eintreffen ›message queue‹) eingereiht. Eine Textin eine Nachrichtenwarteschlange ( bildschirmanwendung erwartet Eingaben von der Tastatur, die dann im besagten Tastaturpuffer darauf warten, mit getchar oder scanf gelesen zu werden. Jetzt riskiere einmal einen Blick auf WinMain.cpp. Wie in 4.0 schon erwähnt, beginnt 210
Kapitel 8 Fensternachrichten
Sandini Bib
mit dem Aufruf der Funktion WinMain die Ausführung unseres Windowsprogramms. Wenn du einfach nur die Kommentare liest, wird dir klarwerden, dass allerlei Kleinkram zu erledigen ist, bis ein Fenster aufgemacht wird. Bevor das Programm mit dem Verlassen von WinMain beendet wird, findest du die folgende Schleife: while (GetMessage(&msg, 0, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); }
Das ist die Nachrichtenschleife eines typischen Windowsprogramms. GetMessage holt eine Nachricht aus der Warteschlange und speichert sie in der Variablen msg (›message‹). Wie bei scanf steht hier das Und-Zeichen & vor dem Variablennamen (siehe Kapitel 10). Die Schleife wird beendet, wenn der Rückgabewert von GetMessage gleich 0 ist, und das ist der Fall, wenn die Nachricht WM_QUIT gelesen wurde. ›transIn der Schleife wird mit TranslateMessage die Nachricht übersetzt ( late‹ heißt übersetzen). Das ist nötig, falls Tastatureingaben in Zeichen vom Typ char umgewandelt werden sollen, siehe Beispiel 8.6. Der Aufruf von DispatchMessage liefert die Nachricht an die zuständige Fensterprozedur oder Rückruffunktion ab. In unserem Fall führt das dazu, dass WindowProc aufgerufen wird. Im Allgemeinen ist es möglich, dass es verschiedene Rückruffunktionen für verschiedene Zwecke gibt, deshalb wird DispatchMessage als Verteiler und nicht WindowProc direkt aufgerufen. Davon abgesehen, dass bei einem Windowsprogramm mehr Möglichkeiten der Eingabe bestehen, funktioniert die Nachrichtenschleife also genauso wie eine typische Eingabeschleife bei einer Textbildschirmanwendung.
8.2 WM PAINT Die Nachricht WM_PAINT spielt eine Sonderrolle in der Nachrichtenschlange. Falls ein Fenster aufgedeckt oder seine Größe geändert wird, verschickt Windows die Nachricht WM_PAINT nicht auf der Stelle, sondern wartet, bis die Nachrichtenschlange leer ist. Solange noch andere Nachrichten zu bearbeiten sind, merkt sich Windows, für welche rechteckigen Teile eines Fensters Malwünsche eingehen. Mit dem Funktionsaufruf InvalidateRect(hwnd, 0, 0);
kannst du selber veranlassen, dass der ganze Inhalt des Fensters mit Handle hwnd für das Malen vorgemerkt wird. ›Invalid‹ heißt ungültig. Falls die Nachrichtenschlange leer ist und falls ein Fenster ganz oder teilweise neu zu malen ist, liefert GetMessage die Nachricht WM_PAINT ab, die wir dann in 8.2 WM PAINT
211
Sandini Bib
WindowProc bearbeiten. In unseren Beispielen kümmern wir uns nicht darum,
ob ganz oder teilweise neu gemalt werden soll, und malen immer das ganze Fenster. Nach dem Malen erklären wir mit ValidateRect(hwnd, 0);
den gesamten Fensterinhalt wieder für gültig. Wenn wir das nicht tun, denkt Windows, dass noch nicht gemalt wurde, und GetMessage liefert bei leerer Nachrichtenschlange immer wieder ein neues WM_PAINT. Normalerweise ist es sinnvoll, mit dem Malen zu warten, bis die Nachrichtenschlange leer ist, damit dein Programm vor der Ausgabe auf alle Eingaben reagieren kann. Du hast aber sicher auch schon Fenster gesehen, die einfach weiß bleiben, weil irgendeine Aufgabe unerwartet lange dauert, und die Ausgabe erst weiter hinten in der Nachrichtenschlange drankommt. Mit UpdateWindow(hwnd);
kannst du die Nachrichtenschlange umgehen und ein WM_PAINT direkt an deine Fensterprozedur schicken. UpdateWindow verschickt WM_PAINT aber nur, wenn die Vormerkliste von Windows nicht leer ist. In Windows gibt es außer WM_PAINT noch jede Menge andere Nachrichten. Einige besonders interessante Nachrichten werden wir jetzt in mehreren Beispielen besprechen.
8.3
Fenster auf, zu, groß, klein
Zwei Nachrichten, die mit der Verwaltung von Fenstern zu tun haben, sind WM_CREATE und WM_SIZE:
212
Kapitel 8
Fensternachrichten
Sandini Bib Nachricht1.c WinMain.cpp
#include <windows.h> #include <stdio.h> /* externe Variable */ HDC hdc; char text[1000]; int n = 0; int xmax = 0; int ymax = 0; /* Rueckruffunktion unseres Fensters */ LRESULT CALLBACK WindowProc(HWND hwnd, UINT m, WPARAM wParam, LPARAM lParam) { // Fenster auf if (m == WM_CREATE) { SetWindowText(hwnd, "Beispiel fuer WM_SIZE"); hdc = GetDC(hwnd); SetTextAlign(hdc, TA_CENTER); } // Neue Fenstergroesse else if (m == WM_SIZE) { xmax = LOWORD(lParam); ymax = HIWORD(lParam); } // Malen else if (m == WM_PAINT) { sprintf(text, "Aufruf %d: xmax = %d, ymax = %d", n++, xmax, ymax); TextOut(hdc, xmax/2, ymax/2, text, strlen(text)); ValidateRect(hwnd, 0); } // Standard else if (m == WM_DESTROY) PostQuitMessage(0); else return DefWindowProc(hwnd, m, wParam, lParam); return 0; }
Als Erstes beachte, dass wir fünf externe Variablen definieren. Mit der Variable n zählen wir, wie oft wir die Nachricht WM_PAINT bekommen haben. Der Wert 8.3
Fenster auf, zu, groß, klein
213
Sandini Bib
in n bleibt wie bei allen externen Variablen unabhängig von Funktionsaufrufen erhalten. Siehst du das n++? Wenn du die Größe des Fensters änderst, wird in der Mitte des Fensters die neue Größe ausgegeben. In WindowProc behandeln wir als Erstes den Fall m == WM_CREATE. Diese Nachricht wird genau einmal übermittelt, und zwar bevor das Fenster zum ersten Mal gezeichnet werden soll. Hier können wir verschiedene Initialisierungen für unser Programm vornehmen. Mit SetWindowText(hwnd, "Beispiel fuer WM_SIZE");
ändern wir die Überschrift unseres Fensters, wozu wir hwnd und nicht hdc benötigen. Den Device Context speichern wir bei Beginn des Programms in der externen Variablen hdc und geben ihn nicht wieder her (kein ReleaseDC). Das ist unhöflich, aber in unserem Beispiel in Ordnung. Mit SetTextAlign(hdc, TA_CENTER);
legen wir fest, dass sich die Koordinaten in TextOut auf die Mitte des Textes beziehen sollen. Die Nachricht WM_DESTROY ist das Gegenstück zu WM_CREATE. Wenn du, bevor das Programm beendet wird, noch aufräumen musst, kannst du das bei WM_DESTROY erledigen. Wenn sich die Größe des Fensters ändert, aber auch bevor es zum ersten Mal angezeigt wird, wird die Nachricht WM_SIZE verschickt: // Neue Fenstergroesse else if (m == WM_SIZE) { xmax = LOWORD(lParam); ymax = HIWORD(lParam); }
Hier merken wir uns die neuen Maximalwerte für die x- und y-Koordinaten des Anzeigebereichs des Fensters in externen Variablen. Sieht etwas seltsam aus, aber so musst du mit Hilfe von LOWORD und HIWORD die Variable lParam in xmax und ymax zerlegen. Windows 3.0 oder noch älter lässt grüßen. Wir könnten die Nachricht WM_SIZE auch zum Anlass nehmen, das Fenster neu zu zeichnen. Aber das ist nicht nötig, denn bei unserem Fenster wird bei Größenänderungen auch ein WM_PAINT verschickt. Bei WM_PAINT verwenden wir in TextOut die Koordinaten xmax/2 und ymax/2, um den Text in die Mitte des Fensters zu zeichnen. Jetzt kannst du gut ausprobieren, wozu ValidateRect da ist. Weg damit, oder setze // davor. Das Programm zählt, was das Zeug hält. Der Grund ist, dass WindowProc so lange immer wieder mit WM_PAINT aufgerufen wird, bis dein Programm mitteilt, dass das Fenster wieder auf dem neuesten Stand ist.
214
Kapitel 8 Fensternachrichten
Sandini Bib
8.4
Klick mich links, klick mich rechts
Können Mäuse Briefe schreiben? In Windows gibt es z. B. die folgenden Mausnachrichten: Maustaste
heruntergedrückt
losgelassen
doppelgeklickt
Links Mitte Rechts
WM_LBUTTONDOWN
WM_LBUTTONUP
WM_LBUTTONDBLCLK
WM_MBUTTONDOWN
WM_MBUTTONUP
WM_MBUTTONDBLCLK
WM_RBUTTONDOWN
WM_RBUTTONUP
WM_RBUTTONDBLCLK
›Button‹ heißt Knopf oder Taste. Weil nicht jede Maus eine mittlere Taste hat, werden wir diese Möglichkeit ignorieren. Die Doppelklicknachrichten werden verschickt, wenn die Taste nach kurzer Zeit erneut runtergedrückt wurde. Hier ist ein erstes Beispiel, zur Abwechslung mit statischen lokalen Variablen: RasendesHallo.c WinMain.cpp
#include <windows.h> /* Male ins Fenster */ void malen(HDC hdc, int xmax, int ymax) { char text[100] = "Hallo, Welt!"; int x, y; COLORREF farbe; x = rand() % xmax − 30; y = rand() % ymax − 10; farbe = RGB(rand()%256, rand()%256, rand()%256); SetTextColor(hdc, farbe); TextOut(hdc, x, y, text, strlen(text)); }
Klick mich links, klick mich rechts
215
Sandini Bib RasendesHallo.c WinMain.cpp
/* Rueckruffunktion unseres Fensters */ LRESULT CALLBACK WindowProc(HWND hwnd, UINT m, WPARAM wParam, LPARAM lParam) { static HDC hdc; static int xmax = 0, ymax = 0; static int warten = 1; static int textbkmode = OPAQUE; // Fenster auf if (m == WM_CREATE) { SetWindowText(hwnd, "Hallo Zufall: Klick mich links/rechts"); hdc = GetDC(hwnd); } // Neue Fenstergroesse else if (m == WM_SIZE) { xmax = LOWORD(lParam); ymax = HIWORD(lParam); } // Malen else if (m == WM_PAINT) { malen(hdc, xmax, ymax); if (warten) ValidateRect(hwnd, 0); } // Maus // Linke Maustaste schaltet um zwischen warten und nicht warten else if (m == WM_LBUTTONDOWN) { warten = !warten; InvalidateRect(hwnd, 0, 0); } // Rechte Maustaste schaltet Durchsichtigkeit des Texthintergrunds else if (m == WM_RBUTTONDOWN) { if (textbkmode == TRANSPARENT) textbkmode = OPAQUE; else textbkmode = TRANSPARENT; SetBkMode(hdc, textbkmode); } // Standard else if (m == WM_DESTROY) PostQuitMessage(0); else return DefWindowProc(hwnd, m, wParam, lParam); return 0; }
Lass das Programm laufen. Fängt harmlos an, ein buntes Hallo. Maximiere das Fenster, und klicke mit der linken Maustaste irgendwo ins Fenster. Nach dem Mausklick fängt das Programm an, fortlaufend in großer Geschwindigkeit ›Hallo, Welt!‹ zu schreiben. Das rasende Hallo wird überall hingeschrieben: 216
Kapitel 8
Fensternachrichten
Sandini Bib
Wenn du mit der rechten Maustaste klickst, wird der Texthintergrund auf durchsichtig geschaltet:
Klicke mehrmals. Die linke Taste ist die Start- und Stopptaste, die rechte schaltet zwischen durchsichtigem und weißem Hintergrund hin und her. Im Programm steht als Erstes die Funktion malen. Hier wird ein Text mit zufälligen Koordinaten und zufälliger Farbe ausgegeben. Die Variablen xmax und ymax bestimmen, wo überall hingeschrieben wird. Die Korrekturen in x und y um 30 beziehungsweise 10 Pixel bewirken, dass das Hallo auch jenseits des linken und oberen Randes des Fensters beginnen kann und deshalb das Fenster gleichmäßig ausgemalt wird. Wie in Kapitel 4.2 besprochen, müssen wir uns keine Sorgen
Klick mich links, klick mich rechts
217
Sandini Bib
machen, wenn beim Malen der Anzeigebereich des Fensters verlassen wird. Dafür sorgt das automatische Clipping des Fensters. In der Funktion WindowProc werden unter anderem auch die Nachrichten WM_LBUTTONDOWN und WM_RBUTTONDOWN behandelt. In else if (m == WM_LBUTTONDOWN) { warten = !warten; InvalidateRect(hwnd, 0, 0); }
verwenden wir die Variable warten wie einen Schalter, den wir bei jedem Linksklick mit der Maus von wahr auf falsch und von falsch auf wahr schalten. Dazu muss warten natürlich eine statische Variable sein. Nach jedem Linksklick soll das Fenster neu ausgemalt werden. Dazu rufen wir InvalidateRect(hwnd, 0, 0);
auf, was dazu führt, dass eine WM_PAINT-Nachricht erzeugt wird, wenn die Nachrichtenwarteschlange leer ist. In unserem Beispiel wird WM_PAINT mit else if (m == WM_PAINT) { malen(hdc, xmax, ymax); if (warten) ValidateRect(hwnd, 0); }
bearbeitet. Also wird nach dem Malen ganz normal der Fensterinhalt mit ValidateRect für gültig erklärt, wenn die Variable warten wahr ergibt. Wenn warten falsch ergibt, wird ValidateRect nicht aufgerufen, WM_PAINT wird immer noch mal erzeugt und es kommt zum rasenden Hallo. Aber wenn du zwischen all den WM_PAINT mit der linken Maustaste klickst, erzeugst du ein WM_LBUTTONDOWN und kannst so wieder auf Warten umschalten. Ausprobieren, verhält sich WM_LBUTTONUP wie erwartet? Die Situation für die rechte Maustaste (WM_RBUTTONDOWN) ist viel einfacher, weil ›background mode‹) für TextOut umlediglich der Hintergrundmodus ( geschaltet wird. Weil wir hier InvalidateRect nicht aufrufen, hat die rechte Maustaste keinen sofortigen sichtbaren Effekt, wenn das Programm auf Abwarten geschaltet ist.
8.5
Na, wo läuft sie denn, die Maus?
Das folgende Programm zeigt an, bei welchen Koordinaten sich der Mauszeiger gerade im Fenster befindet: 218
Kapitel 8 Fensternachrichten
Sandini Bib Mausbewegung.c WinMain.cpp
#include <windows.h> #include <stdio.h> /* externe Variable */ HDC hdc; char text[1000]; int n = 0; int xmax = 0, ymax = 0; int xmaus = 0, ymaus = 0; /* Rueckruffunktion unseres Fensters */ LRESULT CALLBACK WindowProc(HWND hwnd, UINT m, WPARAM wParam, LPARAM lParam) { // Fenster auf if (m == WM_CREATE) { SetWindowText(hwnd, "Beispiel fuer WM_MOUSEMOVE"); hdc = GetDC(hwnd); } // Neue Fenstergroesse else if (m == WM_SIZE) { xmax = LOWORD(lParam); ymax = HIWORD(lParam); } // Malen else if (m == WM_PAINT) { sprintf(text, "xmaus = %d, ymaus = %d TextOut(hdc, 20, 20, text, strlen(text)); ValidateRect(hwnd, 0); }
", xmaus, ymaus);
// Maus else if (m == WM_MOUSEMOVE) { xmaus = LOWORD(lParam); ymaus = HIWORD(lParam); InvalidateRect(hwnd, 0, 0); } // Standard else if (m == WM_DESTROY) PostQuitMessage(0); else return DefWindowProc(hwnd, m, wParam, lParam); return 0; }
Jede Mausbewegung im Fenster wird registriert und unsere Rückruffunktion erhält die Nachricht WM_MOUSEMOVE. Die Koordinaten stehen in der Variable lParam, genau wie bei WM_SIZE. So sieht das bei mir aus:
Na, wo läuft sie denn, die Maus?
219
Sandini Bib
Hier wird deutlich, wo der Ursprung der Fensterfläche ist, in die gezeichnet werden darf (xmaus und ymaus gleich 0), und dass die Koordinaten in TextOut sich auf die linke obere Ecke des Textes beziehen. Beachte, wie die Mauskoordinaten sich sprunghaft verändern, wenn der Mauszeiger das Fenster verlässt und von einer anderen Seite wieder ins Fenster bewegt wird. Die Bewegung des Mauszeigers kann auf einfache Weise Spuren im Fenster hinterlassen. Setze mal LineTo(hdc, xmaus, ymaus);
in den Befehlsblock der WM_PAINT-Nachricht, und siehe da, du hast ein ultraprimitives Zeichenprogramm:
Du kannst es auch mit SetPixel(hdc, xmaus, ymaus, 0);
versuchen. Dann siehst du Lücken in deinen Kritzeleien, weil die Mausnachrichten zwar sehr schnell, aber doch nicht schnell genug für eine Linie aus lauter Pixeln kommen. Stelle wieder auf LineTo um und maximiere das Fenster. Versuche durch schnelle Mausbewegungen Ecken in die Linie zu bekommen. Wenn sich die Maus nicht zu schnell bewegt, ergeben die vielen geraden Stücke mit LineTo eine gute Näherung einer runden Kurve. 220
Kapitel 8 Fensternachrichten
Sandini Bib
Und noch ein Spielchen: Wenn du LineTo verwendest und WM_MOUSEMOVE durch WM_LBUTTONDOWN ersetzt, kannst du durch mehrfaches Klicken Zickzacklinien zeichnen:
8.6
Tastatur
Nachrichten von der Tastatur gibt es wie bei den Maustasten in einer DOWN- und UP-Version. Wenn du die Taste C drückst und wieder loslässt, erhältst du WM_KEYDOWN
wParam ist gleich VK_C
WM_CHAR
wParam ist Zeichen ’c’
WM_KEYUP
wParam ist gleich VK_C
›Key‹ heißt hier Taste. Die Variable wParam beschreibt, welche Taste gemeint ist. Der Name VK_C ist der Name einer Konstanten, die in #include <windows.h> definiert wurde. ›VK‹ bedeutet ›Virtual-Key Code‹, also virtueller oder scheinbarer Tastencode. Eine Tabelle aller virtuellen Keycodes für alle Tasten findest du in der Hilfe zum Win32 SDK unter ›Virtual-Key Codes‹. Schau sie dir bei dieser Gelegenheit gleich an. Du findest die Buchstabentasten, aber auch die Umstelltaste ( ) und die Pfeiltasten. Nicht jede Taste erzeugt auch eine WM_CHAR-Nachricht. Aber insbesondere die Zeichen, die in C den Typ char erhalten, werden durch WM_CHAR übergeben. WM_KEYDOWN bezieht sich auf einzelne Tasten der Tastatur, während für WM_CHAR Tastenkombinationen in einzelne Zeichen übersetzt werden (durch TranslateMessage, siehe Kapitel 8.1). Wenn du im Gegensatz zu ’c’ den Großbuchstaben ’C’ eingeben willst, musst du und C gleichzeitig drücken und erhältst 8.6
Tastatur
221
Sandini Bib WM_KEYDOWN
wParam ist VK_SHIFT
WM_KEYDOWN
wParam ist VK_C
WM_CHAR
wParam ist ’C’
WM_KEYUP
wParam ist VK_C
WM_KEYUP
wParam ist VK_SHIFT
Die Shifttaste selbst erzeugt kein WM_CHAR. Wir wollen in diesem Buch keine Textverarbeitung selber programmieren, also werden wir WM_CHAR nicht weiter besprechen. Aber die Pfeiltasten sind auf jeden Fall nützlich:
222
Kapitel 8 Fensternachrichten
Sandini Bib Tastatur0.c WinMain.cpp
#include <windows.h> /* externe Variable */ HDC hdc; int xmax, ymax; int xball, yball; int r = 15; int dx = 5; int dy = 5; /* Rueckruffunktion unseres Fensters */ LRESULT CALLBACK WindowProc(HWND hwnd, UINT m, WPARAM wParam, LPARAM lParam) { // Fenster auf if (m == WM_CREATE) { hdc = GetDC(hwnd); xball = yball = 100; } // Tastatur else if (m == WM_KEYDOWN) { if (wParam == VK_LEFT) xball −= dx; else if (wParam == VK_RIGHT) xball += dx; else if (wParam == VK_UP) yball −= dy; else if (wParam == VK_DOWN) yball += dy; else if (wParam == VK_SPACE) Rectangle(hdc, −1, −1, xmax+1, ymax+1); else if (wParam == VK_ESCAPE) SendMessage(hwnd, WM_CLOSE, 0, 0); InvalidateRect(hwnd, 0, 0); } // Malen else if (m == WM_PAINT) { Ellipse(hdc, xball−r, yball−r, xball+r, yball+r); ValidateRect(hwnd, 0); } // Verschiedenes else if (m == WM_SIZE) { xmax = LOWORD(lParam); ymax = HIWORD(lParam); } else if (m == WM_DESTROY) PostQuitMessage(0); else return DefWindowProc(hwnd, m, wParam, lParam); return 0; }
Hier malen wir einen Kreis, dessen Radius in r und dessen Koordinaten in xball und yball stehen. Mit if (m == WM_KEYDOWN) fangen wir die Nachricht ab, dass eine Taste runtergedrückt wurde. Wann die Taste wieder losgelassen wird, ist 8.6
Tastatur
223
Sandini Bib
uns egal. Falls eine Taste gedrückt wurde, untersuchen wir die Variable wParam, um entsprechend zu reagieren. Falls es eine der Pfeiltasten war, verändern wir die Position des Kreises entsprechend, z. B.
Wenn du eine der Pfeiltasten gedrückt lässt, wird die automatische Wiederholfunktion der Tastatur aktiv und dieselbe WM_KEYDOWN-Nachricht wird wiederholt verschickt: Der Kreis wird immer noch mal versetzt gezeichnet! Das sieht ja schon fast nach einer richtigen Animation aus, aber in Kapitel 8.7 wird es noch besser. Falls sich der Kreis zu langsam bewegt, kannst du die Variablen dx und dy vergrößern. Falls die Leertaste (VK_SPACE) gedrückt wird, löschen wir mit Rectangle(hdc, −1, −1, xmax+1, ymax+1);
das Bild. Die Koordinaten habe ich hier so gewählt, dass der Rand des Rechtecks gerade außerhalb des sichtbaren Bereichs des Fensters liegt. Die Escapetaste (VK_ESCAPE) kann verwendet werden, um das Programm flucht›escape‹ heißt entkommen). Dazu rufen wir artig zu verlassen ( SendMessage(hwnd, WM_CLOSE, 0, 0);
auf. Die Funktion SendMessage ruft direkt die Rückruffunktion des Fensters hwnd auf und kann beliebige Nachrichten übergeben. Im dritten und vierten Argument stehen die Werte für wParam und lParam. Die Nachricht WM_CLOSE, Fenster schließen, wird in unserem Beispiel von DefWindowProc verarbeitet. WM_CLOSE wird verschickt, bevor das Fenster zuklappt, während WM_DESTROY bedeutet, dass das Fenster schon zugemacht wurde. Bei WM_CLOSE könntest du z. B. noch fragen: ›Bist du sicher? Schau’s dir an, so ein schönes Fenster, wirklich zumachen?‹ Eine ausgebaute Version desselben Beispiels zeigt 224
Kapitel 8 Fensternachrichten
Sandini Bib Tastatur1.c WinMain.cpp
#include <windows.h> /* externe Variable */ HDC hdc; HBRUSH hbnull, hblila, hbgelb, hbtemp; int xmax, ymax; int xball, yball; int int int int
r = 15; dx = 5; dy = 5; shift;
/* Rueckruffunktion unseres Fensters */ LRESULT CALLBACK WindowProc(HWND hwnd, UINT m, WPARAM wParam, LPARAM lParam) { // Fenster auf if (m == WM_CREATE) { hdc = GetDC(hwnd); hbnull = GetStockObject(NULL_BRUSH); hblila = CreateSolidBrush(RGB(230,0,230)); hbgelb = CreateSolidBrush(RGB(230,230,0)); SelectObject(hdc, hblila); xball = yball = 100; } // Tastatur else if (m == WM_KEYDOWN) { if (GetAsyncKeyState(VK_SHIFT)) shift = 3; else shift = 1; if (wParam == else if (wParam == else if (wParam == else if (wParam ==
VK_LEFT) VK_RIGHT) VK_UP) VK_DOWN)
xball xball yball yball
−= += −= +=
shift*dx; shift*dx; shift*dy; shift*dy;
else if (wParam == VK_NEXT) { r = 9*r/10 − 1; if (r < 1) r = 1; } else if (wParam == VK_PRIOR) { r = 10*r/9 + 1; if (r > xmax) r = xmax; } else if (wParam == VK_INSERT) SelectObject(hdc, hbnull); else if (wParam == VK_DELETE) SelectObject(hdc, hblila); else if (wParam == VK_SPACE) { hbtemp = SelectObject(hdc, hbgelb); Rectangle(hdc, −1, −1, xmax+1, ymax+1); SelectObject(hdc, hbtemp); } else if (wParam == VK_ESCAPE) SendMessage(hwnd, WM_CLOSE, 0, 0); InvalidateRect(hwnd, 0, 0); }
8.6
Tastatur
225
Sandini Bib Tastatur1.c WinMain.cpp
// Malen else if (m == WM_PAINT) { Ellipse(hdc, xball−r, yball−r, xball+r, yball+r); ValidateRect(hwnd, 0); } // Verschiedenes else if (m == WM_SIZE) { xmax = LOWORD(lParam); ymax = HIWORD(lParam); xball = xmax/2; yball = ymax/2; } else if (m == WM_DESTROY) PostQuitMessage(0); else return DefWindowProc(hwnd, m, wParam, lParam); return 0; }
Jetzt verwenden wir zwei Farben und einen wahlweise durchsichtigen Hintergrund (Einfügen VK_INSERT und Entfernen VK_DELETE). Der Kreis kann kleiner und größer gemacht werden (nächste und vorherige Seite, VK_NEXT und VK_PRIOR). Und ich meine wirklich groß. Maximiere das Fenster und halte VK_PRIOR gedrückt. Dann verschiebe den Kreis. Beachte, wie wir verhindern, dass der Kreis zu groß oder zu klein wird. Mit GetAsyncKeyState(VK_SHIFT) erfragt das Programm, ob in dem Moment, wenn WM_KEYDOWN verarbeitet wird, die Shifttaste gedrückt ist. Bei gedrückter Shifttaste ist die Schrittweite das Dreifache von dx und dy. 226
Kapitel 8 Fensternachrichten
Sandini Bib
8.7
Die Uhr macht tick
Mit der Funktion SetTimer können wir einen Zeitgeber ( ›timer‹) starten, der in regelmäßigen Zeitabständen eine WM_TIMER-Nachricht verschickt. Mit KillTimer können wir diesen Zeitgeber wieder abschalten. Im folgenden Beispiel verwenden wir einen Zeitgeber, um einen Kreis automatisch zu verschieben: Zeitgeber0.c WinMain.cpp
#include <windows.h> /* externe HDC hdc; int stop = int xmax = int xball, int int int int
r dx dy dt
= = = =
Variable */ 1; 0, ymax yball;
= 0;
15; 2; 2; 10;
/* Rueckruffunktion unseres Fensters */ LRESULT CALLBACK WindowProc(HWND hwnd, UINT m, WPARAM wParam, LPARAM lParam) { // Fenster auf if (m == WM_CREATE) { SetWindowText(hwnd, "Ball"); SetTimer(hwnd, 0, dt, 0); hdc = GetDC(hwnd); xball = yball = r; } // Neue Fenstergroesse else if (m == WM_SIZE) { xmax = LOWORD(lParam); ymax = HIWORD(lParam); } // Uhr macht tick else if (m == WM_TIMER) { xball += dx; yball += dy; if (xball < 0 || xball >= xmax) dx = −dx; if (yball < 0 || yball >= ymax) dy = −dy; InvalidateRect(hwnd, 0, 0); } // Malen else if (m == WM_PAINT) { Ellipse(hdc, xball−r, yball−r, xball+r, yball+r); ValidateRect(hwnd, 0); }
8.7
Die Uhr macht tick
227
Sandini Bib Zeitgeber0.c WinMain.cpp
// Start/Stopp else if (m == WM_LBUTTONDOWN) { if (stop) KillTimer(hwnd, 0); else SetTimer(hwnd, 0, dt, 0); stop = !stop; } // Aufraeumen else if (m == WM_DESTROY) { KillTimer(hwnd, 0); PostQuitMessage(0); } else return DefWindowProc(hwnd, m, wParam, lParam); return 0; }
Die linke Maustaste funktioniert als Start- und Stopptaste der Animation. Bei WM_DESTROY schalten wir den Zeitgeber ab. In den Zeilen xball += dx; yball += dy; if (xball < 0 || xball >= xmax) dx = −dx; if (yball < 0 || yball >= ymax) dy = −dy;
bewegen wir erst den Ball weiter und dann drehen wir die Bewegungsrichtung um, falls der Mittelpunkt des Balls den sichtbaren Bereich verlassen hat. Wir rufen SetTimer mit SetTimer(hwnd, nummer, dt, 0);
auf. Falls wir mehrere Zeitgeber in einem Programm verwenden möchten, geben wir jedem seine eigene Nummer. In unserem Beispiel ist die Nummer 0. Diese Nummer wird bei WM_TIMER in wParam übergeben. 228
Kapitel 8 Fensternachrichten
Sandini Bib
Mit dt bestimmen wir, wie viele tausendstel Sekunden (also Millisekunden) der Zeitgeber zwischen seinen Signalen wartet. Die Zeit, nach der sich ein Signal wiederholt, nennt man auch seine Periode. Bei SetTimer ist die kleinste Periode normalerweise ungefähr 55 Millisekunden, was einer Frequenz von ungefähr 18-mal pro Sekunde entspricht. Alle anderen Perioden werden mit einem ganzzahligen Vielfachen dieser Periode angenähert. Wenn du eine höhere Auflösung benötigst, kannst du die Multimediatimer von Windows verwenden, siehe die Microsoft Hilfe, Multimedia Reference. Hier ist eine Version des Beispielprogramms, in der du mit der rechten Maustaste wählen kannst, ob vor dem Zeichnen des neuen Balls der alte gelöscht wird:
8.7
Die Uhr macht tick
229
Sandini Bib Zeitgeber1.c WinMain.cpp
#include <windows.h> /* externe Variable */ HDC hdc; HBRUSH hbschwarz, hbrot; char text[1000]; int n = 0; int xmax = 0, ymax = 0; int xball, yball; int xalt, yalt; int stop = 1; int spur = 0; int int int int
r dx dy dt
= = = =
50; 3; 4; 10;
/* Rueckruffunktion unseres Fensters */ LRESULT CALLBACK WindowProc(HWND hwnd, UINT m, WPARAM wParam, LPARAM lParam) { // Fenster auf if (m == WM_CREATE) { SetWindowText(hwnd, "Ball"); SetTimer(hwnd, 0, dt, 0); hdc = GetDC(hwnd); hbschwarz = CreateSolidBrush(RGB( 0, 0, 0)); hbrot = CreateSolidBrush(RGB(255, 0, 0)); xball = yball = xalt = yalt = r; } // Neue Fenstergroesse else if (m == WM_SIZE) { xmax = LOWORD(lParam); ymax = HIWORD(lParam); SelectObject(hdc, hbschwarz); Rectangle(hdc, 0, 0, xmax, ymax); } // Uhr macht tick else if (m == WM_TIMER) { xalt = xball; yalt = yball; xball += dx; yball += dy; if (xball−r < 0 || xball+r >= xmax) dx = −dx; if (yball−r < 0 || yball+r >= ymax) dy = −dy; InvalidateRect(hwnd, 0, 0); }
230
Kapitel 8 Fensternachrichten
Sandini Bib Zeitgeber1.c WinMain.cpp
// Malen else if (m == WM_PAINT) { if (!spur) { SelectObject(hdc, hbschwarz); Ellipse(hdc, xalt−r, yalt−r, xalt+r, yalt+r); } SelectObject(hdc, hbrot); Ellipse(hdc, xball−r, yball−r, xball+r, yball+r); ValidateRect(hwnd, 0); } // Start/Stopp else if (m == WM_LBUTTONDOWN) { if (stop) KillTimer(hwnd, 0); else SetTimer(hwnd, 0, dt, 0); stop = !stop; } // Spur an/aus else if (m == WM_RBUTTONDOWN) { spur = !spur; SelectObject(hdc, hbschwarz); Rectangle(hdc, 0, 0, xmax, ymax); InvalidateRect(hwnd, 0, 0); } // Aufraeumen else if (m == WM_DESTROY) { KillTimer(hwnd, 0); PostQuitMessage(0); } else return DefWindowProc(hwnd, m, wParam, lParam); return 0; }
8.7
Die Uhr macht tick
231
Sandini Bib
Die Bilder habe ich ohne Löschen gemacht, damit man sieht, welcher Bahn der Ball gefolgt ist. Das Ergebnis mit Löschen ist etwas enttäuschend. Maximiere das Bild. Typischerweise siehst du eine Art Flackern und nicht etwa die glatte Bewegung von Bällen oder Raumschiffen, die du aus Computerspielen kennst. Das liegt nicht oder nur unwesentlich daran, dass die Schrittweite in x- und y-Richtung größer als ein Pixel ist oder dass die Zeitauflösung trotz der Angabe dt = 10 in Wirklichkeit wahrscheinlich nur 55 Millisekunden beträgt. Ein Problem ist, dass wir erst den Kreis mit Schwarz übermalen und dann den neuen Kreis am neuen Ort ausgeben. Das flackert, weil zwischendurch der schwarze Hintergrund aufblitzt, obwohl wir den schwarzen und den roten Kreis direkt nacheinander malen. Um das neue Bild mit nur einem Ausgabebefehl zu malen, gibt es für Kreise einen einfachen Trick. Füge die Zeile SelectObject(hdc, CreatePen(PS_SOLID, 5, 0));
als letzte Zeile dem Befehlsblock von WM_CREATE hinzu. Die Kreise haben jetzt einen 5 Pixel breiten schwarzen Rand, weil der Rand mit dem angegebenen Stift (›pen‹) gezeichnet wird. Du kannst zum Ausprobieren statt der Farbe 0 (Schwarz) auch RGB(0,0,255) einsetzen (Blau). Der breite Rand löscht alles, was vom alten Kreis unter dem neuen hervorguckt. Lass das Programm noch mal laufen. Jetzt 232
Kapitel 8
Fensternachrichten
Sandini Bib
kannst du mit der rechten Maustaste zwischen Flackern und deutlich weniger Flackern umschalten! In Kapitel 11.4 werden wir besprechen, wie du mit Hilfe von so genannten Bitmaps beliebige Grafik in einem Rutsch zeichnen kannst, ohne dass beim Löschen zwischendurch der Hintergrund sichtbar wird.
8.8 In diesem Kapitel haben wir Nachrichtenmanager gespielt. Alle Eingaben und Ereignisse, die unser Programmfenster angeht, werden an die Fensterprozedur WindowProc als Nachricht übergeben. In Kürze: Diese Nachrichten haben mit der Fensterorganisation zu tun: WM_CREATE wird einmal vor der Erzeugung des Fensters aufgerufen und
kann zur Initialisierung des Programms verwendet werden. WM_CLOSE wird vor dem Zumachen des Fensters verschickt und kann abgefangen werden, um das Schließen des Fensters zu verhindern. WM_DESTROY wird nach dem Zumachen des Fensters verschickt und kann zu Aufräumarbeiten verwendet werden. WM_SIZE wird bei der Größenänderung erzeugt, die neue Größe steht in lParam. Wichtige Nachrichten der Maus sind z. B. WM_LBUTTONDOWN für das Runterdrücken der linken Maustaste WM_MOUSEMOVE für Bewegungen des Mauszeigers im Fenster WM_LBUTTONUP für das Loslassen der linken Maustaste
Die Koordinaten des Mauszeigers für diese Ereignise erhältst du mit x = LOWORD(lParam) und y = HIWORD(lParam).
Wichtige Nachrichten der Tastatur sind z. B. WM_KEYDOWN für das Drücken einer Taste, wParam ist der virtuelle Tasten-
code WM_CHAR für Zeichen wie ’C’, die ebenfalls in wParam übergeben werden.
Den momentanen Zustand einer Taste (gedrückt oder nicht gedrückt) kannst du mit GetAsyncKeyState erfahren. Einen Zeitgeber startest du mit SetTimer und stoppst du mit KillTimer. Die dazugehörige Nachricht ist WM_TIMER. Der Eindruck von glatter Bewegung und Animation wird durch die schnelle Abfolge von einzelnen Bildern erzeugt.
8.8
233
Sandini Bib
8.9 1. Schreibe ein Programm, das auf Mausklick das ganze Fenster ausmalt. Ändere
das Programm so ab, dass ein grünes Fensters rot wird, solange du eine der Maustasten gedrückt hältst. Mache die farbige Fläche kleiner und schreibe einen Text hinein, wobei sich Farbe und Text beim Mausklick ändern. Du hast deinen eigenen, primitiven Windowsknopf programmiert. 2. Schreibe ein Programm, mit dem du mit der Maus ein ›Rechteck aufziehen‹
kannst. Also, wenn die Maustaste runtergedrückt wird, merkt sich das Programm die Koordinaten, und nach jeder Mausbewegung wird das alte Rechteck gelöscht und das neue Rechteck zwischen den momentanen und den Anfangskoordinaten gezeichnet. 3. Einen netten Effekt gibt es, wenn du die Koordinaten für jeden Mausklick in zwei Feldern int x[1000], y[1000]; speicherst und beim Malen dann jeden Punkt (x, y) mit jedem anderen Punkt verbindest (doppelte Schleife). 4. Warum steht im zweiten Beispiel in Kapitel 8.6 im Kreisezeichner r = 10*r/ 9 + 1 und nicht einfach nur r = 10*r/9? Wie könntest du es erreichen, dass
beim wiederholten Verkleinern und Vergrößern Machen immer dieselben Radien erzeugt werden? Wie musst du das Programm ändern, damit immer nur ein Kreis und nicht auch die vorhergehenden Kreise sichtbar sind? 5. Baue einen Zeitgeber in das rasende Hallo ein. Lass die Hallos tröpfeln. Die
Zeitrate könntest du per Tastatur einstellbar machen. 6. In den Beispielen mit dem Zeitgeber kannst du die Schrittweite der Bewegung
und die Zeitabstände ändern. Probiere aus, wie glatt die Bewegung bei verschiedenen Werten von dx, dy und dt aussieht. Entferne den Zeitgeber und lass das Programm so schnell zeichnen, wie es kann (kein ValidateRect). Notfalls kannst du das Programm bremsen, indem du den Computer nach jedem Zeichnen bis 1000 000 zählen lässt. 7. Schreibe ein Programm für N Bälle, die alle in verschiedene Richtungen los-
fliegen und vom Fensterrand abprallen. Wie erreichst du, dass die Bälle sich nicht verdecken, sondern voneinander im richtigen Winkel abprallen? Das ist nicht einfach. Fange mit zwei Bällen an, die waagrecht fliegen, aber in der Senkrechten gegeneinander versetzt sind.
234
Kapitel 8
Fensternachrichten
Sandini Bib
9 Datentypen 9.0 9.1 9.2 9.3 9.4 9.5 9.6
Von Bits und Bytes Bit für Bit Datentypen Datentypen klein und groß Mein Typ, dein Typ: die Typenumwandlung Konstante Variable mit const Neue Namen für alte Typen: typedef
236 238 239 240 243 247 247
9.7
Mathematische Funktionen
248
9.8
Die Sinusfunktion
249
9.9
Kreis und Rotation
256
9.10
Farbtiefe
264
9.11
Mandelbrot-Menge
265
9.12
274
9.13
274
Sandini Bib
Viele Eigenheiten von Zahlen und Variablen in C und anderen Programmiersprachen werden erst einsichtig, wenn man sich mit Bits und Bytes vertraut gemacht hat. Das wollen wir in diesem Kapitel tun. Zahlen werden in Bits und Bytes im Computer abgespeichert. Für verschiedene Zahlenformate definiert C verschiedene Datentypen, z. B. werden ganze Zahlen anders abgespeichert als Kommazahlen. Zudem werden je nach Datentyp unterschiedlich viele Bytes pro Zahl verwendet. Die Diskussion von Datentypen bietet eine gute Gelegenheit, uns mit Fließkommazahlen zu beschäftigen. Mathematische Funktionen wie die Wurzelfunktion oder die Sinusfunktion rechnen mit Kommazahlen. Auch zweidimensionale oder dreidimensionale Grafik wird typischerweise in Kommazahlen berechnet, die dann für die Bildschirmausgabe in ganze Zahlen umgewandelt werden müssen. Also, was ist ein Bit, was ist ein Byte?
9.0 Von Bits und Bytes Ein Bit ( ›Stückchen‹) ist die kleinste Speichereinheit in jedem digitalen Computer. Ein Bit kann genau zwei Werte annehmen, 0 oder 1. Das kannst du dir auch als An und Aus eines elektronischen Schalters vorstellen. Der Name ›binary digit‹ (Ziffer des Binärsystems, des Zweiersystems). ›Bit‹ steht für Ein Byte besteht aus 8 Bits. In einem Byte kann man 256 verschiedene Zahlen speichern. Wie das? Das erste Bit kann zwei Werte annehmen: 0, 1
Für jedes Bit, das hinzukommt, verdoppelt sich die Anzahl der Möglichkeiten, denn wir können das neue Bit auf 0 setzen und haben all die vorherigen Möglichkeiten oder auf 1 setzen und haben wieder all die vorherigen Möglichkeiten. Zwei Bit können also 2 × 2 = 4 Zahlen speichern: 00,
01,
10,
11
Das ergibt für acht Bit 2 × 2 × 2 × 2 × 2 × 2 × 2 × 2 = 256
Möglichkeiten. Acht Zweier mit sich malgenommen schreibt man in der Potenzschreibweise als 28 (gesprochen zwei hoch acht). Übrigens ist 102 = 10 × 10 = 100 (zehn hoch zwei). Man muss aufpassen, welche Zahl oben und welche unten steht, z. B. ist 210 = 1024. Wenn du mit 1 anfängst und immer wieder mit 10 malnimmst, erhältst du die ›Zehnerpotenzen‹: 236
Kapitel 9 Datentypen
Sandini Bib 100
101
102
103
104
...
1
10
100
1000
10000
...
Hoch 0 ergibt 1. Wenn du mit 1 anfängst und immer wieder mit 2 malnimmst, kannst du leicht die folgenden ›Zweierpotenzen‹ ausrechnen: 20
21
22
23
24
25
26
27
28
29
210
...
1
2
4
8
16
32
64
128
256
512
1024
...
Normalerweise rechnen wir im Zehnersystem (Dezimalsystem). Wenn der Mensch nur zwei Finger hätte, würde er vielleicht genau wie der Computer im Zweiersystem (Binärsystem) rechnen. Die Zweierpotenzen spielen im Zweiersystem dieselbe Rolle wie die Zehnerpotenzen im Zehnersystem. Im Zehnersystem erhalten die Ziffern 0 bis 9 ihren Wert durch ihre Stellung: 2501 sind 2 Tausender, 5 Hunderter, 0 Zehner und 1 Einser. Eine Zahl im Zweiersystem wird nur mit den Ziffern 0 und 1 geschrieben. Die Zahl 10110 im Zweiersystem enthält 1 Sechzehner, 0 Achter, 1 Vierer, 1 Zweier, 0 Einser. Das aufaddiert ergibt 22 im Zehnersystem. Um die Zahl 22 in einem Byte zu speichern, werden die acht Bits wie folgt gesetzt: 00010110
Der Vollständigkeit halber möchte ich auch noch das Sechzehnersystem erwähnen (das Hexadezimalsystem). Hier verwendet man 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F, um die Zahlen von 0 bis 15 als eine einzelne Ziffer darzustellen. In C kannst du Zahlen im Hexadezimalsystem angeben, indem du 0x voranstellst: 0x01 0x0A 0x0F 0x10 0x100 0xFF
ist 1 ist 10 ist 15 ist 1 × 161 = 16 ist 1 × 162 = 256 ist 15 × 161 + 15 × 160 = 255
Das Hexadezimalsystem ist manchmal praktisch, weil ein Byte genau zwei Ziffern entspricht. Im Binärsystem ist 0xFF gleich 11111111. Du kannst z. B. int i = 0xFF; schreiben, wenn du aus irgendeinem Grund diese acht Bits in i gleich eins setzen möchtest. Wenn dir das alles zu kompliziert vorkommt, keine Sorge. Obwohl der Computer intern im Zweiersystem rechnet, steht dir in C, wie wir schon gesehen haben, ein vollständiger Satz an Rechenoperationen im Dezimalsystem zur Verfügung. Das Wort ›Bit‹ ist dir sicher schon in Angaben wie 32-Bit-Mikroprozessor, 64Bit-Datenbus oder 16 Bit Farbtiefe aufgefallen. Zwei Bytes oder 16 Bit nennt ›word‹), vier Bytes oder 32 Bit ein Doppelwort (›double man auch ein Wort ( 9.0 Von Bits und Bytes
237
Sandini Bib
word‹). Ein 32-Bit-Prozessor ist typischerweise schneller als ein 8-Bit-Prozessor, weil er mehr Daten auf einmal verarbeiten kann. Der Speicher für Daten in Computern wird in Bytes angegeben, wobei ein Kilobyte (kB) ungefähr 1000 Bytes und ein Megabyte (MB) ungefähr 1000 000 Bytes sind. Leider herrscht hier eine gewisse Verwirrung. Ein Kilometer ist exakt 1000 Meter, aber mit Kilobytes sind mal 1000 Bytes, mal 1024 Bytes gemeint. Zum Glück ist der Unterschied gering. Mein erster Computer hatte 8 kB RAM (Arbeitsspeicher), neue PCs haben heutzutage 128 MB oder mehr. Festplattenspeicher wird zurzeit in Gigabyte (GB, 1 GB ist ungefähr 1000 MB) gemessen. Immer wieder tauchen Zweierpotenzen auf, weil der Computer als kleinste Einheit das Bit mit seinen zwei Zuständen kennt.
9.1 Bit für Bit Wenn dich die Bits faszinieren, werden dir die folgenden Rechenoperationen gefallen, die Zahlen Bit für Bit (bitweise) bearbeiten. Diese sind bitweise Negation bitweises Oder bitweises Und bitweises Entweder-Oder bitweise links verschieben bitweise rechts verschieben
˜ | & ˆ << >>
Die ersten vier Operatoren kommen aus der Logik. Ein einzelner Strich | bezieht sich auf Bits, während der doppelte Strich || die Verknüpfung von Aussagen beschreibt, und genauso verhält es sich bei & und &&. Mit ›Negation‹ erreicht man, dass aus 0 die 1 wird und aus 1 eine 0: ˜0 ist 1, ˜1 ist 0.
Mit ›Oder‹ ist das Ergebnis von i | j gleich 1, wenn i oder j gleich 1 ist, d.h. wenn mindestens eine der beiden Variablen 1 ist: 0|0 0|1 1|0 1|1
ist 0, ist 1, ist 1, ist 1.
Was passiert bei & und ˆ ? Bei den Verschiebungsoperatoren werden die Ziffern im Binärsystem nach links oder rechts verschoben: 1 << 3 ist 1000 im Zweiersystem und 8 im Zehnersystem.
238
Kapitel 9 Datentypen
Sandini Bib
Am besten schreibst du ein kleines Programm, mit dem du mit diesen Operatoren experimentieren kannst. Verwende zwei Variablen int i, j;, die du auf 0 oder 1 setzt.
9.2 Datentypen Wie schon erwähnt, unterscheidet C zwischen ganzen Zahlen und reellen Zahlen (Kommazahlen). Um effizient mit Zahlen umgehen zu können, sind in C bestimmte Datentypen für Zahlen festgelegt, die immer dieselbe Anzahl von Bytes im Speicher belegen. Es gibt vier elementare Datentypen in C: char int float double
1 Byte 4 Bytes (2 oder mehr) 4 Bytes (4 oder mehr) 8 Bytes (4 oder mehr)
ein Zeichen oder eine kleine ganze Zahl eine ganze Zahl eine Kommazahl Kommazahl mit höherer Genauigkeit
Weil die Anzahl von Bytes vom Rechner und vom Computer abhängig sind, gebe ich in der Tabelle an, wie viele Bytes auf meinem Pentium II PC mit BCB verwendet werden: int 4 Bytes, float auch 4 Bytes und double 8 Bytes. In Klammern steht die für C festgelegte Mindestgröße. Ganze Zahlen ( ›integers‹) haben wie schon besprochen den Typennamen int. Mit ihnen kann man gut zählen, 0, 1, 2, 3, . . ., und auch die negativen Zahlen gehören dazu. Brüche mit Zähler und Nenner aus ganzen Zahlen kennt C nicht, aber für reelle Zahlen wie 1.5 und −0.001 gibt es die Datentypen float und double. Zeichen ( ›characters‹) sind uns auch schon begegnet, sie erhalten den Typennamen char. Dieser Typ spielt eine Doppelrolle, denn er steht gleichzeitig für 1 Zeichen oder Buchstaben und für die Speichereinheit 1 Byte. Überhaupt stellt sich die Frage, auf welche Weise ganze Zahlen, reelle Zahlen und Zeichen in den Bytes gespeichert werden. Buchstaben und Zeichen werden nicht irgendwie auf geheimnisvolle Weise als etwas anderes als eine Zahl abgespeichert. Alles was der Computer kennt, sind Bits und Zahlen, sonst nichts! Jedem Zeichen wird nach einem Code (namens ASCII) eine Zahl zugewiesen. Weil ein char nur 1 Byte Speicherplatz beansprucht, kann es also nur 256 verschiedene Characters geben. Diesen Code musst du nicht kennen, denn mit dem Apostroph kannst du direkt Buchstaben in Chars speichern, z. B. char buchstabe = ’a’; char klammerauf = ’(’;
Du erinnerst dich, Zeichenketten schreibt man mit "...", aber zwischen ’.’ darf immer nur ein Zeichen stehen. Wie man mit char s[10] = "abc"; mehrere Zeichen zu einer Zeichenkette zusammensetzt, haben wir in Abschnitt 3.3 besprochen. 9.2
Datentypen
239
Sandini Bib
Bei Kommazahlen ist es ähnlich. Nach einem Code, der uns nicht zu interessieren braucht, werden die Ziffern und der Ort des Kommas abgespeichert. Die elementaren Datentypen können wie folgt modifiziert werden. Für char und int kann man bestimmen, ob negative Zahlen erlaubt sind, z. B. unsigned char signed char
Werte von 0 bis 255 Werte von −128 bis 127
›sign‹ heißt hier ›Vorzeichen‹. Des Weiteren gibt es long (›lang‹) und short (›kurz‹), um die Anzahl der Bytes für int zu erhöhen oder zu erniedrigen. long kann man auch für double verwenden. Es gibt short int int long int
für Integers und float double long double
für Kommazahlen. Womöglich kennt dein Compiler noch weitere Datentypen. Als Abkürzung kannst du short i; und long j; schreiben, um die entsprechenden Integertypen zu erhalten.
9.3 Datentypen klein und groß Je mehr Bytes zum Speichern von ganzen Zahlen verwendet werden, desto größer können ganze Zahlen sein. Bei Kommazahlen bedeuten mehr Bytes, dass nicht nur größere, sondern auch Zahlen mit ›mehr Stellen hinter dem Komma‹ gespeichert werden können. Leider hängt die Anzahl der Bytes vom Rechner und vom Compiler ab. Mal ist ein int 2 Bytes, mal 4, mal 8. Was für ein Durcheinander. C zeichnet sich im Allgemeinen durch große Klarheit und Eleganz aus, aber die Sache mit den Bytes pro Datentyp ging daneben. Die Erfinder von C liefern aber meiner Meinung nach immerhin die zweitbeste Lösung des Problems in Form des Operators sizeof (›Größe von‹). Damit kannst du in deinen Programmen die Größe verschiedener Datentypen erfahren:
240
Kapitel 9
Datentypen
Sandini Bib Datentypen0.c
#include <stdio.h> int main() { printf("sizeof(char) printf("sizeof(int) printf("sizeof(long) printf("sizeof(float) printf("sizeof(double) getchar(); return 0; }
ist ist ist ist ist
%d %d %d %d %d
Byte\n", Bytes\n", Bytes\n", Bytes\n", Bytes\n",
sizeof(char)); sizeof(int)); sizeof(long)); sizeof(float)); sizeof(double));
Trotz der runden Klammern ist sizeof keine Funktion, sondern eine C-Anweisung wie return und if. Dieses Programm schreibt auf meinem PC mit BCB sizeof(char) sizeof(int) sizeof(long) sizeof(float) sizeof(double)
ist ist ist ist ist
1 4 4 4 8
Byte Bytes Bytes Bytes Bytes
Das heißt, long ist nicht länger als int. In C gibt es die Dateien limits.h und float.h ( ›limit‹ heißt Grenze), in der verschiedene Grenzwerte für Zahlen festgehalten sind. Hier ist ein Beispiel: Datentypen1.c
#include <stdio.h> #include #include int main() { printf("char: Minimum %d, Maximum %d\n", CHAR_MIN, CHAR_MAX); printf("int: Minimum %d, Maximum %d\n", INT_MIN, INT_MAX); printf("float: Minimum %.2e, Maximum %.2e\n", FLT_MIN, FLT_MAX); printf("double: Minimum %.2e, Maximum %.2e\n", DBL_MIN, DBL_MAX); getchar(); return 0; }
ergibt char: Minimum −128, Maximum 127 int: Minimum −2147483648, Maximum 2147483647 float: Minimum 1.18e−038, Maximum 3.40e+038 double: Minimum 2.23e−308, Maximum 1.80e+308
9.3 Datentypen klein und groß
241
Sandini Bib
Die Zeile #include fügt die Definition der symbolischen Konstanten CHAR_MIN und so weiter ein. Wie du siehst, können wir Ints und Chars mit der Formatanweisung für ganze Zahlen %d drucken. Kommazahlen kannst du mit %e oder %f drucken (ausprobieren). Mit %f kannst du Kommazahlen wie z. B. 0.001 ausgeben. Mit %e erhält man die Exponentenschreibweise. Mit %.2e begrenzt man die Stellen nach dem Punkt auf 2. Vielleicht ist dir die Exponentenschreibweise noch nicht begegnet und sie ist auch für dieses Buch nicht wichtig, besonders schwierig ist sie aber auch nicht. Z.B. ist 5.1e+3 = 5.1 × 103 = 5.1 × 1000 = 5100, 5.1e–3 = 5.1 × 10−3 = 5.1/1000 = 0.0051.
Wie wir sehen, kann man in Chars bis etwas über 100 mit positiven und negativen Zahlen rechnen und mit Ints bis 2 Milliarden. Kommazahlen können auch negativ sein. Das Minimum bedeutet in diesem Fall die kleinste positive Zahl. Wir können mit Kommazahlen rechnen, die bis zu 38 bzw. 308 Nullen vor oder hinter dem Komma haben. Eine 1 mit 308 Nullen! Das sollte reichen. Für Kommazahlen ist es meistens viel wichtiger, auf wie viele Stellen das Ergebnis genau ist. Für float sind es 6 Stellen, für double sind es 15. Wenn Speicherplatz keine Rolle spielt, verwenden wir double. Normalerweise werden Floats sowieso beim Rechnen in Doubles umgewandelt, weil die ›FPU‹ (floating point unit) deines Prozessors nur mit Doubles rechnet. Lass uns eine von diesen Grenzen testen: Datentypen2.c
#include <stdio.h> int main() { char i, j, summe; i = 10; j = 10; summe = i + j; printf("%d + %d ist gleich %d \n", i, j, summe); i = 100; j = 100; summe = i + j; printf("%d + %d ist gleich %d \n", i, j, summe); getchar(); return 0; }
ergibt 10 + 10 ist gleich 20 100 + 100 ist gleich −56
242
Kapitel 9 Datentypen
Sandini Bib
Wau ( ›Wow‹). Das Problem kennst du schon aus Kapitel 2.9 für Integers. Wenn eine ganze Zahl zu groß wird, läuft der Speicherplatz wie ein Fass über ›overflow‹). Die hohen Ziffern werden einfach weggeschmissen und auch ( die Codierung des Minuszeichens kommt durcheinander. Bei Kommazahlen erhältst du bei einem Overflow eine Fehlermeldung, bei Ints und Chars normalerweise nicht. Immerhin warnt uns BCB beim Kompilieren, dass wir in den Zeilen mit summe = i + j Ziffern verlieren könnten. Die letzten drei Beispiele solltest du unbedingt ausprobieren, damit du im wahrsten Sinne des Wortes weißt, womit du zu rechnen hast! Warum rechnet C nicht mit beliebig großen Zahlen? Das dem Computer beizubringen ist gar nicht so schwer, obwohl es bei nur sehr wenigen Programmiersprachen zur Grundausstattung gehört. Ein Grund ist, dass dann alle Rechenoperationen langsamer werden und im Schnitt womöglich mehr Speicher verbraucht wird. Zum Abschluss dieses Themas möchte ich zweierlei festhalten: 1. Bei allen Rechenoperationen muss man beachten, dass jeder Datentyp nur
einen beschränkten Zahlenbereich zulässt. 2. Man kann normalerweise mit char, int und double auskommen.
9.4 Mein Typ, dein Typ: die Typenumwandlung Was passiert wohl, wenn wir in einer Rechnung Datentypen mischen? Jede ganze Zahl wie 1 ist zugleich eine reelle Zahl. Aber eine reelle Zahl wie 2.9 ist nicht zugleich eine ganze Zahl. Also leuchtet es ein, dass Ints in Doubles gespeichert werden können. Was aber geschieht in int i; i = 2.9;
Wird womöglich aufgerundet? Gleich ausprobieren:
9.4 Mein Typ, dein Typ: die Typenumwandlung
243
Sandini Bib Datentypen3.c
#include <stdio.h> int main() { int i; double x; i = 1; x = 2.9; printf("i = %d
x = %f\n", i, x);
i = 2.9; x = 1; printf("i = %d
x = %f\n", i, x);
getchar(); return 0; }
i = 1 i = 2
x = 2.900000 x = 1.000000
Wie du siehst, erhält i durch i = 2.9; den Wert 2. Bei der Umwandlung von Kommazahl in Integerzahl wird der Kommateil einfach weggelassen. Auch in x = 1; geschieht etwas nicht Triviales. Obwohl natürlich 1 und 1.0 gleich viel ist, müssen die vier Byte in einer typischen int erst einmal in die acht Byte einer double Variable umgeschrieben werden, und das im richtigen Format. Wenn mit zwei verschiedenen Datentypen gerechnet wird, werden Zahlen automatisch ineinander umgewandelt, wenn erforderlich: Datentypen4.c
#include <stdio.h> int main() { int i = 1; double x = 2.9; double summe; summe = i + x; printf("summe = %f\n", summe); getchar(); return 0; }
244
Kapitel 9 Datentypen
Sandini Bib
summe = 3.900000
Das heißt, wenn zwei Datentypen verknüpft werden, wird im Allgemeinen vor der Rechnung der ›kleinere‹ Datentyp in den ›größeren‹ verwandelt. Ist ein Wert double und der andere int, wird zunächst die int in double umgewandelt. Handelt es sich um int und char, wird char zu int gemacht. Manchmal möchte man Typen auf Befehl umwandeln (eigentlich wie im richti›cast‹ heißt hier Gussform, gen Leben). Dafür gibt es den ›Cast‹ Operator. ›casting‹ nennt man die Auswahl von Schauspielern für bestimmte Rollen beim Film. Das sieht dann so aus: Datentypen5.c
#include <stdio.h> int main() { double x, y; x = (int) 2.9; y = (int) −2.9; printf("x = %f, y = %f\n", x, y); getchar(); return 0; }
x = 2.000000, y = −2.000000
Die Anweisung lautet (Typname) Ausdruck
In unserem Beispiel ergibt (int) 2.9
die ganze Zahl 2, die wir dann wie in x = 2;
als Kommazahl in x speichern. Das heißt, wir haben mit (int) den ganzzahligen Anteil der Kommazahl 2.9 berechnet. Eine kleine Falle ist im folgenden Beispiel versteckt:
9.4 Mein Typ, dein Typ: die Typenumwandlung
245
Sandini Bib Datentypen6.c
#include <stdio.h> int main() { int i = 1, j = 2; double x = 10.0; printf("%f\n", x * i / j); printf("%f\n", i / j * x); getchar(); return 0; }
5.000000 0.000000
Die erste Antwort ist richtig, 10*1/2 ist 5. In der zweiten Zeile wird 1/2*10 gerechnet. Als Erstes musst du bedenken, dass von links nach rechts gerechnet wird, also erst ¹⁄², dann wird mit 10 malgenommen. Man kann die Abfolge auch als (1/2)*10 schreiben. Aber warum soll das Ergebnis 0 sein?!? Achtung, die automatische Typenumwandlung läuft paarweise ab: Als Erstes wird i/j betrachtet. Für ganze Zahlen 1 und 2 ergibt diese Division, wie wir wissen, 0. Dann wird 0/x gerechnet, und weil x eine double ist, wird 0 zu 0.0, dann wird 0.0 durch 10.0 geteilt, was 0.0 ergibt. Alles ganz logisch und überhaupt nicht, was wir wollten. Um mit ›¹⁄²‹ die Kommazahl 0.5 zu berechnen, kannst du z. B. 1.0/2.0, 1.0/2 oder 1/2.0 schreiben. Für Variablen benötigen wir ein Cast: ((double) i) / ((double) j)
Ein (double) ist aber genug, denn die zweite Umwandlung ist automatisch: ((double) i) / j i / ((double) j) (double) i / j i / (double) j
Probier das mal aus. Obwohl (double) Vorrang vor / hat (siehe Anhang A.0), ist es klarer, ((double) i) zu schreiben. Während Variablen immer ausdrücklich mit einem Typ definiert werden, gilt bei Zahlkonstanten: Konstanten, die ohne Punkt geschrieben sind wie 1 oder 150, erhalten den Typ int. Konstanten mit Punkt erhalten den Typ double. Die Zahl Eins vom Typ double wird als 1.0 eingegeben. 246
Kapitel 9
Datentypen
Sandini Bib
9.5 Konstante Variable mit const In Kapitel 9.2 haben wir besprochen, dass sich aus den elementaren Datentypen wie int durch Voranstellen von bestimmten Schlüsselwörtern wie long neue Typen ableiten lassen. Wenn einer Variablen bei der Definition ein Wert zugewiesen wird, der sich nicht mehr ändern darf, kann man const vorausstellen, z. B. const int wochentage = 7; const double pi = 3.141;
Obwohl ›konstante Variable‹ nach einem Widerspruch in sich klingt, ist das dennoch sinnvoll, denn jetzt kann man einen Namen statt der Zahlen verwenden. Ändert sich die Zahl, braucht man nur diese eine Stelle zu ändern. Namen für Konstanten kannst du auch mit der Preprocessoranweisung #define definieren, siehe Anhang A.1. Weil const int n aber eine echte C-Variable bezeichnet, kann sie auch in Funktionsprototypen verwendet werden. Ein Prototyp wie int f(const int n);
verhindert, dass innerhalb der Funktion f der Wert von n geändert werden kann.
9.6 Neue Namen für alte Typen: typedef Mit typedef kannst du einem schon vorhandenen Datentyp einen neuen Namen geben. Nach typedef int INT;
kannst du überall dort, wo int erlaubt ist, INT schreiben. Wird überall INT verwendet, könnte mit typedef long INT; das ganze Programm auf long umgestellt werden. Windows macht von typedef im großen Stil Gebrauch. In BCB findest du die Headerdatei windef.h, die von windows.h eingelesen wird. Hier stehen sinnige Typdefinitionen wie typedef typedef typedef typedef typedef
int INT; unsigned unsigned unsigned unsigned
int UINT; char BYTE; short WORD; long DWORD:
Solche Definitionen kann man ineinander einsetzen, z. B. typedef UINT WPARAM;
Ich erkläre dir das eigentlich nur, damit der Typenwirrwarr in Windows nicht so mysteriös aussieht. Vielleicht findest du aber auch in deinen eigenen Programmen einen guten Grund, typedef zu verwenden. Besonders nützlich ist typedef für Strukturen, siehe 10.4. 9.5 Konstante Variable mit const
247
Sandini Bib
9.7
Mathematische Funktionen
Ein wichtiges Beispiel für die Verwendung des Datentyps double sind mathematische Funktionen wie die Wurzelfunktion, die in der Math Library von C enthalten sind. Dort findest du viele Rechenoperationen, die über die Grundrechenarten hinausgehen, zum Beispiel double sqrt(double x); double pow(double x, double y); double sin(double x); double cos(double x);
Quadratwurzel von x (›square root‹) x hoch y (›power‹, Potenz) Sinus von x Cosinus von x
Alle diese Funktionen berechnen Doubles und ihre Argumente x und y müssen ebenfalls Doubles sein. Weitere mathematische Funktionen findest du in der BCB-Hilfe unter ›Bibliotheksreferenz‹, ›Bibliotheksroutinen‹, ›nach Kategorien sortiert‹, ›Mathematische Routinen‹. Weil wir hart im Nehmen sind, geben wir uns nicht völlig mit den abgepackten Mathefunktionen zufrieden. Das folgende Programm berechnet Wurzeln per Bisektion: WurzelZiehen.c
#include <stdio.h> #include <math.h> int main() { double zahl = 2; double klein = 0; double gross = zahl; double d, x; double genauigkeit = 1e−4; printf("\nDie Wurzel aus %10.6f ist %10.6f\n", zahl, sqrt(zahl)); printf(" x x*x − %.6f x*x\n", zahl); while (1) { x = (klein + gross) / 2; d = x*x − zahl; printf("%10.6f %10.6f %10.6f", x, d, x*x); if (d < genauigkeit && d > − genauigkeit) break; if (d > 0) gross = x; else klein = x; getchar(); } printf("\n"); getchar(); return 0; }
248
Kapitel 9
Datentypen
Sandini Bib
Die Wurzel aus 2.000000 ist x x*x − 2.000000 1.000000 −1.000000 1.500000 0.250000 1.250000 −0.437500 1.375000 −0.109375 1.437500 0.066406 1.406250 −0.022461 1.421875 0.021729 1.414062 −0.000427 1.417969 0.010635 1.416016 0.005100 1.415039 0.002336 1.414551 0.000954 1.414307 0.000263 1.414185 −0.000082
1.414214 x*x 1.000000 2.250000 1.562500 1.890625 2.066406 1.977539 2.021729 1.999573 2.010635 2.005100 2.002336 2.000954 2.000263 1.999918
Ausprobieren! Genau wie beim Zahlenraten in Kapitel 6.12 setzen wir uns als Erstes eine untere und eine obere Schranke. Die Wurzel aus 2 kann nicht größer als 2 sein und nicht kleiner als 0. Dann fangen wir an, eine Zahl x zu raten, für die x*x gleich zahl gelten soll. Je nachdem ob die Differenz d = x*x − zahl zu groß oder zu klein ist, ändern wir unsere Schranken und raten als Nächstes wieder die Zahl in der Mitte. Charakteristisch für das Rechnen mit Kommazahlen ist, dass wir nicht mit beliebiger Genauigkeit rechnen können. Deshalb beschränken wir uns von vornherein auf eine Genauigkeit bis zur vierten Stelle hinterm Komma und beenden die Schleife, wenn d entsprechend klein geworden ist. Wie musst du das Programm ändern, falls die Zahl zahl kleiner als 1 ist?
9.8
Die Sinusfunktion
Die Sinusfunktion berechnen wir in C mit z. B. y = sin(x);
Genau diese Zeile wollen wir verwenden, um ein Bild von der Sinusfunktion in ein Grafikfenster zu malen. Das wird bei uns so aussehen:
9.8
Die Sinusfunktion
249
Sandini Bib
Lass dich im Folgenden nicht entmutigen. Du kannst dich auch dann mit der Sinusfunktion vergnügen (siehe Bilder weiter unten), wenn dir einige mathematische Einzelheiten in den Erklärungen unverständlich sind. Die Frequenz f ist für dieses Bild gleich 1.00, was in unserem Fall bedeutet, dass eine Schwingung pro Fensterbreite ausgegeben wird. Die Sinusfunktion ist eine periodische Funktion. Wenn x Werte zwischen 0 und 2π annimmt, schwankt y = sin(x) einmal zwischen −1 und +1. Wenn x noch größer wird, wiederholt sich alle 2π dieselbe Wellenform. Wir verwenden dieselbe Datei WinMain.cpp wie in den Beispielen in Kapitel 8. In der .c-Datei stehen die Funktionen malen und WindowProc. Hier ist die Funktion malen des Programms:
250
Kapitel 9 Datentypen
Sandini Bib Sinus.c WinMain.cpp
#include <windows.h> #include <stdio.h> #include <math.h> /* externe Variable */ char text[1000]; int imax, jmax; const double pi = 3.14159265358979323846; double frequenz = 1.0; int n = 100;
/* Male im Fenster */ void malen(HDC hdc) { double x, y, dx, xmin, xmax, ymin, ymax; double xfaktor, yfaktor; int i, j, i0, j0; // Bereich in x und y, der gezeigt werden soll xmin = 0; xmax = 2*pi*frequenz; ymin = −1; ymax = +1; // Intervall in x wird in n Schritte zerlegt dx = (xmax − xmin)/n; // Zoomfaktor fuer Bildschirmfuellung xfaktor = imax/(xmax−xmin); yfaktor = jmax/(ymax−ymin); // Verschiebung im Fenster i0 = 0; j0 = jmax/2; // Ausgangspunkt MoveToEx(hdc, i0, j0, 0); // fuer alle x for (x = xmin; x <= xmax + dx/2; x += dx) { // Funktion y = sin(x); // Umrechnung in Pixel i = i0 + xfaktor * x; j = j0 − yfaktor * y; // Linie und Kaestchen LineTo(hdc, i, j); Rectangle(hdc, i−2, j−2, i+3, j+3); } }
9.8
Die Sinusfunktion
251
Sandini Bib
Wie malt man Funktionen? In der Schule und ohne Computer geht das z. B. so: Man macht sich eine Tabelle mit mehreren Werten x und rechnet für jedes x ein y aus. Dann nimmt man sich ein Karopapier und malt ein Koordinatensystem mit x- und y-Achse. Für jedes x geht man x Schritte auf der x-Achse nach rechts, und dann y Schritte in Richtung der y-Achse nach oben. Im Programm siehst du eine Schleife, die genauso vorgeht, nur verwenden wir statt Karos das Pixelgitter zum Malen. Die Schleife durchläuft verschiedene Werte von x in gleich großen Abständen dx. Weil wir uns wegen Rundungsfehlern bei Kommazahlen nicht darauf verlassen können, dass irgendwann x exakt gleich xmax wird, führen wir den Vergleich mit x <= xmax + dx/2 durch. Für jedes x berechnen wir y = sin(x). In i = i0 + xfaktor * x; j = j0 − yfaktor * y;
rechnen wir die Kommazahlen x und y in ganze Zahlen i und j um, die wir als Koordinaten auf dem Bildschirm verwenden können. Gemalt wird eine Linie zu diesen Koordinaten und ein kleines Kästchen, dass den Punkt bei i und j im Fenster einrahmt. Das Bild der Funktion entsteht, indem wir für jeden Wert von x ein Kästchen malen, dessen Koordinaten von x und y bestimmt werden. Das Drumherum in malen dient nur dazu, die Koordinaten so hinzubiegen, dass die Sinusschwingung genau ins Fenster passt. Zunächst legen wir mit xmin, xmax, ymin und ymax fest, welcher Wertebereich in x und y gezeigt werden soll. Falls die Variable frequenz gleich 1 ist, ist xmax = 2*pi. Die Variable pi ist eine konstante externe Variable, die wir auf 3.14... setzen. Falls die Frequenz 2 ist, wird xmax doppelt so groß. Deshalb erwarten wir, zwei vollständige Schwingungen zu sehen, doch mehr davon später. Die ganzzahlige Variable n bestimmt, mit wie vielen Punkten wir die Sinusfunktion malen wollen. Dazu zerlegen wir das Intervall von xmin bis xmax in n gleich große Stücke, die dx = (xmax−xmin)/n lang sind. dx ist die Schrittweite in der Schleife. Die Faktoren xfaktor und yfaktor dienen dazu, das Bild auf die Größe des Bildschirms zu skalieren. In xfaktor = imax/(xmax−xmin); yfaktor = jmax/(ymax−ymin);
enthalten die externen Variablen imax und jmax die Größe des Fensters in Pixeln. Schau dir noch mal i = i0 + xfaktor * x; j = j0 − yfaktor * y;
an. Der Ursprung des x-y-Koordinatensystems, also x = 0 und y = 0, wird auf i = i0 = 0 und j = j0 = jmax/2 abgebildet. Weil wir y mit 252
Kapitel 9 Datentypen
Sandini Bib
yfaktor = jmax/2 multiplizieren, denn ymax−ymin ist 2, wird der Sinus im Bild zwischen j = 0 und j = jmax schwingen. In j = j0 − yfaktor * y benötigen wir ein Minuszeichen, weil die y-Achse in der Mathematik nach oben zeigt, im Fenster zeigt die j-Achse aber nach unten.
Mit dem Programm kannst du einige nette Experimente machen, denn in der Fensterprozedur reagieren wir auf die Pfeiltasten: Sinus.c WinMain.cpp
/* Rueckruffunktion unseres Fensters */ LRESULT CALLBACK WindowProc(HWND hwnd, UINT m, WPARAM wParam, LPARAM lParam) { HDC hdc; // Tastatur if (m == WM_KEYDOWN) { if (wParam == VK_UP && n < 5000) n = 3*n/2; if (wParam == VK_DOWN && n > 2) n = 2*n/3; if (wParam == VK_LEFT && frequenz < 1024) frequenz *= 2; if (wParam == VK_RIGHT && frequenz > 0.25) frequenz /= 2; InvalidateRect(hwnd, 0, 0); } // Malen else if (m == WM_PAINT) { hdc = GetDC(hwnd); Rectangle(hdc, −1, −1, imax+1, jmax+1); malen(hdc); ReleaseDC(hwnd, hdc); ValidateRect(hwnd, 0); sprintf(text, "Sinus: f = %.2f n = %d", frequenz, n); SetWindowText(hwnd, text); } // Verschiedenes else if (m == WM_SIZE) { imax = LOWORD(lParam); jmax = HIWORD(lParam); } else if (m == WM_DESTROY) PostQuitMessage(0); else return DefWindowProc(hwnd, m, wParam, lParam); return 0; }
Mit ← / → kannst du die Frequenz ändern. Es werden f Schwingungen ins Fenster gezeichnet, z. B. 2 Schwingungen oder ¹⁄² oder ¼ Schwingung:
9.8
Die Sinusfunktion
253
Sandini Bib
Mit ↑ / ↓ veränderst du die Anzahl n der Punkte:
Klarer Fall, du musst eine minimale Auflösung einstellen oder der Sinus ist nicht als Sinus zu erkennen. Je größer die Frequenz, desto mehr Punkte sind nötig:
254
Kapitel 9 Datentypen
Sandini Bib
Wenn die Frequenz zu hoch für die Punktezahl ist, kannst du hübsche Muster erzeugen:
Angenommen, die Schwingung wäre ein Tonsignal, das du in digitaler Form abspeichern möchest. Unser Beispiel führt dir vor Augen, dass eine minimale Ab›sampling rate‹) benötigt wird, sonst hört man Töne, die mit dem tastrate ( eigentlichen Signal nicht mehr viel zu tun haben. 9.8
Die Sinusfunktion
255
Sandini Bib
9.9
Kreis und Rotation
Die Sinusfunktion hat eine Schwester, die Kosinusfunktion. Baue gleich einmal y = cos(x) in das vorhergehende Beispiel ein. Das Ergebnis ist eine Schwingung, die um eine ¼ Periode verschoben ist. Der Witz ist, dass sin und cos kombiniert werden können, um einen Kreis zu zeichnen. Die Formel für einen Kreis mit Radius r ist x = r * cos(alpha); y = r * sin(alpha);
Hier nennen wir den Winkel alpha. Wenn alpha von 0 bis 2π läuft, durchlaufen x und y Punkte auf einem Kreis. Was, das glaubst du nicht? Hier ist die Funktion malen einer nur leicht abgeänderten Version des Programms aus Kapitel 9.8:
256
Kapitel 9 Datentypen
Sandini Bib Kreis0.c WinMain.cpp
/* Male im Fenster */ void malen(HDC hdc) { double alpha, dalpha; double x, y, xmin, xmax, ymin, ymax; double xfaktor, yfaktor; int i, j, i0, j0; // Bereich in x und y, der gezeigt werden soll xmin = −1; xmax = +1; ymin = −1; ymax = +1; // Einmal rum wird in n Schritte zerlegt dalpha = 2*pi/n; // Zoomfaktor fuer Bildschirmfuellung xfaktor = imax/(xmax−xmin); yfaktor = jmax/(ymax−ymin); // Verschiebung im Fenster i0 = imax/2; j0 = jmax/2; // Ausgangspunkt MoveToEx(hdc, i0+xfaktor*r, j0, 0); // fuer alle alpha for (alpha = 0; alpha <= 2*pi + dalpha/2; alpha += dalpha) { // Funktion x = r * cos(alpha); y = r * sin(alpha); // Umrechnung in Pixel i = i0 + xfaktor * x; j = j0 − yfaktor * y; // Linie und Kaestchen if (maleradius) MoveToEx(hdc, i0, j0, 0); LineTo(hdc, i, j); Rectangle(hdc, i−2, j−2, i+3, j+3); } }
Diesmal durchlaufen wir eine Schleife in alpha. Das Programm malt Kreise wie in 9.9
Kreis und Rotation
257
Sandini Bib
Weil wir nach wie vor das Bild auf fensterfüllend skalieren, wird der Kreis zur kannst du Ellipse, wenn das Fenster nicht quadratisch ist. Mit der Leertaste umschalten, ob die Linien im Kreis herum führen oder ob nach jedem Schritt der Stift wieder ins Zentrum bewegt wird, was Sterne aus vielen Linien ergibt. Mit ↑ / ↓ änderst du die Anzahl der Punkte, mit ← / → den Radius. Versuche einmal einen Stern zu zeichnen, dessen Radius größer als das Fenster ist. Bei sehr vielen Punkten bilden die Pixel der Linien interessante Muster. Mit Kreisen geht es rund. Wenn du den Kreis nicht auf einmal mit einer Schleife über alle alpha ausgibst, sondern alpha mit einem Zeitgeber schrittweise größer machst, bekommst du eine Animation eines Zeigers, der gegen den Uhrzeigersinn läuft:
258
Kapitel 9
Datentypen
Sandini Bib
Hier ist das Programm dazu: Kreis1.c WinMain.cpp
#include <windows.h> #include <stdio.h> #include <math.h> /* externe Variable */ char text[1000]; int imax, jmax; const double pi = 3.14159265358979323846; double alpha = 0.0; double dalpha = 0.0; double r = 1.0; int n = 100; int timeran; int dt = 100;
9.9
Kreis und Rotation
259
Sandini Bib Kreis1.c WinMain.cpp
/* Male im Fenster */ void malen(HDC hdc) { double x, y, xmin, xmax, ymin, ymax; double xfaktor, yfaktor; int i, j, i0, j0; // Bereich in x und y, der gezeigt werden soll xmin = −1; xmax = +1; ymin = −1; ymax = +1; // Einmal rum wird in n Schritte zerlegt dalpha = 2*pi/n; // Zoomfaktor fuer Bildschirmfuellung xfaktor = imax/(xmax−xmin); yfaktor = jmax/(ymax−ymin); // Verschiebung im Fenster i0 = imax/2; j0 = jmax/2; // alpha soll im Intervall 0 bis 2*pi bleiben if (alpha < −dalpha/2) alpha = 2*pi − dalpha; if (alpha > 2*pi + dalpha/2) alpha = dalpha; // Funktion x = r * cos(alpha); y = r * sin(alpha); // Umrechnung in Pixel i = i0 + xfaktor * x; j = j0 − yfaktor * y; // Linie und Kaestchen MoveToEx(hdc, i0, j0, 0); LineTo(hdc, i, j); Rectangle(hdc, i−2, j−2, i+3, j+3); }
260
Kapitel 9 Datentypen
Sandini Bib Kreis1.c WinMain.cpp
/* Rueckruffunktion unseres Fensters */ LRESULT CALLBACK WindowProc(HWND hwnd, UINT m, WPARAM wParam, LPARAM lParam) { HDC hdc; // Fenster auf if (m == WM_CREATE) { SetTimer(hwnd, 0, dt, 0); timeran = 1; } // Timer else if (m == WM_TIMER) { alpha += dalpha; InvalidateRect(hwnd, 0, 0); } // Tastatur else if (m == WM_KEYDOWN) { if (wParam == VK_PRIOR) alpha += dalpha; if (wParam == VK_NEXT) alpha −= dalpha; if (wParam == VK_UP && n < 5000) n = 3*n/2; if (wParam == VK_DOWN && n > 2) n = 2*n/3; if (wParam == VK_LEFT && r < 10000) r *= 1.1; if (wParam == VK_RIGHT && r > 0.0001) r /= 1.1; if (wParam == VK_SPACE) { timeran = !timeran; if (timeran) SetTimer(hwnd, 0, dt, 0); else KillTimer(hwnd, 0); } InvalidateRect(hwnd, 0, 0); } // Malen else if (m == WM_PAINT) { hdc = GetDC(hwnd); Rectangle(hdc, −1, −1, imax+1, jmax+1); malen(hdc); ReleaseDC(hwnd, hdc); ValidateRect(hwnd, 0); sprintf(text, "Sinus: r = %.3f n = %d", r, n); SetWindowText(hwnd, text); } // Verschiedenes else if (m == WM_SIZE) { imax = LOWORD(lParam); jmax = HIWORD(lParam); } else if (m == WM_DESTROY) { if (timeran) KillTimer(hwnd, 0); PostQuitMessage(0); } else return DefWindowProc(hwnd, m, wParam, lParam); return 0; }
9.9
Kreis und Rotation
261
Sandini Bib
In der Funktion malen schreiben wir if (alpha < −dalpha/2) alpha = 2*pi − dalpha; if (alpha > 2*pi + dalpha/2) alpha = dalpha;
damit der Winkel alpha das Intervall von 0 bis 2π nicht verlässt. Wie du siehst, habe ich die Schleife aus malen entfernt. In der Funktion WindowProc kannst du ablesen, mit welchen Tasten das Programm bedient werden kann. Insbesondere kannst du den Zeitgeber mit der Leertaste anhalten und starten. Mit Bild ↑ / Bild ↓ kannst du per Hand den Zeiger vorwärts oder rückwärts kreisen lassen (Taste gedrückt halten). Eigentlich ist so ein Zeiger nicht besonders aufregend. Aber jetzt kommt der Clou. Mit demselben Programm kannst du nach kleinen Änderungen beliebige Strichgrafiken rotieren lassen! Mit C geht’s rund:
Der Trick ist der folgende. Angenommen, jemand gibt dir die Koordinaten eines Punktes, x0 und y0. Wie bekommst du die Koordinaten dieses Punktes, wenn du die Zeichenebene um den Winkel alpha um den Ursprung drehst? Die Formel ist x1 = y1 =
cos(alpha) * x0 sin(alpha) * x0
− +
sin(alpha) * y0; cos(alpha) * y0;
Das ist auch nicht viel schwieriger, als was wir bisher gerechnet haben. Wenn du x0 = r und y0 = 0 setzt, bekommst du x1 = y1 =
cos(alpha) * r; sin(alpha) * r;
Das ist die Formel, die wir für den Kreis verwendet haben! Hier sind die wesentlichen Änderungen im Programm für den Zeiger:
262
Kapitel 9 Datentypen
Sandini Bib Rotation.c WinMain.cpp
/* Polygon */ #define N 11 double x0[N] = { 0.0, 1.0, 1.0,−0.5,−1.5,−1.5,−0.5, 1.0, 1.0, 0.0,−0.5}; double y0[N] = { 1.0, 1.0, 2.0, 2.0, 1.0,−1.0,−2.0,−2.0,−1.0,−1.0, 0.0}; double x1[N]; double y1[N]; /* Transformation */ void transformation(void) { int npunkt; for (npunkt = 0; npunkt < N; npunkt++) { x1[npunkt] = cos(alpha) * x0[npunkt] − sin(alpha) * y0[npunkt]; y1[npunkt] = sin(alpha) * x0[npunkt] + cos(alpha) * y0[npunkt]; } } /* Male im Fenster */ void malen(HDC hdc) { ... // transformiere Polygon transformation(); // Stift auf letzten Punkt i = i0 + faktor * x1[N−1]; j = j0 − faktor * y1[N−1]; MoveToEx(hdc, i, j, 0); // fuer alle Punkte for (npunkt = 0; npunkt < N; npunkt++) { // Umrechnung in Pixel i = i0 + faktor * x1[npunkt]; j = j0 − faktor * y1[npunkt]; // Linie und Kaestchen LineTo(hdc, i, j); Rectangle(hdc, i−2, j−2, i+3, j+3); } }
Ein Polygon ist ein Vieleck. Wir definieren Felder x0 und y0 für die Koordinaten von 11 Punkten. Diese initialisieren wir gleich in der Definition mit einer Liste aus Zahlen, die die Koordinaten für den Buchstaben C ergeben. In den Feldern x1 und y1 wollen wir die Koordinaten nach der Rotation speichern. Diese Transformation erledigen wir in der Funktion transformation, die wir in malen aufrufen, bevor mit dem eigentlichen Malen begonnen wird. Mit einer Schleife 9.9
Kreis und Rotation
263
Sandini Bib
über alle Punkte transformieren wir jeden Punkt nach der oben angegebenen Formel. Beim Malen verwenden wir die tranformierten Punkte x1[npunkt] und y1[npunkt]. Probehalber kannst du auch die Felder x0 und y0 einsetzen. Damit sich eine geschlossene Kurve ergibt, setzen wir den Stift vor dem Beginn der Schleife über alle Punkte auf den Punkt mit Index N−1. Und dann geht es mit C rund!
9.10
Farbtiefe
Ein Beispiel, bei dem die Anzahl der verwendeten Bits und Bytes eine Rolle spielt, ist die Farbdarstellung in Windows. Mit einer Schleife können wir leicht alle verschiedenen Farben durchprobieren, also schauen wir uns im folgenden Beispiel alle Rot- und Grün-Mischungen auf einmal an, die wir mit RGB erzeugen können: Farbtiefe.c WinHallo.cpp
#include <windows.h> void malen(HDC hdc) { int i, j; for (i = 0; i < 256; i++) for (j = 0; j < 256; j++) SetPixel(hdc, i, j, RGB(i,j,0)); }
264
Kapitel 9
Datentypen
Sandini Bib
Hier zeige ich nur die Funktion malen. Wie kommt es, dass die Abbildung im Buch und womöglich auch die auf deinem Bildschirm kleine Kästchen zeigt? Das hängt davon ab, wieviele Bit pro Pixel verwendet werden. Die Anzahl der Bit pro Pixel nennt man auch die Farbtiefe der Bildschirmdarstellung. Wenn nur 8 Bit pro Pixel verwendet werden, sind nur 256 verschiedene Farben möglich. Bei 16 Pixeln sind es 256 mal 256 gleich 65536 verschiedene Farben. Im Beispiel fordern wir 256 mal 256 verschiedene Rot- und Grün-Mischungen an. Selbst bei 16 Bit Farbtiefe wird das ein Problem ergeben, denn Blau soll es ja auch noch geben. Bei Farbwerten in Windows, die du mit RGB erzeugst, gibst du drei Byte an, die die Helligkeitsabstufungen der drei Grundfarben bestimmen. Bei 8 Bit Farbtiefe stehen aber z. B. nur 3 plus 3 plus 2 Bit pro Farbe zur Verfügung. Für eine direkte Farbdarstellung benötigst du bei drei Bytes für RGB mindestens 24 Bit. Bei 32 Bit Farbtiefe werden gewöhnlich noch 8 Bit für die Durchsichtigkeit, den so genannten Alpha Channel, verwendet. Eine weitere Methode ist die Verwendung von Farbpaletten. Farben werden dann nicht durch Helligkeitswerte beschrieben, sondern durch einen Zahlencode. Eine Farbpalette ist im Wesentlichen eine Übersetzungstabelle, die z. B. dem Code 1 die Farbe RGB(255,0,0) zuordnet und dem Code 2 die Farbe RGB(0,255,0). Auf diese Weise kann man erreichen, dass selbst bei 8 Bit Farbtiefe 256 beliebige Farben aus einer viel größeren Palette mit allen RGB Farben ausgewählt werden können. Bei Bildern aus Pixeln, so genannten Bitmaps, ist das eine beliebte Methode. Wir werden weiterhin darauf vertrauen, dass Windows automatisch für jede Grafikeinstellung eine vernünftige Näherung für die angeforderten RGB-Werte bereitstellt. Wenn du unternehmungslustig bist und dich mit der Bedienung von Windows auskennst, kannst du das Beispiel bei verschiedenen Farbeinstellungen von Windows laufen lassen. Ich bin mir nicht sicher, ob ich dich dazu ermuntern sollte – nachher beschwert sich jemand, dass du den Computer völlig durcheinander gebracht hast. Bitte nicht vergessen, den Urzustand wieder herzustellen.
9.11
Mandelbrot-Menge
Fraktale hat jeder schon gesehen und ein schönes Beispiel dafür ist die so genannte Mandelbrot-Menge, die von Benoit Mandelbrot 1980 entdeckt wurde. In diesem Beispiel möchte ich die Mandelbrot-Menge aus der Sicht eines Programmierers beschreiben. Nichts, aber wirklich gar nichts, lässt einen erahnen, was sich hinter dieser einfachen Anweisung versteckt: x = x*x + c
9.11
Mandelbrot-Menge
265
Sandini Bib
Wir geben uns eine Konstante c vor, setzen x gleich 0, und dann wiederholen wir diese Anweisung viele Male. Denke ein wenig darüber nach. Die ersten drei Werte von x sind x x x x
= = = =
0 c c*c + c (c*c + c)*(c*c + c) + c
Klarer Fall, wenn c gleich 0 ist, berechnen wir x = 0*0 + 0 = 0, also bleibt x gleich 0. Wenn c gleich 1 ist, durchläuft x die Werte 0, 1, 2, 5, 26, 677, . . ., das heißt, die Zahlen werden immer schneller immer größer. Wenn c kleiner als 1, aber größer als 0 ist, ist nicht ganz klar, was passieren wird. Einerseits wird eine solche Zahl beim Quadrieren kleiner, z. B. ist für c = 0.5 das Quadrat c*c = 0.25. Andererseits sollen wir dann noch c dazuzählen, also wird die Zahl wieder größer. Wir erhalten x = c*c + c = 0.5*0.5 + 0.5 = 0.75
Wie du leicht siehst, werden für c = 0.5 die Zahlen immer größer, aber für c = 0.1 sieht es so aus, als ob die Zahlenfolge klein bleibt, weil das Quadrat nur 0.01 ist. Wie steht es mit Zahlen c < 0? Bei c = −1 ist die Sache klar, x durchläuft 0, −1, 0, −1, 0, −1, und so weiter, denn x = c*c + c = (−1)*(−1) − 1 = 0
Wenn aber c = −1.1, dann – Moment mal, wozu sind wir Programmierer? Spätestens an dieser Stelle schreibst du dir ein Minitextprogramm zum Testen: Mandelbrot0.c
#include <stdio.h> int main() { double c = 0.5; double x = 0.0; while (1) { x = x*x + c; if (x > 100000) break; printf("c = %.3f, getchar(); } return 0; }
266
Kapitel 9
Datentypen
x = %10.3f
", c, x);
Sandini Bib
c c c c c c c c c c c c c c c
= = = = = = = = = = = = = = =
−1.100, −1.100, −1.100, −1.100, −1.100, −1.100, −1.100, −1.100, −1.100, −1.100, −1.100, −1.100, −1.100, −1.100, −1.100,
z z z z z z z z z z z z z z z
= = = = = = = = = = = = = = =
−1.100 0.110 −1.088 0.084 −1.093 0.095 −1.091 0.090 −1.092 0.092 −1.092 0.091 −1.092 0.092 −1.092
Oder auch: c c c c c c c c
= = = = = = = =
0.500, 0.500, 0.500, 0.500, 0.500, 0.500, 0.500, 0.500,
z z z z z z z z
= = = = = = = =
0.500 0.750 1.062 1.629 3.153 10.444 109.567 12005.476
Was passiert bei c = −2? Was bei c = −10? Wenn die Zahl zu negativ wird, macht das Quadrieren x so groß, dass die Zahlen wieder beliebig groß werden. Wir fassen zusammen: Für manche c hüpft die Zahlenfolge munter in Richtung unendlich, für andere c bleiben die Zahlen klein. So weit, so gut. Benoit Mandelbrot hat aber nicht mit reellen Zahlen gerechnet, sondern mit den so genannten komplexen Zahlen. Für unser Beispiel musst du komplexe Zahlen nicht kennen, denn die Iteration ist fast so einfach wie x = x*x + c, nur dass wir zwei Zahlenfolgen gleichzeitig berechnen. Wir geben uns zwei Konstanten c und d vor, setzen x und y gleich 0 und iterieren xneu = x*x − y*y + c yneu = 2*x*y + d
wobei im nächsten Schritt x = xneu und y = yneu sein soll. Na gut, wirst du sagen, das Ergebnis kannst du erahnen. Für manche c und d hüpfen die Zahlen x und y auf und davon, für andere bleiben sie klein. Das könntest du mit einem kleinen Programm testen. Aber hier ist ein viel besserer Vorschlag: Verwende c und d als die Koordinaten eines Punktes. 9.11
Mandelbrot-Menge
267
Sandini Bib
Für jeden Punkt male ein schwarzes Pixel auf dem Bildschirm, wenn die Zahlenfolge klein bleibt, und male ein buntes Pixel, wenn die Zahlenfolge nach unendlich hüpft. Das ergibt ein Bild der so genannten Mandelbrot-Menge. Die schwarzen Punkte sind in der Menge, die bunten nicht. Und was du ohne spezielles Vorwissen und trotz unserer netten kleinen Vorbereitung nicht erahnen kannst, ist das verrückte Bild, das dich erwartet:
Wie seltsam hübsch. Das Programm, das ich gleich beschreiben werde, hat eine Zoomfunktion. Wenn du in das Bild klickst, wird ein Ausschnitt vergrößert. Das mache ich bei kleinem Fenster, weil die Rechnerei eine Weile dauert. Mit den Pfeiltasten kannst du Schärfe und Farbgebung einstellen. Das sieht dann z. B. so aus:
268
Kapitel 9 Datentypen
Sandini Bib
Ein Merkmal dieser Fraktale ist, dass sich Strukturen auf kleineren Skalen immer wieder wiederholen. Hier ist ein etwas anderes Bild,
und es gibt jede Menge schöne Ansichten. Das Programm ist nicht besonders kompliziert. Wir verwenden wieder die WinMain Funktion aus Kapitel 8 zusammen mit einer selbst gemachten WindowProc und verschiedenen Helferfunktionen. In
9.11
Mandelbrot-Menge
269
Sandini Bib Mandelbrot.c WinMain.cpp
#include <windows.h> #include <math.h> #include <stdio.h> /* externe Variable */ HDC hdc; char text[1000]; int imax, jmax; double xmin, ymin, dpixel; /* Parameter fuer Mandelbrot−Menge */ double xmitte = −0.7; double ymitte = 0.0; double breite = 3.5; int itermax int zoomfaktor int nzoom double pfarbe
= = = =
10; 2; 0; 1.0;
siehst du die nötigen Includeanweisungen und die externen Variablen. Es folgt die Mandelbrot-Iteration, die wir in der Funktion mandelbrot verpackt haben: Mandelbrot.c WinMain.cpp
/* Mandelbrot−Iteration */ int mandelbrot(double c, double d) { int i; double x = 0, y = 0, x2, y2; for (i = 0; i x2 = x*x; y2 = y*y; if (x2 + y2 y = 2*x*y + x = x2 − y2 } return i;
< itermax; i++) {
> 1000000) break; d; + c;
}
Wir führen mehrere Iterationen aus. Als Erstes berechnen wir das Quadrat von x und y. Falls die Summe der Quadrate zu groß wird, verlassen wir die Schleife. Falls nicht, berechnen wir die neuen Werte von x und y und die Iteration beginnt von vorne. Verstehst du, warum wir auf temporäre Variablen xneu und yneu verzichten können? Insbesondere dürfen die Zeilen x = ... und y = ... nicht vertauscht werden. Die Schleife wird entweder beendet, wenn x und y zu groß werden oder wenn die maximale Anzahl von itermax Iterationen erreicht wurde. In beiden Fällen ist 270
Kapitel 9 Datentypen
Sandini Bib
der Rückgabewert von mandelbrot die Anzahl der Iterationen, die ausgeführt wurden. Diese Anzahl verwenden wir, um die Farbe des Pixels für x und y zu bestimmen. Eine Möglichkeit wäre /* Farbgebung abhaengig von Anzahl der Iterationen */ COLORREF farbe(int i) { if (i >= itermax) return RGB(0, 0, 0); return RGB(256*i/itermax, 0, 0); }
Wenn itermax Iterationen ausgeführt wurden, soll die Farbe schwarz sein. Natürlich könnte es sein, dass erst nach noch mehr Iterationen die Zahlen zu groß werden und der Punkt zu Unrecht schwarz wurde. Deshalb kann im Programm die maximale Anzahl der Iterationen mit den Pfeiltasten eingestellt werden. Falls weniger Iterationen als itermax ausgeführt wurden, soll das Pixel auf jeden Fall bunt werden. 256*i/itermax ist eine Zahl von 0 bis 255 (trotz ganzzahliger Division, weil erst malgenommen wird). Je näher i der Zahl itermax kommt, desto heller das Rot. Dadurch wird der Übergang von Weghüpfen zu Kleinbleiben scharf hervorgehoben. Eine etwas raffiniertere Version der Farbfunktion ist Mandelbrot.c WinMain.cpp
/* Farbgebung abhaengig von Anzahl der Iterationen */ COLORREF farbe(int i) { int rot; if (i >= itermax) return RGB(0, 0, 0); rot = 256 * pow((double) i/itermax, pfarbe); return RGB(rot, 0, 0); }
Hier kann mit der externen Variablen pfarbe die Farbgebung über die Tastatur gesteuert werden. Beachte, wie wir aus der ganzen Zahl i eine Double machen, dann die Funktion pow aufrufen, die eine Double liefert, und das Ergebnis wird durch Zuweisung an die Integervariable rot wieder zur ganzen Zahl. Die Pixel malen wir mit
9.11
Mandelbrot-Menge
271
Sandini Bib Mandelbrot.c WinMain.cpp
/* Male Mandelbrot−Menge */ void malen(HDC hdc) { int i, j, m; double x, y; dpixel = breite/imax; xmin = xmitte − imax*dpixel/2; ymin = ymitte − jmax*dpixel/2; for (i = 0; i < imax; i++) { for (j = 0; j < jmax; j++) { x = i * dpixel + xmin; y = j * dpixel + ymin; m = mandelbrot(x, y); SetPixel(hdc, i, j, farbe(m)); } } }
In diesem Beispiel nennen wir die Größe des Fensters imax und jmax. Wie du siehst, malen wir mit der doppelten Schleife jedes Pixel des Fensters mit SetPixel in einer Farbe, die durch die Anzahl der Iterationen in mandelbrot bestimmt wird. Das Drumherum ist ein typisches Problem der Koordinatenumrechnung. Jedes Pixel im Fenster mit ganzzahligen Koordinaten i und j soll einen Punkt in den reellen Koordinaten x und y darstellen. Betrachte die Zeilen x = i * dpixel + xmin; y = j * dpixel + ymin;
Hier wird munter von Int nach Double umgewandelt. Wenn i in einer Schleife von 0 bis imax läuft, läuft x von xmin bis zu einem xmax von imax * dpixel + xmin, und entsprechend für j und y. Wir haben also ein Rechteck in i-j-Koordinaten (das Fenster) auf ein Rechteck in x-y-Koordinaten abgebildet. Der Abstand von Pixel zu Pixel ist demnach dpixel, und wir erhalten ihn, indem wir die Breite breite des Ausschnitts der Mandelbrot-Menge durch die Anzahl der Pixel in der i-Richtung teilen. Den Ursprung xmin und ymin bestimmen wir aus zwei Variablen xmitte und ymitte, denn wir wollen ja in das Bild klicken können und die angeklickten Koordinaten sollen die Mitte des neuen Ausschnitts sein. Die Fensterprozedur des Programms bietet nichts Besonderes, außer dass beim Linksklick und beim Rechtsklick mit der Maus die Mauskoordinaten nach derselben Formel wie in malen in die Koordinaten für die Mitte des neuen Rechtecks umgerechnet werden: 272
Kapitel 9 Datentypen
Sandini Bib Mandelbrot.c WinMain.cpp
/* Rueckruffunktion unseres Fensters */ LRESULT CALLBACK WindowProc(HWND hwnd, UINT m, WPARAM wParam, LPARAM lParam) { // Fenster auf if (m == WM_CREATE) { hdc = GetDC(hwnd); SelectObject(hdc, CreateSolidBrush(0)); } // Malen else if (m == WM_PAINT) { sprintf(text, "Mandelbrot Menge, nzoom = %d, itermax = %d", nzoom, itermax); SetWindowText(hwnd, text); malen(hdc); ValidateRect(hwnd, 0); } // Maus else if (m == WM_LBUTTONDOWN) { xmitte = LOWORD(lParam) * dpixel ymitte = HIWORD(lParam) * dpixel breite /= zoomfaktor; nzoom++; InvalidateRect(hwnd, 0, 0); } else if (m == WM_RBUTTONDOWN) { xmitte = LOWORD(lParam) * dpixel ymitte = HIWORD(lParam) * dpixel breite *= zoomfaktor; nzoom−−; InvalidateRect(hwnd, 0, 0); } // Tastatur else if (m == WM_KEYDOWN) if (wParam == VK_UP) if (wParam == VK_DOWN) if (wParam == VK_LEFT) if (wParam == VK_RIGHT) InvalidateRect(hwnd, 0, }
{ itermax itermax pfarbe pfarbe 0);
+ xmin; + ymin;
+ xmin; + ymin;
*= /= *= /=
2; 2; 1.5; 1.5;
// Verschiedenes else if (m == WM_SIZE) { imax = LOWORD(lParam); jmax = HIWORD(lParam); } else if (m == WM_DESTROY) PostQuitMessage(0); else return DefWindowProc(hwnd, m, wParam, lParam); return 0; }
9.11
Mandelbrot-Menge
273
Sandini Bib
In diesem Kapitel kann ich auch erklären, was in imax = LOWORD(lParam); jmax = HIWORD(lParam);
vor sich geht. Wie wir jetzt wissen, bezeichnet ein Wort (›word‹) 2 Bytes und lParam besteht wohl aus 4 Bytes. Mit LOWORD holen wir uns die zwei niedrigen (›low‹) Bytes aus lParam, mit HIWORD die zwei hohen (›high‹) Bytes. Für die Fensternachricht wurden die zwei Mauskoordinaten in nur einer Variablen verpackt.
9.12 Jetzt weißt du womöglich mehr über Datentypen in C, als du jemals wissen wolltest. Dein Computer rechnet mit Bits und Bytes, also begegnen sie dir auch in der Programmiersprache C, wenn es um den Zahlenbereich von Variablen oder die Typenumwandlung geht. Auf jeden Fall erlaubt aber die Kombination von Integerzahlen und Kommazahlen jede Menge interessante Grafikbeispiele. Zur Zusammenfassung: Wichtige Datentypen in C sind char für kleine Zahlen und Zeichen, int für ganze Zahlen und double für Kommazahlen. Jeder Datentyp unterliegt Einschränkungen in Größe und Genauigkeit. Zahlkonstanten wie 1 oder −20 erhalten den Typ int. Kommazahlen benötigen eine Punkt wie in 2.5 und erhalten den Typ double. Man muss 1.0/2.0 schreiben, wenn das Ergebnis 0.5 sein soll. Datentypen wie int und double werden bei Bedarf automatisch ineinander umgewandelt. Man kann die Umwandlung durch z. B. (int) x oder (double) i erzwingen.
9.13 1. Schreibe ein Programm, das jedes Zeichen in einem vordefinierten String einzeln ausgibt, und zwar als Zahl und als Zeichen. Die printf Formatanweisung für char ist %c. Schreibe auch eine Schleife, die alle Chars von 0 bis 255 ausgibt. Darunter sind einige Steuerzeichen wie ’\n’. 2. Beschleunige den Primzahltest aus Kapitel 6.10 durch folgenden Trick: Teste nur solche i, die kleiner gleich der Wurzel von zahl sind. Falls die Zahl einen Teiler i hat, gilt zahl = i * j. Entweder ist i gleich j und zahl ist eine echte Quadratzahl. Oder eine der Zahlen i und j ist größer und die andere kleiner als die Wurzel x von zahl. Wenn wir alle ganzen Zahlen i kleiner 274
Kapitel 9
Datentypen
Sandini Bib
gleich x schon durchprobiert haben, ohne einen Teiler zu finden, kann es für größere i auch keinen Teiler mehr geben, denn dann hätten wir ja den dazugehörige Faktor j schon unter den Zahlen gefunden, die kleiner als die Wurzel sind. Funktioniert dein Programm bei 49 = 7 * 7? 3. Stelle die rekursive Berechnung von n Fakultät in Kapitel 7.11 auf den Datentyp double um. Bei welchem Wert findet der Overflow statt? Ein kleiner Tipp: mit i = x + 0.5; kannst du erreichen, dass eine Kommazahl x, die
wegen Ungenauigkeiten der Kommarechnung knapp unter der gewünschten ganzen Zahl i liegt, tatsächlich diese Zahl ergibt. 4. Mit einer kleinen Änderung des Programms zum Kreisezeichnen bekommst
du Bilder wie dieses:
Die Titelleiste gibt einen Hinweis. Hier habe ich nicht einen konstanten Radius r verwendet, sondern r = r0 * pow(cos(4*alpha), 2.0)
Experimentiere mit verschiedenen Parametern und erzeuge ähnliche Bilder. 5. Lass das C um seine rechte obere Ecke rotieren. Dazu verschiebst du vor der Rotation die Punkte in x0 und y0 um dx0 und dy0, so dass diese Ecke im
Ursprung liegt. Dann rotierst du. Dann machst du die Verschiebung wieder rückgängig:
9.13
275
Sandini Bib
6. Lese in Anhang A.1 darüber nach, wie ›Funktionsmakros‹ definiert werden. RGB ist ein solches Makro, mit dem drei Bytes in einer ganzen Zahl verpackt
werden. Seine Definition findest du in der Hilfe zum Windows SDK unter RGB. Jetzt kannst du versuchen herauszufinden, wie die Bitoperatoren aus 9.1
die drei Bytes verpacken. 7. Denke dir andere Farben für das Mandelbrot-Programm aus. Eine nette Va-
riante ist, in bestimmten Abständen zwischen zwei oder mehr Farben umzuschalten. 8. Ändere das Mandelbrot-Programm wie folgt. Führe die erste Iteration für alle
Punkte aus, dann male alle Punkte, die weggehüpft sind. Dann führe die nachfolgende Iteration nur noch für die Punkte aus, die übrig sind, und so fort. Auf diese Weise wird das Bild nicht linienweise aufgebaut, sondern kristallisiert sich nach und nach heraus. Per Mausklick könntest du die Rechnerei anhalten, wenn du mit dem Ergebnis zufrieden bist. Vorsicht, du benötigst einigen Speicherplatz, um die Zwischenergebnisse für verschiedene Punkte abzuspeichern. 9. Eine andere Zeichenvariante im Mandelbrot-Programm ist, für einzelne
Punkte den Weg der Zahlenfolge durch Linien zu verfolgen. Es gibt übrigens wesentlich effizientere Methoden, die Mandelbrot-Menge und ihren Verwandten zu berechnen. Dazu kannst du sicher im Internet oder in einer Bücherei Informationen finden.
276
Kapitel 9
Datentypen
Sandini Bib
10 Zeiger und Strukturen 10.0 10.1 10.2 10.3 10.4 10.5 10.6
Zeiger Zeiger und malloc Zeiger, Felder und wie man mit Adressen rechnet Zeiger, Variablen, Funktionen Strukturen Zeiger auf Strukturen
279 282 285 286 290 292
Bibliotheksfunktionen für Zeichenketten und Felder
296
10.7
Zeichenketten kopieren
297
10.8
Felder aus Zeigern und main mit Argumenten
299
10.9
Dateien lesen und schreiben
301
10.10
WinMain
304
10.11
309
10.12
310
Sandini Bib
C zeichnet sich dadurch aus, dass dem Programmierer so gut wie keine Grenzen gesetzt sind. Er kann per Software fast alles mit dem Computer anstellen, was er will. Natürlich hat da das Betriebssystem, in unserem Fall Windows, noch ein Wörtchen mitzureden, aber davon abgesehen kann C fast alles. Und das mit Abstand praktischste, mächtigste, aber auch gefährlichste Werkzeug, das C dem ›pointer‹). Programmierer in die Hände gibt, sind Zeiger ( Ein Zeiger erlaubt es, direkt auf den Speicher zuzugreifen. Abstrakt kannst du dir den Arbeitsspeicher deines Computers (das RAM, ›random access memory‹) so vorstellen, dass alle Bytes vom ersten bis zum letzten durchnummeriert sind. Konkret fängt man mit 0 an und zählt 0, 1, 2, 3 usw. bis zum letzten der 128 MB oder so. Der Mikroprozessor greift auf einzelne Bytes zu, indem er sie per Nummer beim RAM abfragt. Man redet nicht von Nummern wie beim Telefon, sondern von Adressen. Der Prozessor liest und schreibt im RAM unter Verwendung der Adressen von verschiedenen Speicherzellen. Bisher haben wir Variable verwendet, um auf gespeicherte Daten zuzugreifen. Wir vereinbaren mit dem Compiler einen Namen für einen bestimmten Speicherbereich, und wenn wir den Variablennamen schreiben, übersetzt der Compiler diesen intern in eine Adresse. Und warum rede ich von Adressen, wenn unser Thema doch Zeiger sein sollen? Ein Zeiger ist eine spezielle Variable, in der man Adressen speichern kann. All das kann dir zu diesem Zeitpunkt nur verwirrend vorkommen. Aber lass mich noch andeuten, dass Zeiger sehr nützlich sind. Mit einem Zeiger p (p wie Pointer) kannst du nicht nur auf den Inhalt einer einzelnen Variable zeigen, sondern auch auf große zusammenhängende Speicherbereiche. Damit lassen sich dann leicht Felder aufbauen, in denen du mit p[50] auf das soundsovielte Element zugreifen kannst. Und wenn du in einer Funktion ein solches Feld verwenden willst, musst du nicht den ganzen Speicherbereich kopieren. Du brauchst nur einen Zeiger auf das erste Byte zu übergeben. In diesem Kapitel wollen wir auch die so genannten Strukturen einführen. Mit Strukturen kannst du verschiedene Variablen unter einem Namen zusammenfassen. Das hört sich nicht sehr aufregend an, erlaubt es uns aber, in C Datenstrukturen zu verwalten, die über einfache Zahlen oder Felder weit hinausgehen. Dabei spielen Zeiger eine wichtige Rolle, weil du wie bei Feldern für gewöhnlich nur einen Zeiger zur Struktur benötigst. Zeiger sind gefährlich, weil der C-Compiler es nicht verhindert, wenn du auf Bytes zugreifst, die nicht angerührt werden sollten. Wenn du nicht aufpasst, kannst du mit Zeigern ungewollt Daten überschreiben und damit dein Programm (und wenn das Betriebssystem nicht aufpasst, den ganzen Computer) völlig durcheinander bringen. Der Computer kann ›abstürzen‹. Ein bekannter Witz stellt für verschiedene Programmiersprachen ein und dieselbe Frage, die dann geistreich beantwortet wird. In unserem Fall: Frage: Wie stellst du dir selbst ein Bein? C: Du stellst dir selbst ein Bein.
278
Kapitel 10 Zeiger und Strukturen
Sandini Bib
Aha. Andere Antworten sind so was wie ›Pascal: Der Compiler lässt dich nicht‹, oder ›C++: Du definierst Bein als Unterklasse von Körperteil als Unterklasse von Körper, und nach zwei, drei Fehlern stellst du dir ein Bein‹. Die Moral von der Geschichte soll sein, dass C dir von allen Programmiersprachen die wenigsten unnötigen Hindernisse in den Weg legt. Das ist gut so. C wäre nicht C, wenn sich Probleme mit Zeigern nicht leicht vermeiden ließen. Unterm Strich sind Zeiger eine elegante und flexible Lösung, dem Programmierer freie Hand bei der Verwendung des Speichers zu lassen. Jetzt habe ich weit ausgeholt, um Zeiger gebührend einzuführen. Zeiger sind ein wesentliches Merkmal der Programmierung mit C. Aber lass dich nicht abschrecken! Wir werden nur die einfachen, wirklich wichtigen Merkmale von Zeigern und von Strukturen besprechen. Das reicht auf alle Fälle aus, um z. B. mit dem Windows SDK zu programmieren.
10.0 Zeiger Ein Zeiger ist eine Variable, in der die Adresse einer Variablen gespeichert werden kann. Als Erstes wollen wir die elementaren Operatoren & und * für Zeiger besprechen. Aus dem Zusammenhang muss klar sein, dass wir mit & und * nicht das Und-Zeichen oder das Mal-Zeichen meinen. Angenommen, wir haben mit int i; eine Integervariable definiert. Der Ausdruck &i
ergibt die Adresse des Speicherplatzes der Variablen i. Diese Adresse können wir in einer geeigneten Zeigervariablen p speichern, z. B. mit p = &i;
Gut, jetzt enthält p die Adresse (die Telefonnummer) der Bytes, in denen der Inhalt der Variablen i gespeichert ist. Wie können wir mit dem Zeiger p auf diese Bytes zugreifen? Das geschieht mit *p
Der Ausdruck *p kann wie eine Integervariable verwendet werden. Mit j = *p;
kannst du den Inhalt der Bytes, auf die p zeigt, in eine Integervariable j kopieren. Aber *p kann auch wie eine richtige Integervariable auf der linken Seite einer Zuweisung stehen, *p = 2;
10.0 Zeiger
279
Sandini Bib
Hier wird die Zahl 2 in die Bytes kopiert, auf die p zeigt. Damit das mit dem Auslesen und dem Zuweisen mit *p klappt, muss klar sein, auf was für einen Datentyp p zeigt. Sonst weiß das Programm ja nicht, wie viele Bytes es auslesen soll. Deshalb wird eine Variable p, die einen Zeiger auf eine ganze Zahl enthält, wie in int *p;
definiert. Der Datentyp ›Zeiger auf Int‹ wird also wie int *
geschrieben und die Definition lässt sich so lesen: p ist etwas, was mit dem * davor eine int ergibt. Wie es sich für die Definition einer Variablen gehört, wird ausreichend Speicherplatz reserviert, um eine Adresse zu speichern. Hier ist ein Beispiel, in dem & und * wie eben besprochen verwendet werden: Zeiger0.c
#include <stdio.h> int main() { int i; int *p; i = 1; p = &i; printf("\nNach p = &i; ist\n"); printf(" i = %d\n", i); printf(" p = %d\n", p); printf("*p = %d\n", *p); *p = 2; printf("\nNach *p = 2; ist\n"); printf(" i = %d\n", i); printf(" p = %d\n", p); printf("*p = %d\n", *p); printf("\nsizeof(int *) = %d\n", sizeof(int *)); getchar(); return 0; }
280
Kapitel 10 Zeiger und Strukturen
Sandini Bib
Nach i = p = *p =
p = &i; ist 1 6553064 1
Nach i = p = *p =
*p = 2; ist 2 6553064 2
sizeof(int *) = 4
Nach der Zeile p = &i;
hat sich der Wert von i nicht geändert, aber p enthält eine Zahl, die die Adresse der Variable i angibt. Der Ausdruck *p in dem printf-Befehl ergibt die Zahl, die unter dieser Adresse gespeichert ist. Hallo, Byte Nummer 6553064! Nett, deine Bekanntschaft zu machen. Was ist in dir gespeichert? Eigentlich ist es üblich, Zeiger mit der Formatanweisung %p als Hexadezimalzahl auszugeben. Aber ich wollte dir vorführen, dass der Zeiger p nichts weiter als eine ganze Zahl enthält. Genau genommen hat diese Zahl kein Vorzeichen und wir sollten zumindest die Formatanweisung %u (›unsigned‹) verwenden. Nach der Zeile *p = 2;
hat sich der Wert von *p geändert, wie nach der Zuweisung mit = zu erwarten, während p unverändert geblieben ist. Und weil p auf den Speicherplatz von i zeigt, hat sich auch i geändert!! Lese den letzten Satz laut vor und lass ihn dir auf der Zunge zergehen. Keine zwei Variablen verwenden normalerweise denselben Speicherplatz. Zwei Integervariablen i und j erhalten nach int i, j; Speicherplatz unter verschiedenen Adressen. Aber die Adresse von i kann in einem Zeiger p gespeichert werden, und i und *p dürfen sehr wohl auf denselben Speicherplatz zugreifen. Im Prinzip könntest du die Adresse auch noch in einen Zeiger q kopieren, q = p;
Jetzt hast du drei Möglichkeiten, den Inhalt von i zu ändern, nämlich mit i = 2, *p = 2 und *q = 2. In der letzten Ausgabezeile des Beispiels sehen wir, dass bei mir sizeof(int *)
10.0 Zeiger
281
Sandini Bib
gleich 4 ist. Den sizeof-Operator haben wir in Kapitel 9.3 eingeführt und wie du siehst, können wir sizeof auch für Datentypen von Zeigern verwenden. Adressen sind also in diesem Beispiel 4 Bytes lang, was wiederum bedeutet (siehe 9.3), dass höchstens bis zu 4 GB an Daten adressiert werden können. Das spiegelt die Architektur meines Pentium II PCs wider. Übrigens nennt man & auch Adressenoperator oder Referenzoperator (Referenz im Sinne von ›verweisen auf‹). * nennt man Inhaltsoperator oder Dereferenzoperator.
10.1 Zeiger und malloc Mit int i; erhältst du Platz für genau eine ganze Zahl. Mit int a[10]; erhältst du Platz für 10 Zahlen, aber die Anzahl 10 muss im Voraus festgelegt werden. Mit der Funktion malloc kannst du dir während der Programmausführung Speicherplatz besorgen, dessen Größe von einer Variablen festgelegt wird: Zeiger1.c
#include <stdio.h> #include <stdlib.h> int main() { int *p; int n = 10; int i; p = (int *) malloc(n * sizeof(int)); if (!p) { printf("Fehler: konnte keinen Speicherplatz bekommen.\n"); getchar(); return 0; } for (i = 0; i < 10; i++) p[i] = 2*i; for (i = 0; i < 10; i++) printf("i = %d p[%d] = %d\n", i, i, p[i]); free(p); getchar(); return 0; }
282
Kapitel 10 Zeiger und Strukturen
Sandini Bib
i i i i i i i i i i
= = = = = = = = = =
0 1 2 3 4 5 6 7 8 9
p[0] p[1] p[2] p[3] p[4] p[5] p[6] p[7] p[8] p[9]
= = = = = = = = = =
0 2 4 6 8 10 12 14 16 18
Vergleiche dieses Beispiel mit dem Beispiel aus 3.0. Genau wie bei Feldern kann p[i] wie eine Variable verwendet werden, mit der das i-te Element einer Reihe von Zahlen gesetzt und gelesen werden kann. Damit das funktioniert, brauchen wir Platz im Speicher, der für uns reserviert ist. Hier verwenden wir die Funktion malloc ( ›memory allocation‹, Speicherzuweisung): p = (int *) malloc(n * sizeof(int));
Das Argument von malloc ist die Anzahl der Bytes, die wir benötigen. Im Gegensatz zur Definition eines Felder darf hier eine Variable wie n verwendet werden. Um von der tatsächlichen Größe einer Integerzahl unabhängig zu sein, benutzen wir sizeof. Du kannst ein Feld mit zehn Elementen mit int a[10]; anlegen, aber int a[n]; ist nicht erlaubt. Man könnte sich denken, dass es für jeden Datentyp eine eigene mallocFunktion gibt, so dass man sich die sizeof-Aktion sparen kann. Stattdessen bedient sich C des folgenden Tricks: Zeiger, für die der Datentyp nicht festgelegt ist, erhalten den Typ void *
›void‹ heißt nichts, leer. Eine Deklaration wie void x; gibt es nicht, denn eine Variable ohne Typ ist sinnlos. Mit einer Deklaration wie void *p; kann man jedoch eine Variable erzeugen, in der ein Zeiger auf einen beliebigen Datentyp gespeichert werden kann. Wenn du dich in der BCB-Hilfe mit F1 nach der Funktion malloc erkundigst, findest du heraus, dass malloc einen solchen ›Zeiger zum Undefinierten‹ liefert: void *malloc( ... );
Dort steht auch, dass malloc die Headerdatei stdlib.h benötigt. In 9.4 haben wir besprochen, wie wir Datentypen mit dem Castoperator ineinander umwandeln. Man schreibt den gewünschten Typ einfach in runden Klammern davor, z. B. (double) i. Der Castoperator funktioniert auch für Zeigertypen. Der Ausdruck (int *) malloc( ... )
10.1 Zeiger und malloc
283
Sandini Bib
verwandelt den Zeiger, den malloc liefert, in einen Zeiger auf ganze Zahlen. Was malloc intern macht, ist, dass es sich Speicherblöcke vom Betriebssystem (Windows) besorgt. Bei jedem Aufruf von malloc erhältst du einen neuen Speicherbereich zugewiesen. Dabei ist zu beachten: Unter Umständen ist kein Speicherplatz mehr frei! In diesem Fall bringt malloc dir den Wert 0 zurück. Dies ist der so genannte Nullzeiger. Zur Verdeutlichung schreibt man für diesen Zeiger oft die symbolische Konstante NULL. Du musst also immer mit if (p == 0)
oder if (p == NULL)
oder einfach mit if (!p)
Code vorsehen, der ausgeführt wird, falls kein neuer Speicherplatz erhalten werden konnte. !p kannst du wie ›nicht p‹ oder ›kein p‹ lesen. Ausprobieren, wie groß darfst du n auf deinem Computer machen? Wenn du den Speicher nicht mehr benötigst, gib ihn mit der Funktion free wieder frei: free(p);
Natürlich musst du dann den Zeiger p erst wieder neu setzen, bevor du ihn verwenden darfst. Bei Beendigung des Programms wird der verwendete Speicher automatisch ans Betriebssystem zurückgegeben. Weitere interessante Speicherfunktionen sind: Mit z. B. realloc(p, (n+100) * sizeof(int)) kann man nachträglich einen von malloc gelieferten Speicherblock vergrößern oder verkleinern. Mit z. B. calloc(n, sizeof(int)) erhält man Speicher, der mit 0 initialisiert ist (beachte das Komma). Moment Mal, was steht denn sonst in dem Speicher drin, den malloc liefert? Die Sache ist genau wie bei automatischen lokalen Variablen. Nach der Definition, aber vor der Initialisierung steht dort im Allgemeinen Müll, also irgendwelche Zahlen, die vielleicht vom letzten Programm übrig sind. In unserem Programmbeispiel kannst du probehalber die Schleife mit p[i] = 2*i; weglassen. Vielleicht erhältst du Nullen, vielleicht irgendwas. Das gleiche gilt auch für Zeigervariablen. Vor der Initialisierung mit p = &i; steht in p womöglich Müll, was für Zeiger bedeutet, dass sie sonstwohin zeigen!
284
Kapitel 10 Zeiger und Strukturen
Sandini Bib
10.2 Zeiger, Felder und wie man mit Adressen rechnet Angenommen, p ist ein Zeiger, der in einen brauchbaren Speicherbereich zeigt. Gibt es sinnvolle Rechenoperationen, die wir mit p und q durchführen können? Das Folgende ist in der Tat erlaubt: p + 5 p − 1
Die Adresse wird nach oben oder nach unten gesetzt, wobei man sicherstellen muss, dass man den zugewiesenen Speicherbereich nicht verlässt. Schlauerweise sind + und − für Zeiger und Zahl so definiert, dass z. B. +1 die Adresse auf das nächste Element setzt und nicht bloß auf das nächste Byte! Wenn also p auf Integers zeigt, ergibt p+1 eine Adresse, die um 4 größer ist (wenn Ints 4 Bytes verwenden). Wenn du darüber etwas nachdenkst, wird dir auffallen, dass man also die Zahl mit Index 1 sowohl mit p[1] als auch mit *(p+1) auslesen kann. In der Tat sind das lediglich verschiedene Schreibweisen ein und derselben Zugriffsoperation. Für einen Zeiger p und eine ganze Zahl i gilt: p[i] und *(p+i) sind gleichwertig. &p[i] und
p+i sind gleichwertig.
Für i gleich 0 erhältst du p[0] und *p sind gleichwertig. &p[0] und p sind gleichwertig.
Das ist ganz offensichtlich der Grund, warum die Erfinder von C beschlossen haben, beim Durchzählen von Elementen mit 0 anzufangen. Wenn man bei 1 anfängt, wäre die Adressenrechnerei nicht so schön einfach. Wirf einmal einen Blick auf die Rangordnungstabelle der Operatoren in Anhang A.0. Die runden Klammern in *(p+i) sind nötig, weil der Dereferenzoperator * stärker bindet als die Addition mit +. Mit *p+i rechnest du die Summe der Zahl i und der Zahl aus, auf die p zeigt. Und weil [ ] stärker bindet als &, wird in &p[i] erst p[i] ausgewertet, was sich wie gesagt wie eine Variable benimmt, und dann mit &p[i] die Adresse der i-ten Variable berechnet. Es wird dich jetzt sicher nicht mehr wundern, dass nach int a[10];
der Variablenname a in vielen Fällen wie ein Zeiger funktioniert. In C sind Felder so definiert, das der Name des Feldes, a, gleichbedeutend mit der Adresse ist, bei der der Speicherplatz beginnt. Mit p = a;
10.2 Zeiger, Felder und wie man mit Adressen rechnet
285
Sandini Bib
kann man diese Adresse z. B. in einen Zeiger kopieren, und genau wie eben besprochen kann man mit a[i], aber auch mit *(a+i) Elemente auslesen. Wodurch unterscheiden sich Felder und Zeiger? a kann nicht geändert werden! Z.B. kannst du nicht a = p; schreiben oder den Speicherbereich vergrößern. Andererseits ist es trivial, ein mehrdimensionales Feld mit int a[10][10] zu definieren, was bei Zeigern nicht ohne weiteres möglich ist. Das ist also das Geheimnis, das Felder und Zeiger verbindet: Der Name eines Feldes a ist so was wie ein konstanter Zeiger. Er kann wie ein Zeiger verwendet werden, nur dass man die Adresse, die zu a gehört, nicht ändern kann. Andererseits wird der Speicherplatz bei einem Feld a gleich mitgeliefert, während du einen Zeiger per Hand mit p = a, p = &i oder p = malloc(...) auf einen gültigen Speicherplatz richten musst. Jetzt können wir das & in der Funktion scanf verstehen, welches in Kapitel 2.6 noch ohne Erklärung akzeptiert werden musste. scanf benötigt die Adressen der Variablen, in denen die gelesenen Werte gespeichert werden sollen: int i; char text[1000]; scanf("%d", &i); scanf("%s", text);
Die Feldvariable text wird ohne & angegeben, weil sie für die Adresse des Feldes steht, während wir von der Integervariablen i erst mit & die Adresse erzeugen müssen. Der Vollständigkeit halber sei noch erwähnt, dass, wenn zwei Zeiger zum selben Speicherblock gehören, ihre Differenz (also Zeiger − Zeiger) und die Vergleichsoperatoren == != < > <= >= definiert sind.
10.3 Zeiger, Variablen, Funktionen In Kapitel 7 über Funktionen haben wir den Gültigkeitsbereich von Variablen besprochen und wie Variablen an Funktionen übergeben werden können. Als Erstes ist festzustellen, dass für Zeigervariablen genau dieselben Regeln gelten wie für andere Variablen auch. Zeiger können extern oder intern und als statisch oder automatisch definiert werden. Mal sind sie sichtbar, mal nicht. Hier ist ein Beispiel für eine Funktion, die einen Zeiger als Argument annimmt:
286
Kapitel 10 Zeiger und Strukturen
Sandini Bib Zeiger2.c
#include <stdio.h> void hallo(char *s); int main() { char *name = "Welt"; hallo(name); getchar(); return 0; } void hallo(char *s) { printf("Hallo, %s!\n", s); }
Hallo, Welt!
Hier siehst du, dass char *name = "Welt"; wie char name[] = "Welt"; funktioniert. Die Variable name ist ein Zeiger auf ein char, und in diesem Fall zeigt name auf das erste Zeichen einer Stringkonstanten. Jede Stringkonstante im Programmtext steht für die Adresse ihres ersten Zeichens. Der Prototyp der Funktion hallo ist void hallo(char *s);
das heißt, ein Zeiger als Argument wird wie in der Definition des Zeigers mit dem Sternchen * geschrieben. In der Funktion hallo ist die Variable name nicht sichtbar, aber die Adresse in name wird in eine neue char * Variable s kopiert, die wir dann verwenden können. Beachte, dass wir nicht etwa die ganze Zeichenkette kopieren, sondern nur den Zeiger zum ersten Zeichen. Das ist auf jeden Fall effizienter. Die Verwendung eines Zeigers als Argument sieht im letzten Beispiel nicht viel Aber trotzdem gibt es einen riesigen anders aus als bei anderen Variablen. Unterschied zwischen der Übergabe eines Zeigers und einer normalen Variable. Am besten schauen wir uns das folgende Beispiel an:
10.3 Zeiger, Variablen, Funktionen
287
Sandini Bib Zeiger3.c
#include <stdio.h> void machzurnull_falsch(int i); void machzurnull_richtig(int *p); void machzurnull_falsch(int i) { i = 0; } void machzurnull_richtig(int *p) { *p = 0; } int main() { int n = 1; printf("n ist %d\n", n); machzurnull_falsch(n); printf("n ist %d\n", n); machzurnull_richtig(&n); printf("n ist %d\n", n); getchar(); return 0; }
n ist 1 n ist 1 n ist 0
Hier soll offensichtlich eine Funktion geschrieben werden, die die Variable n auf 0 setzt. Einmal klappt’s, einmal nicht. Nach Kapitel 7 kann die erste Variante, machzurnull_falsch, nicht funktionieren, denn lokale Variablen sind für die Außenwelt unsichtbar. n wird in die lokale Variable i kopiert. Egal, wie wir i verändern, n bleibt unverändert. Und das ist ganz im Sinne des Erfinders, denn oft will man Daten zwischen Funktionen sauber trennen. Häufig möchte man jedoch, dass eine Funktion Daten in der aufrufenden Funktion ändert, z. B. n gleich 0 setzt. Wir kennen bisher zwei Möglichkeien, Daten an die aufrufende Funktion zu übergeben: mit einer externen Variablen oder einfach mit return. Die dritte Möglichkeit ist, einen Zeiger zu übergeben, wie in machzurnull_ richtig vorgeführt wird. In main wird die Adresse von n, also &n, an die Funktion machzurnull_richtig übergeben und in p gespeichert. Also hat *p = 0; 288
Kapitel 10 Zeiger und Strukturen
Sandini Bib
denselben Effekt, als wenn wir in main n = 0; ausgeführt hätten. Das funktioniert, weil Adressen ihre Gültigkeit behalten und nicht wie Variablennamen gewissen Sichtbarkeitsregeln unterliegen. Vielleicht mache ich zu viele Worte um etwas, das dir schon sonnenklar ist, aber diese Idee ist wichtig. Du kannst den Zugriff auf Daten in verschiedenen Funktionen genau steuern, ohne auf die in Kapitel 7 besprochenen Sichtbarkeitsregeln angewiesen zu sein, indem du Zeiger übergibst. Egal, wie oft ein Zeiger herumgereicht wird, mit * kannst du jederzeit den Inhalt des Speicherplatzes ändern, auf den p zeigt. Denn auch bei Zeigern wird wie bei Funktionsaufrufen üblich das Argument in eine lokale Variable kopiert. Und weil das Argument eine Adresse ist, kennt machzurnull_richtig die Adresse von n. Und das ist alles, was gebraucht wird, um n zu ändern. Gleich noch ein Beispiel. Angenommen, wir möchten den Inhalt zweier Variablen vertauschen. Das geht mit Zeiger4.c
#include <stdio.h> int main() { int i = 0, j = 5; int temp; printf("%d %d\n", i, j); temp = i; i = j; j = temp; printf("%d %d\n", i, j); getchar(); return 0; }
0 5 5 0
Wir können nicht gleich mit i = j; j = i;
loslegen, denn dann überschreiben wir als Erstes den Wert von i, und nachher haben beide Variablen den Wert von j. Falls unklar, ausprobieren! Der Wert von i wäre für immer verloren. 10.3 Zeiger, Variablen, Funktionen
289
Sandini Bib
Kein Problem, wir speichern i in einer temporären Variable (also einer Variable, die wir nur vorrübergehend benutzen), überschreiben i mit j und holen uns dann den Wert für j aus temp. Dieses Vertauschen ist an vielen Stellen nützlich, lass uns eine Funktion dafür schreiben. Die folgende Funktion ist sinn- und zwecklos: void swap_falsch(int i, int j) { int temp; temp = i; i = j; j = temp; }
›Swap‹ heißt austauschen. Lokales swappen bringt nichts. Aber mit void swap(int *pi, int *pj) { int temp; temp = *pi; *pi = *pj; *pj = temp; }
funktioniert es. Verwende diese Funktion in main im letzten Beispiel. Wie musst du die Funktion swap aufrufen? Ausprobieren!
10.4 Strukturen In Feldern wird eine bestimmte Anzahl von Zahlen abgespeichert, die alle denselben Datentyp haben. In C ist es darüber hinaus möglich, Zahlen unterschiedlichen Datentyps zu einem neuen Datentyp zusammenzufassen. Diese Konstruk›structure‹). Strukturen sind nützlicher, als du vieltion heißt Struktur ( leicht im ersten Moment denkst, und nicht nur im Windows SDK werden Strukturen oft verwendet. Im nächsten Unterkapitel 10.5 besprechen wir, warum Zeiger auf Strukturen praktisch sind. Aber erst möchte ich dir erklären, wie du Strukturen definieren kannst. Denke einmal darüber nach, welche Information wir für ein Rechteck in Windows benötigen. Das sind vier ganze Zahlen für die Koordinaten der linken, oberen, rechten und unteren Kante. Diese Zahlen können wir wie folgt zusammenfassen: struct rechteck { int left; int top;
290
Kapitel 10 Zeiger und Strukturen
Sandini Bib int right; int bottom; };
Mit dem Schlüsselwort struct definieren wir einen neuen Datentyp, der den Namen struct rechteck erhält. Zwischen die geschweiften Klammern schreiben wir alle Variablen, die in der Struktur enthalten sein sollen. Beachte den Strichpunkt nach den Klammern. Nach dieser Strukturdefinition kann struct rechteck genau wie andere Datentypen verwendet werden. Eine Variable r vom Datentyp struct rechteck erhältst du mit struct rechteck r;
Du kannst auch die Definition des Strukturdatentyps und die Definition von Strukturvariablen in einer Anweisung zusammenfassen: struct rechteck { int left; int top; int right; int bottom; } fenstergroesse, r1, r2;
Hier wird der Datentyp struct rechteck eingeführt, und wir definieren gleich noch drei neue Variablen. Bei einem Feld verwenden wir Indizes, um auf die Elemente des Feldes zuzugreifen. Bei einer Struktur verwenden wir den Punkt . als Zugriffsoperator wie in xmax = r.right;
Wir rufen die Elemente der Struktur mit ihrem Namen auf, und nicht mit einer Indexzahl wie bei Feldern. In der Strukturdefinition haben wir den einzelnen Elementen der Struktur Namen gegeben, aber Namen wie right müssen in Verbindung mit einer Strukturvariablen verwendet werden. r.right kannst du genau wie eine Integervariable verwenden. Weil die Bezeichnung right eindeutig mit der Struktur struct rechteck verknüpft ist, könntest du zusätzlich noch eine Variable int right; einführen und right = r.right;
schreiben, um den Inhalt von r.right zwischenzuspeichern. Der Name für ein Element einer Struktur kollidiert nicht mit Variablennamen und es ist manchmal nützlich, als Gedächtnisstütze dieselben Namen zu verwenden. Vielleicht findest du es etwas umständlich, den Datentyp struct rechteck immer in zwei Wörtern zu schreiben. Mit typedef kannst du neue Namen für Datentypen definieren, siehe Kapitel 9.6. Für Strukturen sieht das so aus: 10.4 Strukturen
291
Sandini Bib typedef struct { int left; int top; int right; int bottom; } RECHTECK; RECHTECK r;
Schau genau hin, wegen typedef ist RECHTECK nicht etwa eine Variable, sondern eine Typenbezeichnung, die genau wie int zur Definition von Variablen verwendet werden kann. In diesem Fall kann der Name zwischen struct und den geschweiften Klammern weggelassen werden. Wir schreiben RECHTECK mit großen Buchstaben, um daran zu erinnern, dass ein neuer Datentyp dahinter steckt. Im Windows SDK gibt es eine sehr ähnliche Struktur namens RECT. Dass wir den vier ganzen Zahlen in struct rechteck oder RECHTECK Namen geben können, ist nett. Wichtiger ist aber, dass wir wie angekündigt verschiedene Datentypen in einer Struktur zusammenfassen können. Ein Beispiel ist typedef struct { char *name; int waffe[3]; int trefferpunkte; int lebendig; } MONSTER; MONSTER troll[5], ork[10], goblin[20];
Hier siehst du, dass man in die Definition einer Struktur auch Zeiger und Felder schreiben darf. Zudem können wir Felder aus identischen Strukturen definieren! Waffe 0 von Troll 3 erhältst du mit troll[3].waffe[0]. Du kannst sicher erahnen, welche enorme Flexibilität durch Strukturen erreicht wird. Eine Programmieraufgabe bedeutet auch immer, dass irgendwelche Daten verarbeitet werden müssen. Strukturen machen es dir einfach, selbst komplizierte Daten zu organisieren.
10.5 Zeiger auf Strukturen Richtig nützlich werden Strukturen erst, wenn du Zeiger auf Strukturen verwendest. Denn genau wie bei Feldern benötigt man nur den Zeiger auf eine Struktur, um auf alle ihre Elemente zugreifen zu können. Normalerweise wirst du nicht ganze Strukturen an Funktionen übergeben, sondern lediglich Zeiger zu Strukturen. Angenommen, RECHTECK ist wie im letzten Beispiel definiert worden. Mit RECHTECK r, *pr;
292
Kapitel 10 Zeiger und Strukturen
Sandini Bib
kannst du dir eine Strukturvariable r und eine Zeigervariable rp vom Typ ›Zeiger auf Strukturvariable‹ definieren. Nach pr = &r;
zeigt rp auf r. Jetzt hast du drei Möglichkeiten, auf Elemente von r zuzugreifen: xmax = r.right; xmax = (*rp).right; xmax = rp−>right;
Klarer Fall, weil rp die Adresse von r enthält, kannst du *rp wie r verwenden. Achtung, weil der Zugriffsoperator . enger bindet als der Dereferenzoperator * (siehe Anhang A.0), müssen wir in (*rp).right; Klammern setzen. Weil Zeiger auf Strukturen häufig vorkommen, gibt es in C den speziellen Zugriffsoperator −> (ein Pfeil aus ›Minus‹ und ›Größer‹). Der Pfeil bewirkt dasselbe wie (*rp).right, aber rp−>right ist einfacher zu schreiben und zu lesen. Im folgenden Beispiel verwenden wir Strukturen, um die Anzahl der Pixel in einem Fenster zu berechnen. Wir verwenden Zeiger auf Strukturen, aber der Vollständigkeit halber führe ich dir auch vor, dass Strukturen auch ohne Zeiger übergeben werden können:
10.5 Zeiger auf Strukturen
293
Sandini Bib Struktur0.c
#include <stdio.h> /* definiere einen neuen Typ fuer eine Struktur: Rechteck parallel zu Bilschirmkoordinaten */ typedef struct { int left; /* links, kleinster int right; /* rechts, groesster int top; /* oben, kleinster int bottom; /* unten, groesster } RECHTECK;
/* Flaeche = Breite mal Hoehe */ /* alles kopiert, Zugriff mit . */ int flaeche(RECHTECK r) { int hoehe, breite; breite = r.right − r.left; hoehe = r.bottom − r.top; return breite * hoehe; } /* Adresse kopiert, Zugriff mit −> */ int flaechemitzeiger(RECHTECK *r) { int hoehe, breite; breite = r−>right − r−>left; hoehe = r−>bottom − r−>top; return breite * hoehe; }
294
Kapitel 10 Zeiger und Strukturen
x x y y
Wert Wert + 1 Wert Wert + 1
*/ */ */ */
Sandini Bib Struktur0.c
/* Berechne und vergleiche Anzahl von Pixeln von Fenstern */ int main() { RECHTECK fenster1, fenster2; int npixel1, npixel2; /* Bildschirmaufloesung */ fenster1.left = 0; fenster1.top = 0; fenster1.right = 1024; fenster1.bottom = 768; /* kopiere die Rechteckangabe und verkleinere die Groesse um 2 */ fenster2 = fenster1; fenster2.right /= 2; fenster2.bottom /= 2; /* berechne Flaeche, kopiere alle Daten in Struktur */ npixel1 = flaeche(fenster1); /* berechne Flaeche, uebergebe nur einen Zeiger zur Struktur */ npixel2 = flaechemitzeiger(&fenster2); /* erzaehl mal */ printf("Das kleine Fenster benoetigt %d Pixel.\n", npixel2, npixel1); printf("Das doppelt so grosse Fenster benoetigt %d, also %d mal so viel!\n", npixel1, npixel1/npixel2); getchar(); return 0; }
Das kleine Fenster benoetigt 196608 Pixel. Das doppelt so grosse Fenster benoetigt 786432, also 4 mal so viel!
Als Erstes definieren wir den Strukturtyp RECHTECK. Diese Definition gilt für den nachfolgenden Programmtext in dieser Datei. Solche Definitionen können in einer Headerdatei gesammelt werden. Es folgen zwei Funktionen, die für ein gegebenes Rechteck die Fläche berechnen. Die eine Funktion verwendet einen Zeiger, die andere nicht. Ohne Zeiger wird die gesamte Struktur kopiert. In der Funktion main rufen wir diese Funktionen auf, um die Anzahl der Pixel in zwei verschiedenen Fenstern zu berechnen. Im Gegensatz zu Feldern können wir Strukturen praktischerweise wie in fenster2 = fenster1;
kopieren. Die Anzahl der Bytes in einer Struktur kann größer als die Summe der Bytes für die einzelnen Variablen sein, weil z. B. bei einem 32-Bit-Prozessor der Compiler für ein einzelnes Zeichen wie char c; 8 Bit plus ungenutztem Zwischenraum verwenden darf. Also solltest du wie in rp = (RECHTECK *) malloc(sizeof(RECHTECK));
10.5 Zeiger auf Strukturen
295
Sandini Bib
für malloc die Anzahl der Bytes in einer Struktur immer mit sizeof erfragen. Bleibt nur noch anzumerken, dass doppelt so groß in jede Richtung viermal so viele Pixel bedeutet. Das heißt, wenn du die Qualität deiner Grafik ›verdoppeln‹ möchtest, muss der Computer viermal so hart arbeiten!
10.6
Bibliotheksfunktionen für Zeichenketten und Felder
Endlich ist es so weit. Mit deinen neuen Kentnissen über Zeiger kannst du all diese Funktionsprototypen mit dem Sternchen * in der BCB-Hilfe verstehen! Öffne die BCB-Hilfe. Unter ›Bibliotheksreferenz‹, ›Bibliotheksroutinen‹, ›nach Kategorien sortiert‹ findest du einige interessante Funktionen. Unter dem etwas nichts sagenden Titel ›Bearbeitungsroutinen‹ findest du Funktionen für Zeichenketten (Headerdatei string.h), z. B. char *strcpy(char *ziel, char *start);
Kopiere (›copy‹) alle Zeichen in der Zeichenkette start einschließlich der 0 am Ende nach ziel. Rückgabewert ist ziel. char *strstr(const char *s, const char *gesucht); Suche die Zeichenkette gesucht in der Zeichenkette s. Rückgabewert ist ein Zeiger auf die Stelle in s, bei der gesucht gefunden wurde, oder NULL. int strcmp(const char *s1, const char *s2);
Vergleiche (›compare‹) zwei Zeichenketten alphabetisch. Rückgabewert ist < 0, == 0, > 0, je nachdem ob s1 kleiner, gleich oder größer als s2 ist. Z.B. ist "Bernd" kleiner als "Bruegmann". Der Vergleich beruht auf den Zahlenwerten des ASCII Codes, also liefert strcmp auch bei Sonderzeichen ein sinnvolles Ergebnis. Klarer Fall, wenn du Funktionen für Zeichenketten definieren willst, tauchen überall Zeiger auf Chars auf. Falls du es nicht mit Zeichenketten, sondern z. B. mit Feldern zu tun hast, kannst ›memory copy‹, Speicher kopieren), memcmp du Funktionen wie memcpy ( (Speicher vergleichen) oder memset (Speicher setzen) benutzen. Die Funktionen für String- und Speicherbearbeitung sind praktisch, nicht nur, weil du sie nicht selber programmieren musst, sondern auch weil diese Funktionen für die Bibliotheken mit speziellen Maschinesprachebefehlen programmiert werden können, die schneller als Schleifen in C ablaufen.
296
Kapitel 10 Zeiger und Strukturen
Sandini Bib
10.7
Zeichenketten kopieren
Natürlich ist es nicht schwierig, Zeichenketten zu kopieren. Wir können dabei das Rechnen mit Zeigern üben. Hier ist die erste Version einer selbst gemachten Funktion zum Kopieren von Strings: StringCopy0.c
#include <stdio.h> void stringcopy(char *s, char *t); int main() { char s[10], t[10] = "Hallo"; stringcopy(s, t); printf("%s %s\n", s, t); getchar(); return 0; } void stringcopy(char *s, char *t) { int i = 0; while (t[i] != 0) { s[i] = t[i]; i++; } s[i] = 0; }
Hallo Hallo
Beachte, dass wir mit char s[10], t[10]; Speicherplatz für beide Strings bereitstellen. Mit char *s; kopieren wir sonstwohin, wenn s nicht auf gültigen Speicherplatz zeigt. Aufgepasst, nach der while-Schleife ist die Null noch nicht kopiert! Wir machen das nach der Schleife. Das war recht übersichtlich, jetzt machen wir mal etwas Denksport. Wie können wir diese Schleife verkürzen? Hier ist ein Trick:
10.7
Zeichenketten kopieren
297
Sandini Bib StringCopy0a.c
void stringcopy(char *s, char *t) { int i = 0; while ((s[i] = t[i]) != 0) i++; }
Die Zuweisung mit = ist ein Ausdruck, der den Wert der zugewiesenen Zahl liefert, und (s[i] = t[i])
ist ein Ausdruck, der alle Chars von String t durchläuft und sie gleichzeitig in String s speichert. Und weil erst die Null gespeichert und dann die Bedingung != getestet wird, brauchen wir die Null nicht extra nach der Schleife zu speichern. Die runden Klammern sind nötig, damit die Operation = vor != durchgeführt wird, siehe Anhang A.0. Jetzt wollen wir einmal ausnützen, dass s und t lokale Kopien sind, mit denen wir rechnen dürfen. In StringCopy0b.c
void stringcopy(char *s, char *t) { while ((*s = *t) != 0) { s++; t++; } }
sehen wir eine typische Verwendung von ++ in Verbindung mit Zeigern. Statt den Index hochzuzählen, setzen wir einfach die Zeiger eins weiter! Wir dürfen das, weil es sich nur um Kopien handelt. Und auch das geht: StringCopy0c.c
void stringcopy(char *s, char *t) { while ((*s++ = *t++) != 0); }
Hier nutzen wir aus, dass *t++ das Zeichen ergibt, das t enthielt, bevor ++ ausgeführt wurde (Inkrement NACH dem Auslesen, 2.12). Unbedingt ausprobieren, wie unterscheiden sich *t++, *(t++) und (*t)++? Der letzte Ausdruck ist offensichtlich nicht, was in unserem Beispiel geschieht, denn wir addieren nicht 1 zu dem Char *t, sondern 1 zum Zeiger auf Char hinzu. *t++ und *(t++) sind gleichwertig. 298
Kapitel 10 Zeiger und Strukturen
Sandini Bib
Nein, wir sind noch nicht fertig, hier ist noch ein stringcopy: StringCopy0d.c
void stringcopy(char *s, char *t) { while (*s++ = *t++); }
Diesen Trick kennen wir schon. Ungleich Null steht schließlich für wahr, und die Schleife soll laufen, bis die Bedingung falsch, also Null, ergibt. BCB gibt eine Warnung aus, denn meistens ist es ein übler Tippfehler, statt == in einer Bedingung nur = zu schreiben! Wie du siehst, lässt sich mit Zeigern knapp und elegant rechnen. Man muss sich nur an die Schreibweise gewöhnen.
10.8
Felder aus Zeigern und main mit Argumenten
Wir haben es bisher noch nicht diskutiert, aber nichts spricht dagegen, einen Zeiger auf einen Zeiger zu definieren, z. B. int **pp;
Weil solche Zeiger auf Zeiger recht kompliziert werden können, wollen wir uns in diesem Buch nicht auf eine Diskussion einlassen. Aber ein verwandtes Beispiel kommt häufig vor, nämlich eine Liste aus Zeichenketten, z. B. char *wochentag[] = { "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Spasstag", "Schlaftag" };
Die Variable wochentag ist ein Feld, das als Elemente Zeiger auf Zeichen (char *) enthält. Zur Initialisierung schreiben wir eine Liste von Stringkonstanten zwischen die geschweiften Klammern. Stringkonstanten liefern die Adresse zu einer konstanten Zeichenkette. Bei einem einzelnen String können wir nach char *s; s = "Hallo";
schreiben, um die Adresse von "Hallo" in s zu speichern. Für das Feld wochentag schreiben wir Felder aus Zeigern und main mit Argumenten
299
Sandini Bib wochentag[0] = "Schon wieder Montag?";
um die Adresse für eine neue Stringkonstante im 0-ten Element zu speichern. Das erste Zeichen in s ist s[0], das erste Zeichen in wochentag[0] ist wochentag[0][0]. Eine wichtige Verwendung für eine Liste von Zeichenketten ist der Aufruf von main mit Argumenten. Hier ist ein Beispiel: MainArgs.c
#include <stdio.h> int main(int narg, char *arg[]) { int i; printf("Guten Tag, ich wurde mit %d Argument(en) aufgerufen:\n", narg); for (i = 0; i < narg; i++) printf("%3d: %s\n", i, arg[i]); getchar(); return 0; }
Weil wir unsere Beispiele normalerweise von BCB aus starten, bietet es sich nicht an, Argumente an main zu übergeben. Aber zwischen die runden Klammern in main() gehören eigentlich zwei Argumente. int narg ist die Anzahl der Elemente des Feldes char *arg[], das eine Liste aus Zeigern auf Zeichenketten enthält. Die i-te Zeichenkette erhalten wir mit arg[i] und können sie mit printf ausgeben. Das Feld wochentag könntest du genauso ausgeben, ausprobieren. Aber wo kommen die Argumente für main her? Jede andere Funktion wird von main aufgerufen, aber wer ruft main auf? Wenn du das Beispiel in BCB laufen lässt, erhältst du z. B. Guten Tag, ich wurde mit 1 Argument(en) aufgerufen: 0: C:\MAINARGS.EXE
Wie du siehst, ist arg[0] hier der Name des Executables, das BCB als Endprodukt der Kompilierung erzeugt. Wenn du in Windows eine DOS-Konsole startest (z. B. mit ›Start‹, ›Programme‹, ›MS-DOS-Eingabeaufforderung‹), kannst du das Beispielprogramm als Befehl ablaufen lassen: C:\>mainargs Hallo Welt Guten Tag, ich wurde mit 3 Argument(en) aufgerufen: 0: C:\MAINARGS.EXE 1: Hallo 2: Welt
300
Kapitel 10 Zeiger und Strukturen
Sandini Bib
Die Zeichenketten, die main übergeben bekommt, sind die Wörter nach dem Namen unseres Programms in der DOS-Befehlszeile (.exe wird nicht benötigt). Diese Wörter sind die Befehlszeilenargumente, die an die Funktion main in zwei Funktionsargumenten übergeben werden. Textbildschirmanwendungen kannst du auch unter Windows-Startmenü mit ›Ausführen‹ starten. Diese Option öffnet ein kleines Fenster, in das du den Namen der .exe-Datei gefolgt von Argumenten für main eintragen kannst.
10.9
Dateien lesen und schreiben
Bisher haben wir noch kein einziges Wort darüber verloren, wie man in CDateien lesen und schreiben kann. Die Standard I/O-Bibliothek enthält dazu einige Funktionen, die alle einen Zeiger auf eine Struktur FILE benötigen. Hier ist ein Beispiel: DateiLesen.c
#include <stdio.h> int main() { FILE *fp; int c; fp = fopen("DateiLesen.c", "r"); if (!fp) { printf("Fehler: Konnte DateiLesen.c nicht oeffnen.\n"); getchar(); return 0; } while (1) { c = fgetc(fp); if (c == EOF) break; if (c != ’ ’ && c != ’\t’ && c != ’\n’) printf("%c", c); } fclose(fp); getchar(); return 0; }
Mit FILE *fp; definieren wir eine Zeigervariable für die Struktur FILE, die alle nötigen Details für die Filebehandlung enthält. In diesem Fall brauchen wir diese Details gar nicht zu kennen! Im Allgemeinen öffnest du zuerst eine Datei für 10.9
Dateien lesen und schreiben
301
Sandini Bib
das Lesen oder Schreiben mit fopen. Dann folgen einige Ein- und Ausgabeoperationen, z. B. mit fgetc, das einzelne Zeichen liest. Dann schließt du die Datei mit fclose wieder. Offene Dateien werden bei Ende der Funktion main automatisch geschlossen, aber es ist eine gute Angewohnheit, Dateien zu schließen, wenn sie nicht mehr benötigt werden. Es können mehrere Dateien geöffnet sein, aber nur eine begrenzte Anzahl zur gleichen Zeit. Die Sache mit Dateien öffnen und schließen ist auch deshalb nötig, weil dein Betriebssystem nicht mehreren Programmen gleichzeitig erlauben sollte, in dieselbe Datei hineinzuschreiben. In unserem Beispiel versuchen wir mit fp = fopen("DateiLesen.c", "r");
die Datei DateiLesen.c im momentanen Verzeichnis zu öffnen. Falls du ein anderes Verzeichnis angeben willst, musst du daran denken, dass der Schrägstrich \ für spezielle Zeichen verwendet wird, und z. B. "C:\\MeinCode\\DateiLesen.c"
schreiben (Kapitel 3.3). Das Ergebnis von fopen ist ein ›file pointer‹, den wir in fp speichern. Falls fp gleich NULL ist, konnte die Datei nicht geöffnet werden. Man bezeichnet fp auch als ›stream‹ (Strom), und verschiedene Datenströme sind möglich. Das zweite Argument von fopen ist eine Zeichenkette, die die Art des Datenstroms angibt: "r" für Lesen (›read‹) "w" für Schreiben (›write‹) "a" für Schreiben am Ende der Datei (›append‹)
Mit "w" überschreibst du unter Umständen eine vorhandene Datei, während du mit "a" neue Zeichen am Ende einer Datei anfügen kannst. Falls bei "a" die Datei noch nicht existiert, wird eine neue Datei angelegt. Wenn nicht nur gewöhnliche Zeichen und Buchstaben gelesen oder geschrieben werden sollen, verwendet man "rb", "wb" und "ab" (b für ›binary data‹). Die Datei zu schließen ist einfacher, als sie zu öffnen. Du rufst einfach fclose mit dem Filepointer auf, den fopen dir geliefert hatte. In unserem Beispiel lesen wir Zeichen in c = fgetc(fp);
Den Funktionsnamen kannst du als ›file get character‹ lesen. Ist dir aufgefallen, dass wir c nicht als char, sondern als int definiert haben? fgetc liest in der Tat den Datentyp unsigned char, wandelt diese Zeichen aber in den Datentyp int um. Der Grund ist, dass fgetc unter Umständen den Wert EOF liefert, der das Ende der Datei signalisiert (›end of file‹). Die Konstante EOF ist kein ASCII-Zeichen, und weil alle 256 ASCII-Zeichen schon reserviert sind, ist der 302
Kapitel 10 Zeiger und Strukturen
Sandini Bib
Rückgabewert von fgetc ein größerer Datentyp als char. Wie dem auch sei, wir lesen so lange Zeichen, bis EOF auftritt. Du könntest auch while ((c = fgetc(fp)) != EOF)
schreiben (auf die richtigen Klammern achten). Und was macht unser Programm mit den Zeichen? Es druckt alle Zeichen, sofern es sich nicht um Zwischenraumzeichen handelt. Das sieht z. B. so aus: #include<stdio.h>intmain(){FILE*fp;intc;fp=fopen("datei1.c","r");if(!fp){printf ("Fehler:Konntedatei1.cnichtoeffnen.\n");getchar();return0;}while(1){c=fgetc(fp );if(c==EOF)break;if(c!=’’&&c!=’\t’&&c!=’\n’)printf("%c",c);}fclose(fp);getchar ();return0;}
Es gibt mehrere Funktionen zum Testen von Zeichen (Headerdatei ctype.h), isspace(c) (›ist Zwischenraum‹) verwenden, siehe die z. B. könnten wir BCB-Bibliotheksreferenz unter ›Klassifizierungsroutinen‹. Ändere dieses Beispiel doch so um, dass du das Programm mit einem Argument für main aufrufen kannst, das die Datei benennt (siehe Kapitel 10.8). Dann kannst du dir die verschiedensten Dateien im verdichteten Stil anschauen. Zeichen schreiben und lesen kannst du mit verschiedenen Funktionen, siehe insbesondere die BCB-Bibliotheksreferenz zu ›Ein-/Ausgaberoutinen‹: int fgetc(FILE *fp);
Lese nächstes Zeichen. Rückgabewert ist das gelesene Zeichen oder EOF bei Dateiende oder Fehler. int fputc(int c, FILE *fp); Schreibe c als unsigned char. Rückgabewert ist c oder EOF beim Auftreten
eines Fehlers. char *fgets(char *s, int n, FILE *fp);
Lese Zeichen bis einschließlich des nächsten Neuezeilezeichens \n und speichere sie im Feld s ab, plus einer 0 am Ende. Rückgabewert ist s oder NULL, falls ein Fehler oder EOF auftrat. int fputs(const char *s, FILE *fp); Schreibe die Zeichenkette s. Rückgabewert ist größer gleich 0 oder EOF beim
Auftreten eines Fehlers. int fscanf(FILE *fp, const char *format, ...);
Wie scanf, nur dass von fp gelesen wird. Rückgabewert EOF bei Fehler. int fprintf(FILE *fp, const char *format, ...);
Wie printf, nur dass nach fp geschrieben wird. Rückgabewert EOF bei Fehler. Alle diese Funktionen kannst du auch für die Texteingabe von der Tastatur bzw. für die Textausgabe im Textfenster verwenden. Die folgenden Streams sind bei 10.9
Dateien lesen und schreiben
303
Sandini Bib
Programmablauf automatisch verfügbar und können als Filepointer verwendet werden: stdin für Standardeingabe von der Tastatur stdout für Standardausgabe zum Textfenster stderr für Standardfehlerausgabe, normalerweise zum Textfenster
Insbesondere kannst du mit fgets und stdin das Problem lösen, dass gets womöglich zu viele Zeichen liest, siehe 3.6. Es gibt noch einige andere Funktionen in den Standardbibliotheken von C, die mit Dateien zu tun haben. Weil wir uns nicht weiter mit der Dateibehandlung befassen wollen, empfehle ich dir an dieser Stelle bei Bedarf in der BCB-Hilfe zu stöbern. Die genannten Funktionen reichen auf jeden Fall aus, um eine Bestenliste oder einen Spielstand als Datei zu speichern und zu lesen.
10.10
WinMain
Der einzige Grund, warum wir die WinMain-Funktion noch nicht besprochen haben, die wir in unseren Grafikbeispielen einsetzen, ist die Verwendung von Zeigern und Strukturen in dieser Funktion. Also, Augen auf und hingeguckt:
304
Kapitel 10 Zeiger und Strukturen
Sandini Bib WinMain.cpp
/* main fuer Windows */ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE dummy0, PSTR dummy1, int dummy2) { WNDCLASS wc; // Fensterklasse HWND hwnd; // Handle des Fensters MSG msg; // Message (Nachricht) /* Initialisiere wc.lpfnWndProc wc.lpszMenuName wc.cbClsExtra wc.cbWndExtra wc.hInstance wc.hIcon wc.hCursor wc.hbrBackground wc.style wc.lpszClassName
Fensterklasse */ = WindowProc; // Zeiger auf WindowProc, siehe oben = NULL; = 0; = 0; = hInstance; = LoadIcon(NULL, IDI_APPLICATION); // Icon = LoadCursor(NULL, IDC_ARROW); // Cursor = GetStockObject(WHITE_BRUSH); // Hintergrund = CS_OWNDC | CS_VREDRAW | CS_HREDRAW; // Stilparameter = "KlasseHallo"; // Klassenname
/* Registriere Fensterklasse */ RegisterClass(&wc); /* Erzeuge Fenster, merke dir den Handle */ hwnd = CreateWindow("KlasseHallo", "WinHallo", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 250, 250, NULL, NULL, hInstance, NULL);
// // // // //
Klassenname Titel Fenstertyp Ursprung x,y Breite, Hoehe
/* Zeige Fenster */ ShowWindow(hwnd, SW_SHOW); /* Nachrichtenschleife, Ausstieg falls GetMessage 0 liefert */ while (GetMessage(&msg, 0, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); // Ruft WindowProc auf } /* beende WinMain und damit das Programm */ return msg.wParam; }
Du erinnerst dich, bei WinMain fängt die Programmausführung einer Windowsanwendung an. Der Prototyp ist int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE dummy0, PSTR dummy1, int dummy2);
Die Typbezeichner WINAPI, HINSTANCE und PSTR stammen wie alle anderen Typen aus windows.h. Wir benötigen nur das erste Argument, HINSTANCE hInstance. ›Instance‹ heißt hier ›Verwirklichung‹, und hInstance ist 10.10
WinMain
305
Sandini Bib
ein Handle zur Identifizierung unserer Windowsanwendung. Z.B. könnte unser Programm ja mehrmals aufgerufen werden und somit mehrere Kopien derselben Anwendung gleichzeitig ablaufen. Was wir in der Funktion WinMain an Befehlen verpacken, ist uns freigestellt. In unserem Beispiel erledige ich in WinMain verschiedene Standardaufgaben, wie sie in vielen Windowsanwendungen anzutreffen sind. Eigentlich ist ein Windowsprogramm sehr einfach: 1. Fenster auf. 2. Warte auf Fensternachrichten und bearbeite, was da so kommt. 3. Fenster zu.
Weil das Windows SDK ein sehr großes Softwarepaket ist, das komplizierte Aufgaben zu erledigen hat, ist es vergleichsweise kompliziert, ein Fenster auf den Bildschirm zu bekommen. Das liegt eigentlich nur an den vielen Einzelheiten, die wir für unser Fenster festlegen dürfen. ›Fenster auf‹ erfordert mehrere Schritte: 1. Initialisiere Struktur wc für Fensterklasse. 2. Registriere Fensterklasse wc. 3. Erzeuge Fenster der neuen Klasse, speicher Fensterhandle hwnd. 4. Zeige Fenster hwnd.
Als Erstes bestimmen wir die Fensterklasse für unser Fenster, indem wir die Struktur WNDCLASS wc;
initialisieren und anschließend mit RegisterClass(&wc);
bei Windows registrieren lassen. Das ist eine typische Anwendung von Strukturen und Zeigern. Wir geben nur die Adresse &wc der Struktur wc weiter. Die Initialisierung von wc ist wie das Ausfüllen eines Formulars oder eines Wunschzettels. Bei den folgenden Windowsfunktionen solltest du auch mal einen Blick in die Windows SDK-Hilfe werfen. Dabei findest du sicher interessante Zusatzinformationen, die ich im Rahmen dieses Buches nicht besprechen kann. Nicht nur Funktionen, sondern auch Strukturen wie WNDCLASS werden ausführlich beschrieben. Einige Elemente von wc setzen wir einfach auf 0. Die Variable hInstance wurde uns als Argument an WinMain übergeben, und wir tragen sie unter wc.hInstance ein. Mit LoadIcon bestimmen wir, welches Bildchen ›icon‹) gezeigt werden soll, wenn unser Fenster minimiert wird. Mit ( 306
Kapitel 10 Zeiger und Strukturen
Sandini Bib
LoadCursor wählen wir einen Mauszeiger. Mit GetStockObject(WHITE_BRUSH)
bestimmen wir einen Pinsel, der für die Defaulthintergrundfarbe des Fensters verwendet werden soll. Als Namen unserer Fensterklasse geben wir den String KlasseHallo an. ›style‹) des Fensters wird mit einer ganzen Zahl in wc.style Der Stil ( beschrieben. Hier begegnet uns der Oder-Bitoperator |, der verschiedene Konstanten verknüpft: CS_OWNDC | CS_VREDRAW | CS_HREDRAW
Der Witz ist, dass in jeder Konstante nur ein Bit auf 1 gesetzt ist, und durch die Verknüpfung mit Oder ist das Ergebnis eine Zahl, in der drei Bits gesetzt sind. In anderen Worten, statt viele verschiedene Variablen für verschiedene Wahr/ Falsch-Optionen zu definieren, verwenden wir einzelne Bits in der Variablen wc.style. Mit CS_OWNDC stellen wir sicher, dass jedes Fenster in unserer Fen›own‹) Device Context erhält. Das ist für unsere sterklasse seinen eigenen ( Beispielprogramme praktisch. Mit CS_VREDRAW und CS_HREDRAW teilen wir Win›redraw‹) werden soll, dows mit, dass das Fenster jedes Mal neu gemalt ( wenn sich seine vertikale oder horizontale Größe ändert. Besonders interessant ist wc.lpfnWndProc = WindowProc;
Hier geben wir den Namen unserer Fensterprozedur an. Dazu muss der Prototyp der Funktion WindowProc bekannt sein. Du könntest genauso gut einen anderen Namen für die Fensterprozedur verwenden und diesen dann hier angeben. Wie kann das funktionieren? WindowProc ist die Adresse der Funktion! Das ist ähnlich wie bei Feldern, bei denen der Name des Feldes verwendet werden kann, um die Adresse des Feldes in einen Zeiger zu kopieren. Und genau das machen wir hier für eine Funktion. wc.lpfnWndProc ist ein Zeiger auf eine Funktion! Jetzt weißt du, wie es dazu kommt, dass WindowProc aufgerufen wird, obwohl nirgends in WinMain ein Funktionsaufruf von WindowProc zu sehen ist. Windows weiß, wohin die Fensternachrichten geschickt werden sollen, weil wir in unserer Fensterklasse wc die Funktion WindowProc angegeben haben. Mit dieser Erklärung wollen wir uns zufrieden geben, ohne zu diskutieren, wie man Funktionen als Elemente von Strukturen definiert.
Nach RegisterClass(&wc); können wir mit der Funktion CreateWindow ein Fenster erzeugen. Erzeugen bedeutet, dass wir als Rückgabewert von CreateWindow ein Fensterhandle bekommen. Das ist derselbe Handle, den wir später in der Fensterprozedur wiedersehen. CreateWindow rufen wir mit dem Namen der Fensterklasse im ersten Argument auf, "KlasseHallo". Das zweite Argument ist der Name des Fensters, der z. B. im Titelbalken des Fensters angezeigt wird. Das dritte Argument ist der Stil des Fensters. Mit 10.10
WinMain
307
Sandini Bib
WS_OVERLAPPEDWINDOW bekommen wir, was du als ›normales‹ Fenster in un-
seren Beispielen kennen gelernt hast. Es gibt auch Fenster ohne Titelbalken, Fenster mit Scrolleisten und vieles, vieles mehr. Du wirst staunen, wenn du dir die Hilfe zu CreateWindow anschaust. Windows heißt nicht umsonst Fenster. Mit CW_USEDEFAULT im vierten und fünften Argument von CreateWindow bestimmen wir, bei welchen Koordinaten die linke obere Ecke des Fensters auf dem Bildschirm erscheinen soll. Wir wählen den Default von Windows, das heißt, neue Fenster erhalten automatisch neue Koordinaten, damit sie sich nicht überdecken. Hier könntest du zweimal die 0 angeben, damit das Fenster immer ganz links oben erscheint. Mit den nächsten zwei Argumenten bestimmen wir Breite und Höhe des Fensters. Die restlichen Argumente sind im Moment uninteressant. Damit das Fenster sichtbar wird, rufen wir ShowWindow(hwnd, SW_SHOW)
auf. Und nach einigen Fensternachrichten wie WM_CREATE und WM_SIZE, die du schon kennst, erscheint tatsächlich das endgültige Fenster auf dem Bildschirm. Die Diskussion der Nachrichtenschleife hatten wir schon in Kapitel 8.1 vorweggenommen, um das Konzept der Nachrichtenwarteschlange erklären zu können. Jetzt wird klar, dass MSG msg; eine Struktur definiert, die alle nötigen Informationen zur Nachricht enthält. In while (GetMessage(&msg, 0, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); }
rufen wir GetMessage mit der Adresse von msg auf, um die Struktur mit der nächsten Nachricht zu füllen. TranslateMessage bekommt die Adresse von msg, um etwaige Zeichenübersetzungen vorzunehmen. DispatchMessage weiß aufgrund von msg, welche Fensterprozedur aufgerufen werden soll. Zu guter Letzt, wenn die Nachrichtenschleife wegen WM_QUIT verlassen wurde, verwenden wir das Element namens wParam der letzten Nachricht als Rückgabewert von WinMain, return msg.wParam;
Und wieder hat ein Programm sein Ende gefunden. Du wirst mir Recht geben: bis auf die vielen Auswahlmöglichkeiten bei der Fenstererzeugung und die Verwendung eines Zeigers auf eine Funktion, die den Aufruf von WindowProc vor uns versteckt, ist WinMain schön einfach.
308
Kapitel 10 Zeiger und Strukturen
Sandini Bib
10.11 Alles halb so wild mit den Zeigern und Strukturen. Strukturen sind eine Art von Formular, in der verschiedene Variablen zusammengefasst sind. Zeiger speichern die Adressen von Speicherbereichen und können gut für den Austausch von Feldern und Strukturen zwischen Funktionen verwendet werden. Jetzt, da du Zeiger und Strukturen kennst, darfst du dich als waschechter C-Programmierer fühlen! Wichtige Punkte in diesem Kapitel waren: Der Adressenoperator & liefert die Adresse, unter der der Inhalt einer Variablen im Speicher steht, z. B. ist &i die Adresse, unter der der Inhalt der Variablen i gespeichert ist. Der Inhaltsoperator * liefert die Daten, auf die ein Zeiger zeigt. Wenn z. B. p ein Zeiger auf eine ganze Zahl ist, kann mit *p diese Zahl ausgelesen werden. Wenn mehrere Zahlen vom selben Typ hintereinander im Speicher stehen und p auf den Beginn dieses Speicherblocks zeigt, erhält man mit p[0] den Wert der 0. Zahl, mit p[1] den Wert der 1. Zahl und so weiter. Einen Zeiger zu einem zusammenhängenden Speicherblock kann man mit der Funktion malloc erhalten (›memory allocation‹, Speicherzuweisung). Strukturen definiert man mit struct. Auf die Elemente einer Strukturvariable greift man mit dem Operator . zu. Im Falle eines Zeigers auf eine Strukturvariable kann man den Operator −> verwenden.
10.11
309
Sandini Bib
10.12 1. Schreibe eine Funktion, deren Rückgabewert die Länge einer Zeichenkette ist. Vergleiche die Zeit, die deine Funktion und die Bibliotheksfunktion strlen
für sehr lange Zeichenketten benötigt. 2. Schreibe ein Programm, das ein Feld sortiert. Dazu gibt es schlaue Algorithmen, z. B. den Quicksort-Algorithmus, den die Funktion qsort in stdlib.h verwendet (siehe das Beispiel zu qsort in der BCB-Hilfe). Aber bei kleinen
Feldern kannst du wie folgt vorgehen. Initialisiere ein Feld mit ganzen Zahlen. Suche mit einer Schleife das kleinste Element. Vertausche das kleinste Element mit dem ersten Element. Suche das kleinste Element ab dem zweiten Element. Vertausche dieses Element mit dem zweiten, und so weiter. Weil immer das kleinste Element des restlichen Feldes an den Anfang geschrieben wird, ist am Ende das Feld sortiert. Verpacke die Sortierbefehle in einer Funktion, die einen Zeiger auf ein Feld übergeben bekommt. 3. Kannst du dein Sortierprogramm für Zeichenketten umschreiben? Dazu benötigst du die Funktion strcmp zum Vergleichen von Strings. Du könntest
die Liste der Wochentage sortieren. Natürlich kannst du zum Vertauschen von Elementen die Zeichenketten kopieren, dazu muss aber jeder String ausreichend Platz für den längsten bieten, sagen wir char *wochentag[100] = { ... };. Du kannst auch ein Feld int index[N]; mit Indizes für die Liste der Strings anlegen, das du in einer Schleife mit index[i] = i initialisierst. Statt die Zeichenketten zu sortieren, sortierst du die Indizes. 4. Schreibe ein Programm, das die Anzahl der Zeichen, der Wörter und der Zei-
len in einer Datei zählt. Eine etwas andere Version deines Programms könnte zählen, wie häufig einzelne Zeichen vorkommen. Dazu kannst du wie bei dem Balkendiagramm für das Würfeln in Kapitel 6.16 ein Feld einführen, in dem du für alle 256 möglichen Zeichen eine Strichliste führst. 5. Verwende die Funkion DrawText, um einen längeren Text in einem Rechteck
im Fenster auszugeben (siehe Hilfe zum Windows SDK). Das Rechteck wird als Zeiger auf eine Struktur vom Typ RECT übergeben. 6. Nach unserer Diskussion von WinMain in Kapitel 10.10 kannst du jetzt
mit der Erzeugung von Windowsanwendungen in BCB experimentieren. Starte ein neues Projekt im BCB-Menü mit ›Datei‹, ›Neue Anwendung‹. Entferne Unit1.cpp und Project1.res aus dem neuen Projekt. Öffne Project1.cpp im Editor. Ersetze die Funktion WinMain durch die aus Kapitel 10.10, und füge die Funktionen WindowProc und malen aus Kapitel 8.0 zur Datei Project1.cpp hinzu. Jetzt kannst du abspeichern und kompilieren. Vergleiche mit den Windowsprojekten auf der Buch-CD.
310
Kapitel 10 Zeiger und Strukturen
Sandini Bib
11 Bitmaps 11.0 11.1 11.2 11.3
Bitmaps erzeugen und laden BitBlt Bitmapfilmchen Tonausgabe mit PlaySound
312 316 317 324
11.4
Bitmappuffer
326
11.5
Gekachelte Landkarte
334
11.6
Mit Pfeiltasten über Land
340
11.7
Bitmaps mit transparenter Hintergrundfarbe
344
11.8
Ein Held auf weiter Flur
347
11.9
348
11.10
349
Sandini Bib
Als Bitmaps bezeichnet man im Allgemeinen Bilder, die sich aus einzelnen Pi›map‹ heißt Landkarte). Bitmaps sind überall. Alles, xeln zusammensetzen ( was auf deinem Computerbildschirm dargestellt wird, setzt sich aus Pixeln zusammen. Obwohl am Ende alles als Pixel auf deinem Bildschirm erscheint, wird z. B. Text und Liniengrafik nicht als Pixel gespeichert, sondern wird erst für die Ausgabe in Pixel umgewandelt. Bitmaps aber sind so abgespeichert, dass sie im Wesentlichen Pixel für Pixel aus einer Datei oder dem Arbeitsspeicher in den Bildspeicher deiner Grafikkarte kopiert werden können. Im engeren Sinne ist mit Bitmap ein bestimmtes Datenformat für rechteckige Bilder unter Windows gemeint. Bitmaps werden in Dateien mit der Endung .bmp gespeichert. Andere Datenformate für Pixelbilder sind z. B. .gif oder .jpg, aber diese werden wir nicht verwenden. Computerspiele sind ohne Bitmaps nicht denkbar. Es ist erstaunlich, wie viele Spiele mit der einfachsten Verwendung von Bitmaps auskommen. Dabei denke ich an 2D-Spiele. Alles, was diese Programme machen, ist, kleine bunte Bildchen über den Bildschirm zu bewegen und zu animieren. Eine kleine Figur, die läuft, setzt sich z. B. aus 5 einzelnen Bitmaps zusammen. Wie wir schon in Kapitel 8 ausprobiert haben, kommen Animationen durch die schnelle Abfolge einzelner Bilder zustande, und das funktioniert für Bitmaps genauso gut. In all diesen Plattformspielen, in denen du mit einer Figur durch die Gegend hüpfst, ist deine Figur eine animierte Bitmap, die Gegner sind animierte Bitmaps, der Hintergrund ist eine Bitmap, die Schätze sind Bitmaps. Das Gleiche gilt z. B. für Abenteuer- oder Strategiespiele, bei denen du von oben auf eine flache Landkarte schaust. Die Bäume, Bitmaps. Der Ritter, eine Bitmap. Die Burg, eine Bitmap. In diesem Kapitel wollen wir einige der grundlegenden Techniken für Bitmapgrafik ausprobieren.
11.0
Bitmaps erzeugen und laden
Woher bekommst du Bitmaps? Die meisten Malprogramme erlauben es dir, dein Bild als Bitmap abzuspeichern. Falls du einen Scanner hast, kannst du auf Papier zeichnen, das Bild einscannen, das Ergebnis in einem Malprogramm nacharbeiten und dann als Bitmap abspeichern. Es gibt da sehr viele schöne Möglichkeiten. In diesem Kapitel werde ich mich auf Bitmaps beschränken, die ich mit dem Malprogramm im Windowszubehör gemacht habe. Bei mir steht das unter ›Start‹, ›Programme‹, ›Zubehör‹, ›Paint‹. Das Programm Paint ist recht primitiv, bietet aber eine gute Zoomfunktion (siehe ›Ansicht‹, ›Zoomfaktor‹, ›Benutzerdefiniert‹), mit der du das Bild so vergrößern kannst, dass du einzelne Pixel bearbeiten kannst. Bei benutzerdefiniertem Zoomfaktor wähle unter ›Ansicht‹ die Option ›Miniaturansicht einblenden‹, um ein Fensterchen mit der Bitmap in Originalgröße zu erhalten. Ich habe so die Bitmapdatei held_0.bmp erzeugt, die du auch auf der Buch-CD findest. Diese kannst du mit Paint öffnen. Der Einfachheit halber sollte dein 312
Kapitel 11 Bitmaps
Sandini Bib
Bildschirm auf mindestens 16 Bit Farbtiefe eingestellt sein, siehe Kapitel 9.10. Sonst sehen die Farben bei dir anders aus, weil ich 16-Bit-Farben verwendet habe. Wie bekommst du diese Bitmap in das Fenster eines selbst gemachten Programms? Hier ist ein Beispiel, das die WinMain-Funktion aus Kapitel 8 und 10.10 benutzt: Bitmap0.c WinMain.cpp
#include <windows.h> #include <stdio.h> /* externe Variable */ HDC hdc; char text[1000]; char *dateiname = "held_0.bmp"; /* male Bitmap */ void malebitmap(HDC hdc, int x, int y) { HDC hdcmem; HBITMAP hbitmap; BITMAP bitmap; hbitmap = LoadImage(0, dateiname, IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE); if (!hbitmap) { sprintf(text, "Fehler beim Laden von %s", dateiname); TextOut(hdc, 10, 30, text, strlen(text)); return; } GetObject(hbitmap, sizeof(BITMAP), &bitmap); hdcmem = CreateCompatibleDC(hdc); SelectObject(hdcmem, hbitmap); BitBlt(hdc, x, y, bitmap.bmWidth, bitmap.bmHeight, hdcmem, 0, 0, SRCCOPY); DeleteDC(hdcmem); }
11.0
Bitmaps erzeugen und laden
313
Sandini Bib Bitmap0.c WinMain.cpp
/* Malen */ void malen(HDC hdc) { malebitmap(hdc, 10, 10); malebitmap(hdc, 20, 40); malebitmap(hdc, 40, 70); malebitmap(hdc, 80, 100); malebitmap(hdc, 160, 130); } /* Rueckruffunktion unseres Fensters */ LRESULT CALLBACK WindowProc(HWND hwnd, UINT m, WPARAM wParam, LPARAM lParam) { // Fenster auf if (m == WM_CREATE) { SetWindowText(hwnd, "Bitmap"); hdc = GetDC(hwnd); } // Malen else if (m == WM_PAINT) { malen(hdc); ValidateRect(hwnd, 0); } // Verschiedenes else if (m == WM_DESTROY) PostQuitMessage(0); else return DefWindowProc(hwnd, m, wParam, lParam); return 0; }
In WindowProc geschieht nichts Besonderes. Bei WM_PAINT rufen wir die Funktion malen auf. In malen rufen wir die selbst gemachte Funktion malebitmap mit den Koordinaten auf, bei denen die Bitmap ausgegeben werden soll. Das Ergebnis sieht so aus:
314
Kapitel 11 Bitmaps
Sandini Bib
Toll gezeichnet, vielleicht hätte ich doch Künstler werden sollen? Die eigentliche Arbeit wird in malebitmap erledigt. Die Bitmap laden wir aus der Datei mit hbitmap = LoadImage(0, dateiname, IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE);
›Image‹ heißt Bild, und wir rufen die Funktion LoadImage mit den richtigen Argumenten auf, um eine Bitmap aus einer Datei zu laden, deren Namen als Zeichenkette in dateiname steht. Der Rückgabewert ist ein Handle für eine Bitmap, den wir in der Variable HBITMAP hbitmap; speichern. Nun könnte es sein, dass aus irgendwelchen Gründen der Ladevorgang nicht erfolgreich war. Vielleicht hast du dich beim Namen vertippt. In diesem Fall ist hbitmap gleich 0, und wir geben eine Fehlermeldung aus und beenden die Funktion malebitmap mit return;. Im weiteren Verlauf benötigen wir die Abmessungen der Bitmap, die wir wie folgt erhalten. In GetObject(hbitmap, sizeof(BITMAP), &bitmap);
verwenden wir den Handle der Bitmap, hbitmap, um eine Struktur mit Information über die Bitmap zu füllen. Die Strukturvariable definieren wir in BITMAP bitmap;. GetObject benötigt den Handle, die Größe der Struktur in Bytes (sizeof(BITMAP)) und die Adresse der Strukturvariablen (&bitmap). Weiter unten in der Funktion malebitmap siehst du, dass wir die zwei Elemente bitmap.bmWidth und bitmap.bmHeight der Struktur benötigen.
11.0
Bitmaps erzeugen und laden
315
Sandini Bib
11.1 BitBlt Wir fahren mit der Diskussion unseres ersten Bitmapbeispiels fort. Falls die Bitmap erfolgreich geladen wurde, wollen wir sie in das Fenster malen. Das geschieht in BitBlt(hdc, x, y, bitmap.bmWidth, bitmap.bmHeight, hdcmem, 0, 0, SRCCOPY);
Dieser Aufruf der Funktion BitBlt (›bit block transfer‹) kopiert Bitmaps. Der Inhalt unseres Fensters ist nichts weiter als eine große, rechteckige Bitmap, und wir möchten mit der Bitmap, die wir geladen haben, einen Teil der Fensterbitmap übermalen. ›source copy‹, Die Arbeitsweise von BitBlt haben wir mit SRCCOPY ( Quelle kopieren) ausgewählt. In der Windows SDK-Hilfe findest du auch andere Operationen, mit denen du Bitmaps z. B. überlagern kannst. BitBlt spricht man übrigens ›bit blit‹. Das erste Argument von BitBlt ist hdc, also der Handle zum Device Context, in den wir malen möchten. Jetzt könnte man erwarten, dass eine Bitmap als Argument auftaucht, aber stattdessen wird die Bitmap indirekt durch einen zweiten Handle zu einem Device Context namens hdcmem übergeben. Der Grund ist, dass eine Bitmap genau wie ein Stift oder ein Pinsel erst in einem Device Context aktiviert werden muss, bevor sie verwendet werden kann. hdc können wir nicht verwenden, denn hdc ist für den Fensterinhalt reserviert. Deshalb erzeugen wir ›compatible‹) zum HDC, in den wir einen neuen Device Context passend ( malen möchten: hdcmem = CreateCompatibleDC(hdc);
Weil dieser Device Context sich nicht auf das Fenster bezieht, sondern im Spei›memory‹) steht, nennen wir ihn hdcmem. Im nächsten Schritt vercher ( fahren wir wie bei Stiften und Pinseln und aktivieren mit SelectObject(hdcmem, hbitmap);
die Bitmap im Speicher Device Context. Jetzt kann BitBlt die Speicherbitmap in hdcmem in die Fensterbitmap in hdc kopieren. Dazu übergeben wir sechs weitere Parameter. BitBlt kopiert nicht einfach die ganze Bitmap, sondern einen rechteckigen Ausschnitt, den wir allerdings in unserem Beispiel gleich der ganzen Bitmap setzen. Dazu geben wir die Breite und Höhe des Rechtecks als bitmap.bmWidth und bitmap.bmHeight an. Die Koordinaten der linken oberen Ecke der Bitmap im Fenster bestimmen wir mit x und y. Falls du nur einen Teil der Bitmap kopieren möchtest, kannst du die Breite und Höhe des Rechtecks kleiner als die Ausmaße der Bitmap machen und statt der zwei Nullen die Koordinaten angeben, bei denen die linke obere Ecke des Ausschnitts in der Bitmap liegen soll. Ausprobieren. 316
Kapitel 11 Bitmaps
Sandini Bib
Nach getaner Arbeit löschen wir den Speicher-Device-Context mit DeleteDC(hdcmem);
So weit, so gut. Bei dem Schnappschuss in Kapitel 11.0 mit den fünf Kopien der kleinen Figur ist dir sicher aufgefallen, dass sich die Bildchen gegenseitig überdecken. Das ist genau das, was wir zu erwarten haben, wenn wir mit einem Rechteck aus Pixeln pixelweise die Pixel in der Fensterbitmap überschreiben. Weil ich beim Zeichnen der Bitmap einen grauen Hintergrund gewählt habe, steht jede Figur in ihrem eigenen grauen Kästchen, denn die Pixel des Hintergrunds werden genauso kopiert wie die Pixel der Figur im Vordergrund. BitBlt kopiert einfach alle Pixel des Rechtecks und weiß nichts davon, dass der Künstler manche Pixel als Hintergrund und manche als Vordergrund betrachtet. Ein durchsichtiger Hintergrund ist möglich, aber mit dem Windows SDK etwas mühsam, siehe Kapitel 11.7. Als Nächstes wollen wir erst mal den Bitmaps das Laufen beibringen.
11.2 Bitmapfilmchen Animationen und der Eindruck von Bewegung entstehen durch die schnelle Abfolge von Bildern. In Kapitel 8 haben wir so die Bewegung eines Balles simuliert. Mit Bitmaps geht das genauso einfach. In diesem Kapitel wollen wir ein Minifilmchen aus drei Einzelbildern (monster_0.bmp, monster_1.bmp, monster_2.bmp) zum Laufen bringen. Hier sind drei Schnappschüsse, die unser Programm in Aktion zeigen:
Damit das Filmchen noch etwas lustiger wird, habe ich statt der Funktion BitBlt die Funktion StretchBlt verwendet ( ›stretch‹ heißt dehnen). Diese funktioniert genau wie BitBlt, außer dass die Bitmap nicht eins zu eins kopiert wird, sondern auch gedehnt oder gestaucht werden kann:
11.2
Bitmapfilmchen
317
Sandini Bib
Und wenn es schon um Filmchen geht, will ich dir auch gleich noch die einfachste Möglichkeit der Tonausgabe zeigen. Gehen wir das Programm also Schritt für Schritt durch. Wir verwenden wieder unsere Standard WinMain-Funktion. In Bitmap1.c WinMain.cpp
#include <windows.h> #include <stdio.h> /* externe Variable */ HDC hdc, hdcmem; char text[1000]; int xmax, ymax; int stretch = 0; double zoomfaktor0 = 0.1; double zoomfaktor; int ntimer = 0; int dt = 140; #define NBITMAPS 100 HBITMAP hbitmap[NBITMAPS]; int nbitmaps = 0; char bitmapname[] = "monster";
siehst du verschiedene externe Variablen. Insbesondere gibt es eine Variable stretch für Dehnen an/aus. In zoomfaktor speichern wir den momentanen Vergrößerungsfaktor für das Stretching, dessen kleinster Wert zoomfaktor0 = 0.1 ist, also ein Zehntel der normalen Größe. In ntimer zählen wir die Ticks eines Zeitgebers, dessen Periode dt die Geschwindigkeit bestimmt, in der unser Filmchen abläuft. Weil wir mehrere Bitmaps für die Animation verwenden, speichern wir diese in einem Feld HBITMAP hbitmap[NBITMAPS]; ab. Falls du die unglaubliche Geduld aufbringst, mehr als 100 Bitmaps zu zeichnen, musst du den Wert von NBITMAPS erhöhen. Wie wir mehrere Bitmaps laden, siehst du in
318
Kapitel 11 Bitmaps
Sandini Bib Bitmap1.c WinMain.cpp
/* lade mehrere Bitmaps: name_0.bmp, name_1.bmp, ... */ void ladebitmaps(char *name) { int i; for (i = 0; i < NBITMAPS; i++) { sprintf(text, "%s_%d.bmp", name, i); hbitmap[i] = LoadImage(0, text, IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE); if (!hbitmap[i]) break; } nbitmaps = i; }
Die Funktion ladebitmaps rufen wir in unserem Beispiel mit der Zeichenkette "monster" auf. In der for-Schleife erzeugen wir im String text nacheinander die Dateinamen monster_0.bmp, monster_1.bmp und so weiter. Wie schon besprochen verwenden wir LoadImage, um die Bitmaps zu laden. Den Handle für die i-te Bitmap speichern wir in hbitmap[i]. Wir beenden die Schleife mit break, falls keine Bitmap geladen werden konnte (und deshalb hbitmap[i] gleich 0 ist), oder falls i zu groß für unser Feld mit NBITMAPS Elementen geworden ist. Natürlich ist es nach HBITMAP hbitmap[NBITMAPS]; offensichtlich, dass höchstens NBITMAPS Elemente erlaubt sind, aber wer garantiert uns, dass nicht mehr Bitmapdateien vorhanden sind? Wir müssen unbedingt verhindern, dass zu viele Elemente in ein Feld geschrieben werden. In so einem Fall ist Vertrauen viel zu wenig und Kontrolle ein Muss. Also lassen wir i von 0 bis NBITMAPS−1 laufen. Den letzten Wert von i merken wir uns in nbitmaps, denn das ist die Anzahl der Bitmaps, die wir laden konnten. Falls überhaupt keine Bitmapdateien für den angegebenen Namen gefunden werden konnten, ist nbitmaps gleich 0. Je nach Lust und Laune kannst du also eine unterschiedliche Anzahl von Bitmaps für das Filmchen zeichnen. ladebitmaps lädt so viele Bitmaps, wie unter der fortlaufenden Nummer 0, 1, . . ., NBITMAPS−1 gefunden werden können.
11.2
Bitmapfilmchen
319
Sandini Bib
In der nächsten Funktion malen wir eine Bitmap mit StretchBlt: Bitmap1.c WinMain.cpp
/* male Bitmap */ void malebitmap(HBITMAP hbitmap, int x, int y, double faktor) { BITMAP bitmap; int dx, dy; SelectObject(hdcmem, hbitmap); GetObject(hbitmap, sizeof(BITMAP), &bitmap); dx = bitmap.bmWidth * faktor; dy = bitmap.bmHeight * faktor; StretchBlt(hdc, x−dx/2, y−dy/2, dx, dy, hdcmem, 0, 0, bitmap.bmWidth, bitmap.bmHeight, SRCCOPY); if (dx > 3*xmax || dy > 3*ymax) { zoomfaktor = zoomfaktor0; Rectangle(hdc, −1, −1, xmax+1, ymax+1); } }
Die Bitmap übergeben wir als Handle in hbitmap, zudem übergeben wir die Zielkoordinaten und einen Vergrößerungsfaktor in faktor. Beachte, dass in diesem Beispiel hdc und hdcmem externe Variablen sind, die vor dem Aufruf von malebitmap initialisiert werden müssen. Die Größe der Bitmap entnehmen wir der BITMAP-Struktur, die wir mit GetObject erhalten. Im Fenster soll die Bitmap dx Pixel breit und dy Pixel hoch sein, wobei wir Breite und Höhe in bitmap mit dem Faktor faktor multiplizieren. In StretchBlt(hdc, x−dx/2, y−dy/2, dx, dy, hdcmem, 0, 0, bitmap.bmWidth, bitmap.bmHeight, SRCCOPY);
siehst du, wie StretchBlt verwendet wird. Genau wie bei BitBlt kopieren wir von hdcmem nach hdc, aber diesmal geben wir Breite und Höhe für zwei Rechtecke an. Der Ausschnitt in hdcmem hat seine linke obere Ecke bei Koordinaten 0 und 0, und Breite und Höhe sind die der geladenen Bitmap. Das heißt, wir verwenden die ganze Bitmap. Der Ausschnitt in hdc erhält die skalierte Breite dx und Höhe dy. Weil im Beispiel x und y die Mitte der Bitmap im Fenster angeben sollen, aber StretchBlt genau wie BitBlt die linke obere Ecke erwartet, berechnen wir die Koordinaten der linken oberen Ecke als x−dx/2 und y−dy/2. Das Endergebnis ist, dass eine um den Faktor faktor vergrößerte bzw. verkleinerte Kopie der Bitmap hbitmap in das Fenster gemalt wird, wobei x und y die Mitte der gezeichneten Bitmap im Fenster angeben. 320
Kapitel 11 Bitmaps
Sandini Bib
Die Zeilen if (dx > 3*xmax || dy > 3*ymax) { zoomfaktor = zoomfaktor0; Rectangle(hdc, −1, −1, xmax+1, ymax+1); }
haben nichts mit der allgemeinen Verwendung von StretchBlt zu tun, sondern sollen in unserem Beispiel verhindern, dass die skalierte Bitmap zu groß wird. Falls wir die Fenstergröße xmax oder ymax um mehr als einen Faktor 3 überschreiten, setzen wir zoomfaktor auf seinen Minimalwert zoomfaktor0 zurück und löschen das Fenster mit einem Aufruf von Rectangle. Es folgt die Funktion malen: Bitmap1.c WinMain.cpp
/* Malen */ void malen(HDC hdc) { int n; double faktor; if (nbitmaps == 0) { sprintf(text, "Fehler: keine Bitmap %s_0.bmp", bitmapname); TextOut(hdc, 10, 10, text, strlen(text)); return; } n = ntimer % nbitmaps; faktor = stretch ? zoomfaktor : 1.0; malebitmap(hbitmap[n], xmax/2, ymax/2, faktor); }
Falls keine Bitmaps geladen werden konnten, ist nbitmaps gleich 0. In diesem Fall geben wir eine Fehlermeldung aus und verlassen die Funktion malen mit return;. Mit malebitmap(hbitmap[n], xmax/2, ymax/2, faktor);
wollen wir die n-te Bitmap des Filmchens in die Mitte des Fensters (Koordinaten xmax/2 und ymax/2) mit dem Vergrößerungsfaktor faktor malen. Die externe Variable ntimer zählt die Ticks des Zeitgebers, den wir für die Animation verwenden. Der Ausdruck ntimer % nbitmaps durchläuft also bei nbitmaps gleich 3 die Werte 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2 und so weiter. Den Vergrößerungsfaktor bestimmen wir in Abhängigkeit vom Schalter stretch. Falls stretch wahr ist, setzen wir faktor gleich der externen Variable zoomfaktor. Falls stretch falsch ist, setzen wir faktor gleich 1.0, das heißt, wir könnten genauso gut BitBlt verwenden.
11.2
Bitmapfilmchen
321
Sandini Bib
Die nächste Funktion ist eine kleine Helferfunktion, in der wir die Befehle zur Initialisierung des Filmchens gesammelt haben: Bitmap1.c WinMain.cpp
/* Initialisierung */ void init(HWND hwnd) { if (stretch) PlaySound("mami.wav", 0, SND_FILENAME | SND_LOOP | SND_ASYNC); else PlaySound("hilfe.wav", 0, SND_FILENAME | SND_LOOP | SND_ASYNC); zoomfaktor = zoomfaktor0; Rectangle(hdc, −1, −1, xmax+1, ymax+1); InvalidateRect(hwnd, 0, 0); }
Den Befehl PlaySound wollen wir in Unterkapitel 11.3 etwas näher besprechen, damit du die Beschreibung leichter wiederfindest. Aber offensichtlich geht es darum, je nachdem, ob gestretcht wird oder nicht, eine unterschiedliche Tonausgabe zu initialisieren. Des Weiteren initialisieren wir den Zoomfaktor, löschen das Fenster und veranlassen mit InvalidateRect, dass das Fenster neu gemalt wird. Die letzte Funktion des Beispiels ist die Fensterprozedur:
322
Kapitel 11 Bitmaps
Sandini Bib Bitmap1.c WinMain.cpp
/* Rueckruffunktion unseres Fensters */ LRESULT CALLBACK WindowProc(HWND hwnd, UINT m, WPARAM wParam, LPARAM lParam) { // Fenster auf if (m == WM_CREATE) { SetTimer(hwnd, 0, dt, 0); SetWindowText(hwnd, "Bitmap (mit Sound, klick mich)"); hdc = GetDC(hwnd); hdcmem = CreateCompatibleDC(hdc); ladebitmaps(bitmapname); if (nbitmaps > 0) { SelectObject(hdcmem, hbitmap[0]); SelectObject(hdc, CreateSolidBrush(GetPixel(hdcmem, 0, 0))); } } // Malen else if (m == WM_PAINT) { malen(hdc); ValidateRect(hwnd, 0); } // Timer else if (m == WM_TIMER) { ntimer++; if (stretch) zoomfaktor *= 1.15; InvalidateRect(hwnd, 0, 0); } // Tastatur oder Maus else if (m == WM_KEYDOWN || m == WM_LBUTTONDOWN || m == WM_RBUTTONDOWN) { stretch = !stretch; init(hwnd); } // Groesse else if (m == WM_SIZE) { xmax = LOWORD(lParam); ymax = HIWORD(lParam); init(hwnd); } // Aufraeumen else if (m == WM_DESTROY) { KillTimer(hdc, 0); PlaySound(0, 0, 0); PostQuitMessage(0); } else return DefWindowProc(hwnd, m, wParam, lParam); return 0; }
11.2
Bitmapfilmchen
323
Sandini Bib
Beim Erzeugen des Fensters, WM_CREATE, starten wir den Zeitgeber für die Animation, ändern den Titel des Fensters und speichern den Fenster-DC und den Speicher-DC in den externen Variablen hdc und hdcmem. Dann rufen wir ladebitmaps auf, um alle verfügbaren Bitmaps mit Namen bitmapname zu laden. bitmapname haben wir zu Beginn des Programmtextes als externe Variable definiert, damit der Name leichter zu finden ist, falls wir ein neues Filmchen ausprobieren wollen. Falls mindestens eine Bitmap geladen werden konnte (nbitmaps > 0), erzeugen wir mit SelectObject(hdcmem, hbitmap[0]); SelectObject(hdc, CreateSolidBrush(GetPixel(hdcmem, 0, 0)));
den Pinsel für den Fensterhintergrund, den wir wiederholt mit Rectangle malen werden. Als Farbe des Pinsels wollen wir die Farbe des Pixels in der linken oberen Ecke der nullten Bitmap verwenden. Also aktivieren wir hbitmap[0] in dem Speicher DC hdcmem, so dass wir dann mit GetPixel(hdcmem, 0, 0) die Farbe dieses Pixels aus der Bitmap hbitmap[0] auslesen können. Ansonsten ist WindowProc recht einfach zu verstehen. Bei WM_PAINT rufen wir wie gewohnt die Funktion malen auf. Bei einem Tick des Zeitgebers, WM_TIMER, zählen wir ntimer eins weiter, vergrößern den Zoomfaktor um den Faktor 1.15 und veranlassen mit InvalidateRect die nächste WM_PAINT-Nachricht. Das bedeutet, dass wir ein und denselben Zeitgeber verwenden, um die Figur ihre Bildchen und verschiedene Vergrößerungsstufen durchlaufen zu lassen. Auf Tastendruck oder Mausklick links oder rechts betätigen wir den Schalter stretch und starten die Grafikausgabe mit init(hwnd); neu. Wenn sich die Bildschirmgröße ändert, wird bei WM_SIZE neu gestartet. Vor Beendigung des Programms stoppen wir bei WM_DESTROY den Zeitgeber und beenden die Tonausgabe. Probiere verschiedene kleine Änderungen aus. Z.B. kannst du die Periode des Zeitgebers ändern und mit dem Zoomfaktor spielen. Kann dein Computer bei großem Zoomfaktor noch mit dem Zeitgeber Schritt halten? Meiner nicht. Ich sehe deutlich, wie die Animation langsamer als der Zeittakt abläuft. Für ernsthafte Programme oder Spiele ist das natürlich ein Problem. Die eigentliche Herausforderung ist natürlich, neue Filmchen zu machen. Dabei wirst du feststellen, dass das ziemlich mühsam ist, aber trotzdem Spaß macht. Zur Abwechslung kannst du statt "monster" als Bitmapname "held" angeben.
11.3 Tonausgabe mit PlaySound Tonausgabe hat an und für sich nichts mit Bitmaps zu tun. Aber weil Tonausgabe bei Filmchen oder Spielen mit Bitmaps zusammen auftritt, will ich die Funktion 324
Kapitel 11 Bitmaps
Sandini Bib
PlaySound (spiele Geräusch oder Klang) kurz beschreiben. Sound kann Sprache, Musik oder einfach nur Krach bedeuten. Schöne Soundeffekte findest du in Spielen. Die Funktion PlaySound wird in der Microsoft-Hilfe unter ›Multimedia Reference‹ beschrieben.
Sprache, Musik usw. kann im so genannten Wave-Format in digitaler Form in einer Datei gespeichert werden (Endung .wav). ›Wave‹ heißt Welle. Es versteht sich von selbst, dass du nur dann etwas mit solchen Dateien anfangen kannst, wenn dein Computer über eine Soundkarte oder einen Soundchip verfügt. Wenn dem so ist, kannst du normalerweise über ein Mikrofon mit mitgelieferter Software dich selbst oder irgendwelche Geräusche aufnehmen. Im letzten Beispiel hörst du – Vorhang auf – mich. Allerdings habe ich meine Stimme mit einem Klangbearbeitungsprogramm verfremdet. Wave-Dateien mit Soundeffekten und verschiedene Soundsoftware findest du auch im Internet. Es gibt also wie bei Bitmaps bestimmte Dateiformate (.bmp, .wav) und es geht darum, den Inhalt dieser Dateien zu laden und auszugeben. Einen typischen Funktionsaufruf von PlaySound hast du im letzten Beispiel gesehen: PlaySound("hilfe.wav", 0, SND_FILENAME | SND_LOOP | SND_ASYNC);
Das erste Argument ist die Adresse einer Zeichenkette, das zweite ist für uns uninteressant und muss Null sein und das dritte Argument wählt verschiedene Optionen aus. Die Optionen werden durch Konstanten angegeben, die durch den Bitoperator | kombiniert werden. Mit SND_FILENAME legen wir fest, dass das erste Argument den Namen der Datei angibt, die geladen werden soll. Die Wave-Datei wird nicht nur geladen, sondern es wird auch auf der Stelle mit der Wiedergabe des Sounds begonnen, falls kein Fehler auftritt. Bei SND_ASYNC kehrt die Funktion PlaySound sofort nach Beginn des Sounds zurück und die Wiedergabe läuft im Hintergrund ab (deshalb die Bezeichnung asynchron). Dein Programm kann gleichzeitig neue Befehle bearbeiten. Bei SND_SYNC (synchron) kehrt PlaySound erst nach Beendigung der Wiedergabe zurück und dein Programm muss warten. Wegen SND_LOOP wird der Sound wiederholt, bis er gezielt abgeschaltet wird. Im Beispiel tun wir dies mit PlaySound(0, 0, 0);
Ein Sound, der gerade wiedergegeben wird, wird auch dann gestoppt, wenn PlaySound erneut mit einer gültigen Wave-Datei aufgerufen wird. Falls du nicht möchtest, dass ein Sound von einem neuen Sound unterbrochen und ›abgewürgt‹ wird, kannst du SND_NOSTOP angeben. PlaySound hat eine wesentliche Einschränkung. Es kann immer nur eine WaveDatei wiedergegeben werden, d.h. Wave-Dateien können mit PlaySound nicht
gemischt werden. 11.3 Tonausgabe mit PlaySound
325
Sandini Bib
11.4
Bitmappuffer
Angenommen, du willst ein Programm schreiben, das viele Bitmaps durch die Gegend schiebt. Aus zweierlei Gründen ist es nützlich, nicht direkt in die Fensterbitmap zu malen, sondern eine extra Bitmap derselben Größe als Zwischenspeicher oder Puffer zu verwenden. Erst malen wir den Hintergrund in den Puffer, dann malen wir verschiedene kleinere Bitmaps für Objekte und Spielfiguren darüber, die sich gegenseitig teilweise verdecken können. Nachdem wir die Szene zusammengestellt haben, können wir mit einem einzigen BitBlt-Befehl das ganze Fenster auf einmal übermalen. Ein offensichtlicher Vorteil ist, dass ein einziges schnelles BitBlt weniger Flackern erzeugt, als wenn ein Objekt erst mit dem Hintergrund gelöscht wird, sagen wir mit Schwarz, und dann in Rot wieder übermalt wird. Diese Erfahrung haben wir schon ohne Bitmaps in Kapitel 8.7 mit dem bewegten Ball gemacht. Besser geht es nur, wenn du statt der Funktionen des Windows SDK Zusatzsoftware wie DirectX oder OpenGL verwendest. DirectX oder OpenGL können wir hier nicht besprechen, aber zwecks Motivation will ich ein paar Worte dazu sagen. Die Pixel, die du auf dem Bildschirm siehst, stehen als Bitmap im Videospeicher deiner Grafikhardware. Die Bildschirmanzeige wird Pixel für Pixel aufgebaut, zeilenweise von links oben nach rechts unten. Das geht sehr schnell, normalerweise mit mindestens 60 Hertz (60-mal pro Sekunde). Wenn ein Programm schnell genug wäre, den nächsten Bildschirminhalt in den Grafikspeicher zu kopieren, während die Grafikhardware nicht mit der Bildausgabe beschäftig ist, würde man überhaupt kein Flackern sehen. Die gebräuchlichste Methode ist, zwei Bitmappuffer im Grafikspeicher anzulegen. Der Videopuffer wird 60mal pro Sekunde gelesen, wenn die Grafikhardware das nächste Bild braucht. Der Zwischenpuffer wird vom Programm mit den Pixeln für das nächste Bild vollgeschrieben. Wenn dann das Signal kommt, dass die nächste Seite aus dem Grafikspeicher auf den Bildschirm gemalt werden soll, kann man die beiden Puffer austauschen, ohne irgendwelche Pixel kopieren zu müssen. Man vertauscht sozusagen die Zeiger in den Grafikspeicher, die angeben, welcher Puffer wie verwendet werden soll. Das Ergebnis sind perfekte Bildübergänge. Wenn ein Programm nur fünf und nicht 60 Bilder pro Sekunde berechnen kann, wartet es ›Page Flip‹ (Seitenwecheinfach auf das nächste Bildwechselsignal für den sel), um wenigstens gleichmäßige Bildübergänge zu erzeugen. Also gut. Dieses Buch behandelt nicht DirectX oder OpenGL, aber wenn du ernsthaft Grafik programmieren möchtest, stehst du an dieser Stelle sozusagen in den Startlöchern. Du solltest jetzt genügend Programmiererfahrung haben, um in DirectX oder OpenGL einzusteigen. Wir wollen aber trotzdem ein Experiment mit dem Windows SDK und mit Bitmappuffern veranstalten, um herauszubekommen, wie schnell BitBlt aus dem Windows SDK überhaupt ist. Auch in DirectX wird BitBlt für viele Aufgaben verwendet. 326
Kapitel 11 Bitmaps
Sandini Bib
Hier ist der Plan. Wir wollen ein Programm besprechen, das den ganzen Bildschirm mit kleinen Bitmaps vollschreibt. Dabei soll wahlweise direkt in den Videopuffer gemalt werden oder erst in einen Bitmappuffer, der dann mit einem einzigen BitBlt ausgegeben wird. Mit einem Zeitgeber wollen wir zählen, wie ›frames per second‹, ›fps‹) bei verschiedenen Fenviele Bilder pro Sekunde ( stergrößen erzeugt werden können. Unser Beispiel beginnt mit Bitmap2.c WinMain.cpp
#include <windows.h> #include <stdio.h> /* externe Variable */ HDC hdc, hdcfenster; HDC hdcpuffer = 0; HBITMAP hbmpuffer = 0; char text[1000]; int xmax, ymax; int fehler = 0; int nmalen = 0; int ntimer = 0; int npixel = 0; HBITMAP hbmheld[3]; char heldname[] = "held"; /* Breite und Hoehe der Bitmaps */ int bmw = 32; int bmh = 32; /* lade eine Bitmap */ HBITMAP ladeeinebitmap(char *dateiname) { HBITMAP hbm; hbm = LoadImage(0, dateiname, IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE); if (!hbm) fehler = 1; return hbm; } /* lade alle Bitmaps */ void ladebitmaps(void) { int i; for (i = 0; i < 2; i++) { sprintf(text, "%s_%d.bmp", heldname, i); hbmheld[i] = ladeeinebitmap(text); } hbmheld[2] = ladeeinebitmap("schwarz.bmp"); }
11.4
Bitmappuffer
327
Sandini Bib
Hier siehst du gleich am Anfang mehrere Device Contexts, die wir für unsere verschiedenen Bitmappuffer brauchen. Das Laden der Bitmaps haben wir in zwei Funktionen aufgeteilt. Die Funktion ladeeinebitmap lädt eine Bitmap (was für ein schlauer Name) und setzt die externe Variable fehler auf wahr, falls ein Fehler auftrat. Mit ladebitmaps laden wir drei Bitmaps, deren Handles wir uns in hbmheld merken. Aus Faulheit haben wir die Breite bmw und die Höhe bmh der Bitmaps von vornherein auf 32 festgelegt. Die Bildschirmausgabe erledigen wir in Bitmap2.c WinMain.cpp
/* Malen */ void malen(HDC hdc) { HDC hdctmp; int i, dx, x, y; if (fehler) { sprintf(text, "Fehler beim Laden der Bitmaps."); TextOut(hdc, 10, 10, text, strlen(text)); return; } hdctmp = CreateCompatibleDC(hdc); for (x = −3*bmw; x < xmax; x += bmw) { for (y = 0; y < ymax; y += bmh) { dx = nmalen % (3*bmw); i = (3 + x/bmw + y/bmh)%3; SelectObject(hdctmp, hbmheld[i]); BitBlt(hdc, x+dx, y, bmw, bmh, hdctmp, 0, 0, SRCCOPY); npixel += bmw*bmh; } } DeleteDC(hdctmp); if (hdc != hdcfenster) { BitBlt(hdcfenster, 0, 0, xmax, ymax, hdc, 0, 0, SRCCOPY); npixel += xmax*ymax; } }
Falls ein Fehler vorliegt, wird nicht gemalt. Meine erste Version der Doppelschleife war for (x = 0; x < xmax; x += bmw) { for (y = 0; y < ymax; y += bmh) { i = 0; SelectObject(hdctmp, hbmheld[i]);
328
Kapitel 11 Bitmaps
Sandini Bib BitBlt(hdc, x, y, bmw, bmh, hdctmp, 0, 0, SRCCOPY); } }
Wie bei einem Schachbrett durchläuft diese Schleife alle Felder im Abstand der Bitmapgröße. Wir wählen die Bitmap hbmheld[0] für einen temporären Device Context hdctmp und kopieren die Bitmap mit BitBlt nach Device Context hdc. Ausprobieren, mit dieser Schleife bekommst du lauter identische kleine Figuren, als ob du deine Badezimmerwand etwas abgedreht gekachelt hättest. Dann habe ich die Schleife so hingetrickst, dass drei verschiedene Bitmaps ausgegeben werden (siehe Definition von i). Das neue Kachelmuster bewegt sich um ein Pixel nach rechts, wenn sich die Variable nmalen um eins erhöht. Nach jedem BitBlt zählen wir mit npixel += bmw*bmh; die Anzahl der Pixel, die geblittet wurden. Weil ein kleiner Teil der Pixel außerhalb des Fensters liegen und wegen dem Fensterclipping nicht gemalt werden (siehe Kapitel 4.2), ist npixel am Ende etwas zu groß, aber so genau wollen wir es nicht nehmen. Ich verwende drei Bitmaps, die sich bewegen, aus rein psychologischen Gründen, denn dann wird offensichtlich, dass tatsächlich geblittet wird, was das Zeug hält. Für die geplante Geschwindigkeitsmessung ist es ziemlich egal, ob wir eine Kachelwand aus statischen oder bewegten Bitmaps verwenden. Richtig verdächtig sehen die Zeilen if (hdc != hdcfenster) { BitBlt(hdcfenster, 0, 0, xmax, ymax, hdc, 0, 0, SRCCOPY); npixel += xmax*ymax; }
aus. Hier wird klar, dass hdc unter Umständen gar nicht die Fensterbitmap bezeichnet! In der Tat rufen wir die Funktion malen mit verschiedenen Device Contexts auf. Und wenn hdc zum Bitmappuffer und nicht zum Fensterpuffer hdcfenster gehört, kopieren wir die gesamte Bitmap in hdc in Fenstergröße (xmax und ymax) nach hdcfenster. Und zählen auch diese Pixel in npixel. Bei mir sieht das Ergebnis jedenfalls so aus:
11.4
Bitmappuffer
329
Sandini Bib
Jede Menge Bitmapklone, wie niedlich. Jetzt stellt sich die Frage, wie wir die verschiedenen Bitmappuffer verwalten und wie wir die Bilder und Pixel pro Sekunde messen, die in der Titelleiste des Fensters angezeigt werden. Das geschieht in WindowProc: Bitmap2.c WinMain.cpp
LRESULT CALLBACK WindowProc(HWND hwnd, UINT m, WPARAM wParam, LPARAM lParam) { MSG msg; // Fenster auf if (m == WM_CREATE) { SetWindowText(hwnd, "BitBlt um die Wette"); SetTimer(hwnd, 0, 1000, 0); hdcfenster = GetDC(hwnd); ladebitmaps(); sprintf(text, ""); } // Malen else if (m == WM_PAINT) { malen(hdc); nmalen++; ntimer++; if (PeekMessage(&msg, hwnd, WM_TIMER, WM_TIMER, PM_NOREMOVE)) ValidateRect(hwnd, 0); } // Timer else if (m == WM_TIMER) { sprintf(text, "%s: %d Bilder/s, %.1f Megapixel/s", (hdc == hdcfenster) ? "Direkt" : "Puffer", ntimer, npixel/1000000.0); SetWindowText(hwnd, text); ntimer = npixel = 0; InvalidateRect(hwnd, 0, 0); }
330
Kapitel 11 Bitmaps
Sandini Bib Bitmap2.c WinMain.cpp
// Tastatur else if (m == WM_KEYDOWN) { hdc = (hbmpuffer && hdc == hdcfenster) ? hdcpuffer : hdcfenster; } // Groesse else if (m == WM_SIZE) { xmax = LOWORD(lParam); ymax = HIWORD(lParam); if (hbmpuffer) { DeleteDC(hdcpuffer); DeleteObject(hbmpuffer); } hbmpuffer = CreateCompatibleBitmap(hdcfenster, xmax, ymax); if (hbmpuffer) { hdcpuffer = CreateCompatibleDC(hdcfenster); SelectObject(hdcpuffer, hbmpuffer); if (hdc != hdcfenster) hdc = hdcpuffer; } if (!hbmpuffer) hdc = hdcfenster; } // Aufraeumen else if (m == WM_DESTROY) { if (hbmpuffer) { DeleteDC(hdcpuffer); DeleteObject(hbmpuffer); } KillTimer(hwnd, 0); PostQuitMessage(0); } else return DefWindowProc(hwnd, m, wParam, lParam); return 0; }
Bei WM_CREATE starten wir einen Zeitgeber mit einer Periode von 1 Sekunde, und wenn die Uhr mit der Nachricht WM_TIMER tick macht, geben wir die Anzahl der Bilder und Pixel aus, die wir in ntimer und npixel gezählt haben. Nach der Ausgabe setzen wir diese Variablen wieder auf Null, damit bis zum nächsten Tick aufs Neue gezählt werden kann.
11.4
Bitmappuffer
331
Sandini Bib
Gezählt wird bei WM_PAINT. Dort rufen wir wie gewohnt die Funktion malen auf. ntimer verwenden wir für die Messung der Bilder pro Sekunde, während wir nmalen immer weiter hochzählen, damit die Grafikausgabe nicht im Sekundentakt einen Sprung macht. Mit nmalen verschieben wir die Bitmaps nach rechts, siehe Funktion malen. In if (PeekMessage(&msg, hwnd, WM_TIMER, WM_TIMER, PM_NOREMOVE)) ValidateRect(hwnd, 0);
begegnet dir zum ersten Mal die Funktion PeekMessage ( ›peek‹ heißt gucken). Im Gegensatz zu GetMessage (Kapitel 10.10) entfernt PeekMessage nicht in jedem Fall eine Nachricht aus der Nachrichtenwarteschlange. Wir gucken nur nach, ob vielleicht die Nachricht WM_TIMER darauf wartet, abgeholt zu werden. Falls ja, beenden wir das Malen ohne Ende mit ValidateRect(hwnd, 0);. Mit anderen Worten, wir verwenden den uns schon vertrauten Trick, so schnell wie möglich zu malen, indem wir WM_PAINT nicht mit ValidateRect aus der Nachrichtenschlange entfernen. Es sei denn, eine WM_TIMER-Nachricht soll bedient werden. Jetzt ist nur noch die Sache mit dem Bitmappuffer zu erklären. Bei WM_CREATE holen wir uns wie üblich den Device Context für das Fenster hwnd mit GetDC. Den Handle nennen wir hdcfenster, um ihn von dem Handle hdcpuffer für den Bitmappuffer zu unterscheiden. Bevor wir einen Device Context für einen Bitmappuffer erzeugen können, müssen wir aber erst einmal eine neue Bitmap in Fenstergröße anlegen! Das erledigen wir bei WM_SIZE mit CreateCompatibleBitmap. Der Funktionsaufruf ist hbmpuffer = CreateCompatibleBitmap(hdcfenster, xmax, ymax);
Hier erzeugen wir eine neue Bitmap mit Speicherplatz und allen nötigen Zusatzinformationen, die zum Fenster kompatibel ist (also z. B. dieselbe Farbtiefe besitzt). Die Größe können wir frei wählen, solange noch genügend Speicherplatz frei ist. Falls keine neue Bitmap in der gewünschten Größe erzeugt werden konnte, ist hbmpuffer Null. An vier Stellen in WindowProc überprüfen wir, ob hbmpuffer existiert, und reagieren entsprechend. Das Entscheidende ist, dass wir die Bitmap hbmpuffer genauso zum Malen verwenden können wie jede Fensterbitmap. (WM_KEYDOWN und VK_SPACE) zwischen PufferBeachte auch, dass wir mit verwendung und direktem Zugriff umschalten können, sofern ein Puffer überhaupt existiert. Jetzt kannst du munter drauf los testen. Fenster klein, Fenster groß, verschiedene Computer. Ich war angenehm überrascht, wie schnell BitBlt arbeitet. Selbst bei einer Auflösung von 1024 × 768 Pixeln bei 16 Bit Farbtiefe ist das Testprogramm auf meinem Pentium II PC recht flott (ein 433 MHz Celeron Prozessor mit 66 MHz Bustakt und GeForce 2 MX PCI Grafikkarte, falls dir das was sagt): 332
Kapitel 11 Bitmaps
Sandini Bib
Das Programm läuft schneller, wenn du es außerhalb von BCB startest. Wenn ich die Maus ins Fenster bewege, wird das Programm einen Moment langsamer. Typischerweise geht die Bilderrate etwas runter und die Pixelrate hoch, wenn ich von Direkt auf Puffer umschalte. Das liegt daran, dass dann ein zusätzliches großes BitBlt ausgeführt wird. Dieses eine große BitBlt verbraucht wenig Zeit, aber es werden fast doppelt so viele Pixel gezählt. Ausprobieren, wie schnell wird das Programm, wenn du keine kleinen Bitmaps, sondern nur Bitmaps in Fenstergröße kopierst? Für deine eigenen Programme kannst du jetzt abschätzen, bei wie vielen kleinen Bitmaps dein Rechner in die Knie geht und keine vernünftigen Bilderraten mehr erreicht. Der nächste Schritt wäre z. B. DirectX. Dort gibt es eine Funktion BltFast, die Bitmaps innerhalb des Grafikspeichers kopiert. Das ist schneller als das Kopieren vom Arbeitsspeicher des Computers in den Grafikspeicher. Also lädt man möglichst viele Bitmaps in den Grafikspeicher und führt schnelle lokale Kopien innerhalb des Grafikspeichers aus. Unterm Strich würde ich aber sagen, dass sich die Windows SDK-Funktion BitBlt sehr wacker schlägt.
11.4
Bitmappuffer
333
Sandini Bib
11.5
Gekachelte Landkarte
In Kapitel 11.8 wollen wir eine animierte Figur auf einer Landkarte herumlaufen lassen. Sehr wahrscheinlich ist dir dieses Prinzip schon in Spielen begegnet. Als Erstes wollen wir uns überlegen, wie wir die Landkarte erzeugen. Natürlich könntest du eine große Bitmap zeichnen und diese als Hintergrund verwenden. Sehr häufig werden aber Landkarten verwendet, die sich aus kleinen, gleich ›tiles‹) zusammensetzen. Der Vorteil ist, dass nur wenige großen Kacheln ( kleine Bitmaps gezeichnet werden müssen, und so wollen wir es machen. Bei meinen Zeichenkünsten sieht das dann so aus:
Normalerweise passt die Landkarte oder der Hintergrund nicht auf einmal ins Programmfenster. Das Fenster stellt nur einen Ausschnitt dar, der sich verschiebt, wenn die Hauptfigur des Spieles sich bewegt oder wenn der Spieler die Landkarte mit der Maus bewegt. Das geht am einfachsten, wenn der Hintergrund tatsächlich als eine einzige große Bitmap im Speicher steht. Wir wissen schon, dass in diesem Fall die Ausgabe sehr einfach ist, weil BitBlt es erlaubt, Ausschnitte zu kopieren. Etwas schwieriger ist es, einen Ausschnitt aus einer gekachelten Landkarte zu zeichnen, indem nur die Kachelfelder gezeichnet werden, die tatsächlich sichtbar sind. Der Vorteil ist, dass wesentlich größere Landkarten gestaltet werden können. In unserem Beispiel verwenden wir Kacheln mit 32 mal 32 Pixeln, also 1024 Pixel pro Kachel. Statt dieser 1024 Pixel können wir aber auch 1 Nummer pro 334
Kapitel 11 Bitmaps
Sandini Bib
Kachel speichern, die wir anhand einer Liste in die entsprechende Bitmap übersetzen. Unsere Landkarte ist dann ein zweidimensionales Feld aus Kachelindizes. In diesem Kapitel wollen wir als Erstes eine gekachele Landkarte implementieren. In Kapitel 11.6 machen wir den Ausschnitt beweglich und in 11.7 kommt eine animierte Bitmap für die Figur dazu. Fangen wir also mit den Datenstrukturen für unsere gekachelte Landkarte an: Bitmap3.c WinMain.cpp
#include <windows.h> #include <stdio.h> /* externe Variable */ HDC hdc, hdcmem; char text[1000]; int xmax, ymax; /* Breite und Hoehe der Bitmaps */ int bmw = 32; int bmh = 32; /* Kacheln fuer die Karte */ typedef struct { char c; int block; char *name; HBITMAP hbm; } KACHEL; /* Legende fuer die Karte */ KACHEL kachel[] = { {’0’, 1, "schwarz"}, {’w’, 1, "wasser"}, {’m’, 1, "mauer"}, {’g’, 0, "gras"}, {’s’, 0, "sand"}, {0}}; int nkacheln; /* 2d Feld fuer Karte, speichert Indizes fuer Kachelliste */ #define KI 100 #define KJ 100 int karte[KI][KJ]; int ki, kj;
Die Struktur vom Typ KACHEL enthält die folgende Informationen: Ein Zeichen c, das wir als Kürzel für die Kachel verwenden wollen. Eine Integerzahl block, die angibt, ob Figuren hier laufen dürfen. Einen Zeiger auf eine Zeichenkette 11.5
Gekachelte Landkarte
335
Sandini Bib
char *name für den Dateinamen der Kachel. Und schließlich den Handle zur
Bitmap. In KACHEL kachel[] = { ... }; definieren wir ein Feld aus Kacheln, das gleich noch initialisiert wird. Wegen [ ] wird die Anzahl der Elemente des Feldes genau wie bei Zeichenketten automatisch auf die tatsächliche Anzahl der Elemente in der Initialisierung festgelegt. Die Elemente sind als Listen zwischen geschweiften Klammern angegeben, z. B. {’w’, 1, "wasser"}
Jedes Element ist eine Struktur vom Typ KACHEL und ’w’, 1 und "wasser" werden der Reihe nach den Strukturelementen c, block und name zugewiesen. Falls weniger Initialisierer als Strukturelemente vorhanden sind, bleiben die restlichen Strukturelement uninitialisiert. In unserem Beispiel wird der Handle hbm erst bei Aufruf der Funktion ladebitmaps initialisiert. Der letzten Kachel geben wir den Code 0. Auf diese Weise können wir in ladebitmaps das Ende der Kachelliste bestimmen. Die Anzahl der Elemente werden wir in nkacheln speichern. Das ist praktisch, weil wir die Liste der Kacheln leicht erweitern können, ohne ständig die Kacheln nachzählen zu müssen. Hier ist die Funktion ladebitmaps: Bitmap3.c WinMain.cpp
/* lade Bitmaps */ void ladebitmaps(void) { int k; for (k = 0; kachel[k].c; k++) { sprintf(text, "%s.bmp", kachel[k].name); kachel[k].hbm = LoadImage(0, text, IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE); if (!kachel[k].hbm) { nkacheln = 0; return; } } nkacheln = k; }
Wir führen eine Schleife über alle Elemente in kachel aus, die beendet wird, wenn das Zeichen c der k-ten Kachel (kachel[k].c) gleich 0 ist. Den Namen der k-ten Kachel (kachel[k].name) verwenden wir als Dateiname und speichern den Handle zur Bitmap in kachel[k].hbm. Wie speichern wir die Karte? Wir definieren ein zweidimensionales Feld int karte[KI][KJ];, in dem wir die Indizes der Kacheln speichern wollen. Im Prinzip könnten wir das Feld karte direkt in int karte[KI][KJ] = { ... }; initialisieren, ähnlich wie wir das gerade bei der Kachelliste gemacht haben. Stattdessen verwenden wir
336
Kapitel 11 Bitmaps
Sandini Bib Bitmap3.c WinMain.cpp
/* Karte als Liste von Strings */ char *initkarte[] = { "wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww", "wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww", "wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww", "wwwwwwwggggggggggggggggwwwwwwwwwwwwwwwww", "wwwwwssggggggggggggggggggwwwwwwwwwwwwwww", "wwwwwswwwwwwwwwwwwwwwwgggwwwwwwwwwwwwwww", "wwwwwswwwwgggggggwwwwwwggwwwwwwwwwwwwwww", "wwwwwswwwwgggggggwwwwwwggwwwwwwwwwwwwwww", "wwwwwswwwwgggggggwwwwwwggwwwwwwwwwwwwwww", "wwwwwswwwwgggggggwwwwwwggwwwwwwwwwwwwwww", "wwwwwswwwwgggggggwwwwwwgwwwwwwwwwwwwwwww", "wwwwwswwwwwwwgwwwwwwwwwgwwwwwwwwwwwwwwww", "wwwwwswwwwwwwgwwwwwwwwwgwwwwwwwwwwwwwwww", "wwwwwsssssssggwwwwwwwwwgwwwwwwwwwwwwwwww", "wwwwwwwwwwwwwwwwwwwwwwwgwwwwwwwwwwwwwwww", "wwwwwwwwwwwwwwwwwwwwwwwgwwwwwwwwwwwwwwww", "wwwwwwwwwwwwwwwwwwwwwwwgwwwwwwwwwwwwwwww", "wwwwwwwwwwwwwwwwwwwwwwwgwwwwwwwwwwwwwwww", "wwwwwwwwwwwwwwwwwwwwwwwgwwwwwwwwwwwwwwww", "wwwwwwwwggggggggggggggggggggggggggwwwwww", "wwwwwwgggggggggggwwwwwwwwwwwwwwwwgwwwwww", "wwwwggggggggggggggwwwwwwwwwwwwwwwgwwwwww", "wwwwwgggggggggggggwwwwwwwwwwwwwwwgwwwwww", "wwwgggggggggggggggggwwwwwwwwwwwwwgwwwwww", "wwgggggmmmmssmmmggggwwwwwwwwwwwwwgwwwwww", "wwwggggmsssssssmgggwwwwwwwwwwwggggggwwww", "wwwggggmsssssssmggggwwwwwwwwwggggggggsww", "wwwggggmsssssssmgggwwwwwwwwwwggggggggssw", "wwwwgggmmmmmmmmmgggwwwwwwwwwwwggggggsssw", "wwwwwggggggggggggggwwwwwwwwwwwwwgggssssw", "wwwwwwwggggggggggwwwwwwwwwwwwwwwwsssssww", "wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwssssww", "wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww", ""};
Die Variable initkarte ist ein Feld, das als Elemente Zeiger auf Zeichen (char *) enthält (Kapitel 10.8). Schau dir die Initialisierung an. Zwischen den geschweiften Klammern steht eine lange Liste von Stringkonstanten. Das erste Element hätten wir auch mit initkarte[0] = "wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww";
initialisieren können. initkarte[0] ist vom Typ char *, also können wir genau wie bei char *s = "Hallo"; mit einer Stringkonstante initialisieren. Das erste Zeichen von s ist s[0], das erste Zeichen von initkarte[0] ist initkarte[0][0] = ’w’;
11.5
Gekachelte Landkarte
337
Sandini Bib
Dieses Format für die Karte habe ich gewählt, weil ich dann auf primitive, aber einfache Weise Karten entwerfen kann. Ich habe mir eine Zeile mit "www..." wie Wasser erstellt und diese mehrmals kopiert. Dann habe ich den Editor mit der Einf-Taste auf Überschreiben umgestellt und mit der Tastatur drauf los gemalt. Eine etwas bessere Lösung wäre, diese Zeichen in einer Textdatei abzuspeichern und die Datei dann bei Bedarf vom Programm einlesen zu lassen, siehe Kapitel 10.9. Die beste Idee ist natürlich, sich einen kleinen Karteneditor für Windows zu programmieren, in dem du mit der Maus Bitmapkacheln anklicken und auf der Karte platzieren kannst. Auch dann musst du dir ein Format ausdenken, in dem du die Karte abspeichern und wieder einlesen kannst. In der Funktion ladekarte übersetzen wir die Buchstaben in initkarte in Indizes für die Kachelliste: Bitmap3.c WinMain.cpp
/* lade Karte */ void ladekarte(void) { int i, j, k; char c; for (ki = 0; ki < KI; ki++) if (initkarte[0][ki] == 0) break; for (kj = 0; kj < KJ; kj++) if (initkarte[kj][0] == 0) break; for (i = 0; i < ki; i++) { for (j = 0; j < kj; j++) { c = initkarte[j][i]; for (k = 0; k < nkacheln; k++) { if (c == kachel[k].c) karte[i][j] = k; } } } }
In den ersten beiden Schleifen bestimmen wir die Anzahl der Zeichen in initkarte in jede Richtung. initkarte muss ein Rechteck sein. Die Kantenlängen nennen wir ki und kj. In der Doppelschleife lesen wir mit c = initkarte[j][i]
das Zeichen bei Kachelkoordinaten j und i. Wie schon besprochen ist die Reihenfolge der Indizes bei zweidimensionalen Feldern anders herum als die Konvention bei Koordinaten. Dann folgt eine Schleife, mit der wir die Kachelliste von vorne durchsuchen, bis wir das Zeichen c gefunden haben. Den Index k speichern wir in karte[i][j]. 338
Kapitel 11 Bitmaps
Sandini Bib
Das Endergebnis dieser Datengymnastik ist eine schön einfache Ausgabeschleife in malen: Bitmap3.c WinMain.cpp
/* male Bitmap */ void malebitmap(HDC hdc, int x, int y, HBITMAP hbitmap) { SelectObject(hdcmem, hbitmap); BitBlt(hdc, x, y, bmw, bmh, hdcmem, 0, 0, SRCCOPY); } /* Malen */ void malen(HDC hdc) { int i, j, k, x, y; if (nkacheln == 0) { sprintf(text, "Fehler beim Laden der Kacheln"); TextOut(hdc, 10, 10, text, strlen(text)); return; } for (i = 0; i < ki; i++) { for (j = 0; j < kj; j++) { k = karte[i][j]; x = bmw * i; y = bmh * j; malebitmap(hdc, x, y, kachel[k].hbm); } } }
Die Doppelschleife in i und j läuft über alle Felder in karte. Mit k = karte[i][j] erhalten wir den Index für die Kachelliste, und kachel[k].hbm liefert die dazugehörige Bitmap. Im Fenster erscheint die gekachelte Landkarte, die das Bild zu Beginn dieses Unterkapitels zeigt. Du kannst das Fenster maximieren, aber die Karte ist bei dir wahrscheinlich auch nicht ganz sichtbar. Wir malen blindlings alle Bitmaps, auch die, die außerhalb des Fensters liegen. Clipping macht’s möglich. Auf dieses Beispiel trifft sicher zu, dass, wenn erst mal geeignete Datenstrukturen gefunden worden sind, der Rest ziemlich einfach ist.
11.5
Gekachelte Landkarte
339
Sandini Bib
11.6
Mit Pfeiltasten über Land
In diesem Kapitel wollen wir unsere Landkarte wenigstens mit einem Menschen bevölkern. Unser Held beziehungsweise unsere Heldin soll mit den Pfeiltasten gesteuert werden können. Dabei soll sich der Ausschnitt der Landkarte, den das Fenster zeigt, ändern. In diesem Kapitel besprechen wir die Pfeiltastensteuerung, im nächsten kommt eine animierte Bitmap für den Held dazu. Mit Scrolling bezeichnet man am Computer das Abrollen oder Verschieben des ›scroll‹ heißt Schriftrolle). Wenn die ganze Grafik in einer Fensterinhalts ( großen Bitmap steht, genügt ein einziges BitBlt mit den richtigen Koordinaten. Für unsere gekachelte Landkarte wollen wir die Ausgabeschleife so abändern, dass eine Verschiebung der Koordinaten x und y um einen Betrag x0 und y0 möglich ist. Zudem wollen wir nur solche Kartenfelder malen, die im Fenster ganz oder zumindest teilweise sichtbar sind. Das ist nicht weiter schwierig. Etwas verwickelter ist es, den Held mit den Pfeiltasten zu bewegen, weil es zwei Fälle zu behandeln gibt. Angenommen, die ganze Landkarte würde ins Fenster passen. Dann bedeutet jeder Pfeiltastendruck, dass sich die Koordinaten des Heldes im Fenster ändern. Der Held bewegt sich im Fenster. Wenn aber alle vier Ränder der Landkarte außerhalb des Fensters liegen, soll sich die Landkarte bewegen, während der Held im Fenster stillsteht. Das sieht trotzdem so aus, als ob er sich bewegt, denn die Landkarte rollt unter ihm weg. Erst wenn der Rand der Landkarte erreicht ist, soll die Bewegung der Landkarte aufhören (ohne das über den Rand hinausgescrollt wird) und der Held darf im Fenster die Ecken auslaufen. Das hört sich viel komplizierter an, als es ist, einfach mit dem Beispielprogramm ausprobieren. Wir erweitern das Programm für die gekachelte Landkarte in zwei Stufen. Als Erstes programmieren wir die Pfeiltastensteuerung und malen den Held einfach als weißen Kreis. Im zweiten Schritt wollen wir den Kreis durch eine animierte Bitmap mit durchsichtigem Hintergrund ersetzen. Für die Bewegung des Heldes verwenden wir die Funktion bewegeheld:
340
Kapitel 11 Bitmaps
Sandini Bib Bitmap4.c WinMain.cpp
/* bewege Held */ void bewegeheld(int wParam) { int i, j, k, x, y; xheldalt = xheld; yheldalt = yheld; if if if if
(wParam (wParam (wParam (wParam
== == == ==
VK_LEFT) VK_RIGHT) VK_UP) VK_DOWN)
xheld xheld yheld yheld
−= += −= +=
dschritt; dschritt; dschritt; dschritt;
// if if if if
Held darf die Karte nicht verlassen (xheld < 0) xheld = 0; (yheld < 0) yheld = 0; (xheld > kxmax) xheld = kxmax; (yheld > kymax) yheld = kymax;
// Weg blockiert? for (x = xheld; x < xheld + bmw; x += bmw−1) { for (y = yheld; y < yheld + bmh; y += bmh−1) { i = x/bmw; j = y/bmw; k = karte[i][j]; if (kachel[k].block) { xheld = xheldalt; yheld = yheldalt; return; } } } }
Diese Funktion rufen wir bei der Nachricht WM_KEYDOWN auf. Mit xheld und yheld bezeichnen wir die Koordinaten des Heldes auf der Landkarte (und nicht etwa im Fenster). Der Held bewegt sich auf der Landkarte, und erst in der Funktion malen werden wir entscheiden, welcher Ausschnitt der Landkarte im Fenster gezeigt und wo der Held im Fenster gemalt wird. Als Erstes speichern wir die momentanen Koordinaten des Helden in xheldalt und yheldalt. Dann ändern wir die Koordinaten des Heldes um den Betrag dschritt in die Richtung, die von den Pfeiltasten angegeben wird. Falls eine Taste, aber keine Pfeiltaste gedrückt wurde, ändert sich die Position des Helden nicht. Die Schrittgröße dschritt kann sehr wohl kleiner als die Breite bmw oder die Höhe bmh der Kacheln sein. In unserem Beispiel sind die Kacheln quadratisch und wir wählen dschritt so, dass nach einer ganzzahligen Anzahl von Schritten genau eine Kachel überquert wurde. 11.6
Mit Pfeiltasten über Land
341
Sandini Bib
Der Held darf die Karte nicht verlassen. Also überprüfen wir die unteren und die oberen Schranken (kxmax und kymax) der Kartenkoordinaten und schubsen den Held bei Bedarf zurück. Eine andere Art von Blockade soll durch die Art des Untergrunds bestimmt werden. Auf Wasser darf der Held nicht laufen und auch Mauern sollen undurchdringlich sein. Diese Eigenschaft finden wir für die k-te Kachel in kachel[k].block. Weil wir Schrittweiten erlauben, die kleiner als einzelne Kacheln sind, befindet sich der Held meistens auf mehreren Kacheln gleichzeitig. Das heißt, nach einem Schritt kann der Held mit einem Fuß im Gras, mit dem anderen im Wasser stehen. Deshalb testen wir mit einer kleinen Doppelschleife, ob eine der vier Ecken der Heldenbitmap blockiert ist. Das erste Pixel in xRichtung hat Koordinate xheld. Das letzte Pixel hat Koordinate xheld+bmw−1, denn insgesamt gibt es bmw Pixel in x-Richtung. Wie bei Feldern wird von 0 bis bmw−1 gezählt. Durch eine ganzzahlige Division erhalten wir aus x den Index für die Karte mit i = x/bmw. Für jede der vier Ecken des Helden berechnen wir die Kachel, in der sich diese Ecke nach dem Schritt befinden würde. Falls eine Blockade vorliegt, erlauben wir den Schritt nicht, stellen die Koordinaten vor dem Schritt wieder her und verlassen die Funktion bewegeheld. Wir führen also den gewünschten Schritt probehalber aus, und wenn er nicht erlaubt ist, machen wir ihn rückgängig, bevor die Bewegung im Fenster angezeigt wurde. Als ob der Held erst einen Schritt weit mit dem Kopf durch die Wand rennt, bevor er einsieht, dass hier kein Durchkommen ist. Was wo im Fenster angezeigt wird, bestimmen wir in
342
Kapitel 11 Bitmaps
Sandini Bib Bitmap4.c WinMain.cpp
/* Malen */ void malen(HDC hdc) { int i, j, k; int imin, jmin, imax, jmax; int x, y, x0, y0, w0, h0; if (fehler) { sprintf(text, "Fehler beim Laden der Bitmaps."); TextOut(hdc, 10, 10, text, strlen(text)); return; } // Breite und Hoehe des Fensters in ganzzahligen Vielfachen der Kacheln w0 = (xmax/bmw)*bmw; h0 = (ymax/bmh)*bmh; // // x0 y0
linke obere Ecke des Fensters auf der Karte setze Held in die Mitte = xheld − w0/2; = yheld − h0/2;
// if if if if
am Rand bewegt sich der Held weg von der Mitte (x0 > kxmax − w0) x0 = kxmax − w0; (y0 > kymax − h0) y0 = kymax − h0; (x0 < 0) x0 = 0; (y0 < 0) y0 = 0;
// male alle Kartenfelder, die im Fenster ganz oder teilweise sichtbar sind imin = x0/bmw; jmin = y0/bmh; imax = (x0 + w0)/bmw + 1; jmax = (y0 + h0)/bmh + 1; if (imax >= ki) imax = ki−1; if (jmax >= kj) jmax = kj−1; for (i = imin; i <= imax; i++) { for (j = jmin; j <= jmax; j++) { x = bmw * i − x0; y = bmh * j − y0; k = karte[i][j]; malebitmap(hdc, x, y, kachel[k].hbm); } } // male ’Held’ Ellipse(hdc, xheld−x0, yheld−y0, xheld−x0+bmw, yheld−y0+bmh); }
In w0 und h0 speichern wir die Breite und Höhe des Fensterbereichs, der durch ein ganzzahliges Vielfaches der Kacheln abgedeckt werden kann. Denke einmal darüber nach, wir könnten statt (xmax/bmw)*bmw auch xmax−xmax%bmw schreiben.
11.6
Mit Pfeiltasten über Land
343
Sandini Bib
Die linke obere Ecke des Fensters soll auf der Karte Koordinaten x0 und y0 haben, so dass der Held in der Mitte des Kartenausschnitts mit Breite w0 und h0 steht. Also setzen wir z. B. x0 = xheld − w0/2. Jetzt kommt der Schritt, bei dem wir zwischen den zwei Möglichkeiten der Anzeige unterscheiden. Falls der Kartenausschnitt mit dem Held in der Mitte bedeutet, dass das Fensters über den Rand der Karte hinauszeigt, setzen wir den Rand der Karte gleich dem Fensterrand, z. B. mit if (x0 < 0) x0 = 0;. Dann steht der Held auch nicht mehr in der Mitte des Fensters, aber das ist genau das, was wir wollen. Es folgt die Ausgabeschleife mit der berechneten Verschiebung des Ausschnitts um x0 und y0. Zudem schleifen wir nicht wie zuvor über die gesamte Karte von 0 bis ki und von 0 bis kj, sondern berechnen Indizes imin, imax, jmin und jmax, die den sichtbaren Bereich abdecken. Zu guter Letzt malen wir einen Kreis, der den Helden darstellen soll. Lass diese Version des Programms laufen. Du kannst den Kreis auf der Karte mit den Pfeiltasten bewegen. Der Kreis befindet sich entweder in der Mitte des Fensters, wenn der Rand der Karte außerhalb des Fensters liegt, oder die Bewegung der Karte stoppt und der Kreis kann in die Ecken bewegt werden:
11.7
Bitmaps mit transparenter Hintergrundfarbe
Zum Abschluss wollen wir den Kreis durch eine animierte Bitmap für den Helden oder die Heldin ersetzen. Für die Animation verwenden wir einen Zeitgeber, der alle 300 Millisekunden oder so auf die nächste Bitmap für den Helden umschaltet. Ich habe zwei Bitmaps gezeichnet, held_0.bmp und held_1.bmp, die als 344
Kapitel 11 Bitmaps
Sandini Bib
Minifilmchen so aussehen, als ob der Held läuft. Wenn der Held mit den Pfeiltasten bewegt wird, sieht es so aus, als ob er tatsächlich läuft. Wenn der Held sich nicht fortbewegt, sieht es aus, als ob er auf der Stelle tritt. Klarer Fall, hier fehlen unterschiedliche Filmchen für Bewegung nach links, rechts, oben und unten und für das gelangweilt Rumstehen. Das eigentliche Problem ist, dass wir nicht darum herumkommen, für unseren Held einen durchsichtigen Hintergrund zu implementieren. Dummerweise liefert das Windows SDK eine solche Option für BitBlt nicht mit. In DirectX und OpenGL sind Bitmaps mit transparenter Hintergrundfarbe Standard. Zur Not könnten wir das wie folgt programmieren. Statt mit BitBlt kopieren wir in einer Doppelschleife mit GetPixel und SetPixel die Pixel einzeln, sozusagen per Hand, weil das im Vergleich mit BitBlt sehr langsam geht (denke an die Pixelmalerei in Kapitel 9.11). Mit GetPixel lesen wir ein Pixel aus der Quellenbitmap, die kopiert werden soll. Dann vergleichen wir die Farbe des Pixels mit dem Farbwert, der die durchsichtige Farbe darstellen soll. Das kann Schwarz, Weiß, Grau oder auch Hellblau sein. Wenn der Farbwert nicht die durchsichtige Farbe ist, schreiben wir das Pixel mit SetPixel in die Zielbitmap, ansonsten schreiben wir kein Pixel. Auf diese Weise lassen wir in der Zielbitmap alle die Pixel unverändert, die in der Quellenbitmap durchsichtig sein sollen. BitBlt kann zwar keine Bitmaps mit Transparenz kopieren, aber immerhin gibt es noch andere Operationen als SRCCOPY, siehe die Windows SDK-Hilfe. Wir verwenden verschiedene solche Operationen in einer selbst gemachten Funktion void malebitmap_transparent(HDC ziel, int x, int y, int w, int h, HBITMAP bitmap);
um Bit Blits mit Transparenz zu simulieren. Als transparente Farbe wird automatisch das Pixel in der linken oberen Ecke der Quellenbitmap gewählt. Diese Funktion steht in ihrer eigenen Datei, BitBltTransparent.c, die wir dem Projekt in BCB wie in Kapitel 0.6 besprochen hinzufügen. Um malebitmap_transparent zu verwenden, schreiben wir den Prototyp an den Anfang der Datei, in der wir die Landkarte und so weiter programmiert haben: Bitmap5.c BitBltTransparent.c WinMain.cpp
#include <windows.h> #include <stdio.h> /* Prototyp fuer Funktion in Datei BitBltTransparent.c */ void malebitmap_transparent(HDC ziel, int x, int y, int w, int h, HBITMAP bitmap); /* externe Variable */ HDC hdc, hdcmem; char text[1000]; int xmax, ymax; ...
Bitmaps mit transparenter Hintergrundfarbe
345
Sandini Bib
Das ist ein Beispiel dafür, wie du Funktionen auf verschiedene Dateien verteilen kannst, wenn deine Programme zu lang und unübersichtlich werden. So lassen sich Funktionen auch leicht in verschiedenen Programmen wiederverwenden. Was geschieht in malebitmap_transparent? Statt der direkten Kopie mit SRCCOPY stehen verschiedene Bitoperationen für BitBlt zur Verfügung, insbesondere SRCPAINT. Bei SRCPAINT werden die Pixel mit dem Oder-Operator verknüpft. Normalerweise ist das Ergebnis eine seltsame Mischung der beiden Bitmaps. Ausprobieren! Nur wenn eines der beiden Pixel schwarz ist, ändert sich die Farbe nicht, denn bei der Oder-Operation werden dann keine zusätzlichen Bits gesetzt. Die Idee ist, in einer Zweifarbenbitmap eine Maske für die Bitmap herzustellen, die kopiert werden soll. In dieser Maske steht dort eine 0, wo ein Pixel der Bitmap nicht kopiet werden soll, und eine 1, wenn das Pixel kopiert werden soll. Am einfachsten kann ich dir die Verwendung von Masken anhand unseres Programmbeispiels erklären. Hier sind zwei typische Beispiele:
Die Ausgabe am linken oberen Rand erhältst du, wenn du die Variable testen in malebitmap_transparent auf wahr setzt. Du siehst sieben Bitmaps. Die erste ist die Zielbitmap, also z. B. einfach nur Rasen oder Rasen und Sand. Die zweite ist die Quellenbitmap, unser Held mit dem grauen Rand. Die dritte Bitmap ist eine Maske der Quellenbitmap. Diese erhalten wir, wenn wir die Quellenbitmap auf die richtige Weise in eine Schwarzweißbitmap kopieren. Bei dieser Kopie können wir Grau als Hintergrundfarbe bestimmen, die schwarz wird, während alle anderen Farben (inklusive dem Schwarz im Schatten um die Füße) weiß wird! Die vierte Bitmap zeigt die Quellenbitmap, nachdem wir mit Hilfe der Maske alle grauen Pixel auf Schwarz gesetzt haben. Die fünfte Bitmap ist die negierte Version der Maske. Mit dieser umgekehrten Maske stanzen wir sozusagen ein schwarzes Loch in die Zielbitmap, wo die undurchsichtigen Teile der Quellenbitmap hinsollen. Die sechste Bitmap ist die Zielbitmap mit ausgestanztem Loch. 346
Kapitel 11 Bitmaps
Sandini Bib
Die siebte Bitmap zeigt, dass wir mit der Oder-Operation Bitmap 4 und Bitmap 6 zum Endergebnis kombinieren können. Geschafft! Wenn du dir die Funktion malebitmap_transparent anschaust, wirst du acht Aufrufe von BitBlt finden. Das ist natürlich nicht besonders effizient, sollte aber nicht weiter ins Gewicht fallen, wenn du sowieso schon 500 oder 1000 Bitmaps für die Landkarte blittest. Du könntest alle Masken gleich nach dem Laden der Bitmaps berechnen und zusammen mit den Bitmaps in einer Struktur abspeichern. Dann benötigt die Ausgabe der Bitmaps nur 2 oder 3 Bit Blits.
11.8
Ein Held auf weiter Flur
Nachdem du eine Weile mit unserem Held auf der gekachelten Landkarte herumgelaufen bist, solltest du den Blockadetest abschalten (bewegeheld vorher mit return verlassen). Jetzt schwebt der Held wie ein Geist überall hin. Male deine eigene Karte mit neuen Kacheln. Oh, die ist viel schöner geworden als meine. Aber natürlich ist unser Held viel zu einsam in dieser leeren Welt. Was uns zu einem Minibitmapabenteuer noch fehlt, ist eine Liste von Objekten, Personen oder Monstern, die wie der Held aus animierten Bitmaps bestehen. Wir könnten eine Struktur definieren, die alle nötigen Informationen zusammenfasst, insbesondere die Bitmaps und die momentanen Koordinaten des Objekts auf der Landkarte. Bei der Ausgabe wird erst die gekachelte Landkarte gemalt und dann folgt eine Schleife zur Ausgabe der Objekte. Für Tiere oder Monster könnten wir ein Bewegungsmuster programmieren. Außerdem wäre es schön, wenn unser Held oder unsere Heldin sich mit der Welt unterhalten könnte. So könnte das aussehen:
11.8
Ein Held auf weiter Flur
347
Sandini Bib
Das ist ein Vorschlag, wie du das Bitmapabenteuer verbessern könntest. Mit Bedauern stelle ich fest, dass dies das letzte Beispiel im letzten Kapitel dieses Buches ist. Dieses letzte Beispiel ist ein typisches Beispiel aus dem richtigen Leben. Du hast eine Idee, aber niemand sagt dir, wie genau sie zu verwirklichen ist. Egal, ob du diese Idee oder irgendeine andere Idee verwirklichst – ich würde mich freuen, wenn du genügend über das Programmieren gelernt hast, um lustige kleine Programme zu schreiben. Viel Spaß!
11.9 Bitmaps beflügeln die Fantasie. Ich sehe die kleine Figur aus dem letzten Beispiel schon durch Labyrinthe kriechen auf der Suche nach dem Schatz und auf der Flucht vor Monstern. Wo immer ein Bild mehr sagt als tausend Worte, sind in Computerprogrammen Bitmaps angesagt. Das haben wir besprochen: Bitmaps sind rechteckige Bilder, die sich aus einzelnen Pixeln zusammensetzen. Windows-Bitmaps werden in .bmp-Dateien gespeichert. Bitmaps können mit LoadImage aus einer Datei in den Arbeitsspeicher geladen werden. Das Ergebnis ist ein Handle zu einer Bitmap vom Typ HBITMAP. Unabhängig von Bitmapdateien kannst du Bitmaps mit CreateCompatible Bitmap im Arbeitsspeicher erzeugen.
Die Funktion BitBlt wird verwendet, um eine Bitmap in eine andere hineinzukopieren. Insbesondere ist der Fensterinhalt eine Bitmap und du kannst einen (rechteckigen) Teil der Fensterbitmap mit einer Bitmap aus dem Arbeitsspeicher überschreiben. Bitmaps kannst du animieren, indem du bei jedem Tick eines Zeitgebers die nächste in einer Reihe von Bitmaps ausgibst. 348
Kapitel 11 Bitmaps
Sandini Bib
Sprache, Musik und Soundeffekte können mit PlaySound aus Wave-Dateien geladen und im Hintergrund abgespielt werden, während das Programm andere Aufgaben erledigt.
11.10 1. Programmiere eine Tonmaschine mit PlaySound, die den Satz ›C ist cool‹
zerhackt. Die Ausgabe sollte sowas wie ›C C C C ist C ist cool C C ist C C ist cool‹ sein. Vielleicht durch Zufallszahlen gesteuert. Du könntest verschiedene Sounds per Mausklick abspielen. Der Musiker klickt auf verschiedene Stellen im Fenster, um einen bestimmten Sound zu starten. 2. Verwende einen Bitmappuffer in dem Bitmapabenteuer, um das Flackern zu
verringern. Vielleicht stört dich das Flackern auch in anderen Beispielen? 3. Programmiere ein Spiel. Eine gute Idee ist es, ein möglichst einfaches, aber
trotzdem lustiges Spiel nachzuprogrammieren. Dazu solltest du dir eine genaue und vollständige Liste von allen benötigten Spielelementen machen. Ohne Witz. Versuche aufzuschreiben, was genau die Mechanik des Spiels und die Ausgabe ausmacht. Falls dir Computerspiele Spaß machen, hast du sicher einige auf deinem Computer. Schau sie dir genau an, auch die alten, die du gar nicht mehr spielst. Wie wurde wohl die Grafik gemacht? 2D oder 3D, oder vielleicht nur 2DBitmaps, die aber im 3D-Stil gezeichnet wurden? Wie viele verschiedene Bitmaps kannst du erkennen? Setzt sich der Hintergrund vielleicht aus einzelnen Bitmapkacheln zusammen? Wie viele einzelne Bitmaps werden für die Animation von Figuren verwendet? Wie viele Animationen laufen gleichzeitig ab? Du kannst viel lernen, wenn du Spiele aus der Sicht eines Programmierers analysierst. Manche Spiele haben komplizierte Spielideen und womöglich gute künstliche Intelligenz, aber wie sieht es mit der Grafik aus? Manche Spiele haben eine sehr schöne Grafik, aber die Spielmechanik ist sehr simpel gestrickt.
11.10
349
Sandini Bib
Sandini Bib
A Anhang A.0 A.1 A.2 A.3
Rangordnung von Operatoren Der Preprocessor Die Schlüsselwörter von C Buch gelesen, was nun?
352 352 355 357
Sandini Bib
A.0
Rangordnung von Operatoren
Wenn mehrere Operatoren in einem Ausdruck verwendet werden, muss geklärt sein, in welcher Reihenfolge die Operationen ablaufen. Typisches Beispiel ist die Regel ›Punkt vor Strich‹ für die Grundrechenarten. Multiplikation * und Division / binden stärker als Addition + und Subtraktion −. Oft spielt es auch eine Rolle, in welcher Reihenfolge man eine Verkettung von gleichrangigen Operatoren auswertet. In folgender Tabelle nimmt der Rang der Operatoren von oben nach unten ab und die Auswertungsrichtung ist mit Pfeilen angegeben. Die Bedeutung der Operatoren wird nach und nach im Text erklärt: −→
() [] −> . !
++ −− + − * & (typ) sizeof
←−
* / %
−→
+ −
−→
>> <<
−→
< <= > >=
−→
== !=
−→
&
−→
ˆ
−→
|
−→
&&
−→
||
−→
?:
←−
= += −= *= /= %= &= ˆ= |= <<= >>=
←−
,
−→
Manche Zeichen tauchen in dieser Tabelle mehrmals auf. Die Zeichen + und − binden als Vorzeichen von Zahlen stärker, als wenn sie die Grundrechenarten Plus und Minus bezeichnen. Der Dereferenzoperator * für Zeiger bindet stärker als der Multiplikationsoperator *. Der Adressenoperator & bindet stärker als der Bitoperator &. Mit der Hilfe von Klammern ( ) kann man die Auswertungsreihenfolge beliebig festlegen.
A.1 Der Preprocessor Eine wichtige Rolle bei der Programmorganisation spielt der Preprocessor. Preprocessoranweisungen beginnen mit # und werden vor der Kompilierung ausgeführt. Mit #include kann man eine Datei in eine andere einfügen. Mit #define ist es möglich, Konstanten einen neuen Namen zu geben oder ›Makros‹ zu definieren. Mit #if kann man die Kompilierung von Programmteilen steuern. Der 352
Anhang
Sandini Bib
Preprocessor führt diese Anweisungen vor der Kompilierung des Programmtextes aus. Preprocessoranweisungen zielen darauf ab, den Text in einer C-Datei zu verändern, bevor der Compiler diesen Text als C-Programm interpretiert und in Maschinensprache übersetzt. Im Folgenden bespreche ich kurz einige Details zu den Preprocessoranweisungen. Mit #include Datei
kann man beliebigen Text einlesen. Das wird haupsächlich für Headerdateien von Bibliotheken oder selbst gemachte Headerdateien verwendet. Mit #include <...> und #include "..." werden diese Fälle unterschieden. Mit #define Name
Text zum Einsetzen
erzeugt man einen Namen für ziemlich beliebige Zeichenfolgen. Das bezeichnet man auch als ›makro‹. Der Preprocessor durchsucht den gesamten Programmtext und ersetzt Name durch den Text. Es ist üblich, aber nicht notwendig, die Definitionen des Preprocessors durch Namen aus lauter Großbuchstaben hervorzuheben. Ein typisches Beispiel ist #define NMAX 1000 int a[NMAX];
void initfeld() { int i; for (i = 0; i < NMAX; i++) a[i] = 0; }
In diesem Teilstück eines Progamms wird vor der Kompilierung NMAX durch die Zeichen 1000 ersetzt. NMAX ist der einfachste Spezialfall eines Makros, eine so genannte symbolische Konstante. Wenn sich die Größe des Feldes a ändert, die ja eine Konstante sein muss, braucht man nur das #define zu ändern. Es ist auf jeden Fall besser, solchen oft beliebigen Konstanten einen Namen zu geben. Der Preprocessor beachtet beim Einsetzen von NMAX die C-Syntax, das heißt, NMAX muss z. B. als Name einer Variablen auftreten. Falls im Programm NMAXSTRING oder printf("NMAX = ") auftaucht, findet kein Einsetzen statt. Aber #define kann mehr, als neue Namen für Konstanten zu definieren. Man kann Programmtext wie in #define CHARZEIGER char *
A.1 Der Preprocessor
353
Sandini Bib
zusammenfassen (siehe Kapitel 10). Das erste Wort ist der Name des Makros, der Rest inklusive aller Leerstellen ist der Text, der eingesetzt werden soll. Dieses Makro kann in CHARZEIGER string; wie ein neuer Datentyp verwendet werden, wenn man das * nicht schreiben möchte. Ein Makro kann Argumente verwenden. In #define MAX(A,B) (((A) > (B)) ? (A) : (B))
wird ein Makro definiert, das das Maximum zweier Zahlen berechnet. Zur Erinnerung, der ?: Operator funktioniert wie ein if-else für Ausdrücke. A und B sind die Argumente. Weil A und B ganze Ausdrücke enthalten können, muss man durch Klammern sicherstellen, dass A und B ausgewertet werden, bevor die Anweisung im Makro ausgeführt wird. Nochmals zum Verständnis: Alles, was der Preprocessor tut, ist Ausdrücke wie MAX(x,y+1) zeichenweise durch (((x) > (y+1)) ? (x) : (y)) zu ersetzen. Ein Vorteil ist, dass ein solches Makro zu schnellerem Code führt, weil ein Funktionsaufruf immer mit Overhead (zusätzlicher Arbeit) verbunden ist. Zudem müsste eine Funktion sich auf einen Datentyp festlegen, während das Makro MAX für alle Zahlen gilt. Auf ein Problem muss man achten: Funktionsargumente werden nur einmal ausgewertet. Wenn man aber x−− oder so in MAX einsetzt, wird dieser Ausdruck unter Umständen zweimal ausgewertet! Jedes #define kann mit einem #undef für den Rest der Datei aufgehoben werden. Dann gibt es noch Preprocessoranweisungen wie #if TEST > 0
Textzeilen #endif
Wenn die Konstante TEST größer als 0 ist, wird der Text zwischen #if und #endif verwendet, sonst wird er völlig ignoriert. Nach #if 0 kann Blödsinn stehen, weil der Compiler den nachfolgenden Text nie zu sehen bekommt. Mit #ifdef und #ifndef testet man, ob ein Name definiert ist bzw. ob er nicht definiert ist. #else wird wie else verwendet und #elif spielt die Rolle von else if. Mit diesen Anweisungen lässt sich durch das Definieren von Konstanten steuern, welche Programmteile kompiliert werden. Preprocessoranweisungen, die mit #pragma beginnen, dürfen von verschiedenen Compilern bzw. Entwicklungsumgebungen zu unterschiedlichen Dingen verwendet werden. BCB z. B. verwendet #pragma hdrstop, um die Kompilierung von Headern zu steuern. Im Gegensatz zum Compiler nimmt es der Preprocessor genauer, wenn es um Leerzeichen und neue Zeilen geht. Anweisungen müssen am Zeilenanfang stehen (Leerzeichen vor dem # werden normalerweise ignoriert). In der Definition 354
Anhang
Sandini Bib
eines Makros wie MAX(A,B) darf zwischen MAX und (A,B) kein Leerzeichen stehen, weil ab dem ersten Leerzeichen der Text fürs Einsetzen beginnt. Falls eine Preprocessoranweisung länger als eine Zeile sein soll, muss man die Zeilen mit \ beenden, und nach dem \ darf kein Leerzeichen stehen. Der Prepocessor ist für manche Zwecke unentbehrlich. Man sollte aber stets ein Auge darauf haben, dass die Verständlichkeit der Programme durch übermäßiges Herumdefinieren nicht leidet.
A.2 Die Schlüsselwörter von C Die reservierten Schlüsselwörter ( auto break case char const continue default
do double else enum extern float for
goto if int long register return short
›keywords‹) von C sind: signed sizeof static struct switch typedef union
unsigned void volatile while
Diese 32 Wörter bilden das Vokabular von C und können nicht als Name für Variablen oder Funktionen verwendet werden. Im Text werden die meisten Sprachelemente von C ausführlich diskutiert oder zumindest erwähnt, doch einige der eher seltenen oder obskuren Konstruktionen habe ich an dieser Stelle gesammelt: Mit den Schlüsselwörtern auto und register definiert man automatische (im Gegensatz zu statischen) Variablen innerhalb von Funktionen. Eine Definition wie int i; erzeugt sowieso eine automatische Variable. Mit register kann man dem Compiler vorschlagen, die Variable in einem der Register des Mikroprozessors zu speichern, was dieser aber ignorieren darf. Das Schlüsselvolatile (›flüchtig‹) ist für Optimierungszwecke das Gegenteil wort von const. Das Schlüsselwort enum ( ›enumerate‹, aufzählen) wird zur Definition einer Reihe von ganzzahligen Konstanten verwendet. In enum {EINS = 1, ZWEI, DREI};
werden drei Konstanten EINS, ZWEI, DREI definiert, die die Werte 1, 2 und 3 haben. Das ist manchmal praktischer als eine Reihe von #defineAnweisungen, bei denen jede Konstante einzeln angegeben werden muss. Das Schlüsselwort union (›Vereinigung‹) wird so ähnlich wie struct verwendet. Die Elemente einer Union belegen alle ein und denselben Speicherplatz, überschreiben sich also gegenseitig.
A.2
Die Schlüsselwörter von C
355
Sandini Bib
›Bit-fields‹ werden im Zusammenhang mit struct verwendet, um einzelnen Bits Namen zu geben. Nach struct { unsigned int meinbit : 1; } schalter;
ist die Variable schalter ein 1-Bit-Feld. Ziemlich ungebräuchlich, ich erwähne es nur wegen der Verwendung von : innerhalb von struct. Die Anweisung goto (›gehe nach‹) erlaubt es, die Programmausführung an beliebiger Stelle innerhalb einer Funktion fortzusetzen. Man schreibt z. B. goto hoppla;
und das Programm wird in der Zeile hoppla:
mit der Markierung ( ›label‹) hoppla fortgesetzt. Den Namen der Markierung kann man frei wählen. Nach solchen Markierungen schreibt man einen Doppelpunkt : und nicht etwa einen Strichpunkt. Auf den ersten Blick sieht goto recht nützlich aus, in der Praxis kann die Verwendung von goto aber sehr leicht zu ›Spaghetti-Code‹ führen: Das Programm springt wild umher und man verliert völlig den Überblick. goto wird nur von wenigen C-Programmierern und dann auch nur in seltenen Fällen als guter C-Stil akzeptiert. Viel übersichtlicher ist es, den Programmablauf mit if und while und mit Befehlsblöcken zu organisieren, wie wir es durchweg in diesem Buch machen. Mit der switch-Anweisung und den Schlüsselwörtern case und default lassen sich ähnliche Fallunterscheidungen durchführen wie mit if und else (siehe Kapitel 5.5). ›switch‹ heißt schalten, hier im Sinne von verteilen. Jedes switch lässt sich auch mit if und else schreiben, aber mit switch kann man nur Fallunterscheidungen für ganzzahlige Konstanten durchführen. Hier ist ein Beispiel: #include <stdio.h> int main() { int i; printf("Welche Note hast du bekommen? scanf("%d", &i); switch (i) { case 1: case 2: case 3: printf("Gut.\n");
356
Anhang
");
Sandini Bib break; case 4: printf("In Ordnung.\n"); break; default: printf("Eine Woche kein Computer.\n"); break; } getchar(); return 0; }
Hier wird überprüft, ob der Ausdruck in switch (Ausdruck) gleich einer der ganzzahligen Konstanten ist, die mit dem Schlüsselwort case angegeben ›Case‹ heißt Fall und der Ausdruck mit case wird wie bei den werden. Markierungen für das goto mit einem Doppelpunkt beendet. Nur ganzzahlige Konstanten sind für case zugelassen. Wenn der Fall zutrifft, werden die dazugehörigen Befehle ausgeführt, ansonsten kommen die Befehle nach dem ›ausbrechen‹) wird der Block Schlüsselwort default dran. Mit break ( von switch verlassen.
A.3 Buch gelesen, was nun? In diesem kurzen Anhang am Ende des Buches benenne ich ein paar Bücher und Webseiten zu C, C++, Windowsprogrammierung, Spielen und Grafik. Weil die Computerwelt und das Internet Englisch spricht, möchte ich dich ermutigen, auch englischsprachige Titel zu lesen. C für Fortgeschrittene Bücher über C gibt es viele, aber das beste ist meiner Meinung nach das Buch der Erfinder von C: The C Programming Language, Brian W. Kernighan, Dennis M. Ritchie (Second Edition, Prentice Hall, 1998, 272 Seiten)
Dieses Buch gibt es auch auf Deutsch. Ich bin ein großer Fan von diesem Klassiker. Für Anfänger, die noch nie programmiert haben, ist es sicher nicht zu empfehlen, aber das Buch ist kurz, klar und komplett. Dieses Buch ist definitiv ein Beispiel dafür, dass Originalliteratur verständlicher sein kann als so manche gut gemeinte Nachahmung. C++ für Einsteiger C++ ist eine Erweiterung von C. Das doppelte Plus steht natürlich für den Inkrementoperator von C, der die Variable C um eins größer macht. Die ersten Schritte in der objektorientierten Programmierung mit C++ sind gar nicht so schwer. Falls du deine Kenntnisse in C vertiefen und gleichzeitig C++ lernen möchtest, kann ich dir das folgende Buch empfehlen: A.3 Buch gelesen, was nun?
357
Sandini Bib
C++-Programmierung lernen, Andr´e Willms (Addison-Wesley, 1998, 392 Seiten)
Von den 392 Seiten beziehen sich ungefähr 60 Seiten schwerpunktmäßig auf die objektorientierte Programmierung. Dieses Buch ist für (erwachsene) Anfänger geschrieben. Windowsprogrammierung In diesem Buch konnte ich aus verschiedenen Gründen nicht auf die Windowsprogrammierung mit Knöpfen, Menüs, Scrolleisten, Textelementen und vielem mehr eingehen. Ein professionelles Standardwerk zur Programmierung von Windows mit C (ohne C++ und ohne komfortable Entwicklungsumgebung) ist: Programming Windows, Charles Petzold (Fifth Edition, Microsoft Press, 1999, 1479 Seiten)
Dieses Buch ist als Nachschlagewerk erste Wahl, wenn du es in einer Bibliothek finden kannst. Sehr hilfreich ist aber auch die Webseite von Microsoft (www.microsoft.com). Dort findest du die neueste Version der Windows SDKReferenz und viele Artikel zu verschiedenen Themen. Neuere Entwicklungsumgebungen wie der Borland C++Builder erlauben es dir, Fenster und ihre Elemente wie bei einem Malprogramm mit der Maus zurechtzuklicken. Wenn du in BCB ein neues Projekt für eine Windowsanwendung startest, bekommst du eine ›Form‹. In BCBs großer Menüleiste findest du Knöpfe und andere Fensterelemente, die du mit der Maus in die Form hineinsetzen kannst. Programmiert wird in C++. Grafik und Spiele Wer Computerspiele spielt, weiß, dass kaum ein kommerzielles Spiel ohne DirectX oder OpenGL auskommt. Einige Gründe habe ich in Kapitel 11 erwähnt. DirectX und OpenGL sind kostenlos (www.microsoft.com/directx, www.opengl.org). Bevor du mit DirectX oder OpenGL programmieren kannst, musst du einige Erfahrung mit C gesammelt haben. Eine gute Idee ist es, sich eine der sehr schönen Einführungen (›Tutorials‹) anzuschauen, die man im Internet finden kann (www.gamedev.net, www.opengl.org). Das Microsoft DirectX SDK (zurzeit um die 140 MB) muss man nicht heruntergeladen haben, um mit dem Borland C++Builder z. B. Direct-Draw-Beispiele für 2D-Bitmapgrafik kompilieren zu können.
358
Anhang
Sandini Bib
Stichwortverzeichnis Symbole ! Negation, Verneinung 96, 110, 284 != ungleich 93 " Anführungszeichen 15, 47 # Preprocessoranweisung 19, 352 #define 130, 141, 191, 353
und const 247 #include 19, 171, 208, 353 % Divisionsrest 34, 37, 106, 180 % Formatanweisung 28 und TextOut 83 %% 35 %c Zeichen 48 %d ganze Zahl 28, 33, 35, 242 %e Kommazahl mit Exponent
242 Kommazahl 37, 242 Zeiger 281 String 50, 52 ganze Zahl ohne Vorzeichen 281 & Adressenoperator 279 und scanf 33, 286 & bitweises Und 238 && Und 95 ’ Apostroph 47, 49 ( ) runde Klammern 15, 38, 90, 119, 124 * Inhaltsoperator 279 * Multiplikation 34 + Addition 34 ++ Inkrement 40, 125, 298 , Komma 28, 29, 46, 127, 168 − Subtraktion 34 −− Dekrement 40 −> Strukturelement 293 . Strukturelement 293 . statt Komma 36 / Division 34 /* */ Kommentar 54 // Kommentar 54 ; Strichpunkt 14, 20, 29, 31, 124 %f %p %s %u
< kleiner 93 << bitweise links verschieben 238 <= kleiner oder gleich 93 = Zuweisung 30, 31 +=, −=, *=, /=, %= 39 == gleich 93 > größer 93 >= größer oder gleich 93 >> bitweise rechts verschieben 238 ?: Auswahloperator 100 [ ] eckige Klammern 45–47, 285 \ für Sonderzeichen 15, 47 \0 Nullzeichen 47, 49 \\ Schrägstrich 47, 302 \n Neuezeilezeichen 15, 47 ˆ bitweises Entweder-Oder 238 _ Unterstreichung 30 { } geschweifte Klammern 16, 91,
162, 291, 299, 336 | bitweises Oder 238, 307, 325 || Oder 95
Entweder-Oder 96, 115 ˜ bitweise Negation 238 0x Hexadezimalzahl 237
A Abtastrate 255 Adresse 278, 281 Rechenarten 285 Algorithmus 118 Alternative 98 Animation 228, 233, 258, 312 von Bitmaps 317 ANSI-C IX Anweisung siehe Befehl Argument 15, 165, 168 in Befehlszeile 301 variable Argumentenlisten 168 Array siehe Feld ASCII Code 239, 296 Ausdruck 31 mathematisch 34, 39
Sandini Bib
wahr und falsch 93 Auswertungsreihenfolge 38 auto 355 B Balkendiagramm 152 BCB siehe Borland C++Builder Bedingung 89 Befehl 17, 31 überspringen 89 wiederholen 118 Befehlsblock 17, 91, 119, 162, 173 Beispiele auf CD-ROM 8, 62 Bibliothek 3, 248 Bildschirmausgabe 14 Binärsystem 237 Bisektion 111, 141, 248 Bit 236 Bit-Feld 356 Bitoperator 238 Bitmap 311 Animation 317 erzeugen 332 Größe 315 laden 315 malen mit Paint 312 Maske 346 Puffer 326 transparenter Hintergrund 344 Bitoperator 238 Block siehe Befehlsblock Boolesche Algebra 93 Borland C++Builder IX, 2 Compiler IX, 3, 19 Debugger 3, 18, 120, 163 Editor 3, 4, 11, 23, 203 Hilfe 78 Installation IX, 3 Linker 3, 16, 170 neues Projekt 4, 310 Programm beenden 120 Programm kompilieren 7 Programm starten 7 Programmtexteingabe 4 Projektverwaltung 2, 11, 172 Textbildschirmanwendung 4 Windowsanwendung 310 break 121, 135 Buchstabe 47 klein und groß 16
360
Stichwortverzeichnis
Byte 236 C C VIII, X, 278, 357 ANSI-C IX Schlüsselwörter 355 C++ X, 5, 278, 357 case 356 Cast-Operator 245 char 239 Character 47, 239 Clipping 66, 329 Compiler IX, 3, 19 const 247 continue 121, 135 D Datei 2 .bmp 312 .c 5, 9 .cpp 5, 9, 63, 210 .exe 7 .h 19 .mak 4, 9 .wav 325 lesen und schreiben 301 Namen mit Schrägstrich 302 Datentyp 235, 239 const 247 struct 291 typedef 247 char, int, float, double 239 short, long 240 signed, unsigned 240 sizeof 240 Anzahl der Bytes 239, 240 Grenzwert 241 Umwandlung 243 Zeiger auf 280 Debugger 3, 18, 120, 163 default in switch-Anweisung 356 Default 101, 210, 307 Definition Feld 45 Funktion 162 großbuchstabig 208, 353 Makro 353 Struktur 291 und Deklaration 179
Sandini Bib
Variable 29 Zeiger 280 Definitionen, großbuchstabige BYTE 247 CALLBACK 208 CLK_TCK 141 COLORREF 72 CS_HREDRAW 307 CS_OWNDC 307 CS_VREDRAW 307 CW_USEDEFAULT 308 DWORD 247 EOF 302 FILE 301 HBITMAP 315 HBRUSH 76 HDC 64 HFONT 77 HINSTANCE 305 HIWORD 214, 274 HPEN 72 HWND 208 INT 247 LOWORD 214, 274 LRESULT 208 MSG 308 NULL 284 NULL_BRUSH 76 OPAQUE 76 PS_SOLID 72 RAND_MAX 106 RECT 292, 310 RGB 68, 264 SND_ASYNC 325 SND_FILENAME 325 SND_LOOP 325 SND_NOSTOP 325 SND_SYNC 325 SRCCOPY 316, 346 SRCPAINT 346 TA_CENTER 214 TRANSPARENT 76 UINT 208, 247 USEUNIT 11 VK_... 221 WHITE_BRUSH 307 WM_CHAR 221 WM_CLOSE 224 WM_CREATE 212 WM_DESTROY 209
WM_KEYDOWN 221 WM_LBUTTONDOWN 215 WM_MOUSEMOVE 219 WM_PAINT 208, 211 WM_QUIT 210, 211 WM_RBUTTONDOWN 215 WM_SIZE 212 WM_TIMER 227 WNDCLASS 306 WORD 247 WS_OVERLAPPEDWINDOW 308
Deklaration von Variablen 179 Dekrement 40 Device Context 64, 307 erzeugen 316 Dezimalsystem 237 DirectX 326, 358 do 124 DOS-Konsole 62 Befehlszeile 301 double 36, 239 Drehung 256 E Editor 3, 4, 11, 23, 203 Eingabe 33, 34, 52 Eingabeschleife 131, 143 einrücken von Programmtext 91 Element 45, 291 Ellipse 73 else 98 else if 101 enum 355 Executable 7 Exponent 242 extern 179 F Fakultät 182 Fallunterscheidung 101, 111, 208, 356 falsch 93 Farbe 68 Farbpalette 265 Farbtiefe 264, 313 Feld 44 Definition 45, 130 Elemente 45 Initialisierung 46 mehrdimensional 47, 189
Stichwortverzeichnis
361
Sandini Bib
Name als konstanter Zeiger 286 und Schleife 128 Fenster 61, 205 öffnen 306 Fensterklasse 306 Fensternachricht siehe Nachricht Fensterprozedur 206, 307 Fensterstil 307 Fließkommazahl 36 float 36, 239 Font 76, 84 for 124 Formatanweisung siehe %d usw. Fraktal 265 Frequenz 250 Funktion 161 Argument 15, 165 variable Argumentenlisten 168 Aufruf 15, 163 beenden 165 Definition 162 erlaubte Namen 30 Mathematik 248 mehrere Argumente 168 ohne Argumente 163 Prototyp 169 Rückgabewert 165 statisch 179 Zeiger auf 307 Funktionen, Standard C calloc 284 clock 139 cos 248 fclose 301 fgetc 301 fgets 303 fopen 301 fprintf 303 fputc 303 fputs 303 free 284 fscanf 303 getchar 17, 34, 48 und Tastaturpuffer 34, 145 gets 53 isspace 303 main 16 mit Argumenten 300 malloc 282 memcmp 296
362
Stichwortverzeichnis
memcpy 296 memset 296 pow 248 printf 14, 15, 28, 50, 85 puts 53 qsort 310 rand 104 realloc 284 scanf 33, 52 und & 33, 286 sin 248 sizeof 240 sprintf 85 sqrt 248 srand 105 sscanf 85 strcmp 296 strcpy 296 strlen 67 strstr 296 time 106, 139
Funktionen, Windows SDK Arc 74 BeginPaint 209 BitBlt 316 CreateCompatibleBitmap 332 CreateCompatibleDC 316 CreateFont 77, 84 CreatePen 72 CreateSolidBrush 76 CreateWindow 307 DefWindowProc 210 DeleteDC 317 DeleteObject 83 DispatchMessage 211, 308 DrawText 65, 310 Ellipse 73 EndPaint 209 GetAsyncKeyState 226 GetDC 209, 332 GetMessage 211, 308 GetObject 315 GetPixel 78 GetStockObject 76, 307 GetTickCount 141 InvalidateRect 211 KillTimer 227 LineTo 69 LoadCursor 307 LoadIcon 306
Sandini Bib LoadImage 315 MoveToEx 69 PeekMessage 332 PlaySound 324 PostQuitMessage 209 Rectangle 73 RegisterClass 306 ReleaseDC 209 SelectObject 72
Rückgabewert 83 SendMessage 224 SetBkColor 76 SetBkMode 76 SetPixel 68 SetTextAlign 214 SetTextColor 76 SetTimer 227 SetWindowText 214 ShowWindow 308 StretchBlt 317 TextOut 64, 76 mit Format 83 TranslateMessage 211, 221, 308 UpdateWindow 212 ValidateRect 209, 214, 332 WindowProc 208, 211, 307 WinMain 63, 304 G GB, Gigabyte 238 goto 356 Grafik 61, 62 Gültigkeitsbereich von Variablen 173, 175, 177 H Handle 64 Headerdatei 170, 203, 353 Hexadezimalsystem 237 Hilfe 78, 204 Histogramm 152 I
Variable 32 Zeiger 286 Inkrement 40, 357 und Zeiger 298 int 29, 239 Integerzahl siehe Zahl, ganze Zahl K kB, Kilobyte 238 Klammer eckig siehe [ ] geschweift siehe { } rund siehe ( ) Kommazahl 36 Kommentar 54 kompilieren 7 Konstante, symbolische 131 Koordinate 65 umrechnen ganzzahlig nach reell 272 reell nach ganzzahlig 252 Kosinus 256 Kreis 73, 256 L Labyrinth 187 Länge Name 30 Zeichenkette 67 Landkarte 334 Liniengrafik 69 Linker 3, 16, 170 Logik 95 long 240 M Makro 276, 353, 354 Mandelbrot-Menge 265 Markierung für goto 356 Maus 205, 215, 218 Maximum 130 MB, Megabyte 238 Minimum 130
if 90, 97
Index 45 Initialisierung 32 Feld 46 Struktur 336
N Nachricht 205, 308 Nachrichtenschleife 210, 308
Stichwortverzeichnis
363
Sandini Bib
Name von Variablen und Funktionen 30 Nullzeiger 284 O OpenGL 326, 358 Operatoren, Rangordnung von 352 Overflow 37, 184, 243 P Paint, Malprogramm 312 Palette 265 Parameter siehe Argument Periode 229 Pfeiltaste 222, 340 Pinsel 76 Pixel 62, 68, 147, 312 Pointer siehe Zeiger Potenz 236, 248 Preprocessor 19, 352 Anweisung siehe # Primzahl 135 Programm VIII ausführbar 7 beenden 120 kompilieren 7 Programmiersprache VIII, 278 Programmtext 2 einrücken 91 Leerstellen 21 Projektverwaltung 2, 11, 172 Prototyp 169, 204 und * 296 R Rangordnung von Operatoren 352 Rechenarten 34, 36 Rechteck 73 register 355 Rekursion 181, 184 return 17, 166 ohne Argument 167 Rollenspiel 108 Rotation 256 Rückgabewert 165 Rückruffunktion 208
364
Stichwortverzeichnis
S Schachbrett 150 Schleife 118 doppelt 134 endlos 121, 133 und Feld 128 Schlüsselwort 355 Scrolling 340 short 240 signed 240 Sinus 249 sizeof 240 und Zeiger 282 Sonderzeichen 47 Sound 324 Speicherplatz 26, 29, 44, 46, 238, 278 und malloc 282 static 177, 179 stderr 304 stdin 304 stdout 304 Stift 69 Stream 302 String siehe Zeichenkette struct 291 Struktur 278, 290 Bit-Feld 356 Definition 291 Element mit −> 293 Element mit . 291, 293 Feld von Strukturen 292 Größe 296 Initialisierung 336 kopieren 295 und typedef 291 Zeiger auf 292 switch 356 Syntax 22 T Tastatur 205, 221 Tastaturpuffer 34, 52 leeren 145 Textbildschirmanwendung 4, 62, 301 TextFenster.mak 9 Timer 227 Tonausgabe 324 Typ siehe Datentyp typedef 247, 291
Sandini Bib
Typenumwandlung 243
Würfel 103, 106, 153 Wurzel 248
U Umlaut 30 union 355 Union 355 unsigned 240 Ursprung 65 V Variable 28 automatisch 177 Definition 29 erlaubte Namen 30 extern 175, 178, 179 Gültigkeitsbereich 173, 175 Initialisierung 32, 178 lokal 173, 178 statisch 177 Vektor siehe Feld Vergleichsoperator 92 virtueller Tastencode 221 void 163 volatile 355 Vorzeichen 34 W wahr 93 Wave-Datei 325 while 119, 124 Windows, Software Development Kit 62 Windowsanwendung 310 Windowsprogrammierung 358 WinHallo.mak 62 WinMain.mak 206
Z Zahl Eingabe mit Tastatur 33 ganze Zahl 34, 239 Integerzahl siehe ganze Zahl Kommazahl 36 Overflow 37 reelle Zahl 239 Zehnersystem 237 Zeichen 47, 239 Zeichenkette 15, 49 Eingabe mit Tastatur 52 Feld aus Zeichenketten 299 kopieren 296, 297 Zeichenstift 69 Zeiger 277 Adressenoperator 279 als Funktionsargument 286 auf Funktion 307 auf Stringkonstante 287 auf Struktur 292 Definition 280 Inhaltsoperator 279 Initialisierung 284, 286 Nullzeiger 284 Rechenarten 285 Speicherplatz 286 und Feld 283, 285 und Inkrement 298 Zeigervariable 279 Zeit 139 Zeitgeber 227 Zufall 103, 152, 180 Zuweisungsoperator 30, 39 Zweiersystem 237
Stichwortverzeichnis
365
Sandini Bib
Copyright Daten, Texte, Design und Grafiken dieses eBooks, sowie die eventuell angebotenen eBook-Zusatzdaten sind urheberrechtlich geschützt. Dieses eBook stellen wir lediglich als persönliche Einzelplatz-Lizenz zur Verfügung! Jede andere Verwendung dieses eBooks oder zugehöriger Materialien und Informationen, einschliesslich •
der Reproduktion,
•
der Weitergabe,
•
des Weitervertriebs,
•
der Platzierung im Internet, in Intranets, in Extranets,
•
der Veränderung,
•
des Weiterverkaufs
•
und der Veröffentlichung
bedarf der schriftlichen Genehmigung des Verlags. Insbesondere ist die Entfernung oder Änderung des vom Verlag vergebenen Passwortschutzes ausdrücklich untersagt! Bei Fragen zu diesem Thema wenden Sie sich bitte an: [email protected] Zusatzdaten Möglicherweise liegt dem gedruckten Buch eine CD-ROM mit Zusatzdaten bei. Die Zurverfügungstellung dieser Daten auf unseren Websites ist eine freiwillige Leistung des Verlags. Der Rechtsweg ist ausgeschlossen. Hinweis Dieses und viele weitere eBooks können Sie rund um die Uhr und legal auf unserer Website
http://www.informit.de herunterladen