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
=
größer oder gleich
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 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
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 = 10) printf("Das macht 5 Euro pro Nase.\n"); if (n == 1 || n >= 3 && 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 int main() { int n; printf("Wie viele Leute seid ihr? scanf("%d", &n);
");
if (n = 10) printf("Das macht 5 Euro pro Nase.\n"); if (n >= 3) if (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 = 3) if (n = 3) { if (n 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 int main() { int i; printf("Sag mir eine positive ganze Zahl: scanf("%d", &i);
");
if (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 == != > < >= 0) && (j < 1) i − k > 0 i − k > 0 && j − 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 int main() { int i, j, n; printf("Zaehle bis: scanf("%d", &n);
");
j = 2; for (i = 1; i int main() { int i, j, n; printf("Zaehle bis: scanf("%d", &n);
");
for (i = 1, j = 2; i int main() { int i, n, summe; n = 10; summe = 0; for (i = 1; i 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 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 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 %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 == ’’) 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 == ’’. 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 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 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 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 = 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