skript zum kompaktkurs c mit beispielen in opengl

122
Skript zum Kompaktkurs C mit Beispielen in OpenGL Martin Kraus Manfred Weiler Dirc Rose Friedemann R¨ oßler Institut f¨ ur Visualisierung und Interaktive Systeme, Institut f¨ ur Informatik, Universit¨ at Stuttgart Zweite ¨ uberarbeitete Auflage Sommersemester 2006 (basierend auf der Orginalfassung vom Sommersemester 2002)

Upload: others

Post on 12-Jul-2022

6 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: Skript zum Kompaktkurs C mit Beispielen in OpenGL

Skript zum KompaktkursC mit Beispielen in OpenGL

Martin KrausManfred Weiler

Dirc RoseFriedemann Roßler

Institut fur Visualisierungund Interaktive Systeme,Institut fur Informatik,Universitat Stuttgart

Zweite uberarbeitete AuflageSommersemester 2006

(basierend auf der Orginalfassungvom Sommersemester 2002)

Page 2: Skript zum Kompaktkurs C mit Beispielen in OpenGL

Fur Tom

Page 3: Skript zum Kompaktkurs C mit Beispielen in OpenGL

Inhaltsverzeichnis

I Erster Tag 9

1 Einfuhrung in C 111.1 Was ist ein C-Programm? . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . 11

1.1.1 Das erste C-Programm . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . 111.1.2 Ubersetzen und Ausfuhren des Programms . . . . . . . . . . . . . . . .. . . . . . . . . 12

1.2 Definitionen von Variablen und Zuweisungen . . . . . . . . . . .. . . . . . . . . . . . . . . . . 121.2.1 Variablennamen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . 131.2.2 Datentypen und Konstanten . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . 131.2.3 Zuweisungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . 14

1.3 Operatoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . 141.3.1 Unare Operatoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . 151.3.2 Binare und ternare Operatoren . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . 161.3.3 Automatische Typumwandlungen . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . 171.3.4 Vorrang von Operatoren und Reihenfolge der Auswertung . . . . . . . . . . . . . . . . . 17

1.4 Kontrollstrukturen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . 181.4.1 Blocke . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . 181.4.2 if-Anweisungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . 181.4.3 switch-Anweisungen . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . 191.4.4 while-Schleifen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . 201.4.5 do-Schleifen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . 211.4.6 for-Schleifen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . 211.4.7 break-Anweisungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . 211.4.8 continue-Anweisungen . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . 221.4.9 goto-Anweisungen und Marken . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . 22

1.5 Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . 221.5.1 Funktionsdefinitionen . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . 221.5.2 Funktionsaufrufe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . 231.5.3 Funktionsdeklarationen . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . 23

1.6 Beispielprogramme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . 241.6.1 Einfache Textverarbeitung . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . 241.6.2 Mathematische Funktionen . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . 261.6.3 Beispiel: Fakultatsberechnung . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . . 26

Ubungen zum ersten Tag 31

3

Page 4: Skript zum Kompaktkurs C mit Beispielen in OpenGL

4 INHALTSVERZEICHNIS

II Zweiter Tag 33

2 Datenstrukturen in C 352.1 Zeiger . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . 35

2.1.1 Adress- und Verweisoperator . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . 352.1.2 Definitionen von Zeigern . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . 352.1.3 Adressen als Ruckgabewerte . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . 362.1.4 Adressen als Funktionsargumente . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . 372.1.5 Arithmetik mit Adressen . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . 382.1.6 Zeiger auf Zeiger . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . 392.1.7 Adressen von Funktionen . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . 39

2.2 Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . 402.2.1 Eindimensionale Arrays . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . 402.2.2 Initialisierung von Arrays . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . 412.2.3 Mehrdimensionale Arrays . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . 412.2.4 Arrays von Zeigern . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . 422.2.5 Ubergabe von Kommandozeilenargumenten . . . . . . . . . . . . . . . .. . . . . . . . . 43

2.3 Datenstrukturen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . 432.3.1 struct-Deklarationen und -Definitionen . . . . . . . . . . .. . . . . . . . . . . . . . . . 432.3.2 Zugriff auf Elemente von structs . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . 432.3.3 Zeiger auf structs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . 442.3.4 Initialisierung von structs . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . 442.3.5 Rekursive structs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . 44

2.4 Sonstige Datentypen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . 442.4.1 typedef . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . 442.4.2 Bitfelder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . 452.4.3 union . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . 452.4.4 enum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . 46

3 Einfuhrung in OpenGL 473.1 Was ist OpenGL? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . 473.2 Aus was besteht OpenGL? . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . 473.3 Wie sieht ein OpenGL-Programm aus? . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . 483.4 Syntax von OpenGL-Kommandos . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . 503.5 OpenGL als Zustandsautomat . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . 513.6 Rendering-Pipeline . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . 513.7 Verwandte Bibliotheken . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . 51

Ubungen zum zweiten Tag 52

III Dritter Tag 53

4 Der C-Ubersetzungsprozess 554.1 Der Praprozessor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . 55

4.1.1 Was ist ein Praprozessor? . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . 554.1.2 #include . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . 554.1.3 #define und #undef . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . 554.1.4 Makros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . 564.1.5 #if . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . 56

Page 5: Skript zum Kompaktkurs C mit Beispielen in OpenGL

INHALTSVERZEICHNIS 5

4.1.6 Der ##-Operator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . 574.2 Der Linker . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . 57

4.2.1 Was ist ein Linker? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . 574.2.2 extern . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . 584.2.3 static . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . 594.2.4 Deklarationsdateien . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . 59

4.3 make . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . 594.3.1 Was ist make? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . 594.3.2 Makefiles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . 604.3.3 makedepend . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . 62

5 Eigene Datentypen in C 635.1 Gekapselte Datentypen in C . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . 635.2 Referenzierte Datentypen in C . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . 65

6 Zeichnen geometrischer Objekte mit OpenGL 696.1 Ein Survival-Kit zum Zeichnen . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . 69

6.1.1 Loschen des Fensters . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . 696.1.2 Spezifizieren einer Farbe . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . 706.1.3 Leeren des OpenGL-Kommandobuffers . . . . . . . . . . . . . . .. . . . . . . . . . . . 706.1.4 Einfache Koordinatensysteme . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . 70

6.2 Spezifizieren von Punkten, Linien und Polygonen . . . . . . .. . . . . . . . . . . . . . . . . . . 706.2.1 Punkte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . 716.2.2 Linien . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . 716.2.3 Polygone . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . 716.2.4 Spezifizieren von Vertizes . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . 716.2.5 OpenGL Primitive . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . 71

6.3 Darstellen von Punkten, Linien und Polygonen . . . . . . . . .. . . . . . . . . . . . . . . . . . 726.4 Normalen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . 73

Ubungen zum dritten Tag 74

IV Vierter Tag 77

7 Standardbibliotheken 797.1 Die Standardbibliotheken . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . 79

7.1.1 Dateioperationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . 797.1.2 String-Operationen . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . 817.1.3 Operationen fur einzelne Zeichen . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . 817.1.4 Zeit- und Datumsfunktionen . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . 827.1.5 Erzeugung von Zufallszahlen . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . 827.1.6 Makro fur die Fehlersuche . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . 83

8 Einfuhrung in die GLUT-Bibliothek 858.1 Initialisierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . 85

8.1.1 Anfangszustand . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . 868.1.2 Graphikmodi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . 86

8.2 Eventverarbeitung/Event-getriebene Programmierung. . . . . . . . . . . . . . . . . . . . . . . . 878.2.1 Sequentielle Programmierung . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . 87

Page 6: Skript zum Kompaktkurs C mit Beispielen in OpenGL

6 INHALTSVERZEICHNIS

8.2.2 Event-getriebene Programmierung . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . 878.2.3 glutMainLoop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . 87

8.3 Fensteroperationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . 888.4 Menus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . 89

8.4.1 Untermenus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . 908.4.2 Sonstige Menu-Funktionen . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . 90

8.5 Callbacks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . 918.5.1 Ubersicht uber GLUT-Callbacks . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . 928.5.2 Animationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . 94

8.6 Zeichnen von Text . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . 958.7 Zeichnen geometrischer Objekte . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . . 958.8 State-Handling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . 96

Ubungen zum vierten Tag 97

V Funfter Tag 99

9 Entwicklungswerkzeuge 1019.1 Debugger . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . 1019.2 Analysetools . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . 1029.3 Sourcecode-Management . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . 1029.4 Dokumentationssysteme . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . 1029.5 Integrierter Entwicklungsumgebungen . . . . . . . . . . . . . .. . . . . . . . . . . . . . . . . . 102

10 Stilvolles C 10310.1 Was sind Kodierung-Standards? . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . . 10310.2 Wofur sind C-Kodierung-Standards gut? . . . . . . . . . . . .. . . . . . . . . . . . . . . . . . . 10310.3 Allgemeine Grundsatze . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . 10310.4 Kommentare . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . 10410.5 Namen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . 10410.6 Layout . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . 10510.7 Datei-Layout . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . 10510.8 Sprachgebrauch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . 10510.9 Datengebrauch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . 106

11 Beliebte Fehler in C 10711.1 Lexikalische Fallen . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . 107

11.1.1 = != == (oder:= ist nicht==) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10711.1.2 & und| ist nicht&&oder|| . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10811.1.3 Habgierige Analyse . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . 10811.1.4 Integerkonstanten . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . 10811.1.5 Strings und Zeichen . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . 108

11.2 Syntaktische Fallstricke . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . . . 10811.2.1 Funktionsdeklarationen . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . 10811.2.2 Rangfolge bei Operatoren . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . 10911.2.3 Kleiner; große Wirkung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11011.2.4 switch immer mitbreak . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11111.2.5 Funktionsaufrufe . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . 11111.2.6 Wohin gehort daselse ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111

Page 7: Skript zum Kompaktkurs C mit Beispielen in OpenGL

INHALTSVERZEICHNIS 7

11.3 Semantische Fallstricke . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . . 11211.3.1 Zeiger und Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . 11211.3.2 Zeiger sind keine Arrays . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . 11211.3.3 Array-Deklarationen als Parameter . . . . . . . . . . . . . .. . . . . . . . . . . . . . . . 11311.3.4 Die Synechdoche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . 11311.3.5 NULL-Zeiger sind keine leeren Strings . . . . . . . . . . . . . . . . . . . . . .. . . . . . 11311.3.6 Zahler und asymmetrische Grenzen . . . . . . . . . . . . . . .. . . . . . . . . . . . . . 11311.3.7 Die Reihenfolge der Auswertung . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . 11411.3.8 Die Operatoren&&, || und! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11411.3.9 Integeruberlauf . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . 11411.3.10 Die Ruckgabe eines Ergebnisses vonmain . . . . . . . . . . . . . . . . . . . . . . . . . 115

11.4 Der Praprozessor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . 11511.4.1 Leerzeichen und Makros . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . 11511.4.2 Makros sind keine Funktionen . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . 11511.4.3 Makros sind keine Anweisungen . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . 11611.4.4 Makros sind keine Typdefinitionen . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . 116

11.5 Tips&&Tricks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11611.5.1 Ratschlage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . 117

12 Ausblick auf dreidimensionales OpenGL 119

Ubungen zum funften Tag 121

Page 8: Skript zum Kompaktkurs C mit Beispielen in OpenGL

8 INHALTSVERZEICHNIS

Vorbemerkungen zur ersten Auflage

Dies ist ein Skript zu einem C-Kompaktkurs, der im Sommersemster 2002 an der Universitat Stuttgart gehaltenwurde. Es istnicht der Kurs selbst. Der Kurs ist viel besser, weil auch Fragen gestellt werden konnen! DiesesSkript ist also nur eine Erganzung, vor allem zum Nachschlagen. Trotzdem ist es parallel zum Kurs aufgebaut,damit die Orientierung leichter fallt.

Ich danke allen Beteiligten, insbesondere Manfred Weiler fur Kapitel 8 und Dirc Rose fur Kapitel 9 diesesSkripts. Besonderer Dank auch an Simon Stegmaier fur’s Korrekturlesen und die Betreuung derUbungen.

Martin Kraus, Stuttgart im Juli 2002

Vorbemerkungen zur zweiten Auflage

Der Aufbau und Inhalt des C-Kompaktkurses wurde zum Sommersemester 2006 teilweise uberarbeitet. Inhaltlichwurden nur wenige Erganzungen vorgenommen. Großere Umstellungen gab es dahingegen bei der Reihenfolgein der die einzelenen Themen behandelt werden und bei denUbungsaufgaben.

Ich mochte all meinen Vorgangern danken, die diesen Kurs konzipiert haben und ein sehr gutes Skript dazuverfasst haben, insbesonder naturlich Martin Kraus. Meinbesonderer Dank gilt auch Joachim Vollrath, der mirsehr viel Arbeit beim Erstellen der vorlesungsbegleitenden Folien abgnommen hat.

Friedemann Roßler, Stuttgart im Juli 2006

Page 9: Skript zum Kompaktkurs C mit Beispielen in OpenGL

Teil I

Erster Tag

9

Page 10: Skript zum Kompaktkurs C mit Beispielen in OpenGL
Page 11: Skript zum Kompaktkurs C mit Beispielen in OpenGL

Kapitel 1

Einfuhrung in C

1.1 Was ist ein C-Programm?

Ein”C-Programm“ ist ein in C geschriebenes Compu-

terprogramm. Die meisten von Menschen geschriebe-nen C-Programme fangen mit einer einfachen ASCII-Textdatei an, deren Name auf.c endet und damitdeutlich macht, dass sie in der Sprache C geschriebenwurde. Diese.c -Datei kann mit irgendeinem Textedi-tor wie emacs, xemacs, nedit, gedit, etc. geschriebenwerden und wird mit Hilfe eines C-Compilers in einausfuhrbares Programm ubersetzt. D.h. die Befehle inder Programmiersprache C werden in eine Maschinen-sprache ubersetzt, die der Prozessor versteht. Da jedeProzessorfamilie ihre eigene Maschinensprache spricht,muss es auch jeweils einen eigenen C-Compiler geben,und ein C-Programm muss fur jeden Prozessor, auf demes ausgefuhrt werden soll, neu ubersetzt werden. Dasist sehr lastig und wird noch lastiger, wenn sich die C-Compiler leicht unterscheiden. Deswegen ist die Stan-dardisierung von C eine so wichtige Sache.

C wurde in den 70er Jahren von Denis Ritchie undBrian W. Kernighan fur UNIX entworfen. Diese ur-sprungliche Form wird heute oft K&R-C genannt, aberkaum noch verwendet. C in seiner heutigen Form wirdoft ANSI-C genannt und wurde 1989 definiert. DieISO-Standardisierungen von 1990 und 1995 haben ver-gleichsweise wenigAnderungen mit sich gebracht. Derjungste ISO-Standard wurde 1999 verabschiedet. Erbringt durchaus einigeAnderungen mit sich, die fur ei-ne Einfuhrung in C aber kaum relevant sind.

1.1.1 Das erste C-Programm

Bevor wir uns naher mit den einzelnen Elementender Programmiersprache C auseinandersetzen, wollenwir zuachst an einem Beispiel, das den TextHallo

World ausgibt, den grundsatzlichen Aufbau eines C-Programms betrachten:

/ * Standardbibliotheken einfuegen * /#include <stdio.h>

/ * Beginn des Hauptprogramms * /int main(void){

/ * Aufruf der Standardfunktionprintf zur Ausgabe von"Hallo World" * /

printf("Hallo World!");

/ * Erfolgreiche Ausfuehrungrueckmelden * /

return(0);}

Jedes C-Programm besteht aus Funktionen. Notwen-dig fur jedes C-Programm ist die Funktionmain()Die main() -Funktion bildet das Hauptprogramm, al-le anderen Funktionen sind mit Unterprogrammen ver-gleichbar, wie sie auch in anderen Programmierspra-chen ublich sind.

Funktionen werden durch geschweifte Klammern inBlocke eingeteilt. Zu jeder offnenden Klammer{, dieeinen Block einleitet, muss eine schließende Klammer} gehoren, die den Block beendet. Die Funktionmainim obigen Beispiel besteht nur aus einem Block. Dieseraußere Block einer Funktion wird auch als Funktions-rumpf bezeichnet.

Innerhalb eines Block stehen die Anweisungen desC-Programms. Anweisungen werden in C durch Semi-kolon ; abgeschlossen. Ein einzelne Anweisung kannsomit uber mehrere Zeilen gehen.

Mit der Anweisungprintf(...) erfolgt die Aus-gabe einer Zeichenkette (hierHallo World! ) auf

11

Page 12: Skript zum Kompaktkurs C mit Beispielen in OpenGL

12 KAPITEL 1. EINFUHRUNG IN C

dem Bildschirm. Hierbei handelt es sich genau genom-men um den Funktionsaufruf einer Standardfunktion.Standardfunktionen sind fertige Funktionen, die zur C-Implementierung gehoren.

Mit Hilfe der return -Anweisung wird dem Auf-rufer zuruckgemeldet, ob das Programm erfolgreichausgefuhrt wurde. Der Wert 0 gibt in der Regel dieerfolgreiche Ausfuhrung an. Werte ungleich 0 werdenals Fehler interpretiert. Durch Ruckgabe unterschiel-dicher Fehlercodes, kann dem Aufrufer die Art desaufgetretenen Fehlers mitgeteilt werden.

Jede in einem Programm verwendete Funktion,muss deklariert werden. Dies gilt in der Regel auchfur Standardfunktionen. Die Deklarationen der Stan-darfunktionen sind in sogenannten Header-Dateienzusammengefasst. Die Deklarationen fur die Stan-dardfunktionen der Ein- und Ausgabe, unter anderenprintf , sind beispielsweise in der Headerfunktionstdio.h enthalten. Mit der ersten Zeile des obigenBeispielprogramms#include <stdio.h> wirddie Header-Dateistdio.h eingefugt und so diedarin deklarierten Funktionen dem Compiler bekanntgemacht.

Mit / * und * / werden Kommentare eingeschlos-sen. Diese werden vom C-Compiler ignoriert und sinddeshalb nicht Teil des ausfuhrbaren Programm. Trotz-dem sind sie sehr wichtig, weil sie an beliebiger Stel-le in einer.c -Datei Erklarungen, Hinweise, etc. gebenkonnen und so fest mit dem C-Code verbunden sind,dass sie normalerweise nur zusammen mit dem C-Codeverloren gehen. Wie im Beispielprogramm zu sehen ist,konnen Kommentare auch uber mehrere Zeilen gehen.In C gilt fast immer, dass das Zeilenende keine Be-deutung hat, deswegen werden im Folgenden nur dieAusnahmen von dieser Regel erwahnt. Wichtig ist au-ßerdem, dass C-Kommentare nicht geschachtelt werdenkonnen.

Die Beispiele in diesem Kurs sind so klein undim Text so detailliert beschrieben, dass meistens aufKommentare verzichtet wird. Das sollte aber nicht derNormalfall sein!

Die im Beispielprogramm gewahlte Art der Forma-tierung ist zwar in C ublich (und sinnvoll!), aber nichtnotwendig. Grundsatzlich konnen C-Programme for-matfrei geschrieben werden. Das obige Beispiel konntedaher auch folgendermaßen aussehen:

#include <stdio.h>int main(void){printf("Hallo World\n");}

Aber Einruckungen, Zeilenumbruche usw/ erhohen- genauso wie Kommentare - dieUbersichtlickeit undLesbarkeit von Programmen und es ist guter Program-mierstil Programme mit ausreichend Kommentaren zuversehen und durch Formatierungen die Struktur desProgrammablaufs deutlich zu machen

1.1.2 Ubersetzen und Ausfuhren des Pro-gramms

Um unser erstes C-Programm ausfuhren zu konnenmussen wir es, wie oben bereits erlautert, zunachstind Maschinencode ubersetzen. Eigentlich muss manhierfur nur den C-Compiler aufrufen. Unter UNIX-Varianten klappt das meistens mitcc (Default-C-Compiler) bzw.gcc (GNU-C-Compiler). Dem Com-piler muss man noch die.c -Datei angeben. Wur-de das Programm beispielsweise in eine Textda-tei halloWorld.c geschrieben, dann compiliertder Compileraufrufgcc halloWorld.c das Pro-gramm. Nach dem erfolgreichen Compilieren ist dasErgebnis ein Programm namensa.out im gleichenVerzeichnis und kann mit./a.out gestartet werdenkann.

Mochte man das fertige Programm anders alsa.out nennen, kann man mit der Kommandozeilen-option -o einen anderen Namen angeben. Zum Bei-spiel steht mit dem Befehlgcc halloWorld.c -ohalloWorld , das ausfuhrbare Programm nachher inder DateihalloWorld .

C-Compiler haben normalerweise viele weitereKommandozeilenoptionen, z. B.-ansi um sicher-zustellen, dass nur Programme nach ANSI-C klagloscompiliert werden, oder-Wall um alle Warnungendes Compilers auszugeben (wassehrsinnvoll ist). Al-lerdings sind diese Kommandozeilenoptionen meistensabhangig vom Compiler; mehr Informationen sind z. B.mit man gcc oderman cc zu erhalten.

1.2 Definitionen von Variablenund Zuweisungen

Eine Variable ist ein Stuck Speicherplatz des Compu-ters, der zum Speichern eines Wertes verwendet wird,

Page 13: Skript zum Kompaktkurs C mit Beispielen in OpenGL

1.2. DEFINITIONEN VON VARIABLEN UND ZUWEISUNGEN 13

und dem ein Namen gegeben wurde (der Variablen-name), um sich auf den aktuellen Wert der Variable(also das, was gerade im Speicher steht) beziehen zukonnen. Daraus folgen schon drei wesentliche Eigen-schaften von Variablen:

• Sie haben eine Adresse, die sagt, wo sie im Spei-cher zu finden sind.

• Sie haben einen Typ, der sagt, wie die Bits (imSpeicher eines Computers gibt es nur Bits) an derSpeicherplatzadresse zu interpretieren sind. (Text,ganze Zahl, Gleitkommazahl, etc.)

• Jede Variable hat außerdem eine bestimmte An-zahl von Bytes, die der Variablenwert im Speicherbelegt. In C wird diese Große immer durch denTyp festgelegt, d.h. alle Variablen eines Typs brau-chen gleichviele Bytes. Deswegen spricht manauch von der Bytegroße des Typs.

Variablen sind ein sehr grundlegendes Konzept. Re-chenmaschinen ohne Variablen sind ziemlich undenk-bar. Variablen erlauben es insbesondere Programme zuschreiben, die viele Falle (viele verschiedene Werte derVariablen) behandeln konnen, ohne fur jeden Fall eineigenes Programm schreiben zu mussen. Daraus folgtauch: Sobald in einem Programm mehrmals sehr ahn-liche Programmteile auftauchen, sollte man uberlegen,ob man die Programmteile nicht mit Hilfe von Varia-blen zu einem zusammenfassen kann, der alle Falle be-handelt.

In C mussen Variablen immer erst einmal”definiert“

werden, bevor sie verwendet werden. Dabei wird

• der Typ der Variable und

• der Name der Variablen festgelegt und

• zur Laufzeit des Programms ein Stuck Speicher-platz fur den Wert der Variable reserviert (wievieleBytes Speicherplatz benotigt werden, ergibt sichaus dem Typ).

Die einfachsten (und haufigsten) Variablendefinitio-nen folgen diesem Schema:

Datentyp Variablenname;

Zum Beispiel definiert

int i;

eine Variable mit Nameni vom Typ int . D.h. es wirdein Stuck Speicherplatz reserviert, dessen Inhalt im fol-genden Programmteil unter dem Nameni benutzt wer-den kann.

In der Praxis ist der haufigste Fehler bei der Va-riablendefinition ein vergessener Strichpunkt. Wenn erfehlt, verwirrt das den Compiler gelegentlich so sehr,dass er eine unsinnige Fehlermeldung ausgibt, in dernormalerweise auch noch die falsche Zeilennummer an-gegeben wird. Bei Syntax-Fehlermeldungen des Com-pilers gilt allgemein, dass man zunachst mal den Textder Meldung ignorieren und in den Zeilenvor der vomCompiler bemangelten Zeile nach einem Fehler suchensollte. (ImUbrigen ist die Interpretation von Compiler-Fehlermeldungen sowieso eine Kunst fur sich.)

Vor dem Datentyp konnen noch Modifizierer stehen:extern und static werden spater erklart;constdefiniert eine unveranderliche Variable (wird in C sel-ten genutzt, Konstanten werden haufiger mit Hilfe desC-Praprozessors Namen gegeben, was auch spater be-schrieben wird);auto , register , restrict undvolatile sind auch kaum von Bedeutung.

1.2.1 Variablennamen

... werden auchIdentifier genannt. Das erste Zeichenmuss ein Buchstabe (a, ..., z , A, ..., Z) oder Unter-strich ( ) sein, darauffolgende Zeichen durfen auchZiffern (0, ..., 9) sein. Groß- und Kleinschreibungwird unterschieden. Beispiele:i , index , Index ,index5 , ist leer , laenge des vektors ,laengeDesVektors . Anmerkung: In diesem Skriptwerden deutsche Variablennamen, Kommentare undFunktionsnamen benutzt, um sie einfacher von den eng-lischen Keywords und Bibliotheksfunktionen trennenzu konnen. Im Allgemeinen ist es aber viel sinnvoller,ein Programm vollstandig auf Englisch zu schreiben.Schließlich kann man praktisch nie ausschließen, dassnicht zumindest ein Teil eines Programms irgendwannvon jemand gelesen wird, der kein Deutsch kann.(Außer das Programm wird sofort unwiederbringlichgeloscht.)

1.2.2 Datentypen und Konstanten

Tabelle 1.1 zeigt einige Datentypen von C.char geht von (char)-128 bis (char)127 ,

short mindestens von (short)-32768 bis(short)32767 , int auch mindestens von-32768 bis 32767 und long von mindestens

Page 14: Skript zum Kompaktkurs C mit Beispielen in OpenGL

14 KAPITEL 1. EINFUHRUNG IN C

Typ Byte- Beschreibunggroße

char 1 ganz kleine Ganzzahlshort ≥ 2 kleine Ganzzahl

int ≥ 2 Ganzzahllong ≥ 4 große Ganzzahl

float ≥ 4 Gleitkommazahldouble ≥ 8 genaueresfloat

void der Nicht-Typ

Tabelle 1.1: Wichtige elementare Datentypen von C.

(long)-2147483648 bis (long)2147483647 .Gleitkommazahlen sind normalerweisedouble , z. B.3.1415 oder 0.31415e1 . Beispiele fur float ssind(float)3.1415 und3.1415f .

Ausserdem gibt esunsigned Variationen dieserTypen, z. B.unsigned char , die genauso viele By-tes wie die entsprechenden vorzeichenbehafteten Ty-pen brauchen, aber kein Vorzeichen besitzen. Dafurkonnen sie aber mehr postive Werte aufnehmen, z. B.unsigned char von 0 bis 255. Hexadezimale Zah-len konnen mit vorgestelltem0x angegeben werde,z. B. 0xff fur 255. Oktalzahlen mit vorgestellter0,z. B. 0377 fur 255. Schließlich gibt es noch Suffixeanalog zu3.1415f , die aber eigentlich unnotig sind.(Siehe die Typumwandlungen unten.)

Ein weiteres Beispiel furint s sind Zeichen, diemit einfachen Anfuhrungszeichen geschrieben werden,z. B. ’a’ oder’&’ . Der int -Wert ist dann der ASCII-Code dieses Zeichens.int s werden außerdem benutztum Wahrheitswerte (wahr und falsch) zu verarbeiten.Dabei steht0 fur falsch und alles ungleich0 fur wahr.

Die Bytegroßen sind nicht alle festgelegt, sondernhangen vom jeweiligen Compiler ab. Die Idee ist, dassint die Große hat, die der jeweilige Prozessor am be-sten/schnellsten verarbeiten kann. Die Bytegroße vonshort ist kleiner oder gleich und vonlong großeroder gleich der Große vonint . Zur Zeit heißt das mei-stens, dassshort 2 Bytes groß ist,int und long 4Bytes. Darauf kann man sich aber nicht verlassen. Wennein Programm wissen muss, wie groß ein Typ ist, kannes die Lange in Bytes mitsizeof( Typ) erhalten.Der Compiler setzt an dieser Stelle dann die von ihmgewahlte Bytegroße ein. (sizeof( Variablenname)undsizeof Variablennamefunktioniert auch und er-gibt die Bytegroße des Typs der angegebenen Variable.)

1.2.3 Zuweisungen

... werden als

Variablename= Wert;

geschrieben. Wieder ist der Strichpunkt am kritischsten,aber diesmal markiert er das Ende einer Anweisung.

Zuweisung bedeutet, dass derWert in den Speicher-platz der Variable geschrieben wird. Deswegen ist eswichtig, dass derWertrechts von= vom gleichen Typ istwie die Variable links von=, sonst wurde derWerteven-tuell gar nicht in den Speicherplatz der Variable passen.Beispiel:

int i;

i = 10;

Hier wird also ein Stuck Speicherplatz reserviert unddann die Zahl 10 (binar codiert) hineingeschrieben. Daslasst sich auch zusammenfassen als Variablendefinitionmit Initialisierung:

int i = 10;

Welche Zahl stand eigentlich direkt nach der Definiti-on int i; noch vor einer Zuweisung in dem Spei-cherplatz voni ? Das ist nicht definiert, d.h. es kann ir-gendwas gewesen sein. Dieser zufallige Wert kann sichauch noch bei jedem Programmablauf andern, was zusehr seltsamen Fehlern fuhren kann. Deswegen initiali-sieren manche Programmierer jede Variable gleich beider Definition. Andererseits sind Compiler inzwischenmeistens schlau genug, um den Programmierer zu war-nen, wenn der (zufallige) Wert einer Variable benutztwird, bevor die Variable auf einen Wert gesetzt wurde.

1.3 Operatoren

Aus Variablen und Konstanten lassen sich zusammenmit Operatoren und Klammern (( und ) ) arithme-tische, logische und andereAusdruckeerstellen. (EinAusdruckmit einem angehangten Strichpunkt ist eineAnweisung. Ausdruckedurfen uberall stehen wo

”Wer-

te“ erwartet werden.Anweisungendagegen konnennicht beliebig ineinander geschachtelt, sondern nur hin-tereinander aufgereiht werden.)

Es werden unare von binaren und ternaren Operato-ren unterschieden, je nach Anzahl der Werte, die einOperator verarbeitet. Unare Operatoren bekommen nureinen Wert, binare zwei und ternare drei.

Page 15: Skript zum Kompaktkurs C mit Beispielen in OpenGL

1.3. OPERATOREN 15

Operator Beschreibung+ Positives Vorzeichen- Negatives Vorzeichen

++ Inkrementierung-- Dekrementierung

! logisches Komplement∼ bitweises Komplement

( Typ) Typumwandlung& Adressoperator* Verweisoperator

sizeof( Typ/Wert) Bytegroße

Tabelle 1.2: Die unaren Operatoren.

1.3.1 Unare Operatoren

Tabelle 1.2 enthalt die unaren Operatoren. Positives undnegatives Vorzeichen sollten klar sein. Im Beispiel

int a = 10;int b;

b = -a;

wird b auf den Wert-10 gesetzt.Das logische Komplement macht aus einer0 einen

Wert ungleich0 und umgekehrt aus jedem Wert un-gleich 0 eine 0. Das bitweise Komplement invertiertalle Bits einer Zahl.

Die Typumwandlung (engl.:type cast) erlaubt eseinen Wert eines bestimmten Typs in einen anderen Typumzuwandeln, wie in dem Beispiel zu Datentypen mit(float) schon gezeigt. Es ist auch moglich Gleit-kommazahlen in Ganzzahlen (und umgekehrt) zu kon-vertieren:

int i;double d = 3.1415;

i = (int)d;

Da der Datentypint nur ganze Zahlen aufnehmenkann, werden bei der Typumwandlung hier die Nach-kommastellen abgeschnitten undi bekommt den Wert3.

Falls bei einer Zuweisung die Datentypen nicht zu-sammenpassen, wird automatisch eine Typkonvertie-rung vorgenommen, deshalb ist die explizite Typum-wandlung in diesem Beispiel nicht unbedingt notig. Dieexplizite Typumwandlung hat aber den Vorteil, den Le-ser des Programms daran zu erinnern, dass hier Infor-mation verloren geht.

Der Adressoperator& braucht als”Wert“ eine Varia-

ble, deren Speicheradresse er zuruckgibt. (Der Datentypeiner Adresse wird spater erklart.) Umgekehrt brauchtder Verweisoperator* als Wert eine Speicheradresseund gibt den Wert zuruck, der an dieser Adresse imSpeicher steht. Einfaches Beispiel:

int a = 2000;int b;

b = * (&a);

Mit int a = 2000; wird ein Stuck Speicher reser-viert und der Wert2000 hineingeschrieben.(&a) er-gibt die Speicheradresse und mit* (&a) wird der Wertan dieser Speicheradresse ausgelesen, also2000 . Die-ser Wert wird mitb = * (&a) der Variableb zuge-wiesen. (Also in den Speicher kopiert, der furb reser-viert wurde.) Das ist eigentlich gar nicht kompliziert,aber die Datentypen von Speicheradressen und Varia-blen von diesem Typ (sogenannte Zeiger oder Pointer)sind ein Thema fur sich, das spater behandelt wird.

Der sizeof -Operator gibt die Bytegroße eines Da-tentyps an, z. B. istsizeof(char) gleich 1. Er kannaber auch auf Werte angewendet werden und gibt danndie Große ihres Datentyps zuruck. (sizeof selbst gibteinen Wert vom Typsize t zuruck, der normalerwei-se gleichunsigned int ist.)

In- und Dekrementierung brauchen nicht einen Wertsondern eine Variable, die sie in- bzw. dekrementierenkonnen, z. B. hat die Variablea nach den Zeilen

int a = 2000;

++a;

den Wert2001 . (Wie erwahnt ist jeder Ausdruck (hier++a) mit nachfolgendem Strichpunkt eine Anweisung.)

Mit Ausnahme des In- und Dekrementoperatorskonnen alle unaren Operatoren in C nur als Prefix-Operatoren geschrieben werden, das heißt das Symbolfur den Operator steht direkt vor dem Wert, auf dender Operator wirkt. Dagegen konnen der In- und De-krementoperator sowohl als Prefix- als auch hinter einerVariable als Postfixoperator verwendet werden. Der Un-terschied zwischen den beiden Versionen liegt im Ruck-gabewert. Beim Prefixoperator wird zuerst die Varia-ble erhoht bzw. erniedrigt und der sich ergebende Wertzuruckgegeben, d.h. in

int a = 2000;int b;

Page 16: Skript zum Kompaktkurs C mit Beispielen in OpenGL

16 KAPITEL 1. EINFUHRUNG IN C

b = ++a;

bekommt auchb den Wert2001 . ++ und -- lassensich aber auch als Postfix-Operatoren verwenden:

int a = 2000;int b;

b = a++;

Hier gibt der++ Operator den ursprunglichen Wert vona zuruck, deshalb istb dann gleich2000 . Unabhangigvon dem zuruckgegebenen Wert erhoht++ aber denWert vona, deswegen ista wieder gleich2001 . Ent-sprechend funktioniert auch der dekrementierende--Operator.

Die De- und Inkrementierungsoperatoren sind dieeinzigen unaren Operatoren, die Variablen verandern.Alle anderen unaren Operatoren bekommen nur einenWert und berechnen daraus einen neuen Wert, den siezuruckgeben.

1.3.2 Binare und ternare Operatoren

Die binaren Operatoren stellen die wichtigsten Rechen-operationen zur Verfugung. Sie sind in Tabelle 1.3 zu-sammengefasst.

Funktionsaufrufe, Array- und Komponentenzugriffewerden spater diskutiert werden.

Die arithmetischen Operatoren funktionieren mei-stens wie zu erwarten:

int a;

a = 1 + 2 * 3;

setzta auf den Wert 7.Vergleiche ergeben0 (falsch) oder1 (wahr), die

mit den logischen Operatoren weiter verarbeitet werdenkonnen, die dann wieder0 oder1 ergeben.

Die bitweisen Operatoren verknupfen jeweils zweiBits an entsprechenden Stellen. Ein Bitshifta >> nentsprichta/2n (mit Abrundung). Analog bedeuteta<< n soviel wiea ∗ 2n.

Zuweisungen wurden schon besprochen; erganzendist noch zu sagen, dass auch der= Operator etwaszuruck gibt, namlich den zugewiesenen Wert. Deshalbist es moglich zu schreiben

int a;int b;

Operator Beschreibung() Funktionsaufruf[] Arrayzugriff

. Komponentenzugriffuber eine Strukturvariable

-> Komponentenzugriffuber eine Strukturadresse

* Multiplikation/ Division% Modulo (Divisionsrest)+ Addition- Subtraktion

>> Bitshifts nach rechts<< Bitshifts nach links

<,>,<=, >= Vergleiche== Gleichheit!= Ungleichheit

& bitweises Undˆ bitweises exklusives Oder| bitweises Oder

&& logisches Und|| logisches Oder?: Bedingungsoperator

= Zuweisung+=, -= , * =, ... Zuweisung mit

weiterem binaren Operator, Komma

Tabelle 1.3: Die binaren und ternaren Operatoren.

Page 17: Skript zum Kompaktkurs C mit Beispielen in OpenGL

1.3. OPERATOREN 17

a = (b = 42);

und soa undb beide auf den Wert42 zu setzen.Die Kombination von Zuweisungen und binaren

Operatoren erlaubt es z. B. statt

int a = 2000;

a = a + 1;

zu schreiben

int a = 2000;

a += 1;

und sich so das doppelte Ausschreiben des Variablen-namens zu sparen.

Der einzige ternare Operator ist der Bedingungsope-rator?: . Ein Beispiel:

int a = 42;int b;

b = (a > 99 ? a - 100 : a);

In der letzten Zeile wird zuerst der Teil vor? ausge-wertet. Wenn er wahr ist (ungleich0), dann wird derTeil zwischen? und: ausgewertet und zuruckgegeben,sonst der Teil nach: . In unserem Fall ista nicht großer99, deshalb wird der Wert vona also42 zuruckgege-ben.

Mit dem Bedingungsoperator lasst sich schnell dasMaximum von zwei Zahlen finden:a > b ? a : bgibt den großeren Wert vona undb zuruck.

Das Komma macht nichts anderes, als den linken unddann den rechten Wert auszuwerten, und dann den rech-ten Wert zuruckzugeben.

1.3.3 Automatische Typumwandlungen

... funktionieren meistens so wie man erwartet, d.h.wenn ein Operator zwei Werte unterschiedlichen Da-tentyps verknupft, dann wird zunachst der Wert des

”kleineren“ bzw. ungenaueren Typs in den

”großeren“

bzw. genaueren Datentyp umgewandelt und das Ergeb-nis in diesem Datentyp berechnet und zuruckgegeben

Die genauen Regeln unterscheiden sich aber in denverschiedenen Standards; deshalb ist es sinnvoll imZweifelsfall immer explizite Typumwandlungen anzu-geben.

Operator Reihenfolgeder Auswertung

() ,[] ,. ,-> von links! ,∼,++,-- ,( Typ) ,* ,&,sizeof von rechts

* ,/ ,% von links+,- von links

<<,>> von links<,<=,>,>= von links

==,!= von links& von linksˆ von links

| von links&& von links|| von links?: von rechts

=,+=,-= ,... von rechts, von links

Tabelle 1.4: Vorrang und Reihenfolge der Auswertungvon Operatoren. Operatoren, die weiter oben stehen, ha-ben hoheren Vorrang.

Eine Ausnahme wurde schon erwahnt: Der Zuwei-sungsoperator muss den Wert rechts von= in den Da-tentyp der Variable links von= konvertieren, wobeieventuell Information verloren geht.

1.3.4 Vorrang von Operatoren und Rei-henfolge der Auswertung

Bei Ausdrucken mit mehreren Operatoren ist der Vor-rang zwischen den Operatoren wichtig, z. B. ist1 + 2

* 3 aquivalent zu1 + (2 * 3) undnichtgleich(1+ 2) * 3, da * Vorrang vor+ hat. Außerdem kannes unter Umstanden wichtig sein, ob zunachst der Wertlinks vom Operator und dann der Wert rechts ausgewer-tet wird (Auswertung von links) oder umgekehrt (Aus-wertung von rechts). In Tabelle 1.4 sind Vorrang undReihenfolge der Auswertung der C-Operatoren aufgeli-stet.

Der Vorrang der Operatoren ist sehr wichtig undnicht immer klar. Fur die meisten Zweifelsfalle hilft fol-gende Merkregel: Hochste Prioritat haben binare Ope-ratoren, die ublicherweise ohne Leerzeichen geschrie-ben werden (a(b) , a[b] , a.b , a->b ), dann kommenunare Operatoren und dann binare Operatoren, die mitLeerzeichen geschrieben werden (a + b etc.). (Dazumuss man sich naturlich merken, welche Operatorenohne Leerzeichen geschrieben werden sollten, aber das

Page 18: Skript zum Kompaktkurs C mit Beispielen in OpenGL

18 KAPITEL 1. EINFUHRUNG IN C

ist etwas einfacher.)Unter den binaren Operatoren mit Leerzeichen kom-

men zunachst arithmetische Operatoren (mit den ubli-chen Regeln wie

”Punkt vor Strich“), dann Verglei-

che, dann bitweise binare Operatoren und schließlichlogische binare Operatoren. Zuletzt kommt der Bedin-gungsoperator, die Zuweisungen und das Komma.

In Zweifelsfallen sollten immer Klammern gesetztwerden, um die gewunschte Reihenfolge zu erzwingenund den Code moglichst lesbar zu machen. (Von demLeser eines C-Programms ist im Allgemeinen nicht zuerwarten, dass er besser C kann als der Autor des Pro-gramms.)

1.4 Kontrollstrukturen

1.4.1 Blocke

... von Anweisungen fangen mit{ an und werden mit}beendet. Blocke konnen auch geschachtelt werden. DerProgrammablauf fangt am Anfang des Blocks an undarbeitet normalerweise alle Anweisungen innerhalb desBlocks der Reihe nach ab.

In C konnen Variablen entweder außerhalb von Funk-tionen (extern) oder am Anfang eines Blocks innerhalbeiner Funktion definiert werden und sind dann nur in-nerhalb des Blocks gultig, d.h. der Speicherplatz wirdam Anfang des Blocks reserviert und, wenn der Pro-grammablauf das Ende des Blocks erreicht, wieder frei-gegeben.

Diese Variablen heißen deshalb auchlokale Varia-blen, weil ihreGultigkeit auf einen Block beschranktist. Das gilt auch, falls die Adresse einer inzwischenungultigen Variable irgendwie gerettet wurde. Dannkann zwar noch uber die Adresse der Wert aus demSpeicherplatz gelesen werden, aber dieser Wert kannbereits von anderen Variablen uberschrieben wordensein und hat dann mit dem ehemaligen Wert der ungulti-gen Variable nichts mehr zu tun.

Externe Variablen dagegen existieren unabhangigvom Programmablauf, denn ihr Speicherplatz wirdbeim Programmstart reserviert und erst am Program-mende wieder freigegeben. Externe Variablen konnenjederzeit und uberall angesprochen werden, deswegenist es gelegentlich sehr schwer nachvollziehbar, wo undwarum der Wert einer externen Variable verandert wur-de. Entsprechend ist das Programmieren mit externenVariablen nicht ganz ungefahrlich; aufgrund der un-eingeschrankten Verfugbarkeit aber sehr verfuhrerisch.

(Und dann gibt es nochglobaleVariablen, aber die wer-den spater besprochen.)

Um die Lesbarkeit zu erhohen sollte das Textlayoutdie Blocke durch Einruckungen deutlich machen. Bei-spiel:

{int i;

i = 0;{

int a = 5;

i = a;}

}

1.4.2 if-Anweisungen

... haben (in diesem Kurs) diese Syntax:

if ( Bedingungsausdruck){

Anweisungen}

Die Bedeutung ist ganz einfach: Wenn derBedingungs-ausdruckwahr ist (also ungleich0), dann werden dieAnweisungenausgefuhrt, sonst wird nach dem Blockweitergearbeitet. Zum Beispiel:

max = a;if (b > a){

max = b;}

Dies setzt die Variablemaxauf den großeren der beidenWerte vona und b. Eine Erweiterung vonif ist derelse -Block:

if ( Bedingungswert){

1. Anweisungen}else{

2. Anweisungen}

Page 19: Skript zum Kompaktkurs C mit Beispielen in OpenGL

1.4. KONTROLLSTRUKTUREN 19

Hier werden die1. Anweisungenausgefuhrt, wenn derBedingungsausdruckungleich0 ist und sonst die2. An-weisungen. Beispiel:

if (b > a){

max = b;}else{

max = a;}

Das setzt wieder das Maximum, aber diesmal brauchtes eine Zuweisung weniger, fallsb > a ist.

Falls ein Block nachif oderelse nur aus einer An-weisung besteht, konnen die geschweiften Klammernauch weggelassen werden:

if (b > a)max = b;

elsemax = a;

Das kann aber zu sehr unschonen Fehlern fuhren undsollte deshalb vermieden werden. Andererseits erlaubtdiese Regel auch einenelse if -Block. In

if (a > 100}{

a = 100;}else{

if (a < 0){

a = 0;}

}

besteht derelse -Block nur aus einerif -Anweisung,deswegen (und weil Zeilenumbruche hier keine Rollespielen) konnen wir das auch schreiben als

if (a > 100}{

a = 100;}else if (a < 0){

a = 0;}

if und bedingte Befehlsausfuhrung allgemein wirdgelegentlich als eines der wichtigsten Konzepte vonProgrammiersprachen angesehen. Allerdings gibt eshier die Gefahr mitif zwischen vielen ahnlichenFallen zu unterscheiden und fur jeden Fall eine geson-derte Behandlung zu programmieren. Falls sich derif -und derelse -Block stark ahnlich sehen, ist es im All-gemeinen recht wahrscheinlich, dass die beiden Blockezu einem zusammengefasst werden konnen (eventuellmit kleinerenif-else -Blocken).

1.4.3 switch-Anweisungen

Es kommt gelegentlich vor, dass eine Ganzzahl aufGleichheit mit einer Reihe von Konstanten getestet wer-den muss. Anstatt vieleif s zu benutzen, kann zu die-sem Zweck auch eineswitch -Anweisung verwendetwerden:

switch ( Ganzzahl-Ausdruck){

case 1. Konstante:1. Anweisungen

case 2. Konstante:2. Anweisungen

...default:

default-Anweisungen}

Dabei wird zunachst derGanzzahl-Ausdruckausgewer-tet und das Ergebnis mit denKonstantenverglichen.Je nach Ausgang der Vergleiche springt der Program-mablauf dann zu einercase -Marke. Falls das Ergbniskeiner der angegebenenKonstantenentspricht, wird zurdefault -Marke gesprungen, falls eine solche angege-ben wurde, was nicht unbedingt notig ist.

Hier ist wichtig, dass der Programmablauf nachdem Sprung alleAnweisungennach der entsprechendencase -Marke abarbeitet, auch dieAnweisungennachfolgendencase -oderdefault -Marken! (Diese Mar-ken werden dabei einfach ignoriert.) Deshalb verwen-det das nachste Beispielbreak -Anweisungen, um zumEnde desswitch -Blocks zu springen. (break wirdgleich genauer erklart.)

switch (wochentag){

case 0:printf("Montag");break;

Page 20: Skript zum Kompaktkurs C mit Beispielen in OpenGL

20 KAPITEL 1. EINFUHRUNG IN C

case 1:printf("Dienstag");break;

case 2:printf("Mittwoch");break;

case 3:printf("Donnerstag");break;

case 4:printf("Freitag");break;

case 5:printf("Samstag");break;

case 6:printf("Sonntag");break;

default:printf("?");

}

1.4.4 while-Schleifen

Schleifen sind eine Moglichkeit sich wiederholendeProgrammteile zusammenzufassen. Als Beispiel solldie Berechnung der Summe der ersten sechs Quadrat-zahlen dienen (in C wird meistens ab 0 gezahlt):

int summe;

summe = 0 * 0 + 1 * 1 +2 * 2 + 3 * 3 +4 * 4 + 5 * 5;

Diese Art der Formulierung hat ein paar Mangel: Sieist unter Umstanden sehr aufwendig einzutippen, des-wegen passieren leicht Fehler, die auch noch schwerzu finden sind. Außerdem ist sie sehr inflexibel, weildieser Programmteil offenbar nichts anderes kann alsdie ersten funf Quadratzahlen zu addieren. Um zu einerSchleifenformulierung zu kommen, mussen wir erstmaldie Berechnung umformulieren:

int summe = 0;int i = 0;

summe = summe + i * i;i = i + 1;summe = summe + i * i;i = i + 1;

summe = summe + i * i;i = i + 1;summe = summe + i * i;i = i + 1;summe = summe + i * i;i = i + 1;summe = summe + i * i;i = i + 1;

(Statt summe = summe + i * i; konnten wirnaturlich auchsumme += i * i; schreiben; ent-sprechendi += 1; statt i = i + 1; oder nochkurzer:i++; )

Diese Formulierung hat den Vorteil, dass der sichwiederholende Programmteil sofort ins Auge springt.Mit einer einfachenwhile -Schleife lasst sich dieWiederholung ganz einfach herstellen. Die Syntax derwhile -Schleife ist:

while( Bedingungsausdruck){

Anweisungen}

und bedeutet, dass, falls derBedingungsausdruckun-gleich 0 (also wahr) ist, dieAnweisungenausgefuhrtwerden. Danach springt der Programmablauf wiederzuruck und berechnet denBedingungsausdruckneu.(Das ist notig, weil sich Variablenwerte inzwischengeandert haben konnen.) Falls der neue Wert desBe-dingungsausdruckwieder ungleich0 ist, werden dieAnweisungen nochmal ausgefuhrt. Das geht solange,bis derBedingungsausdruckgleich0 (falsch) ist; dannspringt der Programmablauf an das Ende des Anwei-sungsblocks und macht da weiter.

Damit sieht unsere Berechnung der Summe der er-sten funf Quadratzahlen so aus:

int summe = 0;int i = 0;

while (i <= 5){

summe = summe + i * i;i = i + 1;

}

Das sieht nicht so aus, als ob es sehr viel kurzer wareals unsere erste Version,aberwir konnen hiermit auchebenso einfach die Summe der ersten 100 Quadratzah-len berechnen. Wenn wir die5 durch eine Variable er-setzen, konnen wir sogar die Summe von einer Anzahl

Page 21: Skript zum Kompaktkurs C mit Beispielen in OpenGL

1.4. KONTROLLSTRUKTUREN 21

von Quadratzahlen berechnen, die wir noch gar nichtkennen, wenn wir das Programm schreiben, sondern dieerst beim Programmablauf bekannt ist.

1.4.5 do-Schleifen

... gehorchen der Syntax

do{

Anweisungen}while ( Bedingungsausdruck);

Die Bedeutung ist ahnlich wie beiwhile , aller-dings werden dieAnweisungenmindestens einmal aus-gefuhrt. Danach wird derBedingungsausdruckausge-wertet und, falls er ungleich0 (also wahr) ist, dieSchleife wiederholt.do wird verhaltnismaßig selten ge-nutzt.

1.4.6 for-Schleifen

... folgen der Syntax

for ( Startausdruck; Bedingungsausdruck;Iterationsausdruck){

Anweisungen}

(normalerweise ist derfor( ...) -Teil in einer Zeile) undsind aquivalent zu

Startausdruck;while ( Bedingungsausdruck){

AnweisungenIterationsausdruck;

}

Das ist eigentlich alles, was es zufor zu sagen gibt.Vielleicht noch ein Beispiel, statt

x = 0;while (x < 400){

y = 0;while (y < 300){

glColor3f(x / 300.0,

y / 400.0, 1.0);glVertex2i(x, y);y = y + 1;

}x = x + 1;

}

konnen wir jetzt kurzer schreiben:

for (x = 0; x < 400; x++){

for (y = 0; y < 300; y++){

glColor3f(x / 300.0,y / 400.0, 1.0);

glVertex2i(x, y);}

}

wobei x = x + 1 durch x++ und y = y + 1durchy++ ersetzt wurde. Die Formulierung mitfor istzumindest kompakter und hat sich so sehr eingeburgert,dass sie fur erfahrene C-Programmierer auch etwasleichter lesbar ist.

Anfanglich ist diefor -Schleife aber vielleicht etwasschwieriger zu verstehen, vor allem weil nicht deutlichwird, ob die Schleife mindestens einmal durchlaufenwird. (Wird sie nicht!) Das ist auch der wesentliche Un-terschied zwischenwhile undfor auf der einen Seiteund derdo-Schleife auf der anderen Seite.

1.4.7 break-Anweisungen

... konnen nicht nur hinter das Ende einerswitch -Anweisung springen, sondern springen allgemein hin-ter das Ende der innersten umgebendenwhile -, for -, do-Schleife oder ebenswitch -Anweisung, je nachdem wo diebreak -Anweisung steht. Dabei wird dieAusfuhrung der entsprechenden Schleife abgebrochen.Zum Beispiel:

x = 0.5;

for (i = 0; i < 1000; i++){

x = a * x * (1.0 - x);if (x > 1.0){

break;}

}

Page 22: Skript zum Kompaktkurs C mit Beispielen in OpenGL

22 KAPITEL 1. EINFUHRUNG IN C

Hier wird 1000mal die Iterationsvorschriftx ← a · x ·(1 − x) mit Startwert0.5 angewendet. Wenn aberx ir-gendwann großer als1.0 ist, wird diese Schleife durchdie break -Anweisung beendet. Das ist fast gleichbe-deutend mit

x = 0.5;

for (i = 0;i < 1000 && x <= 1.0;i++)

{x = a * x * (1.0 - x);

}

Der Unterschied ist nicht ganz einfach zu sehen (mitwhile stattfor ware er klarer). Er besteht darin, dassfalls x jemals großer als1.0 wird, im zweiten Fall ein-mal mehr eini++ ausgefuhrt wird. (Außerdem ergibtsich fur einen Startwert vonx großer als1.0 ein unter-schiedliches Verhalten .)

1.4.8 continue-Anweisungen

... springen ahnlich wiebreak an das Ende der in-nersten umgebendenwhile -, for - oderdo-Schleife,aber fahren dann in der normalen Schleifenabarbei-tung fort anstatt die Schleife abzubrechen. In derfor -Schleife wird auch die Iterationsanweisung ausgefuhrt.continue -Anweisungen werden etwas seltener alsbreak -Anweisungen eingesetzt.

1.4.9 goto-Anweisungen und Marken

Mit goto kann der Programmablauf vor einer beliebi-gen Anweisung der gleichen Funktion fortgesetzt wer-den. Dazu muss vor dieser Anweisung eine Marke ge-setzt werden, deren Name denselben Einschrankungenwie Variablennamen unterliegt. Marken selbst werdendurch einen nachfolgenden Doppelpunkt markiert. ImBeispiel

for (x = 0; x < 400; x++){

for (y = 0; y < 300; y++){

...if (fehler){

goto Fehlerbehandlung;}

}}...

Fehlerbehandlung:...

werden die beiden geschachtelten Schleifen durch einenSprung zur MarkeFehlerbehandlung beendet.

Es gibt wenige Grundegoto zu verwenden, aberviele es zu lassen. (Stichwort: Spaghetti-Code durchverschlungene Sprunge.) Das Beispiel zeigt zwei Aus-nahmen: Reaktion auf Fehlersituationen und das Ab-brechen von verschachtelten Schleifen, was mit einerbreak -Anweisung nicht moglich ist, dabreak nurdie innerste Schleife abbricht.

1.5 Funktionen

1.5.1 Funktionsdefinitionen

Funktionen werden meistens nach dem Schema

Ruckgabetyp Funktionsname( ArgumenttypArgumentname, ... ){

...}

definiert (und zwar immerextern, also nicht innerhalbvon anderen Funktionen oder Blocken). Zum Beispiel:

int verdopple(int eingabe){

int ausgabe;

ausgabe = 2 * eingabe;

return(ausgabe);}

Das ist so zu lesen: Hier wird eine Funktion namensverdopple definiert, die einen Wert vom Typintzuruckgibt und ein Argument vom Typint erwar-tet. Innerhalb der Funktionsdefinition ist eine Variablenamenseingabe definiert, die bei einem Funktions-aufruf mit dem Argumentwert initialisiert wird. Au-ßerdem wird eine weitere Variable vom Typint na-mensausgabe definiert, der der verdoppelte Wert des

Page 23: Skript zum Kompaktkurs C mit Beispielen in OpenGL

1.5. FUNKTIONEN 23

Arguments zugewiesen wird. Mit Hilfe derreturn -Anweisung wird dieser verdoppelte Wert dann zuruck-gegeben.

Die in der Funktion definierten lokalen Variablen(einschließlich der

”Argumentvariablen“) verlieren ihre

Gultigkeit (ihre Speicherplatzreservierung), sobald derProgrammablauf das Ende der Funktion erreicht. Zweiwichtige Punkte dazu:

Erstens: Innerhalb einer Funktion sind nur die dortdefinierten, lokalen Variablen und die im Modul de-finierten externen Variablensichtbar, konnen also be-nutzt werden. Alle lokalen Variablen anderer Funktio-nen konnen nicht uber ihren Namen benutzt werden.Deshalb mussen alle Informationen, die die Funkti-on benotigt, entweder uber externe Variablen (nur inAusnahmefallen empfehlenswert!) oder als Argumenteubergeben werden.

Zweitens:Immerwenn der Programmablauf an denAnfang der Funktion kommt, wirdneuerSpeicherplatzfur die lokalen Variablen der Funktion angelegt. Unddas auch wenn schon einmal fur diese Variablen Spei-cherplatz reserviert wurde, der noch nicht wieder frei-gegeben wurde! In diesem Fall existieren dann mehre-re Variablen (an verschiedenen Positionen im Speicher)mit gleichen Namen. Aufgrund der Sichtbarkeitsregelist aber immer nur genau eine davon ansprechbar.

Durch das Voranstellen des Modifizierersstatic(Bsp. static int a; ) kann verhindert werden,dass eine lokale Variable bei jedem Funktionsaufruf neuerzeugt wird. Diese sogenanntenstatischenVariablenbehalten ihren Wert auch zwischen Funktionsaufrufenund konnen z.B. Werte aus vorhergehenden Aufrufenspeichern, ohne extern definierte Variablen verwendenzu mussen. Die Verwendung vonstatic fur externeVariablen und fur Funktionen hat eine ganz andereBedeutung. Diese wird allerdings erst in Kapitel 4.2.3beprochen.

Falls eine Funktion keine Argumente braucht, mussals Argumenttypvoid (ohne Argumentname) angege-ben werden. Falls sie keinen Ruckgabewert hat, mussvoid als Ruckgabetyp angegeben werden. Zum Bei-spiel:

void tue_nichts(void){

return;}

Die return -Anweisung am Blockende ist dabei nichtnotwendig, aber auch nicht verboten.

1.5.2 Funktionsaufrufe

Nach dieser Funktionsdefinition konnte an anderer Stel-le im Programm ein Funktionsaufruf stehen, z. B. so

int d;

d = verdopple(100);

Bei dem Aufruf von verdopple(100) wirdzunachst das Argument ausgewertet, was hier einfachist, weil es gerade100 ist. Jetzt erfolgt ein Sprungin die Funktion. Hier wird eine Variable namenseingabe definiert (Speicherplatz reserviert) undauf den Argumentwert100 gesetzt. Dann wird eineVariable ausgabe definiert. Anschließend wird derWert der Variableeingabe mit 2 multipliziert unddas Ergebnis in der Variableausgabe gespeichert.Die return -Anweisung liest diesen Wert aus derVariableausgabe und springt damit wieder zuruck indie Zeile d = verdopple(100); . Hier wird derRuckgabewert200 dann in der Variabled gespeichert.

Allgemein sieht ein Funktionsaufruf so aus:

Funktionsname( Argument, ...)

Funktionsaufrufe sind ganz normale Ausdrucke undkonnen deshalb durch Anhangen eines Strichpunkts zuAnweisungen gemacht werden. Die Klammern in ei-nem Funktionsaufruf konnen mit etwas Phantasie alsbinarer Operator aufgefasst werden, dessen linker WertderFunktionsnameist und dessen rechter Wert die ListederArgumenteist.

Wichtig ist, dass die Klammern auch gesetzt werden,wenn die Funktion gar keine Argumente erwartet. DieKlammern bleiben dann einfach leer und kennzeichnen,dass es sich um einen Funktionsaufruf handelt. (Was derFunktionsname alleine bedeutet, wird spater erklart.)

1.5.3 Funktionsdeklarationen

Falls der Funktionsaufruf vor der Funktionsdefinitionsteht, bekommt der Compiler beimUbersetzen des Pro-gramms ein Problem, weil er einen Funktionsaufrufubersetzen soll, ohne die Funktion zu kennen. Um trotz-dem solche Funktionsaufrufe zu ermoglichen, muss dieFunktion dem Compiler bekannt gemacht, sprich dekla-riert, werden. Dazu dient der Funktionsprototyp, der nuraus dem Funktionskopf der Funktionsdefinition (allesaußer dem Anweisungsblock) gefolgt von einem Strich-punkt besteht. In unserem Beispiel ware das:

Page 24: Skript zum Kompaktkurs C mit Beispielen in OpenGL

24 KAPITEL 1. EINFUHRUNG IN C

int verdopple(int eingabe);

Das gleiche Problem tritt auch bei Bibliotheksfunk-tionen auf: Jede Bibliotheksfunktion, die benutzt wer-den soll, muss vorher durch einen Funktionsprototypenbekannt gemacht werden. Deshalb werden Funktions-prototypen (mit anderen Deklarationen) in sogenanntenHeader- oder.h -Dateien zusammengefasst, damit siemittels der#include -Anweisung in ein C-Programmaufgenommen werden konnen. Zum Beispiel gibt es ei-ne Standard-Headerdatei namensstdio.h in der un-ter anderem die wichtige Funktionprintf zur for-matierten Ausgabe deklariert wird. Wennprintf ver-wendet werden soll, muss deshalb im Programm (nor-malerweise am Anfang) eine Zeile

#include <stdio.h>

stehen. Wichtig dabei ist, dass das# am Zeilenanfangstehen sollte. (Es ist hier nur eingruckt, um es besservom Haupttext zu trennen.)

1.6 Beispielprogramme

Am Ende dieses Kapitels werden noch einige Beispiel-programme besprochen, die alle mit den bisher vor-gestellten Elementen der Sprache C realisiert werdenkonnen. Außerdem werden noch einige nutzliche Bi-bliotheksfunktionen eingefuhrt.

1.6.1 Einfache Textverarbeitung

In Kapitel 1.1.1 wurde bereits die Funktionprintfzur Ausgabe von Zeichenketten auf der Standard-ausgabe vorgestellt. Sie ist ein Bestandteil der C-Standardbibliothek und wird in der Header-Dateistdio.h deklariert. Im Folgenden wird diese Funkti-on genauer erlautert und weitere hilfreiche Funktionenzur Ein- und Ausgabe von Text vorgestellt.

Aus- und Eingabe einzelner Zeichen

Die Funktionputchar() gibt ein einzelnes Zeichenauf der Standardausgabe aus. Beispielsweise erscheintmit

int c = ’a’;

putchar(c);

das Zeichen a auf dem Bildschirm.Mit getchar() wird ein einzelnes Zeichen (immer

das nachste im Eingabestrom) von der Standardeingabeeingelesen. Nach der Ausfuhrung der folgenden Zeilenenthalt die Variable c also das nachste Eingabezeichen.

int c;

c = getchar();

Mit diesen zwei einfachen Funktionen konnen bereitssehr viele interessante Programme zur Verarbeitung vonEin- und Ausgabestromen realisiert werden. Das fol-gende Beispielprogramm kopiert z.B. die Standardein-gabe Zeichen fur Zeichen auf die Standardausgabe.

#include <stdio.h>

int main(void){

int c;

c = getchar();while (c != EOF){

putchar(c);c = getchar();

}

return(0);}

EOF ist eine Konstante und wird von getchar()zuruckgegeben, wenn das Ende einer Datei (end of fi-le) bzw. des Eingabestroms erreicht wurde. Hierdurchwird auch klar warum die Variablec nicht vom Typchar sonder vom Typint ist. c muss namlich großgenug sein, damit es nicht nur alle moglichen Zeichenspeichern kann, sondern auch den WertEOF.

Da die Standardausgabe in der Regel gepuffert ist,werden durch obiges Beispielprogramm die eingege-benen Zeichen normalerweise nicht sofort nach derEingabe ausgegeben, sondern z.B. erst beim auftre-ten eines Zeilenumbruchs. Das Ende einer Datei (EOF)kann auf der Standardeingabe unter Unixsystemen ubri-gens durch die TastenkombinationSTRG + Dsimu-liert werden.

Da unter Unixsystemen mit Hilfe des<- und des>-Zeichens der Inhalt einer Datei auf die Standardeingabebzw. die Standardausgabe auf eine Datei umglenkt we-ren kann, konnen mit dem obigen Programm auch Da-

Page 25: Skript zum Kompaktkurs C mit Beispielen in OpenGL

1.6. BEISPIELPROGRAMME 25

teien ausgegeben bzw. Texte in eine Datei geschriebenwerden.

Wenn das ausfuhrbare Programm z.B. den NamencopyChars hat, kann man mit

copyChars <test

den Inhalt der Dateitest auf dem Bildschirm ausge-ben. Mit

copyChars <quelle >ziel

wird der Inhalt der Dateiquelle in die Dateizielgeschrieben.

Formatierte Textausgabe

Mit der Funktionprintf konnen auch Werte von Va-riablen ausgegeben werden, was sehr praktisch ist, umnach Fehlern in Programmen zu suchen. Dazu muss indem ersten Argument vonprintf (dem Formatstring)fur jede Variable einFormatelementangegeben werden,z. B. %d fur int s, %ld , fur long s, %g fur float sunddouble s und%s fur Zeichenketten (Strings oderchar -Arrays, werden spater genauer behandelt). DieWerte der Variablen mussen als weitere Argumente anprintf ubergeben werden und zwar in der Reihenfol-ge, wie sie durch die Formatelemente bestimmt ist. ZumBeispiel erzeugt das Programm

#include <stdio.h>

int main(void){

int i = 42;long o = 1000000000;double x = 3.1415;

printf("i=%d, o=%ld, d=%g\n",i, o, x);

printf("dies %s ein string\n","ist");

return(0);}

die Ausgabe

i=42, o=1000000000, d=3.1415dies ist ein string

Escapesequenz Bedeutung\a Gongzeichen (Bell)\b Backspace\f Seitenvorschub (Formfeed)\n Zeilenendezeichen (Newline)\r Zeilenrucklauf (Carriage Return)\t horizontaler Tabulator\v vertikaler Tabulator\\ das Backslashzeichen selbst\’ Apostroph\" Anfuhrungszeichen\? Fragezeichen

\ooo numerische Angabe eines Zeichensin Oktal

\xhh numerische Angabe eines Zeichensin Hexadezimal

Tabelle 1.5: Escapesequenzen

Um in einer Zeichenkette auch spezielle Kontrollzei-chen, z.B. das Ende einer Zeile, darstellen zu konnen,muss man sogenannte Escapesequenzen verwenden.Diese werden durch zwei Zeichen reprasentiert, wobeidas einleitende Zeichen ein Backslash (\ ) ist. EinBeispiel fur solch eine Escapesequenz, das Zeilenen-dezeichn \n taucht bereits in obigen Beispiel auf.Mit \t wird ein Tabulatorzeichen gesetzt. In Tabelle1.5, werden alle moglichen Escapsequenzen aufgefuhrt.

Bei Zeichenketten tritt das Problem auf, dass dasZeilenende doch wichtig ist, denn innerhalb einer inAnfuhrungszeichen gesetzten Zeichenkette darf keinZeilenumbruch stattfinden. Die Losung ist, dass Zei-chenketten hintereinander zu einer zusammengefasstwerden, also"Halli" "hallo" das gleiche wie"Hallihallo" ist. Wenn eine Zeichenkette so auf-geteilt wird, darf aber auch ein Zeilenumbruch zwi-schen den Teilen stehen, z. B.:

#include <stdio.h>

int main(void){

int i = 42;

printf("Hier kommt""ein int: %d und""ein double: %g\n",i, 3.1415);

Page 26: Skript zum Kompaktkurs C mit Beispielen in OpenGL

26 KAPITEL 1. EINFUHRUNG IN C

return 0;}

Formatierte Texteingabe

Mit getchar() kann man einzelne Zeichen von derStandardeingabe einlesen. Fur Programme bei denendie Standardeingabe fur die Interaktion mit dem An-wender genutzt wird, z.B. um Zahlenwerte fur Berech-nungen einzulesen, ist die Verwendung dieser Funktionjedoch relativ aufwendig.

Deshalb stellt die Standardbibliothek von C auchFunktionen zur Texteingabe, bei denen die Texteingabedirekt in eine oder mehrere Variablen vom gewunsch-ten Typ eingelesen wird. Eine dieser Funktionen ist diescanf . Die Verwendung vonscanf ahnelt weitge-hend der vonprintf , mit der Ausnahme das nichtdie Werte der ubergebenen Variablen ausgegeben wer-den sondern ihr Inhalt entsprechend belegt wird. Wieprintf verwendet auchscanf einen Formatstringsder die Struktur der Eingabe beschreibt, das heißt erlegt uber Formatelemente fest wo innerhalb des Ein-gabestrings Werte stehen die den Variablen zugewiesenwerden sollen. Dabei werden dieselben Formatelemen-te verwendet, wie sie schon im vorherigen Abschnitt be-schrieben wurden.

Naturlich kann es vorkommen, dass der Benutzer ei-ne fehlerhafte Eingabe tatigt, z.B. eine Fließkommazahleingibt, wenn eine Ganzzahl erwartet wird. Deshalb lie-fert scanf als Ergebnis die Anzahl der erfolgreich zu-gewiesenen Eingabelemente zuruck.

Ein einfaches Programm, das vom Benutzer die Ein-gabe einer ganzen Zahl erwartet und das Quadrat dieserZahl als Ausgabe zuruckliefert, konnte folgendermaßenaussehen:

#include <stdio.h>

int main(void){

int i;

printf("Bitte eine Ganzahl""eingeben: ");

if (scanf("%d", &i) == 1) {printf("Das Quadrat von %d"

"ist %d\n", i, i * i);} else {

printf("Die Eingabe war""fehlerhaft\n");

}

return 0;}

1.6.2 Mathematische Funktionen

C selbst enthalt nur Grundrechenarten, aber es gibteine mathematische C-Standardbibliothek, die vielenutzliche Funktionen enthalt. Folgende Funktionen be-kommen als Argument alle eindouble und gebenein double zuruck: sin , asin (Arcussinus),cos ,acos , tan , atan , sinh , cosh , tanh , exp , log ,sqrt (Wurzel), fabs (Betrag),floor (abgerundet),ceil (aufgerundet). Fur zweidouble s a und b be-rechnetpow(a,b) a b und fmod(a,b) den Divisi-onsrest vona / b.

Um diese Funktionen zu benutzen, muss die Header-dateimath.h #include d werden:

#include <stdio.h>#include <math.h>

int main(void){

double x = 0.1;

printf("sin(%g)= %g\n",x, sin(x));

return 0;}

Eine Schwierigkeit ergibt sich bei derUbersetzungvon Programmen die Funktionen ausmath.h ver-wenden. Hier muss dem C-Compiler namlich mitge-teilt werden, dass die Mathematik-Bibliothek verwen-det wird. Dies geschieht meistens mit der Kommando-zeilenoption-lm , also heißt der komplette Aufruf zumCompilieren dann:

gcc main.c -lm

1.6.3 Beispiel: Fakultatsberechnung

Als Beispiel fur ein paar einfache Programme folgenverschiedene Berechnungsmoglichkeiten fur die Fa-kultat von naturlichen Zahlenn! = 1·2·3·. . .·(n−1)·n,wobei0! = 1 definiert wird. Zum Beispiel ist1! = 1,2! = 2, 3! = 6, 4! = 24, 10! = 3628800, usw.

Page 27: Skript zum Kompaktkurs C mit Beispielen in OpenGL

1.6. BEISPIELPROGRAMME 27

Iterative Berechnung

So wie die Fakultat gerade definiert wurde, lasst sie sichauch mit einer Schleife (iterativ) berechnen. Zum Bei-spiel furn = 10:

int n = 10;int i = 1;long ergebnis = 1;

while (i <= n){

ergebnis = ergebnis * i;i = i + 1;

}

Diese Schleife zahlti von 1 bis 11 . Die Anweisun-gen in der Schleife werden aber nur fur die Werte1 bis10 ausgefuhrt. Dabei wirdi auf das Ergebnis multipli-ziert und danni weitergezahlt.

Hier wurde furn und i der Datentypint benutzt;weil aber die Fakultat sehr schnell wachst, ist die Va-riable ergebnis vom Typ long . Die automatischeTypkonvertierung stellt sicher, dassi bei der Multipli-kation zuerst in einlong umgewandelt wird, so dass eskeine Probleme mit den Datentypen gibt.

Diese Schleife sollte besser als Funktion geschriebenwerden:

long fakultaet(int n){

int i = 1;long ergebnis = 1;

while (i <= n){

ergebnis = ergebnis * i;i = i + 1;

}

return(ergebnis);}

Dann sieht das ganze Programm zum Beispiel so aus:

#include <stdio.h>

long fakultaet(int n){

int i = 1;long ergebnis = 1;

while (i <= n){

ergebnis = ergebnis * i;i = i + 1;

}

return(ergebnis);}

int main(void){

printf("10! = %ld\n",fakultaet(10));

return 0;}

Das ist zwar ein ganzes Programm, aber noch lan-ge nicht komplett. Hier mal ein Beispiel wie ein kom-plettes, auf Englisch geschriebenes und kommentiertesProgramm aussehen konnte:

/ ** main.c

** Example for the calculation of

* the factorial 10!

** written by Martin Kraus

* ([email protected])

** Revision History

* ----------------

* 07/21/2000 mk created

** /

/ * includes * /

#include <stdio.h>

/ * function prototypes * /

long factorial(int n);

/ * function definitions * /

int main(void)/ * prints the factorial 10!. * /

Page 28: Skript zum Kompaktkurs C mit Beispielen in OpenGL

28 KAPITEL 1. EINFUHRUNG IN C

{printf("10! = %ld\n",

factorial(10));

return 0;}

long factorial(int n)/ * returns the factorial of n. * /

{int i = 1;long result = 1;

while (i <= n){

result = result * i;i = i + 1;

}

return(result);}

Beim ersten Lesen lenken die Kommentare in die-sem kleinen Beispiel wahrscheinlich eher vom Inhaltab. Bei großeren Projekten mit standardisierten Kom-mentaren erleichtern sie die Orientierung aber unheim-lich — auch bei Dateien die nicht großer sind als diesehier.

Rekursive Berechnung

Es gibt noch einen ganz anderen Weg, die Fakultat zuberechnen, namlich nach der rekursiven Formeln! =n · (n− 1)! zusammen mit der Definition0! = 1. Dabeiwird ausgenutzt, dassn! leicht berechnet werden kann,wenn(n− 1)! bekannt ist. Entsprechend kann(n− 1)!aus(n−2)! berechnet werden, usw. Das darf aber nichtendlos weiter getrieben werden, sondern muss irgend-wann aufhohren, wozu die Definition0! = 1 gut ist.

Unserefakultaet() -Funktion kann dann so ge-schrieben werden:

long fakultaet(int n){

int ergebnis;

if (n == 0){

ergebnis = 1;}else

{ergebnis = n *

fakultaet(n - 1);}return(ergebnis);

}

Weil sich die Funktionfakultaet() selbst auf-ruft, nennt man das auch direkte Rekursion.

Bei rekursiven Funktionen wird das Konzept der lo-kalen Variablen besonders wichtig: Bei jedem Aufrufvon fakultaet() werden neue Variablen namensnundergebnis definiert, also neuer Speicherplatz re-serviert. Zum Beispiel wird bei dem Funktionsaufruffakultaet(2) eine Variablen mit Wert 2 und eineVariableergebnis definiert. In dieser Funktion wirdfakultaet(n - 1) , alsofakultaet(1) aufge-rufen. IndiesemFunktionsaufruf wird eineneueVaria-ble n mit Wert 1 und eine neue Variableergebnisdefiniert. Dann wirdfakultaet(0) aufgerufen undwieder eine neue Variablen diesmal mit Wert0 defi-niert und eine neue Variableergebnis , die auf1 ge-setzt wird. In diesem Moment gibt es also jeweils 3 Va-riablen namensn undergebnis , wobei allens unter-schiedliche Werte haben und nur einergebnis bishereinen definierten Wert besitzt. Wenn der Programmab-lauf dann zuruckspringt verlieren die jeweils sichtbarenVariablen ihre Gultigkeit und dieergebnis Variablenwerden auf definierte Werte gesetzt.

Berechnung mit Gleitkommazahlen

Leider sind unsere bisherigen Definitionen vonfakultaet() bestenfalls akademische Beispiele.Das liegt daran, dass schon13! = 6227020800 einegroßere Zahl ist als mit einem 4-Bytelong verarbeitetwerden kann. Mit Gleitkommazahlen kommen wiretwas weiter:

#include <stdio.h>

double fakultaet(int n){

int i = 1;double ergebnis = 1.0;

while (i <= n){

ergebnis = ergebnis * i;i = i + 1;

}

Page 29: Skript zum Kompaktkurs C mit Beispielen in OpenGL

1.6. BEISPIELPROGRAMME 29

return(ergebnis);}

int main(void){

printf("13! = %g\n",fakultaet(13));

return 0;}

Aber auch der Exponent von Gleitkommazahlen un-terliegt gewissen Einschrankungen, so dass wir beieinem 8-Byte-double typischerweise nur bis etwa170! = 7.25741 . . . ·10306 kommen. (Ein Ausweg waredie Verwendung des in diesem Kurs nicht behandel-ten Typslong double stattdouble , falls das etwashilft. Viel weiter kommt man aber auch damit nicht.)

Man konnte der Meinung sein, dass es keinen Sinnmacht großere Fakultaten zu berechnen, wenn sie so-wieso nicht mitdouble s dargestellt werden konnen.Das ist aber nicht ganz richtig. Dazu genugt ein kurzerBlick auf die Anwendungen fur Fakultaten. Unter an-derem werden sie intensiv in der Stochastik verwendet.Sie tauchen zum Beispiel in Binomialkoeffizienten auf.

Binomialkoeffizienten

Der Binomialkoeffizient(

nk

)

, sprich:”n uberk“ oder

”k ausn“, ist definiert als

(

nk

)

= n!k!·(n−k)! . Er ist sehr

nutzlich, vor allem in der binomischen Formel von derer seinen Namen hat:

(a + b)n =

n∑

k=0

(n

k

)

akbn−k

Außerdem gibt er zum Beispiel an, wieviele Moglich-keiten es gibt,k verschiedene Elemente aus einer Men-ge vonn unterscheidbaren Elementen auszuwahlen.

Die Berechnung mit Hilfe derdouble -Variante vonfakultaet() ist einfach:

double binomial(int n, int k){

return(fakultaet(n) /fakultaet(k) /fakultaet(n - k));

}

binomial(170, 3) ergibt zum Beispiel804440.0 . Aber

(

2003

)

ist 1313400 und kann mit

8-Byte-double s schon nicht mehr so ausgerechnetwerden, weil die Fakultaten nicht berechnet werdenkonnen.

In der Praxis wollen Anwender aber naturlich dochsolche Binomialkoeffizienten (oder allgemein: Verhalt-nisse von großen Fakultaten) berechnen. Eine sehr ef-fiziente Moglichkeit dazu, soll im nachsten Abschnittkurz vorgestellt werden.

Stirlingsche Formel

... ist eine Naherung fur den naturlichen Logarithmuseiner Fakultatlnn!:

lnn! ≈(

n +1

2

)

lnn− n +1

2ln(2π)

Oder in C:

#include <math.h>

double fakultaet_ln(int n){

return((n + 0.5) * log(n) - n +0.91893853320467274178);

}

Wieder fuhrt die automatische Typkonvertierung (z. B.von n in n + 0.5 ) zum erwarteten Ergebnis. Weilmath.h benutzt wird, muss beim Compilieren wiedermit -lm die Mathematikbibliothek angegeben werden.

Damit konnen wir die Funktionbinomial() um-schreiben, denn

(n

k

)

=n!

k! · (n− k)!

=exp(lnn!)

exp(ln k!) · exp(ln(n− k)!)

= exp(lnn!− ln k!− ln(n− k)!)

Oder in C:

#include <math.h>

double binomial(int n, int k){

return(exp(fakultaet_ln(n) -fakultaet(k) -fakultaet(n-k)));

}

Page 30: Skript zum Kompaktkurs C mit Beispielen in OpenGL

30 KAPITEL 1. EINFUHRUNG IN C

Damit ergibt binomial(200, 3) dann1.35027e+06 , was zumindest ein Naherung fur1313400 ist. Hier nocheinmal das ganze Programmdazu:

#include <stdio.h>#include <math.h>

double fakultaet_ln(int n){

return((n + 0.5) * log(n) - n +0.91893853320467274178);

}

double binomial(int n, int k){

return(exp(fakultaet_ln(n)-fakultaet_ln(k) -fakultaet_ln(n - k)));

}

int main(void){

printf("(200 ueber 3) = %g\n",binomial(200, 3));

return 0;}

Page 31: Skript zum Kompaktkurs C mit Beispielen in OpenGL

1.6. BEISPIELPROGRAMME 31

Ubungen zum ersten Tag

Aufgabe I.1

Schreiben Sie ein C-Programm das den Flacheninhaltund den Umfang eines Kreises berechnet. Dabei sollder Radius vom Anwender interaktiv eingegeben wer-den. Bei einer fehlerhaften Eingabe, soll der Anwen-der eine Fehlermeldung erhalten. Ansonsten sollen diebeiden errechneten Werte mit einer Beschreibung aus-gegeben werden. Fur die Kreiszahlπ konnen Sie denNaherungsert3.1415 verwenden.

Aufgabe I.2

Schreiben Sie ein C-Programm, das Zeichen fur Zei-chen von der Standardeingabe einliest und dabei dieAnzahl aller auftretenden Zeichen, aller ganzen Worterund aller Zeilenumbruche zahlt und die jeweiligen An-zahlen nach dem Ende der Eingabe bzw. am Dateiendeausgibt. Worter sind dabei durch Leerzeichen, Tabula-torzeichen oder Zeilenumbruche getrennte Zeichenfol-gen. Testen Sie ihr Programm, indem Sie die C-Dateides Programms selbst auf die Standardeingabe umlen-ken.

Aufgabe I.3

In der folgenden Aufgabe soll eine iterative Berechnungdurchgefuhrt werden, bei der ein neuer Wert aus demjeweils vorhergehenden mit Hilfe einer Iterationsvor-schrift berechnet wird.

Gegeben seif0 = 0.5 und die Berechnungsvorschriftfn+1 = a · fn · (1− fn).

Schreiben Sie ein C-Programm, dass fura =2.0, 2.1, 2.2, . . . , 3.8, 3.9 die Werte f100, f101, f102,f103 undf104 berechnet und zusammen mita ausgibt.Erweitern Sie dann Ihr Programm so, dass der minimalund maximal Wert furn fur den Werte ausgegeben wer-den sollen vom Benutzer interaktiv eingegeben werdenkann.

Aufgabe I.4

Schreiben Sie einen einfachen Taschenrechner, derdie vier Grundrechenarten beherrscht. Der Anwen-der soll dabei die Rechenvorschrift in der FormZAHL OPERATOR ZAHLuber die Standardeingabeeingeben.ZAHL ist dabei eine beliebige Gleitkom-mazahl undOPERATORein Zeichen aus der Menge

{+,−, ∗, /}. Die Eingabe soll auf Korrekheit uberpruftwerden und bei Bedarf eine aussagekraftige Fehlermel-dung ausgegeben werden. Nach Ausgabe des Ergebnis-ses bzw. der Fehlermeldung soll der Benutzer entschei-den konnen, ob er das Programm fortsetzen will.

Page 32: Skript zum Kompaktkurs C mit Beispielen in OpenGL

32 KAPITEL 1. EINFUHRUNG IN C

Page 33: Skript zum Kompaktkurs C mit Beispielen in OpenGL

Teil II

Zweiter Tag

33

Page 34: Skript zum Kompaktkurs C mit Beispielen in OpenGL
Page 35: Skript zum Kompaktkurs C mit Beispielen in OpenGL

Kapitel 2

Datenstrukturen in C

2.1 Zeiger

Zeiger sind Variablen, die Adressen speichern. DieWerte von Zeigern sind also Adressen. Das hort sicheinfach an und ist es auch. Gefahrlich ist es deswegen,weil an den Adressen irgendetwas stehen kann. Dennmit Hilfe von Zeigern ist es moglich, beliebige Spei-cherinhalte zu andern, z. B. auch Speicherplatze, die dieMaschinenbefehle eines ubersetzten Programms bein-halten! Werden Maschinenbefehle willkurlich uber-schrieben und das so veranderte Maschinenprogrammausgefuhrt, fuhrt das in der Regel zum Absturz des ent-sprechenden Programms, wenn nicht des ganzen Be-triebssystems.

Programmieren mit Zeigern ist daher nicht ganz un-gefahrlich und vielleicht der kritischste Aspekt der C-Programmierung. Nicht unbedingt, weil hier die mei-sten Fehler passieren, sondern weil diese Fehler diegefahrlichsten Auswirkungen haben.

Außerdem gelten Zeiger als ein schwer zu verstehen-des Konzept, obwohl sie wirklich nur Variablen sind,deren Werte Speicheradressen sind.

2.1.1 Adress- und Verweisoperator

In dem Beispiel

int a = 10;int b;

b = * (&a);

wird mit &a die Adresse des Speicherplatzes der Varia-ble a ermittelt. Der (unare!) Verweisoperator* (zu un-terscheiden von dem binaren Multiplikations-Operator* ) nimmt diese Speicherplatzadresse und gibt den Wertzuruck, der dort im Speicher steht, also10 . Dieser Wertwird dann der Variableb zugewiesen.

Diese Beschreibung war nicht ganz korrekt, weilsie eine Frage offenlasst: Woher

”weiß“ der Verweis-

Operator, dass an dieser Adresse einint steht? (Wenner das nicht wusste, wurde der den Speicherinhalt mithoher Wahrscheinlichkeit falsch interpretieren, z.B. alsdouble .) Die Anwort ist: Es gibt nicht nur einenTyp von Adressen, sondern viele, z.B. den Typ der

”Adresse einesint s“. Obwohl jede Speicherplatz-

adresse normalerweise eine einfache, vorzeichenlose 4Byte große Ganzzahl ist, mussen also verschiedene Ty-pen von Adressen unterschieden werden, z.B. Adressenvonchar s,short s,double s etc.

Dann sieht die Erklarung des Beispiels so aus:&agibt die Adresse einesint s zuruck, weila vom Typint ist. Der Verweis-Operator bekommt die Adressevon einemint und interpretiert deshalb den Speicher-platzinhalt an dieser Adresse alsint und gibt diesenint -Wert zuruck, der dannb zugewiesen wird.

2.1.2 Definitionen von Zeigern

Zeiger sind Variablen, die Adressen speichern. Da esverschiedene Typen von Adressen gibt (Adresse einesint s, einesdouble s, etc.), muss es auch verschiedeneTypen von Zeigern geben. Um etwas Systematik in dieTypangabe von Adressen (und damit Zeigern) zu brin-gen, wird der Verweisoperator in der Typbezeichnungverwendet, nach folgender Regel (von Andrew Koe-nig): Variablen werden so definiert, wie sie verwendetwerden.Ein Beispiel:

int * c;

definiert einen Zeiger aufint s. D.h. die Variablecspeichert Adressen vonint s. Wird der Verweisopera-tor aufc angewendet, also* c , dann nimmt sich der Ver-weisoperator die Adresse, die inc gespeichert ist, und

35

Page 36: Skript zum Kompaktkurs C mit Beispielen in OpenGL

36 KAPITEL 2. DATENSTRUKTUREN IN C

holt sich an dieser Adresse einint aus dem Speicher,das er zuruckgibt. Die Zeile

int * c;

kann also so gelesen werden: Die Variablec wird so de-finiert, dass wenn der Verweisoperator aufc angewen-det wird, einint rauskommt. Das ist konsistent mit

int a;

Denn die letzte Zeile heißt einfach:a wird so definiert,dass im Programm fura ein Wert vom Typint einge-setzt wird.

C-Datentypen, insbesondere die von Zeigern, konnengelegentlich relativ seltsam aussehen, aber es giltim-mer die Regel:Variablen werden so definiert, wie sieverwendet werden.

Das Beispiel von oben kann jetzt so geschrieben wer-den:

int a = 10;int b;int * c;

c = &a;b = * c;

Mit c = &a; wird die Adresse der Variablea in cgespeichert. Mit* c wird an dieser Adresse einint ausdem Speicher gelesen, das dann inb gespeichert wird.Das Beispiel ist also nur eine komplizierte Schreibweisevon

int a = 10;int b;

b = a;

Noch ein Beispiel:

double a = 3.1415;int b;double * c;

c = &a;b = * c;

Hier wird c als Zeiger vom Typ”Adresse eines

double s“ definiert.c = &a; speichert die Adressevon a in c . * c holt an dieser Adresse eindouble ausdem Speicher, also3.1415 , und gibt diesesdoublezuruck.b ist aber vom Typint , deshalb wird der Wert3.1415 vor der Zuweisung in einint umgewandelt,d.h. zu3 abgerundet, und dann inb gespeichert.

Und noch ein Beispiel:

int a = 10;int b;int * c;

c = &b;

* c = a;

Hier wird zunachst mitc = &b; in c die Adresse vonb gespeichert. Das ist moglich und sinnvoll, obwohl imSpeicherplatz vonb noch kein definierter Wert steht,weil die Adresse des Speicherplatzes vonb nach derDefinition vonb sehr gut definiert ist. Dann wird mit* c= ... der Speicherplatz, dessen Adresse inc enthaltenist, uberschrieben. In diesem Beispiel wird mit* c =a; in diesen Speicherplatz der Wert vona gespeichert.In c war aber die Adresse vonb, deshalb ist* c = a;aquivalent zub = a;

Was ware passiert, wenn wir vergessen hatten mitc = &b; , die richtige Adresse zu setzen, und einezufallig Zahl inc stehen wurde? Dann wurde der Wertvona in eine zufallig Speicheradresse geschrieben. Dasgeht oft gut, weil normalerweise nur ein Teil des Spei-chers benutzt wird, und auch davon nur ein Teil wirk-lich kritische Daten enthalt. Gelegentlich hat es aberauch fatale Folgen, zum Beispiel den Absturz des Pro-gramms.

Wieviele Bytes werden benotigt, um eine Adres-se einesint s zu speichern? Das konnen wir mitsizeof( Typ der Adresse einesint s) erfahren, aberwie wird der Typ einer Adresse einesint s angegeben?Dazu wieder eine einfache Regel:Die Angabe eines Da-tentyps ergibt sich aus der Definition einer Variable die-ses Typs ohne den Variablennamen und ohne den Strich-punkt. Da int * c; eine Variable vom Typ Adresseeinesint s definiert, istint * der Typ Adresse einesint s. D.h.sizeof(int * ) ergibt die benotigte By-tegroße, um eine Adresse zu speichern (meistens vierBytes).

Zur Typkonvertierung wird auch die gleiche Typan-gabe verwendet. Das heißt eine Speicheradresse einesanderen Typs (z.B.double * ) kann der Typkonver-tierung(int * ) in eine Adresse vom Typ Adresse ei-nesint s konvertiert werden.

2.1.3 Adressen als Ruckgabewerte

Ahnlich wie fur Variablendefinitionen gibt es auch furFunktionsdefinitionen eine einfache Regel:Funktionenwerden so definiert, wie sie verwendet werden.D.h. einFunktionskopf

Page 37: Skript zum Kompaktkurs C mit Beispielen in OpenGL

2.1. ZEIGER 37

int f(int a)

sollte so gelesen werden: Wenn eine Funktion namensf mit f(...) aufgerufen wird, dann ergibt das einint . Entsprechendes gilt fur Adressen als Ruckgabe-werte. Z.B. kann

int * f(int a)

so interpretiert werden: Wenn der Verweisoperator mit* f(...) (aquivalent zu* (f(...)) ) auf den Ruck-gabewert der Funktion namensf angewendet wird,dann ergibt das einint . D.h. f() gibt die Adresse ei-nesint s zuruck.

2.1.4 Adressen als Funktionsargumente

Eine wichtige Anwendung von Adressen entsteht beiFunktionen. Nehmen wir als Beispiel die Funktion

void f(int a){

a = 5;}

Und einen Funktionsaufruf dieser Art:

int b = 0;

f(b);

Was passiert hier?b wird auf den Wert0 initialisiert,deswegen entsprichtf(b) dem Aufruf f(0) . BeimFunktionsaufruf wird dann eineneueVariablea defi-niert, die mit dem Funktionsargument, also0, initiali-siert wird. Mita = 5; wird dieser Wert uberschriebenund eine5 in a gespeichert. Dann wird die Funktion be-endet und die lokale Variablea deshalb ungultig. DerProgrammablauf wird hinterf(b); fortgesetzt. Wel-chen Wert hat dannb? Naturlich immer noch0, derFunktionsaufruf hat daran gar nichts andern konnen unddas ist gut so.

Allerdings ware es manchmal nutzlich mit Hil-fe einer Funktion irgendwie den Inhalt einer Varia-ble andern zu konnen. (Ahnlich wie die In- undDekrement-Operatoren den Inhalt von Variablen andernkonnen.) Mit Hilfe von Adressen und Zeigern geht dastatsachlich. Die neue Funktion

void g(int * a){

* a = 5;}

bekommt als Argument eine Adresse auf einint . Beieinem Funktionsaufruf wird eine Variablea definiertund diese Adresse ina gespeichert. (a ist also einZeiger.) Mit * a = 5; wird anschließend der Wert5in den Speicherplatz geschrieben, dessen Adresse derFunktion ubergeben wurde. D.h.jetztkann mit

int b = 0;

g(&b);

die Adresse vonb ubergeben werden. Ing() wird dannein Zeiger namensa definiert und mit dieser Adresseinitialisiert. Dann wird in den Speicherplatz vonb eine5 geschrieben und die Funktion wieder verlassen. Nachdem Funktionsaufrufg(&b); hat sich also der Wertvonb tatsachlich verandert.

Wenn wie hier an eine Funktion Variablen uberge-benen werden und die Funktion die Werte der Varia-blen andern kann, wird das auch als

”Argumentuber-

gabe per Referenz“ bezeichnet, was hier nichts ande-res heißt, als dass die Adresse einer Variable statt ihresWertes ubergeben wird. Dazu noch ein nutzliches Bei-spiel. Die Funktion

void swap_ints(int * x, int * y){

int temp;

temp = * x;

* x = * y;

* y = temp;}

bekommt zwei Adressen aufint s, holt sich den Wert,der am Speicherplatz mit der ersten Adresse steht, undspeichert ihn in der lokalen Variabletemp zwischen.Dann schreibt sie mit* x = * y; den Wert aus demSpeicherplatz an der zweiten Adresse in den vorherausgelesenen Speicherplatz. Und schließlich wird inden Speicherplatz an dieser zweiten Adresse mit* y =temp; der gerettete Wert geschrieben, der fruher amSpeicherplatz der ersten Adresse stand. Der Effekt istalso, dass dieint s, deren Adressen ubergeben wurden,gerade vertauscht wurden. Mit

int a = 2;int b = 3;

swap_ints(&a, &b);

werden also die Werte vona undb vertauscht, so dassa danach den Wert3 undb den Wert2 bekommt.

Page 38: Skript zum Kompaktkurs C mit Beispielen in OpenGL

38 KAPITEL 2. DATENSTRUKTUREN IN C

2.1.5 Arithmetik mit Adressen

Wie erwahnt werden Adressen intern als (meist vierByte große) Ganzzahlen verarbeitet. Deswegen ist esmoglich eine Adresse auf den Wert0 zu setzen. Die-se0-Adresse wird in C meistens alsNULLgeschriebenund dient dazu, deutlich zu machen, dass ein Zeigerkeinegultige Adresse enthalt. (UmNULL zu verwen-den, sollte zum Beispielstddef.h oder stdio.h#include d werden.)

Um zu uberprufen, ob ein Zeiger eine gultige AdresseoderNULLenthalt, muss mitNULLverglichen werden.Das ist moglich weil== und!= mit Adressen genausofunktioniert wie mit Ganzzahlen. Auch<, <=, >, >=sind moglich.

Es ist sogar moglich, Ganzzahlen zu Adressen zu ad-dieren bzw. den De- oder Inkrementoperator auf Zeigeranzuwenden. Dabei muss aber beachtet werden, dassnicht einfach die Ganzzahl zur Adresse addiert wird.Das wurde auch nicht viel Sinn machen, denn wennzur Byteadresse einerdouble -Variable1 addiert wird,steht an der so erhohten Adresse ganz sicher kein sinn-volles double , da jedesdouble mehr als ein Bytebelegt. Die neue Adresse wurde also mitten in die By-tes einesdouble s zeigen. Vielmehr wird zur Speicher-adresse die Ganzzahl multipliziert mit der Bytegroßedes entsprechenden Datentyps addiert (also im Beispielobensizeof(double) ). Ebenso erhoht++ eine Zei-gervariable nicht um1 sondern um die Bytegroße desDatentyps, auf den der Zeiger zeigt. Analog erniedrigt-- um diese Große.

Ein Beispiel zu einfacher Adressarithmetik:

double * a = NULL;

a = malloc(10 * sizeof(double));

if (NULL != a){

* a = 0.0;

* (a + 1) = 3.1415;

* (a + 2) = * (a + 1);

free(a);}

Die Funktionenmalloc() undfree() werden zumBeispiel in stdlib.h oder malloc.h deklariert,und dienen dazu Speicher

”dynamisch“ zu resevie-

ren, bzw. freizugeben.malloc() erwartet als Argu-ment eine Ganzzahl, die angibt, wieviele Bytes reser-

viert werden sollen.malloc() gibt dann entwederNULL zuruck, falls der Speicher nicht reseviert wer-den konnte, oder die Adresse des reservierten Speicher-blocks. Der Typ der Ruckgabeadresse istvoid * , alsodie Adresse eines unbekannten Datentyps. (malloc()kann nicht wissen, welche Daten in den Speicherblockgeschrieben werden sollen.) Adressen vom Typvoid

* verhalten sich ahnlich wie andere Adressen, aber eskann keine Adressarithmetik durchgefuhrt werden, weilvoid keine Bytegroße hat. Dievoid * -Adresse wirdbei der Zuweisung vona aber ohne weiteres in einedouble * -Adresse umgewandelt.

Das Gegenstuck zumalloc() ist free() unddient dazu den Speicherblock an einer angegebe-nen Adresse wieder freizugeben. Das ist wichtig, da-mit immer genug Speicher fur neuemalloc() s zurVerfugung steht.

Im Beispiel wird ein Speicherblock fur10 doublesangefordert. Die benotigte Bytegroße wird dabei mitdem sizeof -Operator berechnet. (Ubrigens wirdpraktisch jeder C-Compiler die Multiplikation10 *sizeof(double) schon wahrend des Compilierensausfuhren und nur noch das Ergebnis in den Programm-code einsetzen, da der Compilersizeof(double)kennt und das Produkt zweier Konstanten immer nocheine Konstante ist, also nicht immer neu berechnet wer-den muss.)

Nach dem Reservieren des Speichers und der Spei-cherung der zuruckgegebenen Adresse ina wird abge-fragt, oba ungleichNULL ist, d.h. ob tatsachlich Spei-cher reserviert wurde. Wenn dem so ist, wird an den An-fang des Speicherblocks mit* a = 0.0 ein doublevom Wert0.0 gespeichert. Aber in dem Speicherblockist noch Platz fur 9 weiteredouble s. Mit a + 1 wirddeshalb die Adresse hinter dem erstendouble berech-net. (Falls eindouble 8 Bytes groß ist, ista + 1 um8 großer als die Startadresse des Speicherblocks.) Mit* (a + 1) = 3.1415; wird dann an dieser Adres-se der Wert3.1415 gespeichert. In der nachsten Zeilewird dieser Wert mit* (a + 1) ausgelesen und gleichmit * (a + 2) = ... in die nachfolgenden Bytes imSpeicherblock gespeichert. Dann wird der Speicher-block wieder freigegeben. (Ja, das ist ein rein akade-misches Beispiel.)

Da die Schreibweise* ( Zeiger + Ganzzahl) etwasumstandlich ist, kann sie auch mitZeiger[ Ganzzahl]abgekurzt werden. Das Beispiel wurde dann so ausse-hen:

double * a = NULL;

Page 39: Skript zum Kompaktkurs C mit Beispielen in OpenGL

2.1. ZEIGER 39

a = malloc(10 * sizeof(double));

if (NULL != a){

a[0] = 0.0;a[1] = 3.1415;a[2] = a[1];

free(a);}

Das kann folgendermaßen interpretiert werden:a ist derName einer Reihe vondouble s, die hintereinander imSpeicher stehen. Mita[ Index] wird auf ein Elementin dieser Reihe zugriffen, bzw. mita[ Index] = ... derWert eines Elements gesetzt. Deswegen wird[] auchals Arrayzugriff interpretiert. Aber das ist nur eine In-terpretation, tatsachlich bedeuteta[ Index] nichts an-deres als* (a + Index) . (Und das ist das Gleiche wie* ( Index+ a) und deswegen auch das Gleiche wieIn-dex[a] !)

2.1.6 Zeiger auf Zeiger

Zeiger sind Variablen, haben also auch einen Speicher-platz in dem sie ihren Wert (eine Adresse) speichern.Dieser Speicherplatz hat auch eine Adresse und zwarvom Typ Adresse auf eine Adresse auf einen bestimm-ten Datentyp. Beispiel:

int a = 42;int * b;

b = &a;

b speichert hier die Adresse einesint s. Deswegen istdie Adresse der Variableb, also&b vom Typ Adres-se einer Adresse einesint s. Wenn wir diese Variablein einer Variablec speichern wollen, mussen wir wis-sen wie dieser Typ in C formuliert wird. Dazu benut-zen wir einfach die Regel von Koenig:Variablen wer-den so definiert, wie sie verwendet werden.Wir wissen,dassc eine Adresse einer Adresse einesint s speichernsoll, d.h. der Wert* c ist die Adresse einesint s. Dannist aber* ( * c) (oder gleichbedeutend** c ) ein int -Wert. Wenn aber** c ein int ist, dann istint ** c;die richtige Definition. Also konnen wir mit

int * b;int ** c;

c = &b;

in c die Adresse des Speicherplatzes vonb speichern.(In dem Speicherplatz vonb muss dazu kein definierterWert stehen.)

Der Typ einer Adresse einer Adresse einesint sergibt sich wieder durch Weglassen desc; in int

** c; , ist also int ** . Analog wird der Typ vonAdressen von Adressen von Adressen vonint s mitdrei * angegeben. Da so viele Indirektionen das Vor-stellungsvermogen doch erheblich beanspruchen, sindsolche Typen aber eher selten.

2.1.7 Adressen von Funktionen

Der C-Compiler ubersetzt Funktionsdefinitionen inausfuhrbare Maschinenbefehle. Die mussen aber auchim Speicher abgelegt werden, damit der Prozessor sieausfuhren kann. Daher hat zur Laufzeit jede Funk-tionsdefinition eine definierte Adresse. Erhalten kannein Programm diese Adresse mit dem Adressoperator:&Funktionsnameergibt also die Adresse der FunktionnamensFunktionsname.

Wozu sind Adressen von Funktionen gut? Zum Bei-spiel um die Funktion, deren Maschinenbefehle ander angegebenen Adresse im Speicher stehen, auf-zurufen. Das funktioniert mit dem unaren Verweis-operator* . Allerdings mussen beijedemFunktions-aufruf Argumente ubergeben werden. Dazu wird derFunktionsaufruf-Operator() benutzt. Dabei muss be-achtet werden, dass der Funktionsaufruf-Operator Vor-rang vor dem Verweisoperator hat (

”weil“ binare Opera-

toren, die so wie der Funktionsaufruf ohne Leerzeichengeschrieben werden, Vorrang vor unaren Operatorenwie dem Verweisoperator haben). Deswegen mussenwir Klammern setzen, um zuerst den Verweisoperatorauf die Funktionsadresse wirken zu lassen. Nehmen wirwieder unsereverdopple() Funktion:

int verdopple(int a){

return(2 * a);}

Jetzt kann die Funktion mit

int b;

b = ( * (&verdopple))(100);

Page 40: Skript zum Kompaktkurs C mit Beispielen in OpenGL

40 KAPITEL 2. DATENSTRUKTUREN IN C

aufgerufen werden. Zur Wiederholung:&verdopple ist die Adresse der Funktion,( * (&verdopple)) ergibt

”die Funktion selbst“,

und ( * (&verdopple))(100) ist dann derkomplette Funktionsaufruf, der aquivalent zuverdopple(100) ist.

Wie konnen wir einen Zeigerc auf eine Funktion de-finieren? Entsprechend der Regel

”Variablen werden so

definiert, wie sie verwendet werden“ gehen wir so vor:c enthalt die Adresse einer Funktion, die einint alsArgument bekommt und einint zuruckgibt. Dann ist* c

”die Funktion selbst“, und mit dem Funktionsaufruf-

Operator heißt der Funktionsaufruf dann( * c)(int) .(An dieser Stelle mussen in der Argumentliste keineVariablennamen aufgefuhrt werden, aber die Typen derVariablen.) Dieser Funktionsaufruf ergibt aber einint .Deswegen istint ( * c)(int); die richtige Defini-tion fur c und wir konnen schreiben:

int b;int ( * c)(int);

c = &verdopple;b = ( * c)(100);

Der Typ einer Funktionsadresse ergibt sich dann wie-der durch Weglassen des Variablennamens und desStrichpunkts. Als Typ

”Adresse einer Funktion, die

ein int als Argument erwartet und einint zuruck-gibt,“ ergibt sich dann:int ( * )(int) . Das sieht aufden ersten Blick etwas albern aus, aber weil wir unsstrikt an die Regeln gehalten haben ist das richtig undsizeof(int ( * )(int)) gibt dann auch brav dieubliche Bytegroße von Adressen zuruck. Typkonvertie-rungen in diesen Typ funktionieren entsprechend mit(int ( * )(int)) .

Bei Funktionsadressen sind die Regeln, wie Varia-blen definiert werden, und sich daraus Typen ergeben,unentbehrlich. Damit konnen dann auch Ungetume, wieZeiger auf Funktionen, die Adressen von Funktionenzuruckgeben, definiert werden.

Etwas verwirrend bei Funktionsadressen ist Folgen-des: Die Adress- und Verweisoperatoren sind nicht im-mer notwendig, denn wenn ein Funktionsname oh-ne Funktionsaufruf-Operator, also ohne nachfolgendeKlammern, erscheint, kann nur die Adresse der Funkti-on gemeint sein. Also kann stattc = &verdopple;auchc = verdopple; geschrieben werden. Eben-so: Wenn eine Adresse auf eine Funktionmit nachfol-genden Klammer erscheint, kann nur ein Funktionsauf-ruf der Funktion selbst gemeint sein. Also kann statt

( * c)(100) auch(c)(100) oderc(100) geschrie-ben werden. Das gilt abernicht fur die Definition vonFunktionszeigern und den Typ von Funktionsadressen.Deswegen sollte darauf verzichtet werden, diese syn-taktischen Moglichkeiten auszunutzen.

2.2 Arrays

Ahnlich wie lokale Variablen definiert werden konnen,also Speicher fur einen Wert eines bestimmen Daten-typs reserviert werden kann, kann auch Speicher furmehrere Werte eines bestimmten Datentyps reserviertwerden. Die Werte werden dabei direkt hintereinanderim Speicher abgelegt. Diese Konstruktion wird Arraygenannt und unterliegt den gleichen Gultigkeits- undSichtbarkeitsregeln wie alle lokalen Variablen.

2.2.1 Eindimensionale Arrays

Ein Array wird (wie alle Variablen) so definiert, wiees verwendet wird. Wenn alsoa ein Array von int sist, dann ist z.B.a[0] das 0. Element dieses Arrays(Vorsicht: Zahlen in C beginnt mit 0!). Da jedes Ele-ment des Arrays einint ist, ergibt sich die Definiti-on int a[0]; . Die 0 als Index macht hier nicht so-viel Sinn, deswegen wird an dieser Position die An-zahl der Element des Arrays angegeben.int a[6] ;wurde also ein Array definieren, das Platz fur 6int sbietet. Da das Zahlen in C mit 0 beginnt, werden dieElemente mita[0] , a[1] , a[2] , a[3] , a[4] unda[5] angesprochen. Mehr Speicherplatz ist nicht reser-viert, d.h.a[6] wurde uber den reservierten Speicher-platz hinauszeigen. Das Problem in C ist nun, dass beisolchen sogenannten index-out-of-bounds-Fehlern kei-ne Fehlermeldungen erzeugt werden, genauso wie Zei-ger auf willkurliche Adressen nicht direkt Fehler erzeu-gen.

Ein anderer Nachteil in C ist, dass die Große des Ar-rays dem Compiler bekannt sein muss, d.h. es darf keineVariable als Große in einer Arraydefinition verwendetwerden. Hier ein Beispiel:

double a[5];

a[0] = 3.1415;a[1] = a[0];

Dabei wird Speicher fur 5double s reserviert und indie 0. und 1. Position der Wert3.1415 geschrieben.

Page 41: Skript zum Kompaktkurs C mit Beispielen in OpenGL

2.2. ARRAYS 41

Entsprechend der Große des fur das Array reservier-ten Speicherblocks ergibtsizeof(a) in diesem Fall5 mal die Bytegroße einesdouble s. Die Große einesArrays zu bestimmen ist eine von zwei Sachen, die mitArrays gemacht werden konnen. Die andere Sache ist,die Adresse des 0. Elements zu bestimmen.

Die Adresse des 0. Elements des Arraysa kannals &a[0] geschrieben werden. (Das ist aquivalentzu &(a[0]) weil der binare Arrayzugriff-Operator(ohne Leerzeichen!) Vorrang vor dem unaren Adress-Operator hat.) Aber es ist genauso richtig, einfachazu benutzen, denn der Arrayname stehtimmer fur dieAdresse des 0. Elements,außerer steht insizeof .

Wie bei der Adressarithmetik erwahnt, ista[1]aquivalent zu* (a + 1) . Der Arraynamea steht da-bei fur die Adresse des 0. Elements (die Adresse einesint s), zu der mita + 1 entsprechend der Adressa-rithmetik die Bytegroße einesint s addiert wird.* (a+ 1) ergibt dann den Speicherinhalt an dieser Adres-se, die auch als Adresse des 1. Elements des Arraysaangesehen werden konnte.

In diesem Sinne gibt es also gar keinen”Arrayzu-

griff“, sondern nur die AbkurzungArrayname[ Index]fur * ( Adresse-des-0.-Elements-des-Arrays+ Index) .Daraus ergibt sich auch, warum C nicht uberpruft, obein Index großer als erlaubt ist. Denn Adressarithmetikkennt solcheUberprufungen nicht.

2.2.2 Initialisierung von Arrays

Arrays konnen bequem initialisiert werden. Dazu wer-den die Elementwerte in geschweiften Klammern alsListe angegeben. Zum Beispiel:

int a[5] = {2, 3, 5, 7, 11};

Diese Definition reserviert nicht nur Speicher fur funfint s, sondern schreibt auch die funf angegebenen Wer-te in den reservierten Speicher. Da der C-Compiler dieWerte auch abzahlen kann, muss hier in der Definition,die Große des Arrays nicht explizit angegeben werden,d.h. es ist auch moglich

int a[] = {2, 3, 5, 7, 11};

zu verwenden. Arrays vonchar s werden auch Zei-chenketten oder Strings genannt und konnen ahnlich in-itialisiert werden:

char a[] = {’H’, ’a’, ’l’,’l’, ’o’, ’!’};

Viele Bibliotheksfunktionen erwarten aber Zeichenket-ten, die mit dem’\0’ -Zeichen abgeschlossen werden.(Das ’\0’ -Zeichen hat praktischimmer den ASCII-Code0.) Deswegen ist es besser ein’\0’ anzuhangen:

char a[] = {’H’, ’a’, ’l’,’l’, ’o’, ’!’, ’\0’};

Dafur gibt es auch eine Abkurzung:

char a[] = "Hallo!";

Das ist aber etwas ganz anderes als:

char * a = "Hallo!";

Im letzten Beispiel wird kein Array definiert, sondernein Zeiger aufchar s, d.h. es wird nur der Speicherplatzfur eine Adresse reserviert und darin die Adresse einerZeichenkette gespeichert, die der Compiler irgendwo indas Programm geschrieben hat. Diese Adresse bleibtbei allen Ausfuhrungen dieser Definition die gleiche,wahrend bei der Definition eines Arrays immer wiederein anderer Speicherplatz reserviert werden kann. Dasist ganz ahnlich wie in

printf("Hallo!\n");

Hier wird auch die Zeichenkette"Hallo!\n" vomCompiler irgendwo im Programmcode abgelegt und dieAdresse dann anprintf() ubergeben. Entsprechendkann auch mit

printf(a);

das oben definiertechar -Array ausgegeben werden,indem seine Adresse anprintf() ubergeben wird.Hier ist ganz entscheidend, dass ein’\0’ -Zeichen dasEnde des Strings markiert, weilprintf() sonst mun-ter alles ausgibt, was dahinter im Speicher steht, bis eszufallig auf ein’\0’ -Zeichen trifft. Das ist allerdingsnicht ganz sauber, weilprintf() sein erstes Argu-ment alsFormatstringinterpretiert und deswegen nichteinfach die Zeichen der Reihe nach ausgibt. Sicherer istes deshalb mit dem Formatelement%s fur Strings zuschreiben:

printf("%s", a);

2.2.3 Mehrdimensionale Arrays

Wie wir gesehen haben, gibt es eigentlich keine Arraysin C, sondern nur Adressen und Speicherblocke und ei-ne abkurzende Schreibweise fur die Adressarithmetik.

Page 42: Skript zum Kompaktkurs C mit Beispielen in OpenGL

42 KAPITEL 2. DATENSTRUKTUREN IN C

Ahnliches gilt auch fur mehrdimensionale Arrays in C:Es gibt sie eigentlich nicht. Der Ersatzmechanismus sollan einem Beispiel erklart werden:

Ein zweidimensionales Feld von Zahlen, z.B. eine3×4 Matrix, kann auch in einem eindimensionalen Ar-ray der Große 12 gespeichert werden. Nach der Defini-tion

double m[3 * 4];

kann mitm[i ∗ 4 + j] der Eintrag in deri -ten Zeile undder j -ten Spalte angesprochen werden. (Wieder wirdbei 0 angefangen zu zahlen, also gehti von 0 bis 2und j von 0 bis 3.) Naturlich sind

”Zeile“ und

”Spal-

te“ keine gut definierten Begriffe hier. Wichtiger ist dieUnterscheidung zwischen

”außerem“ (hier:i ) und

”in-

nerem“ (hier:j ) Index. Positionen, die sich im innerenIndex um 1 unterscheiden, sind auch im Speicher be-nachbart. Unterscheiden sich die Positionen in einemaußeren Index um 1, sind sie im Speicher deutlich wei-ter entfernt. (In diesem Beispiel um 4 Speicherpositio-nen furdouble s, wie an dem Ausdrucki * 4 + jzu sehen ist.) (Vorsicht: Andere Autoren verwenden dieBegriffe innereundaußere Indizesgerade umgekehrt.)

Die abkurzende Schreibweise fur dieses Beispiel ist:

double m[3][4];

Dann kann mitm[i][j] auf das Element, das an deri * 4 + j -ten Speicherposition furdouble s steht,zugegriffen werden. Die notige Multiplikation und Ad-dition wird also genauso durchgefuhrt wie vorher. Umes genau zu sagen:m[i][j] ist die Abkurzung fur* (&m[0][0] + i * 4 + j) , wobei &m[0][0]die Adresse desdouble s an der 0. Position imSpeicherblock ist.

Unterschiede zwischen dieser Schreibweise und demexplizit eindimensionalen Array ergeben sich durch denTyp und dadurch auch fur densizeof -Operator. Wasist der Typ vonm nach einer Definition alsdoublem[3][4]; ? Nach unserer Regel (Variablenname undStrichpunkt weglassen) ist dasdouble [3][4] . Fursizeof() muss auch wirklich dieser Typ ubergebenwerden. Wird ein Array als Funktionargument uber-geben, dann wird aber immer nur die Adresse des 0.Elements ubergeben, deshalb spielt die Dimension desaußersten Index keine Rolle. (Im Beispiel taucht die3in der Berechnungi * 4 + j nicht auf.) Daher kanndie Dimension des außersten Index fur eine Typangabein einem Funktionskopf weggelassen werden.

Oben wurde die Adresse des erstendouble s als&m[0][0] (aquivalent zum[0] ) stattmgeschrieben,

weil mgleich&m[0] ist. m[0] ist aber ein Array aus4 double s. Der Typ vonm[0] ist deshalbdouble[4] und die Große von diesem Datentyp ist 4 malsizeof(double) . Dieser Große wurde auch bei derAdressarithmetik mit&m[0] verwendet werden, wasnicht gut ist, wenn alle einzelnendouble s angespro-chen werden sollen.

Dagegen ist der Ausdruckm[0] die Adresse seines0. Elements, also&m[0][0] und das ist die Adresseeinesdouble s. Also wird bei der Adressarithmetik mitm[0] oder&m[0][0] die Bytegroße einesdouble sverwendet.

Hoherdimensionale Arrays funktionieren ganz ana-log, z.B. ist nach

double m[2][3][4];

der Ausdruck m[i][j][k] aquivalent zu* (&m[0][0][0] + i * 3 * 4 + j * 4+ k) .

2.2.4 Arrays von Zeigern

Flexibler als mehrdimensionale Arrays sind Arrays vonZeigern. Z.B.:

double * m[3];

m[0] = (double * )malloc(4 *sizeof(double));

m[1] = (double * )malloc(4 *sizeof(double));

m[2] = (double * )malloc(4 *sizeof(double));

Hier wird ein Array aus drei Zeigern aufdouble sdefiniert. Jedem Zeiger wird die Adresse eines mitmalloc() reservierten Speicherblocks zugewiesen,der jeweils 4double s aufnimmt.m[i] ist die Adres-se eines solchen Speicherblocks, deswegen kann mitm[i][j] bzw. * (m[i] + j) ein double ange-sprochen werden. Der Vorteil der Verwendung vonmalloc() ist, dass die Speicherblockem[0] , m[1]und m[2] auch unterschiedlich groß sein durfen unddie Große beim Programmieren noch nicht feststehenmuss. Um auch die erste Dimension (also die 3) frei an-geben zu konnen, muss auch das Array von Zeigern mitmalloc() reserviert werden:

double ** m;

m = (double ** )malloc(3 *

Page 43: Skript zum Kompaktkurs C mit Beispielen in OpenGL

2.3. DATENSTRUKTUREN 43

sizeof(double * ));m[0] = (double * )malloc(4 *

sizeof(double));m[1] = (double * )malloc(4 *

sizeof(double));m[2] = (double * )malloc(4 *

sizeof(double));

Hier ist malso ein Zeiger auf Adressen aufdouble s.Das erstemalloc() reserviert Speicher fur 3 Adres-sen aufdouble s (deswegensizeof(double * ) )und die restlichenmalloc() s reservieren wieder dieSpeicherblocke fur je 4double s. Die Adressarithme-tik macht es moglich, dass der Zugriff auf einzelne Ele-mente noch mitm[i][j] moglich ist.

Diese Art ein Feld von Zahlen zu verwalten erlaubtdie großte Flexibilitat, aber erfordert auch einiges anProgrammieraufwand zum Reservieren und spateremFreigeben von Speicher. In den Beispielen fehlen außer-dem Abfragen, obmalloc() uberhaupt erfolgreichwar und nicht etwa eineNULL-Adresse zuruckgegebenhat.

2.2.5 Ubergabe von Kommandozeilenar-gumenten

Eine weitere Anwendung von Zeigerarrays ist dieUber-gabe der Kommandozeilenargumente des Aufrufs ei-nes compilierten Programms. Diese Argumente konnender Funktionmain() ubergeben werden, dazu ein Bei-spiel, das die Argumente ausgibt:

#include <stdio.h>

int main(int argc, char ** argv){

int i;

for (i = 0; i < argc; i++){

printf("%s ", argv[i]);}printf("\n");

return(0);}

main() bekommt dabei immer einint , das die An-zahl der Argumente enthalt (plus 1, weil das 0. der Pro-grammname ist), und eine Adresse eines Arrays mitZeigern aufchar s. Letzters kann alschar * argv[]

oder aquivalentchar ** argv angegeben werden, daArrays immer nur als Adressen ihres 0. Elements uber-geben werden.

In dem Beispiel werden alle Elementeargv[i] ,also Adressen einechar s anprintf() ubergeben.Diese Adressen geben nicht nur einzelnechar s an,sondern Arrays von Zeichen, die mit dem Zeichen’\0’ beendet sind. Das Formatelement fur solche’\0’ -terminierten Zeichenketten ist%s und steht imFormatstring, der als erstes Argument anprintf()ubergeben wird.

2.3 Datenstrukturen

2.3.1 struct-Deklarationen und -Definitionen

Arrays sind dafur geeignet, Felder von Werten glei-chen Datentyps zusammenzufassen. Falls Werte unter-schiedlichen Datentyps zusammengefasst werden sol-len, werden in C sogenanntestruct s eingesetzt. Einstruct -Datentyp muss zunachst deklariert werden.Dabei wird noch kein Speicher reserviert, sondern nurder struct -Datentyp spezifiziert. (Sobald Speicherbelegt oder reserviert wird, wird ein Vorgang Definitionstatt Deklaration genannt.) Mit

struct Kreis{

double m_x;double m_y;double r;

};

wird zum Beispiel ein struct -Datentyp namensKreis deklariert. Jede Variable dieses Datentypsenthalt diex- und y-Koordinate des Mittelpunktsmxundmy , sowie den Radiusr .

Nach dieser Deklaration kann mit

struct Kreis k;

eine Variable vom Typstruct Kreis definiert wer-den. Dabei wird furk wieder ein Stuck Speicher reser-viert, dessen Große mitsizeof(struct Kreis)abgefragt werden kann.

2.3.2 Zugriff auf Elemente von structs

Zugriff auf die Elemente der Struktur ist dann uber denbinaren. -Operator moglich:

Page 44: Skript zum Kompaktkurs C mit Beispielen in OpenGL

44 KAPITEL 2. DATENSTRUKTUREN IN C

k.m_x = 0.0;k.m_y = k.m_x;k.r = 10.0;

Die Struktur-Deklaration und Definition einer Variablekann auch zusammengefasst werden:

struct Kreis{

double m_x;double m_y;double r;

} k;

2.3.3 Zeiger auf structs

Bei derUbergabe vonstruct -Werten als Argumen-te von Funktionen und dem Zuweisen vonstruct -Werten werden jeweils alle Elemente kopiert, was et-was langsam ist. Deswegen werden meistens nur Adres-sen vonstruct -Variablen ubergeben, was wesent-lich schneller ist. Die Adresse derstruct -Variablek kann mit&k ermittelt werden. Wie wird ein Zeigerk zeiger auf einestruct -Variable definiert? Da* k zeiger vom Typstruct Kreis ist, ist die De-finition der Variablek zeiger gegeben durch

struct Kreis * k_zeiger;

und mit

k_zeiger = &k;

kann die Adresse vonk in k zeiger gespeichert wer-den. Damit lautet das Beispiel von oben:

( * k_zeiger).mx = 0.0;( * k_zeiger).my = ( * k_zeiger).mx;( * k_zeiger).r = 10.0;

(Die Klammern sind notig, da der binare Operator.(keine Leerzeichen!) Vorrang vor dem unaren Verweis-Operator hat.) Als Abkurzung kann der-> -Operatorverwendet werden. Damit wurden die Elemente so ge-setzt werden:

k_zeiger->mx = 0.0;k_zeiger->my = k_zeiger->mx;k_zeiger->r = 10.0;

2.3.4 Initialisierung von structs

struct -Variablen konnen aber auch bei ihrer Defini-tion initialisiert werden:

struct Kreis k = {0.0, 0.0, 10.0};

wobei die Reihenfolge der Element in der Dekla-ration implizit verwendet wird. Bei umfangreicherenstruct s ist die Zuordnung der Werte zu den Elemen-ten daher nicht ganz einfach, deshalb wird meistens aufeine derartige Initialisierung verzichtet.

2.3.5 Rekursive structs

Noch eine Bemerkung zurstruct -Deklaration: Wirk-lich rekursivestruct s, alsostruct s die sich selbstals Element beinhalten, machen wenig Sinn, weil sieunendlich groß waren. Aber Elemente einesstruct -Datentyps durfen auch Zeiger auf die gleichestructsein, was sich insbesondere fur vernetzte Datenstruktu-ren, wie verbundene Listen, Baume und Graphen, eig-net.

2.4 Sonstige Datentypen

In diesem Abschnitt sollen weitere Datentypen erklartwerden. Allerdings ist nur die Moglichkeit eigene Da-tentypen mittypedef zu deklarieren wirklich interes-sant. Die Konzepte von Bitfeldern,enum und unionwerden nur der Vollstandigkeit halber erwahnt.

2.4.1 typedef

Inzwischen sollte klar geworden sein, dass Typangabenin C gelegentlich relativ aufwendig werden, z.B. dieDatentypen von Funktionsadressen. Es gibt daher dieMoglichkeit, fur solche komplexen Datentypen einenneuen Namen zu deklarieren. Das wird auch

”Typde-

finition“ genannt, was kein guter Name ist, weil dabeikein Speicher reserviert wird. Deswegen sollte besservon einer Typdeklaration gesprochen werden.

Hier ein Kochrezept fur Typdeklarationen:

1. Man nehme die Definition einer Variable von demDatentyp, fur den ein neuer Name deklariert wer-den soll, und ersetze den Variablennamen durchden neuen Typnamen. (Fur den Typnamen geltendie gleichen Regeln wie fur Variablennamen.)

Page 45: Skript zum Kompaktkurs C mit Beispielen in OpenGL

2.4. SONSTIGE DATENTYPEN 45

2. Die geanderte Variablendefinition wird nun an dasKeyword typdef angehangt, womit die Typde-klaration fertig ist.

Damit sind Typdeklarationen nicht schwerer als Va-riablendefinitionen. (Und fur die gilt:Variablen werdenso definiert, wie sie verwendet werden.)

Als Beispiel soll ein Datentyp namensfTyp dekla-riert werden, der die Adresse einer Funktion beschreibt,die die Adresse eineschar s als Argument erwartet unddie Adresse einesdouble s zuruckgibt. Wie wurde ei-ne Variable namensf dieses Typsverwendetwerden?Ein Funktionsaufruf funktioniert mit( * f)(...) . AlsArgument mussen wir hier den Typ Adresse eineschareinsetzen, alsochar * . Der Funktionsaufruf gibt eineAdresse einesdouble zuruck, alsodouble * . Des-halb ergibt sich als Variablendefinition:

double * ( * f)(char * );

(Dies kann auch so gelesen werden: Wenn( * f)(...) die Adresse einesdouble ergibt,dann ist* ( * f)(...) ein double . Damit kommenwir zur gleichen Definition wie oben.)

In dieser Variablendefinition sollen wir nun den Va-riablennamenf durch den TypnamenfTyp ersetzenundtypedef voranstellen. Also:

typedef double * ( * fTyp)(char * );

Nach so einer Typdeklaration konnen nun entsprechen-de Variablendefinitionen mit diesem Typ viel einfachervorgenommen werden, z.B. definiert

fTyp f;

einen Funktionszeigerf auf eine Funktion, wie sie obenbeschrieben wurde.

Noch ein viel einfacheres Beispiel: Der Typsignedchar wird oft byte genannt. Wie sieht eine ent-sprechende Typdeklaration aus? Wir beginnen einfachmit der Definition einer Variablea vom Typ signedchar :

signed char a;

Ersetzen des Variablennamensa durchbyte und Vor-anstellen vontypedef ergibt die Typdeklaration:

typedef signed char byte;

womit ein neuer Datentypbyte eingefuhrt wurde, des-sen Werte von-128 bis127 gehen.

Oft wird einestruct - mit einer Typdeklaration zu-sammengefasst, um gleich den Typ einer Adresse aufdiestruct zu deklarieren, z.B.:

typedef struct s{

int i;double d;

} * s_adresse;

Damit wird nicht nurstruct s deklariert, sondernauch der Typs adresse alsstruct s * .

2.4.2 Bitfelder

Um Speicherplatz zu sparen, konnen innerhalb vonstruct s Bitfelder definiert werden, d.h. Variablen dieeine individuelle Anzahl von Bits benotigen. Zum Bei-spiel:

struct ampel{

int rot_an : 1;int gelb_an : 1;int gruen_an : 1;

};

struct ampel a;

Die Elemente a.rot an , a.gelb an unda.gruen an belegen nur ein Bit und konnendeshalb nur Werte0 und 1 annehmen. Bitfelderwerden relativ selten eingesetzt; stattdessen wird ofteine Ganzzahl benutzt und die einzelnen Bits mit denbitweisen Operatoren angesprochen.

2.4.3 union

Zu Zeiten, als Speicherplatz noch knapp und teuer war,war es ublich den gleichen Speicherplatz in verschiede-nen Programmteilen fur verschiedene Daten zu benut-zen. Dabei ergibt sich das Problem, dass sich auch derTyp andert. Einunion erlaubt es, verschiedene Daten-typen in dem Speicherplatz einer Variable zu speichern.(Wobei zu einem Zeitpunkt immer nur ein Wert in demSpeicherplatz stehen darf.) Die Auswahl des Typs funk-tioniert dabei syntaktisch so wie beistruct s. ZumBeispiel:

union Koordinate{

long ganzzahlig;float fliesskomma;

};

union Koordinate x;

Page 46: Skript zum Kompaktkurs C mit Beispielen in OpenGL

46 KAPITEL 2. DATENSTRUKTUREN IN C

Hier wird zunachst eineunion Koordinate dekla-riert und dann eine Variablex von diesem Typ definiert.Mit x.ganzzahlig = 123; kann dann einlongin dem Speicherplatz abgespeichert werden und mitx.fliesskomma = 456.789; ein float . Ent-sprechend konnen die Werte auch wieder ausgelesenwerden. Adressen, Zeiger und-> funktionieren wie beistruct s.

Nachdem Speicherplatz inzwischen selten ein Pro-blem ist, und die Benutzung vonunion mit relativgroßem Aufwand verbunden ist, wird es praktisch nichtmehr verwendet.

2.4.4 enum

Aufzahlungen mitenumwerden sehr selten verwendet,vor allem weil es eine andere, machtigere Moglichkeitgibt, Konstanten Namen zu geben. (Diese#define swerden aber in diesem Kurs erst zusammen mit demPraprozessor besprochen.) Mit

enum Wochentag {Montag, Dienstag,Mittwoch, Donnerstag, Freitag,Samstag, Sonntag};

kann ein Datentypenum Wochentag definiert wer-den, der nur die WerteMontag , ..., Sonntag anneh-men kann. (Intern werden diese Werte durch Ganzzah-len reprasentiert.) D.h. mit

enum Wochentag t = Montag;

wird eine Variable vom Typenum Wochentag defi-niert und mit dem WertMontag initialisiert.

Page 47: Skript zum Kompaktkurs C mit Beispielen in OpenGL

Kapitel 3

Einfuhrung in OpenGL

(Dieses Kapitel ist mehr oder weniger eineUberset-zung des ersten Kapitels des

”OpenGL Programming

Guide, Third Edition“ von Woo, Neider, Davis undShreiner.)

3.1 Was ist OpenGL?

Leider gibt es in C und seinen Standardbibliothekenkeine Funktionen fur graphische Ausgaben. OpenGLdagegen steht fur “open graphics library” und bietet ei-ne Vielzahl von Funktionen, um Graphiken zu erstellen.

Das eigentliche OpenGL ist eine sehr hardware-naheSoftware-Schnittstelle aus etwa 200 Kommandos undreicht (im Idealfall) diese Kommandos direkt an dieGraphik-Hardware weiter. Falls die Graphik-Hardwarekeine oder nicht alle OpenGL-Kommandos versteht,mussen die fehlenden Kommandos in Software ausge-wertet werden, was sehr aufwendig sein kann ist.

Der Vorteil der OpenGL-Schnittstelle ist aber, dasssich der Programmierer (solange Geschwindigkeit kei-ne Rolle spielt) keine Gedanken daruber machen muss,wie die OpenGL-Kommandos tatsachlich ausgewertetwerden: Wichtig fur den Programmierer sind nur die C-Funktionen, um OpenGL-Kommandos aufzurufen.

In diesem Kurs wird OpenGL nur ganz oberflachlicherklart, damit uberhauptzweidimensionalegraphischeBeispiele undUbungen moglich sind. Außerdem sollam Beispiel von OpenGL gezeigt werden, wie großeBibliotheken, die nicht zum C Standard gehoren, ver-wendet werden.

Eine tiefergehende Einfuhrung in die Grundlagen derzwei- und dreidimensionalen Computergraphik wirdzum Beispiel in der

”Grundlagenvorlesung Interaktive

Systeme“ von Prof. Thomas Ertl jeweils im Winterse-mester angeboten. Spezielle Algorithmen unter Aus-nutzung spezifischer OpenGL-Fahigkeiten werden im

Sommersemester auch von Prof. Ertl in der”Bildsyn-

these“–Vorlesung besprochen.

3.2 Aus was besteht OpenGL?

OpenGL besteht aus dem eigentlich OpenGL-Kern undeiner Utilities-Bibliothek GLU, die weitere Funktionenenthalt, die nicht von der Grafik-Hardware ausgefuhrtwerden konnen. Daruberhinaus wird in diesem Kursdie Bibliothek GLUT verwendet, die mit den meistenOpenGL-Implementierung mitgeliefert wird, und zumOffnen von Fenstern sowie zum Abfragen von Benut-zeraktionen dient und auch die restliche Kommunikati-on mit dem Betriebssystem erledigt.

Ein C-Programmierer sieht von OpenGL vor allemdie .h -Dateien, namlichgl.h , glu.h undglut.h .Es reicht aber ausglut.h zu #include n, da dieseDatei,#include s fur die beiden anderen enthalt.

Ausserdem mussen die eigentlichen Bibliothekenfur OpenGL, GLU und GLUT vorhanden sein unddem C-Compiler bekannt gemacht werden, genausowie auch die Mathematikbibliothek mit-lm ange-geben werden musste. Bei den OpenGL-Bibliothekenunter UNIX-Systemen sind ublicherweise folgendeC-Compiler-Kommandoschalter notwendig:-lglut-lGLU -lGL -lX11 -lXmu . X11 und Xmu sindnotig da GLUT unter UNIX-Systemen auf X-Windowsaufbaut.

Im Grundstudiumspool mussen außerdem nochPfade zu den .h -Dateien und den OpenGL-Bibliotheken angegeben werden. UmGL/glut.h#include n zu konnen, wird der Pfad mit-I/usr/X11R6/include gesetzt (-I wie

”include“), fur die OpenGL-Bibliotheken mit

-L/usr/X11R6/lib vor den -l Schaltern (-Lwie

”libraries“).

47

Page 48: Skript zum Kompaktkurs C mit Beispielen in OpenGL

48 KAPITEL 3. EINFUHRUNG IN OPENGL

3.3 Wie sieht ein OpenGL-Programm aus?

Hier mal ein ganz einfaches OpenGL-Programm, dasszwei Punkte setzt:

/ * Includes * /

#include <stdio.h>#include <stdlib.h>#include <GL/glut.h>

/ * Funktionsprototypen * /

void malen(void);

/ * Funktionsdefitionen * /

int main(int argc, char ** argv){

/ * Initialisierung * /

glutInit(&argc, argv);glutInitDisplayMode(GLUT_SINGLE |

GLUT_RGB);/ * Fensterbreite und -hoehe * /glutInitWindowSize(400, 300);/ * Fensterposition setzen * /glutInitWindowPosition(100, 100);/ * Fenstertitel setzen * /glutCreateWindow("Hallo!");/ * Hintergrundfarbe setzen * /glClearColor(0.0, 0.0, 0.0, 0.0);/ * Koordinatensystem setzen * /glMatrixMode(GL_PROJECTION);glLoadIdentity();glOrtho(0.0, 400.0, 0.0, 300.0,

-1.0, 1.0);/ * Zeichnende Funktion setzen * /glutDisplayFunc(&malen);/ * Aktivieren des Fensters * /glutMainLoop();return(0);

}

void malen(void)/ * malt in das Fenster * /

{/ * Fenster loeschen * /glClear(GL_COLOR_BUFFER_BIT);

/ * Punkte setzen * /glBegin(GL_POINTS);

/ * Farbe angeben * /glColor3f(1.0, 1.0, 1.0);/ * Koordinaten angeben * /glVertex2i(150, 150);/ * Farbe angeben * /glColor3f(1.0, 0.0, 0.0);/ * Koordinaten angeben * /glVertex2i(250, 150);

glEnd();

/ * alle Kommandos ausfuehren * /glFlush();

}

Hier werden nur zwei Punkte gezeichnet. Angenom-men der Text steht in einer.c -Datei namensmain.c ,dann lasst sich das Programm im Grundstudiumspoolmit der Kommando-Zeile (die hier leider zu breit fureine Zeile ist)

gcc main.c -I/usr/X11R6/include-L/usr/X11R6/lib -lglut -lGLU -lGL-lX11 -lXmu

compilieren und anschließend als./a.out aufrufen.Im Folgenden soll das Programm genauer analysiertwerden:

Wie erwahnt mussen mit

#include <GL/glut.h>

die Funktionen der GLUT-, OpenGL- und GLU-Bibliotheken deklariert werden. Aus welcher Biblio-thek eine Funktion kommt, lasst sich leicht an denNamen ablesen:glut ... steht fur GLUT,gl ... furOpenGL undglu ... fur GLU, letzteres kommt hiernicht vor.

Die main -Funktion ist nur dazu da, um das ganzeSystem zu initialisieren, ein OpenGL-Fenster zu offnenund diese fur das Zeichnen vorzubereiten.

Die GLUT-Funktionen glutInit() ,glutInitWindowSize() ,glutInitWindowPosition() ,glutCreateWindow() und glutMainLoop()werden spater behandelt, bzw. sind fast selbster-klarend. Zur FunktionglutDisplayFunc() sollteangemerkt werden, dass sie eine Funktionsadressevom Typ void ( * )(void) erwartet, die hier

Page 49: Skript zum Kompaktkurs C mit Beispielen in OpenGL

3.3. WIE SIEHT EIN OPENGL-PROGRAMM AUS? 49

einfach mit &malen als Adresse der Funktionsdefi-nition von malen() angegeben wird. Durch dieseFunktionsadresse kann GLUT innerhalb der Funk-tion glutMainLoop() immer dann die Funktionmalen() aufrufen, wenn der Fensterinhalt neugezeichnet werden muss.glutMainLoop() ruftzu diesem Zweck gewissermaßen unser Programmzuruck, weswegen Funktionen wiemalen() auchcallback-Funktionengenannt werden.

Die OpenGL-Kommandos glMatrixMode() ,glLoadIdentity() und glOrtho() legendas verwendete Koordinatensystem bzw. die ver-wendete

”Kameraposition und -richtung“fest. Diese

Moglichkeiten sind insbesondere fur dreidimensionaleGraphiken von besonderem Interesse, um zum Beispieleine Kamerafahrt um ein Objekt herum zu simulie-ren. Da in diesem Kurs aber nur zweidimensionalesOpenGL benutzt wird, brauchen wir diese Funktionennicht weiter zu betrachten. Zu erwahen ist nur, das indiesem Beispiel uberglOrtho das zweidimensionalesKoordinatensystem so gesetzt wird, dass der Ursprungin der linken, unteren Ecke des Fensters liegt unddie Koordinaten einfach die Position der Bildpunkte(Pixel) im Fenster angeben. Das hat den Vorteil, dasswir spater die Koordinaten von Bildpunkte mitint sangeben konnen.

Die Funktion glClearColor() setzt die Rot-,Grun- und Blau-Anteile der Hintergrundfarbe jeweilsvon0.0 (vollig dunkel) bis1.0 (so hell wie moglich).Außerdem wird derα-Kanal der Hintergrundfarbe ge-setzt, der aber in den allermeisten Anwendungen be-deutungslos ist. (Fur Farben von Objekten im Vorder-grund gibt derα-Kanal meistens die Opazitat oderUndurchsichtigkeit an von0.0 (durchsichtig, trans-parent) bis1.0 (undurchsichtig, opaque).) Die Hin-tergrundfarbe wird in der Funktionmalen() vonglClear(GL COLORBUFFERBIT); benutzt, wo-mit der gesamte Fensterinhalt mit der angegebenen Hin-tergrundfarbe gefullt wird.

GL COLORBUFFERBIT ist eine von OpenGL defi-nierte Konstante. Genauer gesagt ist es eine Zweierpo-tenz, also eine Ganzzahl mit nur einem gesetzten Bit.Solche Bits werden auch

”flags“ genannt, also Flag-

gen, die bestimmte ja/nein-Aussagen signalisieren sol-len. Zweierpotenzen konnen mit dem bitweisen ODER-Operator| kombiniert werden. Ein Beispiel dazu ist in

glutInitDisplayMode(GLUT_SINGLE |GLUT_RGB);

zu sehen. Hier werden die KonstantenGLUTSINGLEund GLUTRGBverODERt. Der Vorteil dieser Tech-nik ist, dass mehrere ja/nein-Werte (Boolsche Werte) ineinem int ubergeben werden konnen und nicht eineganze Reihe von Argumenten ubergeben werden muss.Zum Beispiel erlaubtglClear() durch VerODE-Rung mehrerer Bits einige weitere Buffer zu loschen.Aber wenn nur ein Buffer geloscht werden soll, wie hierder Farbbuffer, dann reicht es, die jeweilige Bit-Flagge(hierGL COLORBUFFERBIT ) anzugeben.

Das eigentliche Zeichnen des Fensterinhalts findetin der Funktionmalen() statt. Hier wird zunachstmit glClear() der Fensterinhalt geloscht und dannObjekte gezeichnet.glBegin(GL POINTS) undglEnd() rahmen die Kommandos zum Zeichnen einund geben an, dass Punkte gezeichnet werden sollen,andere graphischePrimitive sind zum Beispiel Linienund Dreiecke. Diese Primitive konnen keine kompli-zierten geometrischen Objekte sein, weil sie von derGraphik-Hardware direkt dargestellt werden mussen.

Mit glColor3f(1.0, 0.0, 0.0); werdendie Farben (Rot-, Grun- und Blau-Anteil) und mitglVertex2i(250, 150); die Koordinaten derPunkte gesetzt.

DasglFlush(); Kommando stellt sicher, dass alleOpenGL-Kommandos ausgefuhrt und nicht langer ge-puffert werden.

Beispiel: Farbverlauf

Um das Fenster mit anderen Inhalten zu fullen, mussnur diemalen() -Funktion geandert werden. Folgen-de Funktion geht in zwei geschachtelten Schleifen furjede Koordinatex (jede Spalte) uber alle Koordinateny (alle Pixel in der Spaltex ) und setzt den Pixel in ei-ner Farbe, deren Rotanteil mit derx -Koordinate wachstund entsprechend der Grunanteil mit dery -Koordinate.

void malen(void){

int x;int y;

glClear(GL_COLOR_BUFFER_BIT);glBegin(GL_POINTS);x = 0;while (x < 400){

y = 0;while (y < 300)

Page 50: Skript zum Kompaktkurs C mit Beispielen in OpenGL

50 KAPITEL 3. EINFUHRUNG IN OPENGL

{glColor3f(x / 300.0,

y / 400.0, 1.0);glVertex2i(x, y);y = y + 1;

}x = x + 1;

}glEnd();glFlush();

}

3.4 Syntax von OpenGL-Kommandos

In dem Beispiel wurden schon einige syntaktische undlexikalische Eigenschaften von OpenGL-Kommandosdeutlich: OpenGL-Befehle beginnen mitgl ..., woransich direkt weitere groß geschriebene Worter anschlie-ßen. Das erste Wort ist oft ein Verb (wenn nicht, istmeistensSet gemeint) und beschreibt, welche Aktio-nen das Kommando durchfuhrt. Konstanten beginnenmit GL ... und sind durchgangig groß geschrieben mitzwischen den Wortern.

Einige Kommandos haben zusatzliche, angehangteBuchstaben wie3f und 2i in glColor3f() bzw.glVertex2i() . Diese Buchstaben beschreiben nichtdas Kommando, sondern die Anzahl (hier3 bzw. 2)und den Typ (hierf fur Fließkomma- undi fur Ganz-zahlen) der Argumente. Viele OpenGL-Kommandossind in verschiedenen Versionen mit unterschiedlichenArgumentanzahlen und -typen definiert, zum Beispielgibt es auch eineglColor4f() -Funktion, die zusatz-lich einen Wert fur die Opazitat erwartet, wahrendglColor3f() die Opazitat einfach auf1.0 setzt.Tabelle 3.1 zeigt eineUbersicht der Buchstabenkurzelfur Argumenttypen und die entsprechenden C- bzw.OpenGL-Typen.

Entsprechend konnte statt

glVertex2i(150, 150);

auch

glVertex2f(150.0, 150.0);

stehen, mit dem einzigen Unterschied, dass die Ar-gumente einmal als Ganzzahlen und im zweiten Fallals Fließkommazahlen ubergeben werden. Eine weite-re gleichbedeutende Variante ist

C-Typ OpenGL-Typb signed char GLbytes short GLshorti int oderlong GLint ,

GLsizeif float GLfloat ,

GLclampfd double GLdouble ,

GLclampdub unsigned char GLubyte ,

GLbooleanus unsigned short GLushortui unsigned int oder GLuint ,

unsigned long GLenum ,GLbitfield

Tabelle 3.1: Die OpenGL-Buchstabenkurzel fur Argu-menttypen.

glVertex3i(150, 150 , 0);

Diese Variante zeigt auch, dass die dritte Koordinate (z)eines Vertex vonglVertex2i() implizit auf 0 ge-setzt wird.

Von einigen OpenGL-Kommandos gibt es auch eineVektor-Version, die durch ein angehangtesv kenntlichgemacht wird, und einen Vektor (Array, also: Adresse)von Werten als Argument erwarten statt mehrere Argu-mente. Z.B. konnte statt

glColor3f(1.0, 0.0, 0.0);

auch geschrieben werden

GLfloat farben[] = {1.0, 0.0, 0.0};

glColor3fv(farben);

Die letzte Zeile ist wieder aquivalent zu

glColor3fv(&farben[0]);

Wenn sich die OpenGL-Dokumentation auf alleVersionen eines OpenGL-Kommandos wieglColorbeziehen will, wird oft z.B.glColor * () verwen-det, wobei der* als Joker klarmachen soll, dasshier noch die richtigen Kurzel, je nach Argument-typ erganzt werden mussen. Entsprechend beziehtsich glColor * v() auf alle Vektor-Versionen vonglColor .

Page 51: Skript zum Kompaktkurs C mit Beispielen in OpenGL

3.7. VERWANDTE BIBLIOTHEKEN 51

3.5 OpenGL als Zustandsautomat

OpenGL ist ein Zustandsautomat, d.h. durch Befehlewird es in bestimmte Zustande versetzt, die solange ak-tiv bleiben, bis sie wieder durch Befehle geandert wer-den. Wie oben gezeigt, ist die aktive Farbe so eine Zu-standsvariable. Nachdem mitglColor * () eine Farbegesetzt wurde, bleibt sie aktiv bis sie durch eine ande-re Farbe ersetzt wird. D.h. bis dahin werden alle Ob-jekte in dieser Farbe gezeichnet. Die aktive Farbe istaber nur eine von vielen Zustandsvariablen, andere be-stimmen die Blickrichtung, die Projektionstransforma-tion, Linienstil, Position der Lichtquellen, Materialei-genschaften, etc. Viele Zustandsvariablen konnen aberauch nur mitglEnable() undglDisable() ein-bzw. ausgeschaltet werden.

Jede Zustandsvariable hat einen Default-Wert.Der aktuell aktive Wert der Variable kann mitglGetBooleanv() , glGetDoublev() ,glGetFloatv() , glGetIntegerv() ,glGetPointerv() oder glIsEnabled()abgefragt werden. Einige andere Zustandsva-riablen besitzen weitere Abfragen, wie z.B.glGetError() . Daruberhinaus konnen Satzevon Zustandvariablen mitglPushAttrib() undglPushClientAttrib() auf einen Stack gerettetwerden und von dort mitglPopAttrib() undglPopClientAttrib() wieder aktiviert werden.

3.6 Rendering-Pipeline

OpenGL verarbeitet Befehle in einer Pipeline, hier nurdie wichtigsten englischen Schlagworter:

• display lists (konnen OpenGL-Kommandos spei-chern und bei Bedarf wieder abspielen; sie sind ei-ne Alternative zum

”immediate mode“ in dem je-

des OpenGL-Kommando sofort ausgefuhrt wird),

• evaluators (dienen zum Berechnen von komplexe-ren Objekten wie gekrummten Flachen),

• per-vertex operations (sind alle Berechnungen,die fur jeden Vertex durchgefuhrt werden, z.B.Transformation, Beleuchtung, Materialberechnun-gen, etc.),

• primitive assembly (darunter laufen einige Opera-tionen auf den Primitiven (Punkte, Linien, Drei-ecke,...) wie z.B. Clipping),

• pixel operations (Bilder, z.B. Texturen, nehmeneinen etwas anderen Weg in der OpenGL-Pipeline,der unter diesem Begriff lauft),

• texture assembly (Texturierung von Primitiven mitBildern ist ein wesentliches Element von Compu-tergraphik; texture assembly organisiert die Textu-ren),

• rasterization (beschreibt die Umwandlung einesgeometrischen Primitivs in eine Menge von Pi-xeln),

• fragment operations (sind Operationen auf Frag-menten, wie Texturierung und verschiedene Tests.Bevor ein Pixel tatsachlich gesetzt wird, heißt dieFarbe fur ein Pixel nur

”Fragment“).

3.7 Verwandte Bibliotheken

Neben der schon erwahnten OpenGL Utility Library(GLU, Prafixglu ), auf die in diesem Kurs nicht wei-ter eingegangen wird, und dem OpenGL Utility Toolkit(GLUT, Prafixglut ), das spater noch genauer vorge-stellt wird, gibt es noch einige Plattform-abhangige Bi-bliotheken wie die OpenGL Extension to the X WindowSystem (GLX, PrafixglX ), fur Windows 95/98/NT dasWindows to OpenGL interface (WGL, Prafixwgl ), furIBM OS/2 das Presentation Manager to OpenGL inter-face (PGL, Prafixpgl ) und fur Apple das AGL (Prafixagl ).

GLUT erlaubt es zwar, einfache OpenGL-Anwendungen Plattform-unabhangig zu program-mieren. Aber da es nicht mehr als der kleinstegemeinsame Nenner der verschiedenen Plattform-abhangigen Schnittstellen ist, muss fur vollausgebauteOpenGL-Applikationen meistens doch auf einePlattform-abhangige Bibliothek zuruckgegriffenwerden. In diesem Kurs begnugen wir uns aber mitGLUT.

Page 52: Skript zum Kompaktkurs C mit Beispielen in OpenGL

52 KAPITEL 3. EINFUHRUNG IN OPENGL

Ubungen zum zweiten Tag

Aufgabe II.1

Zeichenketten (Strings) werden in C bekanntermaßenals null-terminierte char-Arrays implementiert (sieheAbschnitt 2.2.2). In dieser Aufgabe sollen ein paargrundlegende String-Operation implementiert werden.Schreiben sie drei moglichst effiziente Funktionen,die die folgenden Aufgaben losen: Die Lange einesStrings berechnen, zwei gegebene Strings zu einemneuen String verbinden und das Vorhandensein einesTeilstrings innerhalb eines zweiten Strings uberprufen.Schreiben Sie ein Programm das zwei Zeichenketten alsKommandozeilenargumente erwartet und das Ergebnisder drei vorgenannten Operation auf die ubergebenenStrings ausgibt.

Aufgabe II.2

Diese Aufgabe baut auf der Losung von Aufgabe I.3vom Vortag auf.

Schreiben Sie nun ein OpenGL-Programm, dassein Diagramm folgender Art zeichnet: Die horizontaleAchse soll Werte von0.0 bis 4.0 von a (siehe AufgabeI.2) und die vertikale Achse soll Werte von0.0 bis 1.0vonfn (siehe Aufgabe I.2) reprasentieren.

Zeichnen Sie in dieses Diagramm die Punkte mit Ko-ordinaten(a, f100), (a, f101), (a, f102), ...,(a, f199) einund zwar jeweils fura von 0.0 bis 4.0 mit einer geeig-neten Schrittweiten.

Diese Art von Diagramm wird auch Feigenbaum-Diagramm (nach ihrem Erfinder Mitchel Feigenbaum)genannt. Versuchen Sie sich klarzumachen, was die dar-gestellten Strukturen bedeuten.

Aufgabe II.3

Die beruhmten von Benoit B. Mandelbrot entdecktenApfelmannchen beruhen auf einer zweidimensionaleniterativen Berechnungsvorschrift:

(

x0

y0

)

=

(

00

)

,

(

xn+1

yn+1

)

=

(

x2n − y2

n + cx

2xnyn + cy

)

,

Diese Iteration wird furjedenBildpunkt durchgefuhrtbis x2

n + y2n > 4 odern = 100 (bzw. noch großer)

ist. Die Koordinaten des Bildpunktes entscheiden da-bei uber die Konstantencx undcy. Achten Sie bei der

Implementierung darauf, dass die Skalierung und Ver-schiebung der Koordinaten so gewahlt wird, dass sichfur cx Werte von−2.3 bis1 und furcy Werte von−1.25bis1.25 ergeben. Die Punktfarbe ergibt sich aus demn,bei dem die Iteration fur den jeweiligen Punkt abgebro-chen wird. Traditionellerweise werden aber Punkte, furdie der maximale Wert vonn erreicht wird, schwarz ge-zeichnet. Die Menge dieser schwarzen Punkte ist danneine Naherung der Mandelbrotmenge, also derjenigenPunkte, fur die die Iteration niemalsx2

n + y2n > 4 er-

reicht.

Aufgabe II.4

Um Fensterinhalte wie in Aufgabe II.3 und nicht im-mer wieder neu berechnen zu mussen, speichert man dieBilddaten oft in einem Array zwischen und kopiert siedann bei Bedarf aus diesem Array. Modifizieren Sie da-zu Ihre Losung von Aufgabe II.3 so, dass die berechne-ten Farben nicht direkt als Punkte auf dem Bildschirmausgegeben werden, sondern zunachst in einem Arraygespeichert werden. Zur Ausgabe auf dem Bildschirmsollen dann die Daten des Arrays als Punkte dargestelltwerden.

Als Beispiel fur elementare Bildverarbeitung weisenSie vor der Ausgabe außerdem jedem Bildpunkt des Ar-rays (außer den Randpunkten) den Mittelwert aus sei-ner Farbe und der Farbe seiner vier Nachbarn zu. (Die-se Operation kann auch mehrfach angewendet werdenund fuhrt zu immer

”verwascheneren“ Bildern, d.h. ho-

he Frequenzen des Farbsignals werden herausgefiltert.Deswegen ist diese Operation auch ein einfaches Bei-spiel fur einen Tiefpassfilter.)

Erweitern sie ihr Programm so, dass die Fenster-große dynamisch uber Kommandozeilenargumente an-gegeben werden kann. Die Große in x-Richtung soll da-bei uber die Option-x Pixelanzahl , in y-Richtunguber -y Pixelanzahl angegeben werden. Wennbeide oder einer der beiden Parameter fehlen, soll je-weils ein Defaultwert gesetzt werden. Da die Fenster-große nicht mehr fest ist, muss fur das Array zum Zwi-schenspeichern jetzt dynamisch Speicher allokiert wer-den.

(Tipp: Wandeln Sie die Angabe der Pixelanzahl Zei-chen fur Zeichen mit Hilfe des ASCII-Codes der Zei-chen[’0’-’9’] in eine Integer-Zahl um.)

Page 53: Skript zum Kompaktkurs C mit Beispielen in OpenGL

Teil III

Dritter Tag

53

Page 54: Skript zum Kompaktkurs C mit Beispielen in OpenGL
Page 55: Skript zum Kompaktkurs C mit Beispielen in OpenGL

Kapitel 4

Der C-Ubersetzungsprozess

Bisher waren die Beispiele auf einzelne.c -Dateienbeschrankt mit eventuell mehreren Funktionsdefinitio-nen. Komplexe C-Programme bestehen aber meistensaus mehreren Dateien. DerUbersetzungsprozess, derdiese C-Quelldateien in ein einzelnes ausfuhrbares Pro-gramm uberfuhrt, verlauft dabei in mehreren Phasen.

Zunachst liest ein Praprozessor die Quelltexte undfuhrt einige Textersetzungen aus. Das Ergebnis die-ser Textersetzungen wird an den Compiler weiterge-reicht. Dieser erzeugt fur die Eingabedatei eine Objekt-datei mit Maschinencode. Greift diese Objektdatei aufFunktionlitat anderer Objektdateien zu, muss sie mitden anderen Objektdateien gebunden werden. Dies er-folgt durch den Linker. Das Ergebnis schließlich ist dasausfuhrbare Programm. Die folgende Abbildung gibteinenUberblick uber die durchlaufenen Phasen:

*.c/*.h

LinkerCompilerPräpr.

ext. Bibl.

Programm

Da die Aufgabe des Compilers bereits bekannt ist,sollen im Folgenden noch die Aufgaben des Praprozes-sors und des Linkers genauer beleuchtet werden.

4.1 Der Praprozessor

4.1.1 Was ist ein Praprozessor?

Bevor der eigentliche C-Compiler den Text einer.c -Datei auch nur anschaut, lauft der sogenannte Prapro-zessor uber den Text. Der Praprozessor ist ein relativschlichtes Gemut und kann vor allem Text aus ande-ren Dateien einfugen, Textteile ersetzen und weglassen.Der Praprozessor kennt nur wenige Kommandos, die al-le mit einem# beginnen. Die wichtigsten werden imFolgenden beschrieben.

4.1.2 #include

Mit Hilfe von

#include < Filename>

oder

#include " Filename"

kann an der Stelle dieses Kommandos der Inhalt der Da-tei namensFilenameeingefugt werden. Im Allgemei-nen werden damit nur.h -Dateien#include d.

4.1.3 #define und #undef

Mit

#define Symbolname Ersetzungstext

wird der Praprozessor angewiesen, im nachfolgendenText Symbolnamedurch Ersetzungstextzu ersetzen.#define wird vor allem genutzt, um Konstanten einenNamen zu geben, z. B. kann mit

#define PI 3.1415927

55

Page 56: Skript zum Kompaktkurs C mit Beispielen in OpenGL

56 KAPITEL 4. DER C-UBERSETZUNGSPROZESS

erreicht werden, dass vor der CompilierungPI jeweilsdurch3.14151927 ersetzt wird. DerSymbolnamefursolche Konstanten besteht meistens nur aus Großbuch-staben. Ein Beispiel, das meist instddef.h verein-bart wird, istNULL fur 0.

DerErsetzungstextdarf auch fehlen, also”leer“ sein.

Das Gegenstuck zu#define ist #undef Symbol-nameund macht ein vorheriges#define diesesSym-bolnamens unwirksam.

4.1.4 Makros

#define ist aber noch machtiger, weil auch”Argu-

mente“ benutzt werden konnen.#define s mit Argu-menten werden auch Makros genannt. Zum Beispiel:

#define betrag(a) ((a) >= 0 ? \(a) : -(a))

Falls eine Makrodefinition (wie diese) mehr als eineZeile lang werden sollte, muss vor dem Ende der Zeileein\ stehen, um dem Praprozessor mitzuteilen, dass dienachste Zeile auch noch zur Makrodefinition gehort.

Wenn nach dieser”Makrodefinition“ (ein unge-

schickter Begriff, weil es keine Definition im Sinne vonC ist) im Programmtext z. B.

betrag(100. * x)

vorkommt, wird dieser Ausdruck durch das Makro er-setzt, wobei dasa aus der Makrodefinition durch dasArgument100. * x ersetzt wird. Das Ergebnis ist al-so

((100. * x) >= 0 ? (100. * x) :-(100. * x))

Falls also100. * x großer oder gleich0 ist, ergibtsich 100. * x sonst der Wert-(100. * x) , derdann großer0 ist.

Die vielen Klammern in der Makrodefinition sindnotwendig, weil wir nicht wissen welche Operatoren indem Makroargument auftauchen. Beispiel: Wir benut-zen die Makrodefinition

#define betrag(a) a >= 0 ? a : -a

und verwenden spater

5 * betrag(a - b)

Daraus macht der Praprozessor dann

5 * a - b >= 0 ? a - b : -a - b

Das funktioniert aus zwei Grunden nicht: Die5 multi-pliziert nicht das Ergebnis sondern nur dasa in dem Be-dingungsausdruck und das zweite Ergebnis-a - b istetwas anderes als das gewunschte-(a - b) . Es ist al-so notig in Makrodefinitionen den Argumentnamen imErsetzungstextimmer zu klammern und zusatzlich dengesamtenErsetzungstexteinzuklammern.

4.1.5 #if

Der Praprozessor kann angewiesen werden, Text unterder Bedingung einzufugen, dass ein bestimmterSym-bolname#define d wurde oder auch, dass ein be-stimmterSymbolname nicht#define d wurde. ZumBeispiel:

#if defined DEBUGGINGprintf("Hallo!\n");

#endif

Der Text zwischen der#if - und#endif -Anweisungwird nur dann vom Praprozessor durchgelassen, wennder SymbolnameDEBUGGINGvorher mit #definevereinbart wurde. Ansonsten wird der Text (hier alsoder Aufruf vonprintf() ) einfach weggelassen unddeshalb auch nicht compiliert.

#if defined ist aquivalent zu#ifdef . DasGegenstuck ist#if !defined Symbolnamebzw.#ifndef Symbolname, dabei wird der Text bis zum#endif nur dann weitergereicht wenn derSymbolna-menicht#define d wurde.

In dieser Familie gibt es zusatzlich noch diePrapozessor-Anweisung#else , die zusammen mit#if genauso funktioniert, wieif undelse in der ei-gentlichen Sprache C.

Anders als Kommentare konnen#if -#endif -Blocke auch geschachtelt werden. Sie eignen sich des-halb auch zum

”Auskommentieren“ von (kommen-

tierten) Programmteilen. Einrucken von Praprozessor-Anweisungen ist aber problematisch, da altere C-Compiler verlangen, dass das# am Anfang der Zeilesteht. Nach dem# durfen aber meistens noch Leerzei-chen stehen.

Hier noch ein Beispiel: Oft wirdTRUEals 1 undFALSEals0 definiert. Manchmal ist aber nicht klar, obdiese Symbole schon in einer#include ten .h -Dateivereinbart wurden. Mit den folgenden Zeilen kann danneine doppelte Vereinbarung desselben Symbols vermie-den werden:

#if !defined TRUE

Page 57: Skript zum Kompaktkurs C mit Beispielen in OpenGL

4.2. DER LINKER 57

# define TRUE 1#endif#if !defined FALSE# define FALSE 0#endif

Doppelte Vereinbarungen und Deklarationen konnenunter Umstanden unangenehme Konsequenzen ha-ben, das trifft naturlich um so mehr auf doppelt#include te .h -Dateien zu. Eine.h -Datei kann sichaber dagegen schutzen, mehr als einmal#include dzu werden. Zum Beispiel kann eine Datei namensmeins.h mit den Zeilen

#if !defined MEINS_H#define MEINS_H

beginnen und den#if -#endif -Block ganz am Endemit

#endif

schließen. Dadurch wird der Inhalt der Datei nur dannvom Praprozessor weitergereicht, wenn das SymbolMEINS H noch nicht definiert wurde, also normaler-weise beim ersten#include vonmeins.h .

4.1.6 Der ##-Operator

Mit Hilfe des ##-Operators kann ein neuerBezeichner(z. B. Variablen-, Funktions- oder Typname) mit Hil-fe eines Makros aus zwei Teilen zusammengesetzt wer-den. Zum Beispiel:

#define verbinde(a,b) a##b

Folgt nun im Programmtext zum Beispiel

int verbinde(ich, bin);

dann wird daraus

int ichbin;

Der##-Operator wird sehr selten eingesetzt.

4.2 Der Linker

(Dieser Abschnitt ist eine Art uberarbeiteter Auszug ausKapitel 4 des Buchs

”Der C-Experte“ von Andrew Koe-

nig.)

4.2.1 Was ist ein Linker?

Ein C-Compiler ubersetzt in der Regel nur einzelne.c -Dateien in Maschinenbefehle. Das hat fur große Pro-gramme den ungeheuren Vorteil, dass nicht alle.c -Dateien neu ubersetzt werden mussen, wenn nur ei-ne einzelne.c -Datei geandert wurde und deshalb neucompiliert werden muss. Außerdem mussen Biblio-theksfunktionen (die ja meistens auch in C program-miert sind) nicht jedesmal neu ubersetzt werden.

Wie lassen sich trotz diesergetrennten Compilierungverschiedene.c -Dateien und Funktionen aus Biblio-theken zu einem Programm zusammenfassen? Im We-sentlichen ist das die Aufgabe eines Programms, dasLinker genannt wird. Als Aufgabe des C-Compilers er-gibt sich dann,.c -Dateien so zu ubersetzen, dass derLinker sie verwenden kann.

In den Beispielen bisher wurde der Linker immerautomatisch vom Compiler aufgerufen, der nur eine.c -Datei ubersetzen musste. Dem Compiler konnenaber auch mehrere.c -Dateien zumUbersetzen gege-ben werden. Z. B. eine Datei namensmain.c in derstehen konnte:

#include <stdio.h>

int verdopple(int a);

int main(void){

printf("%d\n", verdopple(100));return (0);

}

und eine andere Datei namensverdopple.c , in derdie Definition der Funktionverdopple() steht:

int verdopple(int a){

return(2 * a);}

Das Programm aus diesen beiden kleinen.c -Dateienkann dann z. B. mit

gcc main.c verdopple.c

erzeugt werden. Dabei ubersetzt der Compiler die bei-den .c -Dateien getrennt und der Linker verbindet dieubersetzten Dateien zum Programma.out . (EinzelneFunktionsdefinitionen konnen nicht uber mehrere Da-teien verteilt werden, sondern jede.c -Datei enthalt

Page 58: Skript zum Kompaktkurs C mit Beispielen in OpenGL

58 KAPITEL 4. DER C-UBERSETZUNGSPROZESS

wie in dem Beispiel nur vollstandige Funktionsdefini-tionen.)

Normalerweise erzeugt der Compiler aus einer.c -Datei ein Objektmodul (Dateiendung:.o ). Solche Ob-jektmodule fasst der Linker zu einem ausfuhrbaren Pro-gramm zusammen. Dabei benutzt er manche Objekt-module direkt als.o -Datei, andere holt er aus Biblio-theken von Objektmodulen, z. B. aus den Standardbi-bliotheken oder anderen Bibliotheken, z. B. der vonOpenGL. Entsprechend wird jetzt auch klar, dass dieKommandozeilenbefehle-L und -l des C-Compilersdirekt an den Linker weitergegeben werden, damit derLinker die notwendigen Bibliotheken findet.

Ein Linker betrachtet ein Objektmodul als eine Men-ge externer Objekte. Jedes externe Objekt entsprichtdem Inhalt eines Teils des Speichers und wird ubereinen externen Namenangesprochen. Jede Funktion,die nicht alsstatic deklariert wurde, ist ein externesObjekt. Das gilt auch fur externe Variablen, die nichtals static deklariert wurden. Beispiel: Wenn in derDateiverdopple.c stehen wurde

static int verdopple(int a){

return(2 * a);}

dann ware die Funktionverdopple() nur innerhalbder Dateiverdopple.c sichtbar, d.h. sie darfnichtaus dermain() -Funktion inmain.c aufgerufen wer-den. Allerdings kann dieser Fehler dem Compiler nichtauffallen, da er immer nur einzelne Dateien betrach-tet und in diesem Fall ist weder inmain.c noch inverdopple.c ein Fehler. Dem Linker wird dieserFehler aber auffallen, und er wird eine Fehlermeldungausgeben.

Externe Variablen, sind Variablen, die außerhalb vonFunktionsdefinitionen deklariert (oder definiert) wer-den. Externe Variablen werden aber nur dann zu ex-ternen Objekten (und heißen dannglobaleVariablen),wenn sie nicht alsstatic deklariert wurden. Beispie-le dazu sind in den nachsten Abschnitten.

Ein Linker funktioniert also in etwa so: Er bekommteine Reihe von Objektmodulen und Bibliotheken alsEingabe und produziert daraus als Ausgabe ein ausfuhr-bares Programm.

Im Allgemeinen sollten in einem ausfuhrbaren Pro-gramm alle externen Objekte, insbesondere Funktionenverschiedene Namen besitzen, damit es nicht zu Zwei-deutigkeiten bei der Verwendung eines Namens kommt.Das wurde insbesondere dann zu Problemen fuhren,

wenn ein Objektmodul eine Referenz auf ein externesObjekt enthalt, das in mehreren anderen Objektmodu-len unter dem gleichen Namen existiert. Normalerwei-se muss der Linker nur alle externen Objekte, die in denObjektmodulen definiert sind, sammeln und die Refe-renzen auf diese externen Objekte auflosen, d. h. ihretatsachliche Adresse einsetzen.

4.2.2 extern

Eine Deklaration, die zur Reservierung oder Belegungvon Speicherplatz fuhrt, wird als Definition bezeichnet.Deshalb ist die Deklaration

int a;

außerhalb einer Funktionsdefinition dieDefinition desexternen Objektsa. Sie deklarierta als externe Ganz-zahlvariable und reserviert entsprechend viel Speicher-platz. Die Deklaration

int a = 42;

ist eine Definition vona, die den Anfangswert42 vor-gibt. Also wird nicht nur Speicherplatz reserviert, son-dern auch der Anfangswert fur diesen Speicherplatz an-gegeben. Variablen, die extern definiert, aber nicht in-itialisiert werden, bekommen zu Beginn des Program-mablaufs den Wert0, bzw.0.0 .

Das Keywordextern dient dazu externe Variablenin einem Modul zu deklarieren, dienicht in diesem Mo-dul definiert werden, d.h. fur die in diesem Modul keinSpeicherplatz reserviert wird. Die Deklaration

extern int a;

ist alsokeineDefinition von a. a wird zwar als eineexterne Integervariable deklariert, aber durch Angabedes Keywordsextern wird ausgesagt, dassa in ei-nem anderen Modul definiert wird. Fur den Linker istdiese Deklaration eine Referenz auf ein externes Objekta. Deshalb hat sie auch innerhalb einer Funktionsdefi-nition die gleiche Bedeutung.

Da jedes externe Objekt eines ausfuhrbaren Pro-gramms Speicherplatz benotigt (Variablen, um ihreWerte zu speichern, Funktionen, um die entsprechen-den Maschinenbefehle zu speichern), muss jedes exter-ne Objekt einmal definiert werden. Deshalb muss in ei-nem Programm, in dem

extern int a;

steht, irgendwo auch

Page 59: Skript zum Kompaktkurs C mit Beispielen in OpenGL

4.3. MAKE 59

int a;

vorkommen, entweder in demselben Objektmodul odereinem anderen.

Was passiert, wenn diese Definition mehrmals vor-kommt? Manche Compiler erlauben das und erwarten,dass der Linker die Objekte zu einem Objekt zusam-menfuhrt. Dann ergibt sich aber ein Problem, wenn dasObjekt in den Definitionen unterschiedlich initialisiertwurde. Deshalb ist der Standard die Forderung, dass esgenau eine Definition jedes externen Objekts gibt.

Fur Funktionen gelten dieselben Regeln. Allerdingsist der Unterschied zwischen Funktionsdefinitionen und-deklarationen (Prototypen) an der An- oder Abwesen-heit des Funktionsblocks zu erkennen. Das Keywordextern ist deshalb fur Funktionen nicht notig.

4.2.3 static

Um Namenskonflikte von externen Objekten zu vermei-den, konnen externe Variablen mit dem Modifiziererstatic definiert werden und sind dann nur noch in-nerhalb des Moduls sichtbar. Zum Beispiel bedeutet dieDefinition der externen Variablea

static int a;

innerhalb derselben.c -Datei dasselbe wie

int a;

mit dem Unterschied, dassa vor anderen Objektmodu-len versteckt ist. Wenn sich also mehrere Funktionendieselben externen Objekte miteinander teilen, dannsollten diese Funktionen in einer.c -Datei zusammen-gefasst werden und die benotigten Objekte alsstaticdefiniert werden.

Wieder ubertragt sich dieses Konzept auf Funktio-nen: Eine mitstatic definierte Funktion ist nur in-nerhalb derselben.c -Datei sichtbar. Die Definition ei-ner Funktion, die nur von einer Funktion (oder wenigenanderen) aufgerufen wird, sollte daher mit den Defini-tionen der aufrufenden Funktionen in einer.c -Dateistehen und alsstatic definiert werden.

Der Modifiziererstatic wurde bereits in Kapitel1.5 in einem anderen Zusammenhang verwendet undzwar zum definierenstatischerVariablen innerhalb vonFunktionen. Diese werden nicht bei jedem Funktions-aufruf neu erzeugt und am Ende des Funktionsaufrufswieder ungultig, sondern behalten ihren Wert auch zwi-schen Funktionsaufrufen bei.

4.2.4 Deklarationsdateien

... werden auch Header- oder kurz.h -Dateien genannt.Sie enthalten normalerweise dieDeklarationenvon ex-ternen Objekten, die in einer.c -Datei definiert wer-den, die bis auf die Endung den gleichen Namen hat..h -Dateien von Bibliotheken enthalten oft die Dekla-rationen von mehreren.c -Dateien, und#include nweitere.h -Dateien.

Es ist sinnvoll, dass die.h -Datei von der entspre-chenden.c -Datei#include d wird, denn damit sindzumindest die externen Objekte alle deklariert und inder.c -Datei mussen keine weiteren Funktionsprototy-pen angegeben werden.static -Objekte sollten abernicht in die.h -Datei aufgenommen werden. Vielmehrsollten diestatic -Funktionsprototypen am Anfangder.c -Datei aufgelistet sein.

Der eigentliche Sinn von.h -Dateien ist, dass sie von.c -Dateien#include d werden, die Bezug auf exter-ne Objekte anderer.c -Dateien nehmen, um so die kor-rekte Deklaration der externen Objekte zu gewahrlei-sten. Daneben konnen aber auch alle Typ-,struct -und andere Deklarationen der.h -Datei benutzt werden.

4.3 make

4.3.1 Was ist make?

In den Beispielen wurde das Kommando zum Com-pilieren durch die Benutzung von OpenGL erheblichlanger. Bei großeren C-Programmen aus vielen.c -Dateien und mehreren Bibliotheken ist das Eintippensolche Kommandos noch lastiger. Außerdem ist es sinn-voll den Vorteil des getrennten Compilierens von.c -Dateien auszunutzen:.c -Dateien mussen nur dann neucompiliert werden, wenn sie oder die.h -Dateien, die#include d werden, geandert wurden. Die neuen Ob-jektmodule konnen dann mit den unveranderten vomLinker zu einem Programm zusammengefasst werden.Bei einer großen Zahl von.c -Dateien kann das denZeitaufwand furs Compilieren ganz erheblich verrin-gern.

make ist ein Kommando bzw. Teil einer C-Entwicklungsumgebung, das diese Idee unterstutzt. Da-zu muss einMakefileangegeben werden, das ublicher-weiseMakefile odermakefile heißt. Wennmakeaufgerufen wird, sucht es nach einer Datei namensMakefile odermakefile und baut ausgehend vonden enthaltenen Informationen ein Programm neu auf.

Page 60: Skript zum Kompaktkurs C mit Beispielen in OpenGL

60 KAPITEL 4. DER C-UBERSETZUNGSPROZESS

4.3.2 Makefiles

Ublicherweise enthalten Makefiles im Wesentlichendiese Informationen:

• Den Namen des ausfuhrbaren Programms,

• Dateinamen von.c -Dateien und.o -Dateien (Ob-jektmodulen),

• Compiler- und Linker-Optionen,

• Beschreibungen der Abhangigkeiten,

• Kommandos zur Erzeugung des Programms, bzw.der Objektmodule.

(Nicht berucksichtigt ist dabei die Moglichkeit C-Bibliotheken zu erzeugen.)

”Abhangigkeit“ einer Da-

tei bedeutet, dass z.B. ein Objektmodul neu er-zeugt werden muss, wenn sich die entsprechende.c -Datei seit dem letzten Aufruf vonmake geanderthat. Ebenso muss sie neu erzeugt werden, wenn ei-ne der #include ten .h -Dateien geandert wurde.Die .o -Datei eines Objektmoduls ist also

”abhangig“

von der entsprechenden.c -Datei und allen darin#include ten.h -Dateien.

Das ausfuhrbare Programm wird vom Linker aus den.o -Dateien und den Bibliotheken erzeugt. Da sich Bi-bliotheken meistens nicht andern (wie gehen davon aus,dass keine Bibliotheken neu erzeugt werden), ist dieDatei des ausfuhrbaren Programms nur von den.o -Dateien abhangig.

Die Syntax des Makefiles soll anhand des erstenOpenGL-Beispiels klargemacht werden. Damit derUmgang mit mehreren Dateien klar wird, ist das Pro-gramm in drei (unkommentierte) Dateien aufgeteilt.main.c sieht so aus:

#include <stdio.h>#include <stdlib.h>#include <GL/glut.h>

#include "malen.h"

int main(int argc, char ** argv){

glutInit(&argc, argv);glutInitDisplayMode(GLUT_SINGLE |

GLUT_RGB);glutInitWindowSize(400, 300);glutInitWindowPosition(100, 100);glutCreateWindow("Hallo!");

glClearColor(0.0, 0.0, 0.0, 0.0);glMatrixMode(GL_PROJECTION);glLoadIdentity();glOrtho(0.0, 400.0, 0.0, 300.0,

-1.0, 1.0);glutDisplayFunc(&malen);glutMainLoop();return(0);

}

Hier steht wirklich nur noch diemain() -Funktion.Die Funktion malen() wird beim Aufruf vonglutDisplayFunc() referenziert und muss da-her durch das#include von malen.h deklariertwerden. (Hier wird #include "malen.h" statt#include <malen.h> verwendet, um deutlich zumachen, dassmalen.h nicht aus einer fertigen Biblio-thek kommt, sondern Teil des (sich anderenden) Pro-gramms ist.)malen.h sieht so aus:

#if !defined MALEN_H#define MALEN_H

#include <stdio.h>#include <stdlib.h>#include <GL/glut.h>

void malen(void);

#endif

Wie erwahnt dienen die ersten beiden und die letzteZeile dazu, ein wiederholtes#include n zu verhin-dern. Nach der Bearbeitung durch den Praprozessor ste-hen hier nur noch Deklarationen, keine Definitionen.Die Definition vonmalen() findet sich schließlich inmalen.c :

#include "malen.h"

void malen(void){

glClear(GL_COLOR_BUFFER_BIT);glBegin(GL_POINTS);

glColor3f(1.0, 1.0, 1.0);glVertex2i(150, 150);glColor3f(1.0, 0.0, 0.0);glVertex2i(250, 150);

glEnd();glFlush();

}

Page 61: Skript zum Kompaktkurs C mit Beispielen in OpenGL

4.3. MAKE 61

Die erste Zeile dient nicht nur dazu die Funktionmalen() zu deklarieren (was hier sowieso nicht not-wendig ist), sondern ist notwendig um die.h -Dateienvon OpenGL zu#include n.

Die Entscheidung, ob diese.h -Dateien in der.c -Datei oder der entsprechenden.h -Datei #include twerden sollen, ist hier nicht wichtig. Manchmal mussensie aber in der.h -Datei stehen, weil sie Typen dekla-rieren, die auch in den Funktionsprototypen verwen-det werden. Entsprechend sind z. B. auchstruct -Deklarationen meistens nicht in einer.c -Datei, son-dern in der entsprechenden.h -Datei.

Ein einfaches Makefile fur diese Programm konntenun so aussehen:

SRCS = main.c malen.cOBJS = main.o malen.oCC = gccCFLAGS = -g -ansi -Wall \

-I/usr/X11R6/includeSYSLIBS = -L/usr/X11R6/lib -lglut \

-lGLU -lGL -lX11 -lXmu -lm

main : $(OBJS)$(CC) $(OBJS) $(SYSLIBS) \$(CFLAGS) -o $@

main.o : malen.h

malen.o : malen.h

.c.o :$(CC) -c $(CFLAGS) $<

Uberlange Zeilen sind hier durch ein\ getrennt, so wiebei mehrzeiligen Praprozessor-Anweisungen. Wichtigist auch, dass die Einruckungen in Makefiles durch

”harte“ Tabulatoren (also nicht durch eine Reihe von

Leerzeichen, sondern durch ein Tabulatorzeichen) er-zeugt werden mussen.

Dieses Makefile definiert zunachst einige Symbo-le: SRCS(fur sources) als die Liste der.c -Dateien,OBJS (fur objects) als die Liste der entsprechenden.o -Dateien,CC als das Kommando zum Aufruf desC-Compilers undCFLAGSals die Optionen des C-Compilers.

Dann werden mit

main : $(OBJS)

die Abhangigkeiten des ausfuhrbaren Programmsmain angegeben. (Links von: steht das zu erzeugen-

deZiel (in diesem Fall die Dateimain ) und rechts da-von die Dateien, von denen es abhangt.) Wie erwahnthangt das Programm nur von den.o -Dateien ab, diein dem SymbolOBJSdefiniert wurden. (Mit$(...)wird in einem Makefile der Wert eines vorher im Ma-kefile definierten Symbols eingesetzt.) In den nachstenZeilen mussen die Anweisung stehen, um die Zieldatei(hier also das ausfuhrbare Programm) zu erzeugen. Vorjeder Anweisung muss ein Tabulatorzeichen stehen, da-mit make die Zeile als Anweisung versteht. Werden inder Anweisung

$(CC) $(OBJS) $(SYSLIBS) \$(CFLAGS) -o $@

alle Symbole eingesetzt, dann ergibt sich eine ganz ahn-liche Kommandozeile, wie sie schon vorher in den Bei-spielen verwendet wurde. Das$@in -o $@ beziehtsich auf den Namen der Zieldatei (hiermain ) undweist deshalb den C-Compiler an, das fertige Programmmain zu nennen.

Die beiden Zeilen

main.o : malen.h

malen.o : malen.h

enthalten keine Anweisungen, sondern nur die schonerwahnten Abhangigkeiten von.h -Dateien.

Mit der Suffix-Regel

.c.o :$(CC) -c $(CFLAGS) $<

wird make angewiesenjede.o -Datei aus der gleichna-migen.c -Datei mit der Anweisung

$(CC) -c $(CFLAGS) $<

zu erzeugen. Das-c weist dabei den C-Compiler an,nur zu compilieren und nicht den Linker aufzurufen,deshalb mussen auch keine Bibliotheken angegebenwerden. ($< steht fur den Namen der jeweiligen.c -Datei.)

Mit Hilfe dieses Makefiles ist es nun moglich ein-fach make main (oder nurmake, das ist aquivalentweil main die erste angegebene Zieldatei ist) aufzu-rufen. make findet dann automatisch heraus, welche.c -Dateien geandert wurden und deshalb neu ubersetztwerden mussen.

Manchmal ist es aber notwendig das Projekt kom-plett neu zu ubersetzen, z. B. wenn andere C-Compiler-Optionen verwendet werden sollen. Dazu mussen alle

Page 62: Skript zum Kompaktkurs C mit Beispielen in OpenGL

62 KAPITEL 4. DER C-UBERSETZUNGSPROZESS

.o -Dateien geloscht werden, was auch einfach in einMakefile eingebaut werden kann:

clean :rm $(OBJS)

Fallsmake clean eingegeben wird, werden mit Hil-fe desrm Kommandos alle.o -Dateien geloscht undmussen bei spaterenmake-Aufrufen gegebenenfallsneu erzeugt werden. Weilclean keine echte Datei ist,und damit es nicht zu einem Konflikt mit einer Datei na-mensclean kommt, sollte noch.PHONY : cleaneingefugt werden.

Außerdem wird ublicherweise das erste Zielall ge-nannt und fuhrt zur Erstellung aller Programme. In un-serem Fall also nur vonmain . Die Zeile nach den Sym-boldefinitionen sollte deshalb heißen:

all : main

4.3.3 makedepend

Bei großeren C-Programmen mit sehr vielen.c - und.h -Dateien ist die Eingabe der Abhangigkeiten miteinem gewissen Aufwand verbunden.makedependist in der Lage, die Abhangigkeiten der.o -Dateienselbstandig zu ermitteln und in das Makefile einzubau-en. Zur Demonstration beginnen wir mit einem Makefi-le ohne die Abhangigkeiten:

SRCS = main.c malen.cOBJS = main.o malen.oCC = gccCFLAGS = -g -ansi -Wall \

-I/usr/X11R6/includeSYSLIBS = -L/usr/X11R6/lib \

-lglut -lGLU -lGL \-lX11 -lXmu -lm

all : main

main : $(OBJS)$(CC) $(OBJS) \$(SYSLIBS) $(CFLAGS) -o $@

.PHONY : cleanclean :

rm $(OBJS)

.c.o :$(CC) -c $(CFLAGS) $<

depend :makedepend -Y $(SRCS)

Hier wurde ein Zieldepend angegeben, das zum Auf-ruf von makedepend fuhrt und die .c -Dateien anmakedepend ubergibt. Mitmake depend wird die-se Anweisung ausgefuhrt.makedepend fugt dann amEnde des Makefiles z. B. die Zeilen:

# DO NOT DELETE

malen.o: malen.hmain.o: malen.h

ein, die die Abhangigkeiten der.o -Dateien enthalten.Naturlich wird an diesen akademischen Beispielen

der Nutzen vonmakedepend und vielleicht von Ma-kefiles uberhaupt noch nicht allzu deutlich. Beispieleaus dem

”richtigen Leben“ sind aber nicht nur großer,

sondern auch unubersichtlicher, so dass sie vielleichtfur eine Einfuhrung noch ungeeigneter sind.

Page 63: Skript zum Kompaktkurs C mit Beispielen in OpenGL

Kapitel 5

Eigene Datentypen in C

Nachdem das Konzepts der Aufteilens eines Pro-gramms in mehrere Module und deren Zusammenuhrenmit Hilfe des Linkers nun bekannt ist, ist es uns jetztmoglich Programmcode zu schreiben, den wir im-mer wieder in verschiedenen Programmen verwendenkonnen. Durch die Auslagerung in eigene Program-module muss der Quelltext nicht kopiert werden, son-dern kann durch einbinden der entsprechenden Header-Dateien und dazulinken der zugehorigen Objektmodulewiederverwendet werden. Diese Konzept hat den Vor-teil, das eventuelleAnderungen und Fehlerbehebungennur einmal vorgenommen werden mussen und fur alleProgramme, die die enstprechende Funktionalitat ver-wenden, sofort verfugbar sind.

In diesem Abschnitt soll die Erzeugung von wieder-verwendbarem Code am Beispiel der Implementierungeines neuen Datentyps verdeutlicht werden..

5.1 Gekapselte Datentypen in C

Als Beispiel fur einen nutzlichen Datentyp sollen hierdreidimensionale Vektoren betrachtet werden. Zunachstwerden die Strukturelemente(data members)festge-legt. Fur einen dreidimensionalen Vektor bieten sich 3double s an. Wir deklarieren also einestruct mit 3double s:

struct Vector3d{

double x;double y;double z;

};

Das Kurzel3d steht fur 3double s. Da der Datentypstruct Vector3d etwas umstandlich zu schreiben

ist, vereinbaren wir besser einen neuen Typ. Aus obigerstruct Deklaration wird also:

typedef struct{

double x;double y;double z;

} Vector3d;

Der Datentyp heißt jetztVector3d (statt structVector3d ), sonst hat sich nichts geandert.

Damit haben wir schon eine wichtige Entscheidunggetroffen: Der Typ unseres Datentyps ist einestruct ,nichteine Adresse auf einestruct -Variable. Der Un-terschied ist bei großenstruct s ganz erheblich, dennum eine großestruct -Variable als Argument an eineFunktion zu ubergeben, muss einiges an Speicherplatzreserviert und kopiert werden. Die Adresse als Argu-ment zu ubergeben, ist fur großestruct s wesentlichschneller. Meistens werden deshalb Adressen uberge-ben. Hier sollen beide Moglichkeiten vorgestellt wer-den. Zunachst ist unserestruct mit 3 double s nochklein genug, um sie komplett ubergeben zu konnen. Imnachsten Abschnitt wird die Alternative mit Adressen(Referenzen) vorgestellt.

Um Namenskonflikte zu verhindern, lassen wir dieNamen aller Funktionen, die diesen Datentyp verarbei-ten (function members), mit v3d ... beginnen (ahn-lich wie OpenGL seine Kommandos mit dem Pre-fix gl... kenntlich macht). Ausserdem schreibenwir alle unsere Funktionen zum DatentypVector3din eine Datei vector3d.c mit einer .h -Dateivector3d.h .

Welche Funktionen brauchen wir? Neben den mathe-matischen Operationen fur dreidimensionale Vektorengibt es einige wichtige Funktionen, die furjedenso zu-sammengesetzten Datentyp sinnvoll sind:

63

Page 64: Skript zum Kompaktkurs C mit Beispielen in OpenGL

64 KAPITEL 5. EIGENE DATENTYPEN IN C

• eine Funktion zur Erzeugung einer Variable vondiesem Typ (der sogenannteKonstruktor, das Ge-genstuck (einenDestruktor) brauchen wir hiernoch nicht),

• Funktionen zum Setzen und Auslesen der Struktu-relemente (sogenanntedata access functions).

Der zweite Punkt ist sehr wichtig zur Kapselung: EinProgramm-Modul, das unsere Funktionen verwendet,sollte nicht von der Implementation des Datentypsabhangig sein. D. h. wenn wir uns entscheiden sollten,unseren Vektor nicht mehr mit kartesischen Koordina-ten zu speichern, sondern z. B. mit Kugelkoordinaten,dann sollte es ausreichen, unsere Funktionen entspre-chend umzuschreiben. Alle anderen Programm-Modulesollten davon nicht betroffen werden. Die Implemen-tation sollte im Sinne desinformation hidingverstecktsind. In C ist es leider so, dass wir gezwungen sind, un-sere Datentypdeklaration in der.h -Datei anzugeben, sodass jeder Nutzer (auch wenn er die.c -Datei nicht hat),Einblick auf die Strukturelemente haben kann. Echtesinformation hiding ist also in C nicht moglich: Wirmussen darauf vertrauen, dass Benutzer unserer Funk-tionen nie auf die Strukturelemente zugreifen und statt-dessen unsere Zugriffsfunktionen benutzen. Damit die-se Funktionen wenigstens nicht langsamer sind als derdirekte Zugriff, verwenden wir Makros.

Neben den erwahnten Funktionen soll in unseremBeispiel nur die Lange des Vektors und der normali-sierte Vektor berechnet werden. Damit konnte unsere.h -Datei so aussehen (auf Englisch, ohne Kommenta-re):

#if !defined VECTOR3D_H#define VECTOR3D_H

#include <stdlib.h>#include <math.h>

typedef struct{

double x;double y;double z;

} Vector3d;

#define v3d_get_x(v) ((v).x)#define v3d_get_y(v) ((v).y)#define v3d_get_z(v) ((v).z)

#define v3d_set_x(v, new_x) \((v).x = (new_x))

#define v3d_set_y(v, new_y) \((v).y = (new_y))

#define v3d_set_z(v, new_z) \((v).z = (new_z))

Vector3d v3d_new(double x,double y, double z);

double v3d_get_length(Vector3d v);Vector3d v3d_normalized(

Vector3d v);

#endif

Und die entsprechende.c -Datei sieht so aus:

#include "vector3d.h"

Vector3d v3d_new(double x,double y, double z)

{Vector3d v;

v.x = x;v.y = y;v.z = z;

return(v);}

double v3d_get_length(Vector3d v){

return(sqrt(v.x * v.x +v.y * v.y + v.z * v.z));

}

Vector3d v3d_normalized(Vector3d v)

{Vector3d n;double length;

length = v3d_get_length(v);

if (length > 0.0){

n.x = v.x / length;n.y = v.y / length;n.z = v.z / length;

Page 65: Skript zum Kompaktkurs C mit Beispielen in OpenGL

5.2. REFERENZIERTE DATENTYPEN IN C 65

}else{

n.x = 0.0;n.y = 0.0;n.z = 0.0;

}

return(n);}

Funktionen, die nur innerhalb vonvector3d.c ver-wendet werden und nicht von außen sichtbar sein sol-len, mussen alsstatic definiert werden. Ihre Prototy-pen durfen auch nicht in der.h -Datei stehen, sondernsollten am Anfang der.c -Datei stehen.

Der Datentyp konnte dann z. B. so verwendet wer-den:

#include "vector3d.h"

...

Vector3d a;Vector3d n;

a = v3d_new(10.0, 5.0, -3.0);n = v3d_normalized(a);

5.2 Referenzierte Datentypen in C

Fur großestruct s ist die Vorgehensweise im vor-herigen Abschnitt sehr ineffektiv, denn dieUbergabeund Ruckgabe von großen Datenstrukturen ist mit demKopieren von viel Speicherplatz verbunden. Alternativkonnen wir ausschließlich mit Adressen (also Referen-zen) arbeiten.

Da wir nur noch Referenzen speichern, deklarierenwir nur einen TyprVector3d als Adresse auf un-serestruct (nicht mehr diestruct selbst). EineVariable vom Typ rVector3d ist keine struct -Variable, sondern ein Zeiger darauf.struct -Variablenverwenden wir nicht mehr, deswegen mussen wir unsauch den Speicherplatz selbst mitmalloc() reser-vieren, d. h. unser Konstruktor ruft jetztmalloc()auf. Entsprechend brauchen wir auch einenDestruktor,derfree() aufruft. Außerdem lohnt sich gelegentlichein sogenannterCopy-Constructor, der eine Kopie einerexistierenden Variable dieses Typs herstellt.

Um nicht in Namenskonflikte zu geraten, nehmen wirals neues Kurzel fur Funktionenrv3d... und nennendie .c -Datei fur den TyprVector3d entsprechendrvector3d.c . Die Dateirvector3d.h konnte soaussehen:

#if !defined RVECTOR3D_H#define RVECTOR3D_H

#include <stdlib.h>#include <math.h>

typedef struct{

double x;double y;double z;

} * rVector3d;

#define rv3d_get_x(v) ((v)->x)#define rv3d_get_y(v) ((v)->y)#define rv3d_get_z(v) ((v)->z)

#define rv3d_set_x(v, new_x) \((v)->x = (new_x))

#define rv3d_set_y(v, new_y) \((v)->y = (new_x))

#define rv3d_set_z(v, new_z) \((v)->z = (new_x))

rVector3d rv3d_new(double x,double y, double z);

rVector3d v3d_copy(rVector3d v);void rv3d_delete(rVector3d v);

double rv3d_get_length(rVector3d v);

rVector3d rv3d_normalized(rVector3d v);

#endif

In der entsprechenden Dateirvector3d.c mussenwir jetzt leicht umdenken, weilrVector3d ein Adres-styp ist:

#include "rvector3d.h"

rVector3d rv3d_new(double x,double y, double z)

{

Page 66: Skript zum Kompaktkurs C mit Beispielen in OpenGL

66 KAPITEL 5. EIGENE DATENTYPEN IN C

rVector3d v;

v = (rVector3d)malloc(sizeof( * v));

if (NULL == v){

return(NULL);}v->x = x;v->y = y;v->z = z;

return(v);}

rVector3d rv3d_copy(rVector3d v){

rVector3d new;

new = rv3d_new(v->x, v->y,v->z);

return(new);}

void rv3d_delete(rVector3d v){

free((void * )v);}

double rv3d_get_length(rVector3d v){

return(sqrt(v->x * v->x +v->y * v->y + v->z * v->z));

}

rVector3d rv3d_normalized(rVector3d v)

{rVector3d n;double length;

length = rv3d_get_length(v);

if (length > 0.0){

n = rv3d_new(v->x / length,v->y / length,v->z / length);

}

else{

n = rv3d_new(0.0, 0.0, 0.0);}return(n);

}

Das Anwendungsbeispiel sieht dann so aus:

#include "rvector3d.h"...rVector3d a;rVector3d n;

a = rv3d_new(10.0, 5.0, -3.0);if (NULL != a){

n = rv3d_normalized(a);if (NULL != n){

...}rv3d_delete(n);

}rv3d_delete(a);

Den Destruktorrv3d delete() immer aufrufen zumussen, ist naturlich lastig. Noch schlimmer sind aberdie vielen Aufrufe vonmalloc() (beziehungswei-se des Konstruktors) und die damit verbundenen Feh-lerabfragen. Deswegen werden bei referenzierten Da-tentypen eher Prozeduren geschrieben, die Variablenandern, anstatt neue zu erzeugen. Also z. B. eine Pro-zedurnormalize() , die einen Vektor normalisiert,anstatt einer Funktionnormalized() , die einen nor-malisierten Vektor zuruckgibt. Beispiel:

void rv3d_normalize(rVector3d v)

{double length;

length = rv3d_get_length(v);

if (length > 0.0){

v->x = v->x / length,v->y = v->y / length,v->z = v->z / length,

}}

Page 67: Skript zum Kompaktkurs C mit Beispielen in OpenGL

5.2. REFERENZIERTE DATENTYPEN IN C 67

Damit wurde die Normalisierung von vorhin so ausse-hen:

#include "rvector3d.h"...rVector3d a;

a = rv3d_new(10.0, 5.0, -3.0);if (NULL != a){

rv3d_normalize(a);...

}rv3d_delete(a);

Das ist schon deutlich weniger aufwendig als das dieAnwendung vonrv3d normalized .

Page 68: Skript zum Kompaktkurs C mit Beispielen in OpenGL

68 KAPITEL 5. EIGENE DATENTYPEN IN C

Page 69: Skript zum Kompaktkurs C mit Beispielen in OpenGL

Kapitel 6

Zeichnen geometrischer Objekte mitOpenGL

(Dieses Kapitel ist mehr oder weniger eineUber-setzung des zweiten Kapitels des

”OpenGL Program-

ming Guide, Third Edition“ von Woo, Neider, Davisund Shreiner.)

Obwohl mit OpenGL sehr komplexe und interessan-te Bilder gezeichnet werden, sind sie doch alle aus ei-ner kleinen Zahl primitiver graphischer Elemente auf-gebaut. Auf der hochsten Abstraktionsstufe gibt es dreigrundlegende Operationen zum Zeichnen: Loschen desFensters, Zeichnen eines geometrischen Objekts undZeichnen eines Rasterbilds; letzteres wird hier nicht be-handelt. Vielmehr beschranken wir uns auf das Zeich-nen geometrischer Objekte, wie Punkte, gerade Stricheund flache Polygone.

Aus diesen wenigen Teilen konnen schon beein-druckende Szenarien entstehen, wenn nur genug einfa-che Objekte zusammengesetzt werden, um z. B. sanftgebogene Flachen oder Kurven darzustellen. Danebenerlaubt OpenGL eine Menge von speziellen Algorith-men, die es zum Beispiel Spielen ermoglichen immerrealistischere Bilder auch in Echtzeit zu erzeugen, ob-wohl in diesen Fallen verhaltnismaßig wenige Polygo-ne gezeichnet werden. Auf diese Tricks wird hier nichteingegangen. (Sie werden zum Teil in der Vorlesung

”Bildsynthese“ im Sommersemester von Prof. Thomas

Ertl besprochen.) Vielmehr soll hier die grundlegendeVorgehensweise beim Zeichnen einfacher Objekte mitOpenGL erklart werden.

6.1 Ein Survival-Kit zum Zeich-nen

In diesem Abschnitt geht es noch nicht um geometri-sche Objekte, sondern um Voraussetzungen, wie einemgeloschten Fenster, der Auswahl einer Farbe, dem Lee-ren den OpenGL-Kommandobuffers, um die Ausgabezu erzwingen, und die Spezifikation einfacher Koordi-natensysteme.

6.1.1 Loschen des Fensters

Vor dem Zeichnen in ein Fenster sollte zunachst der In-halt geloscht werden, d. h. alle Bildpunkte auf eine ge-meinsame Hintergrundfarbe gesetzt werden. Naturlichkonnte das durch das Zeichnen eines großen Recht-ecks erreicht werden, aber ein spezieller Befehl zumLoschen des Fensters erlaubt eine schnellere Implemen-tierung — vor allem in Hardware.

Der Befehl zum Setzen dieser Hintergrundfarbe wur-de schon erwahnt. Hier die Funktionsdeklaration:

void glClearColor(GLclampf Rot,GLclampf Grun, GLclampf Blau,GLclampf Alpha);

Damit werden dieRot-, Grun-, Blau-undAlpha-Anteileder Hintergrundfarbe gesetzt, die alle auf das Inter-vall [0, 1] geclampt werden, was durch den DatentypGLclampf deutlich gemacht wird.Clampingheißt indiesem Fall, dass Werte kleiner 0 auf 0 und Wertegroßer 1 auf 1 gesetzt werden.

Fur OpenGL hat jedes Pixel nicht nur eine Far-be, sondern auch andere Eigenschaften, wie eineTie-fe (bei dreidimensionalen Graphiken), eventuell eine

69

Page 70: Skript zum Kompaktkurs C mit Beispielen in OpenGL

70 KAPITEL 6. ZEICHNEN GEOMETRISCHER OBJEKTE MIT OPENGL

Maske, die ihn gewissermaßen unsichtbar fur bestimm-te Zeichenoperationen macht, oder auch weitere Far-ben, die unabhangig von der dargestellten Farbe gespei-chert werden. Diese Informationen sind in verschiede-nen Buffern gespeichert.

Der Befehl zum Loschen der Buffer eines Fensters ist

void glClear(GLbitfield Bitmas-ke);

Die Bitmaskeergibt sich aus der bitweisen VerODE-Rerung der KonstantenGL COLORBUFFERBIT (furden Farbbuffer),GL DEPTHBUFFERBIT (fur denTiefen- oder z-Buffer), GL ACCUMBUFFERBIT(fur den Accumulation-Buffer) undGL STENCIL BUFFERBIT (fur den Stencil-Buffer).

Also kann mit

glClear(GL_COLOR_BUFFER_BIT |GL_DEPTH_BUFFER_BIT);

sowohl der Farb- als auchz-Buffer geloscht werden,was eventuell schneller ist, als die beiden Buffer separatzu loschen. Loschen ist tatsachlich eine verhaltnism¨aßigaufwendige Operation, da jeder Pixel betroffen ist.

6.1.2 Spezifizieren einer Farbe

Farbberechnungen konnen in OpenGL durch Beleuch-tungen und Texturen sehr aufwendig sein. Im einfach-sten Fall wird aber lediglich eine Farbe spezifiziert, diedann fur alle Objekte verwendet wird, bis eine neue Far-be angegeben wird. Diese Vorgehensweise ist wesent-lich effizienter als fur jedes Objekt eine Farbe angebenzu mussen, da oft viele Objekte die gleiche Farbe besit-zen.

Eine Farbe wird mitglColor3f( Rot, Grun, Blau)angegeben. Man kann sich vorstellen, dass die drei An-teile (additiv) gemischt werden.0.0 bedeutet, dass voneinem Anteil nichts genommen wird;1.0 bedeutet, dassmoglichst viel davon genommen wird. Damit ergebensich z. B. diese Farben:

Farbe Rot Grun BlauSchwarz 0.0 0.0 0.0

Rot 1.0 0.0 0.0Grun 0.0 1.0 0.0Gelb 1.0 1.0 0.0Blau 0.0 0.0 1.0

Violett 1.0 0.0 1.0Turkis 0.0 1.0 1.0Weiß 1.0 1.0 1.0

OpenGL kennt noch eine vierte Komponente (Al-pha), die vonglColor3f auf 0.0 gesetzt wird. DerAlpha-Anteil kann z. B. genutzt werden, um transpa-rente Farben zu spezifizieren.

6.1.3 Leeren des OpenGL-Kommandobuffers

OpenGL-Kommandos werden in einer Pipeline bear-beitet, womit schon ein gewisser Grad an Parallelisie-rung erreicht wird. In einigen Fallen werden Komman-dos aber auch gebuffert, um sie spater effizienter uber-tragen oder ausfuhren zu konnen. Mit

void glFlush(void);

kann sichergestellt werden, dass zumindest angefangenwird, alle Kommandos tatsachlich auszufuhren.

Falls das nicht ausreicht, kann mit

void glFinish(void);

solange gewartet werden, bis alle OpenGL-Kommandos beendet wurden.

6.1.4 Einfache Koordinatensysteme

Die Angabe einer dreidimensionalen Kamerapositionund -richtung ist naturgemaß etwas kompliziert, insbe-sondere wenn die Kamera (wie jede gute Kamera) zen-tralperspektivische Bilder machen soll. OpenGL kannzentralperspektivische Darstellungen berechnen, aberhier soll nur ein ganz einfaches parallelperspektivischesKoordinatensystem spezifiziert werden.

glViewport(0, 0, (GLsizei)b,(GLsizei)h);

glMatrixMode(GL_PROJECTION);glLoadIdentity();gluOrtho(xmin, xmax,

ymin, ymax);

spezifiziert ein Koordinatensystem in einem Fensterder Pixelhoheh und der Pixelbreiteb mit dem Punkt(xmin ,ymin ) in der linken unteren Ecke des Fenstersund (xmax,ymax) in der rechten, oberen Ecke.

6.2 Spezifizieren von Punkten, Li-nien und Polygonen

Die geometrischen Primitive von OpenGL haben nichtganz die Bedeutung der gleichnamigen mathematischen

Page 71: Skript zum Kompaktkurs C mit Beispielen in OpenGL

6.2. SPEZIFIZIEREN VON PUNKTEN, LINIEN UND POLYGONEN 71

Objekte. Positionen konnen z. B. nicht beliebig genauangegeben werden und auf realen Bildschirmen konnendie Objekte nicht mit beliebig hoher Auflosung wieder-gegeben werden.

6.2.1 Punkte

Ein Punkt wird mit OpenGL durch einen Vertex ange-geben, wobei die Koordinaten des Vertex durch dreiGleitkommazahlen spezifiziert werden. Vertizes sindfur OpenGL immer dreidimensional, falls keine dritteKoordinate (z) angegeben wurde, wird0.0 angenom-men.

6.2.2 Linien

... sind eigentlich Linienstucke, also die gerade Verbin-dung zwischen den Vertizes an den Endpunkten. DurchVerbindung von vielen Linienstucken an den jeweili-gen Endpunkten konnen auch

”glatte“ Kurven darge-

stellt werden. Zumindest so glatt, dass die Zusammen-setzung aus geraden Stucken nicht mehr auffallt.

6.2.3 Polygone

... bestehen aus der Flache, die von einer Folge vonLinienstucken eingeschlossen wird. Die Linienstuckewerden durch die Vertizes an ihre Endpunkte, also denEcken des Polygons, beschrieben.

OpenGL garantiert nicht, dass jedes Polygon richtigdargestellt wird, denn das ware sehr schwer in Hard-ware zu implementieren. Aber OpenGL kann jedesfla-che, konvexe und einfache Polygondarstellen. Einkon-vexesPolygon ist dadurch gekennzeichnet, dass die Ver-bindungslinie zwischen allen Paaren von Punkten in-nerhalb des Polygons wieder im Polygon liegt. EineinfachesPolygon ist ein Polygon ohne Locher oderSelbstuberschneidungen der Kanten. Bei einemflachenPolygon mussen alle Eckpunkte auf einer Ebene lie-gen. Wenn alle drei Eigenschaften erfullt sind, ist si-cher, dass OpenGL das Polygon zeichnen kann. Dasklingt nach reichlich vielen Einschrankungen, anderer-seits stellt sich heraus, dassjedesDreieckflach, konvexund in dem oberen Sinneinfachist.

Um andere Polygone zeichnen zu konnen mussen siezuerst in Polygone aufgeteilt werden, die diese Forde-rungen erfullen. Dazu gibt es Funktionen in der GLU-Bibliothek, die hier aber nicht besprochen werden.

6.2.4 Spezifizieren von Vertizes

OpenGL beschreibt jedes geometrische Objekt letztlichals eine Folge von Vertizes, die mitglVertex * () an-gegeben werden:

void glVertex {234}{sifd }[v ]( TypKoordinaten);

Vertizes in OpenGL sind zwar dreidimensional, konnenaber auch mit vierhomogenenKoordinaten angege-ben werden, so dass bis zu vier Koordinaten moglichsind. Bei der Variante mit zwei Koordinaten wird diedritte Koordinate auf0.0 gesetzt. Fur den Datentypsind GLshort (s ), GLint (i ), GLfloat (f ) undGLdouble (d) moglich. Die Vektor-Variante (durcheinv markiert) kann eventuell schneller sein. Beispiele:

glVertex2s(2, 3);glVertex3d(0.0, 0.0, 3.14159);glVertex4f(2.3, 1.0, -2.2, 2.0);{

GLdouble dfeld[3] = {5., \9., 1.};

glVertex3dv(dfeld);}

Das erste Beispiel spezifiziert einen dreidimensionalenVertex mit Koordinaten(2, 3, 0). (Die dritte Koordina-te wird automatisch auf 0 gesetzt.) Im zweiten Beispielwerden alle drei Koordinaten als doppelt genaue Gleit-kommazahlen angegeben. Das dritte Beispiel benutztvier homogene Koordinaten und spezifiziert deshalbden Vertex(2.3/2.0, 1.0/2.0,−2.2/2.0). (HomogeneKoordinaten werden durch eine Division der ersten dreiKoordinaten durch die vierte Koordinate in gewohnli-che dreidimensionale Koordinaten verwandelt.)

6.2.5 OpenGL Primitive

Aus einer Liste von Vertizes kann OpenGL verschiede-ne geometrische Primitive erzeugen. Dazu mussen dieVertizes zwischenglBegin() und glEnd() ange-geben werden. Das Argument vonglBegin() gibtdabei an, welche geometrischen Objekt aus den Verti-zes erzeugt werden sollen.

void glBegin(GLenum Modus);

markiert den Start einer Reihe von Vertizes, die geo-metrische Objekte beschreiben.Modusgibt die Art dergeometrischen Objekte an. Tabelle 6.1 enthalt die er-laubten Konstanten und deren Bedeutung.

Page 72: Skript zum Kompaktkurs C mit Beispielen in OpenGL

72 KAPITEL 6. ZEICHNEN GEOMETRISCHER OBJEKTE MIT OPENGL

Konstante BedeutungGL POINTS einzelne Punkte

GL LINES getrennteLinienstucke

GL LINE STRIP zusammenhangendeLinienstucke

GL LINE LOOP wie GL LINE STRIPaber geschlossen

GL TRIANGLES getrennte DreieckeGL TRIANGLESTRIP Band aus Dreiecken

GL TRIANGLE FAN Facher aus DreieckenGL QUADS getrennte Vierecke

GL QUADSTRIP Band aus ViereckenGL POLYGON einfaches, convexes

und flaches Polygon

Tabelle 6.1: Namen und Bedeutungen von geometri-schen Primitiven.

Funktion setzt...glVertex * () Vertex-Koordinaten

glColor * () aktuelle FarbeglNormal * () aktuelle Normale

glTexCoord * () aktuelle Texturkoord.glEdgeFlag * () aktuellen Kantenstil

Tabelle 6.2: Funktionen zum Setzen von Vertex-spezifischen Informationen.

Wie die Vertizes genau interpretiert werden, um diePrimitive zu erzeugen, soll hier nicht erklart werden.Offensichtlich gibt es verschiedene Moglichkeiten Lini-enstucke, Dreiecke oder Vierecke zu spezifizieren. Wel-che die beste ist, hangt von den Vertexdaten ab.

ZwischenglBegin() undglEnd() werden abernicht nur Vertizes spezifiziert, sondern auch Eigen-schaften der Vertizes, wie Farben, Normalen, Textur-koordinaten, etc. In Tabelle 6.2 sind ein paar vonden dazu verwendeten Befehlen aufgelistet. Wie mitglColor * () immer eine aktuelle Farbe gesetzt wird,die dann fur Vertizes verwendet wird, werden auch mitden anderen Funktionen aktuelle Werte gesetzt. NurglVertex * () erzeugt einen Vertex und verwendetdazu die jeweils aktuellen Werte.

Zwischen glBegin() und glEnd() sind nichtviele andere OpenGL-Kommandos erlaubt, abernaturlich alle C-Konstruktionen.

6.3 Darstellen von Punkten, Lini-en und Polygonen

Normalerweise werden Punkte als einzelne Pixel ge-zeichnet, Linien sind auch einen Pixel dick und durch-gezogen und Poygone werden gefullt. Diese Voreinstel-lungen konnen aber auch geandert werden:

void glPointSize(GLfloat Brei-te);

setzt die Punktbreite in Punkten. Und

void glLineWidth(GLfloat Brei-te);

setzt entsprechend die Linienbreite in Punkten. Mit

void glLineStipple(GLint Faktor,GLushort Bitmuster);

kann angeben werden, wie Linien gestrichelt werdensollen. Nach einem Aufruf vonglLineStipple()muss noch mit glEnable(GL LINE STIPPLE)das Stricheln von Linien aktiviert werden. (MitglDisable(GL LINE STIPPLE) wird es wiederdeaktiviert.) Bitmuster wird als Binarzahl aufgefasstund gibt so ein Muster an, das zum Stricheln verwendetwird. Die Skalierung dieses Musters kann mitFaktorangegeben werden.

Fur Polygone unterscheidet OpenGL zwischen derVorder- und der Ruckseite eines Polygones. Entspre-chend konnen Vorder- und Ruckseite verschieden dar-gestellt werden. Wie OpenGL festlegt, was die Vorder-seite ist, wird mit

glFrontFace(GLenum Modus);

festgelegt.Modus kann entwederGL CCW(counter-clockwise: gegen den Uhrzeigersinn) oderGL CW(clockwise: im Uhrzeigersinn) sein. Normalerweise giltGL CCW, d. h. von Polygone, deren Vertizes auf demBildschirm entgegen dem Uhrzeigersinn erscheinen, istdie Vorderseite zu sehen. Falls die Vertizes auf demBildschirm im Uhrzeigersinn erscheinen, ist die Ruck-seite zu sehen. MitGL CWdreht sich diese Definitionder Vorder- und Ruckseite gerade um.

Fur Vorder- und Ruckseite kann mit

void glPolygonMode(GLenum Sei-te, GLenum Modus) ;

Page 73: Skript zum Kompaktkurs C mit Beispielen in OpenGL

6.4. NORMALEN 73

getrennt angegeben werden, ob nur die Eckpunkte (Mo-dus= GL POINT), nur die Kanten (Modus= GL LINE )oder die Polygonflache (Modus = GL FILL ) darge-stellt werden sollen.Seite gibt dabei die betroffe-ne Seite an, und kannGL FRONT, GL BACK oderGL FRONTANDBACKsein.

Falls nur die Polygonkanten dargestellt werden, spie-len dieedge flagsder Vertizes eine Rolle. Mit

void glEdgeFlag(GLboolean flag) ;

kann fur jeden Vertex einedge flagangegeben werden.Dasedge flagentscheidet, ob eine Polygonkante, die andiesem Vertex beginnt, gezeichnet werden soll.

Unter Umstanden ist es sinnvoll, die Vorder- oderRuckseite gar nicht darzustellen (sogenanntesFace-Culling). Das muss mitglEnable(GL CULL FACE)aktiviert werden. Ob Vorder- oder Ruckseite

”gecullt“

werden, kann mit

void glCullFace(GLenum Seite) ;

eingestellt werden. (Seite kann wieder die gleichenWerte wie oben annehmen.)

Das”Stricheln“ von Polygonen wird hier nicht er-

klart, vor allem weil der gleiche Effekt mit Texturen vielbesser erreicht werden kann.

6.4 Normalen

Mit OpenGL kann an jedem Vertex eine Normalenrich-tung definiert werden, also eine Richtung die senkrechtauf einer Flache steht. Die Funktion heißt

void glNormal3f( nx, ny, nz)

wobei(nx, ny, nz)den Normalenvektor bezeichnet. Die-se Normalenrichtung wird vor allem zur Berechnungder Beleuchtung verwendet. In zwei Dimensionen ma-chen Beleuchtung und Normalen aber wenig Sinn, des-wegen werden sie hier nicht weiter erklart.

Page 74: Skript zum Kompaktkurs C mit Beispielen in OpenGL

74 KAPITEL 6. ZEICHNEN GEOMETRISCHER OBJEKTE MIT OPENGL

Ubungen zum dritten Tag

Verwenden Sie ab jetzt immer Makefiles fur dieUber-setzung der Programme!

Aufgabe III.1

Schreiben Sie eine Funktion, die zwei Punkte

(

x0

y0

)

und

(

x1

y1

)

verbindet. Die Funktion soll mit OpenGL

einen geraden Strich zeichnen, wenn das Quadrat desAbstands der Punkte(x0 − x1)

2 + (y0 − y1)2 klei-

ner 4 Bildpunkte ist. Sonst soll sich die Funktion selbstaufrufen, um folgende Punktepaare zu verbinden:(~a,~b),(~b,~c),(~c, ~d) und(~d,~e). Mit

~a =

(

x0

y0

)

,

~b =

(

(2x0 + x1)/3(2y0 + y1)/3

)

,

~c =

(

(x0 + x1)/2−√

3/6(y1 − y0)

(y0 + y1)/2 +√

3/6(x1 − x0)

)

,

~d =

(

(x0 + 2x1)/3(y0 + 2y1)/3

)

,

~e =

(

x1

y1

)

.

Testen Sie Ihre Funktion, indem Sie die Eckpunkte ei-nes Dreiecks verbinden. Fur ein gleichseitiges Drei-eck sollte die sogenannte Kochsche Schneeflockenkur-ve entstehen.

Eine Funktion, die sich selbst aufruft, wird rekursivgenannt. Dadurch entsteht eine Schachtelung von Auf-rufen. Die Tiefe dieser Schachtelung wird Rekursions-tiefe genannt.Andern Sie das Programm so, dass auchdann eine gerade Linie gezeichnet wird, wenn die Re-kursionstiefe einen einstellbaren Wert erreicht hat.

Aufgabe III.2

Turtle-Graphics (Schildkroten-Graphiken) sind ein we-sentlicher Bestandteil der Programmiersprache Logo.Die Idee dabei ist, mit Hilfe von einfachen Komman-dos eine Schildkrote, die eine Spur hinterlasst, auf demBildschirm zu bewegen. Geht die Schildkrote z. B. 10Bildpunkte vorwarts, dann entsteht eine Linie, die 10Punkte lang ist. Die Kommandos zum Bewegen der

Schildkrote sind: Setzen der Schildkrote an einem ange-gebenen Bildpunkt mit einer angegeben Blickrichtung,Vorwartsgehen um eine angegebene Lange (ruckwartsbei negativer Lange) und Drehen nach rechts um einenangegebenen Winkel (links bei negativem Winkel). Ent-sprechend wird der Zustand der Schildkrote vollstandigdurch ihre Position und einen Winkel spezifiziert, derihre Blickrichtung angibt.

Implementieren Sie diese drei Kommandos mit Hil-fe von OpenGL. Achten Sie auf eine saubere Trennungvon Deklaration und Implementierung der Funktionen.Schreiben Sie ein Testprogramm, das mit Hilfe IhrerSchildkrote eine gleichseitiges Sechseck zeichnet.(Bei Interesse, konnen Sie auch versuchen, die Koch-sche Schneeflockenkurve aus Aufgabe III.1 mit Hilfeder Schildkroten-Kommandos zu Zeichen.)

Aufgabe III.3

In dieser und in den folgenden Aufgaben soll ein ganzeinfacher Raytracer implementiert werden. Also einProgramm, das mittelsray-tracing (Sehstrahlenverfol-gung) das Bild einer dreidimensionalen Szene darstellt.

Ein Raytracer benutzt sehr viel Vektoralgebra, des-halb soll zuerst eine einfache Bibliothek von Vektor-funktionen implementiert werden. Verwenden Sie dazuden in Kapitel 5 vorgestellten Vektordatentyp und er-weitern diesen um folgende Opertationen:

• einen mit einem Skalars multiplizierten Vektors~aberechnen,

• die Summe~a + ~b von zwei Vektoren~a und~b be-rechnen,

• den Differenzvektor~a−~b von zwei Vektoren~a und~b berechnen (~a−~b ist der Vektor von~b nach~a),

• das Skalarprodukt zweier Vektoren~a und~b berech-nen:~a ·~b = axbx + ayby + azbz,

• das Kreuzprodukt zweier Vektoren~a und~b berech-

nen:~a×~b =

aybz − azby

azbx − axbz

axby − aybx

,

• die Projektion eines Vektors~a auf einen Vektor~b

berechnen:~a~b= ~b ~a·~b

|~b|2,

• die Reflektion~a′eines Vektors~a an einer Ebene be-rechnen, die durch einen Normalenvektor~b gege-ben ist:~a′ = ~a− 2~a~b

.

Page 75: Skript zum Kompaktkurs C mit Beispielen in OpenGL

6.4. NORMALEN 75

Trennen Sie die Deklaration dieser Funktionen von dereigentlichen Implementierung und schreiben Sie eineinfaches Testprogramm fur ihre Funktionen.

Aufgabe III.4

Schreiben Sie einen Raytracer, der eine Szene aus meh-reren Kugeln zeichnet. Ein Raytracer schickt von ei-nem virtuellen Augpunkt (in einem virtuellen Raum)je einen Sehstrahl zu jedem Bildpunkt einer virtuel-len Bildebene, die genauso viele Bildpunkte hat, wiedas Bild (Fenster), das berechnet wird. Fur jeden dieserSehstrahlen wird dann eine Farbe berechnet, die in demBild (Fenster) gesetzt wird.

Um die Sache zu vereinfachen, geben Sie der virtuel-len Bildebene die gleichen Koordinaten (z-Koordinategleich 0), wie dem OpenGL-Fenster. Die positivez-Achse geht in OpenGL ublicherweise senkrecht aus derBildebene heraus auf den Betrachter zu. Wahlen Siein diesem dreidimensionalen Koordinatensystem einengeeigneten Augpunkt. Berechnen Sie dann normierteRichtungsvektoren vom Augpunkt zu jedem Bildpunktdes OpenGL-Fensters.

Fugen Sie nun Kugeln mit unterschiedlichem Durch-messer und Position in Ihre Szene ein. Schneiden Siedazu jeden Sehstrahl mit allen Kugeln und finden Siedenjenigen Schnittpunkt, der am nachsten zum Betrach-ter ist. (Außerdem muss der Schnittpunkt naturlichvor(in Blickrichtung) und nichthinter dem Betrachter lie-gen.) Farben Sie dann den entsprechenden Bildpunktanhand der Koordinaten dieses Schnittpunktes ein.

Hinweis: Die beiden Schnittpunkte~s1,2 eines Strahls(von Punkt~a in Richtung~b) mit einer Kugel (mit Radiusr und Mittelpunkt~m) sind gegeben durch~s1,2 = ~a +

λ1,2~b. Mit

λ1,2 =1

~b2

(

−~b · (~a− ~m)±

±√

(~b · (~a− ~m))2 −~b2((~a− ~m)2 − r2))

(Falls der Ausdruck unter der Wurzel negativ werdensollte, gibt es keine Schnittpunkte.)

Zusatzaufgabe III.5

Durch Bearbeiten dieser Aufgabe konnen Zusatzpunkteerreicht werden. IhrUbungsbetreuer kann Ihnen nahereInformation zur Abgabe geben.

Erweitern Sie den Raytracer um eine oder mehrereder folgenden Funktionalitaten.

Hintergrund

Die Kugeln schweben bisher noch im leeren Raum.Schoner ware es aber, wenn im Hintergrund z.B. einWuste unter blauem Himmel zu sehen ware. Farben Siedazu jedes Pixel, dessen Sehstrahl keine Ihrer Kugelntrifft, entsprechend der folgenden Vorschrift ein:Die Farbe fur jeden Sehstrahl seiRot-Anteil= Grun-Anteil = 1

2 − atan(100y)/π und Blau-Anteil = 12 +

atan(100y)/π, wobei y die zweite Komponente dernormierten Richtung des Sehstrahls ist.

Beleuchtung

Um den Kugeln einen besseren raumlichen Eindruckzu geben, mussen deren Oberflachen beleuchtet wer-den. Ein sehr einfaches und in der Computergra-phik haufig eingestztes Beleuchtungsverfahren ist dasPhong-Modell. Eine gute Beschreibung dieses Beleuch-tungsmodells finden Sie bspw. beiWikipedia.

Farben Sie die Punkte, auf den Kugeloberflachenentsprechend des Phong-Modells ein. Untersuchen Siedie Effekte, die Sie durch unterschiedliche Einstellungder Lichtfarbe, der Lichtposition und der verschiedenenKonstanten erreichen konnen.

Reflektion

Mit Hilfe von Raytracing konnen auch auf einfa-che Weise Reflektionen berechnet werden. Dazu muss,nachdem der nachstgelegene Schnittpunkt mit einerKugel berechnet wurde, ein neuer Sehstrahl von die-sem Schnittpunkt aus, in Richtung des reflektierten Seh-strahls verfolgt werden. Zur Berechnung dieser Reflek-tion ist die Normalenrichtung~n der Kugeloberflachenotig. Die ist aber einfach~n = (~s− ~m)/r, wobei~s derSchnittpunkt ist,~m der Mittelpunkt der geschnittenenKugel undr ihr Radius. Wird der Richtungsvektor desSehstrahls an~n reflektiert (s. Aufgabe III.3), dann kannvom Schnittpunkt aus mit der reflektierten Richtung einneuer Sehstrahl gestartet werden. D. h. auch dieser Seh-strahl muss wieder mit allen Kugeln geschnitten wer-den, um den nachstgelegenen Schnittpunkt zu finden, andem der Sehstrahl wieder reflektiert wird, etc. (Vermei-den Sie die Implementierung einer Endlosrekursion!)

Page 76: Skript zum Kompaktkurs C mit Beispielen in OpenGL

76 KAPITEL 6. ZEICHNEN GEOMETRISCHER OBJEKTE MIT OPENGL

Page 77: Skript zum Kompaktkurs C mit Beispielen in OpenGL

Teil IV

Vierter Tag

77

Page 78: Skript zum Kompaktkurs C mit Beispielen in OpenGL
Page 79: Skript zum Kompaktkurs C mit Beispielen in OpenGL

Kapitel 7

Standardbibliotheken

7.1 Die Standardbibliotheken

C ist eine verhaltnismaßig”kleine“ Sprache, zum Bei-

spiel gemessen an der Zahl der Keywords. Das istdeswegen moglich, weil viel Funktionalitat nicht indie Programmiersprache aufgenommen wurde, sondernin standardisierte Funktionsbibliotheken zu finden ist.Zum Beispiel besitzt die Programmiersprache C kei-ne Kommandos zur Ein-/Ausgabe, aber mit Hilfe vonprintf() aus der Standardbibliothek konnen trotz-dem Textausgaben produziert werden.

Dieses Design von C wurde gelegentlich kritisiert,aber es hat sich bezahlt gemacht. Zum Beispiel hat sichdie Ein-/Ausgabe von Computerprogrammen seit den70er Jahren so stark geandert, dassprintf() in Pro-grammen mit graphischer Oberflache praktisch nichtmehr verwendet wird. Das ist nicht allzu schlimm, weiles nur heißt, dass die Funktionprintf() aus einerStandardbibliothek weniger genutzt wird. Ware ein der-artiger Ausgabemechanismus aber direkt Teil der Spra-che C, dann wurde dieser Teil von C inzwischen ver-altet sein und musste uberarbeitet werden.Anderungenin einer Computersprache durchzufuhren ist aber sehrschwierig, weil es unter Umstanden dazu fuhrt, dassProgramme geandert werden mussen.

Es gibt sehr viele Funktionen in der Standardbi-bliothek. Einige wieprintf() (in stdio.h de-klariert), malloc() und free() (malloc.h undstdlib.h ) und ein paar mathematische Funktionen(math.h ) wurden schon erwahnt. Wichtig sind auchdie Dateioperationen ausstdio.h . Dabei tauchenoft Fehler auf, die mit Hilfe der Deklarationen inerrno.h diagnostiziert werden konnen. Um Strings(Zeichenketten) zu verarbeiten, sind die Funktionen ausstring.h unentbehrlich.

Tabelle 7.1 listet ein paar wenige nutzliche.h -Dateien aus der Standardbibliothek auf, es gibt noch

viel mehr. Und noch wesentlich mehr Funktionen. Umdamit zurecht zu kommen, noch ein paar Tips:

• Der C-Compiler muss.h -Dateien lesen konnen,deshalb kann der Programmierer sie normalerwei-se auch lesen (unter UNIX sind sie meistens unter/usr/include gespeichert)..h -Dateien zu le-sen lohnt sich aber nicht nur bei Standard-, sondernbei allen Bibliotheken, denn dort sind zumindestdie verfugbaren Funktionen deklariert, wenn nichtsogar ihre Funktion kommentiert.

• Unter UNIX sind zu den meisten Funktionen ausder Standardbibliothekman-pagesverfugbar. Mitman printf werden z. B. die Details der For-matangaben erklart.

• Viele C-Entwicklungsumgebungen enhalten auchErklarungen zu den Bibliotheksfunktionen. Darinsind normalerweise auch Beschreibungen der.h -Dateien und Verweise auf andere Funktionen, sodass die meisten Funktionen relativ schnell gefun-den werden konnen, auch wenn ihr Name unbe-kannt ist.

• Es sollte klar geworden sein, dass die jeweilige.h -Datei #include d werden sollte. Im Prinzipist dies nicht unbedingt notig, da die Deklarationenauch direkt in das Programm geschrieben werdenkonnen. Davon ist aber dringend abzuraten.

Im Folgenden werden noch einige nutzliche Funktio-nen aus der Standardbibliothek vorgestellt, die im Le-ben eines C-Programmierers haufiger vorkommen.

7.1.1 Dateioperationen

Als einfaches Beispiel fur die Benutzung der Dateiope-rationen ausstdio.h sollen hier die Zeilen aus einerTextdatei namensmain.c ausgelesen werden:

79

Page 80: Skript zum Kompaktkurs C mit Beispielen in OpenGL

80 KAPITEL 7. STANDARDBIBLIOTHEKEN

.h -Datei Beschreibungerrno.h Fehlerabfragenmalloc.h Speicherverwaltungmath.h mathematische Funktionenstdio.h Ein-/Ausgabestddef.h Standarddefinitionenstdlib.h nutzliche Deklarationenstring.h String-Verarbeitungctype.h Zeichenoperationentime.h Zeitabfragenassert.h Debugoperationen

Tabelle 7.1: Beispiele von.h -Dateien der Standardbi-bliothek.

#include <stdio.h>

#define MAX_LAENGE 100

int main(void){

FILE * datei;char textzeile[MAX_LAENGE];

datei = fopen("main.c", "r");if (NULL == datei){

return(1);}while (!feof(datei)){

fgets(textzeile, MAX_LAENGE,datei);

printf("%s", textzeile);}fclose(datei);return(0);

}

Mit

datei = fopen("main.c", "r");

wird die Adresse auf eineFILE -Struktur zuruckgege-ben, fallsfopen() die Datei namensmain.c erfolg-reich zum Lesen ("r" fur read) offnen konnte. (Sonstwird NULLzuruckgegeben.)

In der folgendenwhile -Schleife wird dann die Da-tei Zeile fur Zeile gelesen. Das Ende dieser Schleife ist

erreicht, wenn das Ende der Datei (end of file = eof) er-reicht wurde, was sich bequem mitfeof() abfragenlasst. (feof() gibt genau dann einen Wert ungleich 0zuruck, wenn das Dateiende erreicht wurde.)

fgets(textzeile, MAX LAENGE, datei)liest aus der Datei bis zu 99 Zeichen ein und speichertsie in textzeile . (fgets() steht fur “file getstring”.) fgets stoppt mit dem Einlesen bei jedemZeilenumbruch oder wenn die Datei zu Ende ist.Außerdem garantiertfgets() , dass die Zeichenkettemit ’ \0 ’ beendet wird, deswegen wird auch ein Byteweniger eingelesen, als im Funktionsaufruf angegebenwird. Mit printf() kann dann der so gelesene Textausgegeben werden. Nach dem vollstandigen Auslesenmuss die Datei mitfclose() geschlossen werden.

Neben fopen() , fclose() , feof() undfgets() gibt es noch eine Reihe sehr nutzlicherDateifunktionen, z.B. fprintf() , fgetc() ,fputc() , fread() , fwrite() , etc.

• fprintf( Datei, Formatstring, Werte) gibtformatierte Werte wiefprintf() aus, abernicht auf den Bildschirm sondern in eineDatei.

• fgetc( Datei) liest ein einzelnes Zeichen (0 bis255) aus einerDatei.

• putc( Zeichen, Datei) schreibt ein Zeichen ineineDatei.

• fread( Adresse, Anzahl, Große, Datei)schreibt Anzahl×Große Bytes aus Datei undspeichert sie abAdresse.

• fwrite( Adresse, Anzahl, Große, Datei)schreibt Anzahl×Große Bytes ab Adresse inDatei.

Hilfe zu diesen Funktionen ist am einfachsten mitmanzu erhalten.

Bei den obigen Funktionen zur Ein- und Ausgabevon Text kann statt einer Datei auch die Standardein-gabe bzw. Standardausgabe als Quelle bzw. Ziel an-gegeben werden. Hierfur existieren standardmaßig derEingabestromstdin und der Ausgabestromstdoutvom Typ FILE * mit denen die Standardein- bzw. -ausgabe angesteuert werden konnen. Die bereits in Ka-pitel 1 vorgestellten Operationen zur Ein- und Ausga-be von Zeichen und Zeichenketten auf der Standardein-bzw. -ausgabe, sind also Spezialisierungen der obenvorgestellten Ein- und Ausgabefunktionen.

Page 81: Skript zum Kompaktkurs C mit Beispielen in OpenGL

7.1. DIE STANDARDBIBLIOTHEKEN 81

Ein weiterer Ausgabestrom iststderr , auf demin der Regel Fehlermeldungen des Programms ausge-geben werden. In diesem Zusammenhang ist auch dieFunktionexit(int) zu erwahnen, mit der das Pro-gramm zu einem beliebigen Zeitpunkt beendet und anden Aufrufer ein enstprechender Fehlercode ubergebenwerden kann. Die typische Verwendung vonstderrundexit() ist in folgendem Beispiel dargestellt.

#include <stdio.h>

int main(void){

...

if (error){

/ * Programm mit einer ** Fehlermeldung beenden * /

fprintf{stderr,"Programmfehler!");

exit(1);}

...

/ * regulaeres Programmende * /return(0);

}

7.1.2 String-Operationen

Eigentlich kennt C keinen Datentyp fur Strings,sondern hochstens Folgen vonchar s, die mit ei-nem ’ \0 ’ -Zeichen beendet werden. Fur diesegibt es in string.h aber einige Funktionen, z.B.strlen( Adresse) zum Bestimmen der Lange derZeichenfolge, die abAdressegespeichert ist. Ande-re String-Operationen instring.h sind strcpy()(zum Kopieren),strcat() (zum Zusammenfugen),strcmp() (zum Vergleichen) oderstrchr() (zumSuchen nach einem Zeichen). Daneben gibt es auchin stdlib.h ein paar Funktionen fur null-terminierteZeichenfolgen, z.B.atoi() (zum Umwandeln einesStrings in einint ) oderatof() (zum Umwandeln ei-nes Strings in eindouble ).

Als Beispiel hier ein Programm, das einen String mitdurch Komma getrennten Ganzzahlen analysiert unddie Zahlen ausgibt:

#include <stdio.h>

#include <stdlib.h>#include <string.h>

int main(void){

char text[] = "42,007,137,0815";char * position;

position = text;while (TRUE){

printf("%d\n",atoi(position));

if (NULL ==strchr(position, ’,’))

{break;

}position =

strchr(position, ’,’) + 1;}return(0);

}

Dabei wird in dem Zeigerposition gespeichert,wo die nachste Zahl erwartet wird. Solange diesePosition noch nicht uber das Ende vontext hin-ausweist, wird mit atoi() die Zeichenkette, aufdie position zeigt, in eine Zahl umgewandelt undmit printf() ausgegeben. Dann wird das nachsteKomma mit strchr() gesucht. Falls kein Kommamehr kommt, wird die Schleife beendet; sonst wirdposition auf das Zeichennachdem nachsten Kom-ma gesetzt.

7.1.3 Operationen fur einzelne Zeichen

Neben Funktionen zur Manipulation von ganzen Zei-chenketten stellt die Standardbibliothek auch Funk-tionen zur Untersuchung und Manipulation einzelnerZeichen zur Verfugung. Diese sind in der Header-Date ctype.h deklariert. Beispielsweise kann mitisdigit(c) uberpruft werden, ob es sich bei einemZeichen um eine Ziffer handelt,isupper(c) testet,ob das ubergebene Zeichen ein Großbuchstabe ist. Mittolower(c) und toupper(c) kann ein Zeichenin einen Klein- bzw einen Großbuchstaben gewandeltwerden.c ist jeweils eine Variable vom typ int, die ent-weder eine ASCII-Zeichen reprasentiert oder den WertEOFenthalt. In Tabelle 7.2 sind einige Beispielfunktio-

Page 82: Skript zum Kompaktkurs C mit Beispielen in OpenGL

82 KAPITEL 7. STANDARDBIBLIOTHEKEN

Funktion Erlauterungint isalnum(int c) alphanumerisches Zeichen?int isblank(int c) Leerzeichen?int isdigit(int c) Ziffer?int islower(int c) Kleinbuchstaben?int isupper(int c) Großbuchstaben?int tolower(int c) wandelt c in Kleinbuchstabenint toupper(int c) wandelt c in Großbuchstaben

Tabelle 7.2: Beispielfunktionen ausctype.h

nen ausctype.h aufgelistet.

7.1.4 Zeit- und Datumsfunktionen

Haufig kommt es auch vor, dass man die aktuelle Sy-stemzeit oder das aktuelle Datum benotigt. Insbesonde-re bei zeitabhangigen Simulationen und zeitgesteuertenAnimationen ist dies wichtig. Die Funktionen zur Ab-frage der Uhrzeit bzw. des Datums sind weitgehend inder Dateitime.h deklariert.

Die Funktiontime() gibt die Anzahl der Sekundenzuruck, die seit dem 1. Januar 1970 vergangen sind. DerTyp dentime() zuruckgibt, isttime t , der eigent-lich dem Typlong entspricht.

Da die wenigsten Menschen mit der Zahl der Se-kunden seit dem 1.1.1970 umgehen konnen, gibt eshilfreiche Umrechnungsfunktionen. Mit der Funktionlocaltime() konnen Sie z.B. anhand einer Varia-blen vom Typtime t eine Struktur namenstm fullen.Diese Struktur enthalt Elemente fur die Bestandteile desDatums und der Uhrzeit:

struct tm {/ * Sekunden - [0,61] * /int tm_sec;

/ * Minuten - [0,59] * /int tm_min;

/ * Stunden - [0,23] * /int tm_hour;

/ * Tag des Monats - [1,31] * /int tm_mday;

/ * Monat im Jahr - [0,11] * /int tm_mon;

/ * Jahr seit 1900 * /

int tm_year;

/ * Tage seit Sonntag - [0,6] * /int tm_wday;

/ * Tage seit Neujahr - [0,365] * /int tm_yday;

/ * Sommerzeit-Flag * /int tm_isdst;

};

Mochte man eine genauere Zeitmes-sung durchfuhren, kann man die Funktiomgettimeofday() die im Headersys/time.hdeklariert ist verwenden, die auch zusatzlich dievergangenen Mikrosekunden angibt. Sie hat folgendeSyntax:

int gettimeofday(struct timeval * tv, void * tz);

Die Datenstruktur des ersten Parameters enthalt die Se-kunden und Mikrosekunden und ist folgendermaßen de-finiert:

struct timeval {/ * Sekunden seit 1.1.1970 * /time_t tv_sec;

/ * Mikrosekunden * /long tv_usec;

};

Der zweite Parametertz war ursprunglich fur die Zeit-zone vorgesehen. Allerdings hat sich die Umsetzung derverschiedenen Sommerzeitregeln als nicht so einfacherwiesen, sodass man als zweiten Parameter am besten0 angibt.

7.1.5 Erzeugung von Zufallszahlen

Es gibt viele Algorithmen, die fur ihre Durchfuhrungzufallig erzeugte Zahlenwerte benotigen. Auchhierfur stellt die C-Standardbibliothek Funktionenzur Verfugung, die instdlib.h deklariert sind.Mit rand() wird eine “Zufallszahl” zwischen 0und RANDMAX zuruckgeliefert. Hierbei handelt essich genauer gesagt nur um eine Pseudozufallszahl,d.h. bei wiederholtem Start eines Programms wirdimmer wieder die gleiche Folge von Zufallszahlenzuruckgegeben.

Page 83: Skript zum Kompaktkurs C mit Beispielen in OpenGL

7.1. DIE STANDARDBIBLIOTHEKEN 83

Diesem Problem kann abgeholfen werden, indemman bei jedem neuen Start des Programms den Saat-punkt fur die Berechnung der Pseudoufallszahlen neusetzt. Hierfur gibt es die Funktionsrand(unsignedint) . Um immer wieder einen neuen Saatpunkt zuerhalten, wird haufig die Systemzeit verwendet. Einmoglicher Aufruf ware z.B.srand(time()) .

Mochte man Zufallszahlen erzeugen, die in einemanderen Bereich als von 0 bisRANDMAX liegen,mussen die Ruckgabewerte vonrand() entsprechendskaliert werden. Die folgende Funktion gibt z.B. zufalli-ge Gleitkommazahlen im Intervall[0, 1] zuruck:

float getProp(){

return((float)rand() / RAND_MAX);}

7.1.6 Makro fur die Fehlersuche

In der Header-Dateiassert.h ist bei der Pro-grammanalyse und Fehlersuche sehr hilfreiche Makroassert() definiert. Dies erhalt als Argument einenlogischen Ausdruck. Ist dieser nicht erfullt, bricht dieProgrammausfuhrung an dieser Stelle ab und es wirdeine Fehlermeldung mit Angabe der Datei, der Funk-tion und der Zeilennumer, in derassert aufgerufenwurde, zuruckgegeben.

Mit Hilfe vons assert ist es moglich Zusicherun-gen (englischassertions) uber den aktuellen Programm-zustand zu machen. So kann z.B. zugesichert werden,dass der Parameter einer Funktion sich in einem be-stimmten Wertebereich findet oder das Ergebnis einesAlgorithmus gewissen Bedingungen entspricht. In derfolgenden Beispielfunktion wird z.B. zugesichert dassder Ubergabeparameter ungleich 0 ist, um somit eineDivision durch 0 zu vermeiden.

float doSomeCalculations(float val){

float result;assert(val != 0.0);...

result = 1.0 / val;...

}

Wichtig ist, dass mitassert nur Programmierfeh-ler abgefangen werden durfen, d.h. Fehler, die z.B. auf-grund eines falsch implementierten Algorithmus entste-

hen. Fehler die jederzeit im laufenden Betrieb des Pro-gramms, z.B. durch falsche Benutzereingaben, verur-sacht werden konnen, durfen nicht mitassert abge-fangen werden, sondern mussen einer geeigneten Feh-lerbehandlung unterzogen werden.

Damit durch die Verwendung desassert -Makrosim spateren Betrieb keine Performanceeinbusen entste-hen, kann dem Compiler das Argument-DNDEBUGmitgegeben werden. Dies fuhrt dazu, dass durch dasassert -Makro kein Code erzeugt wird, also auchder logische Ausdruck nie ausgewertet wird. Hier-durch wird nochmals klar, dassassert nur fur dasAufdecken von Programmierfehlern verwendet werdendarf. Außerdem darf der logische Ausdruck keinerleiSeiteneffekte auf den Programmzustand haben.

Page 84: Skript zum Kompaktkurs C mit Beispielen in OpenGL

84 KAPITEL 7. STANDARDBIBLIOTHEKEN

Page 85: Skript zum Kompaktkurs C mit Beispielen in OpenGL

Kapitel 8

Einfuhrung in die GLUT-Bibliothek

(Dieses Kapitel ist zum Teil eineUberset-zung von Mark Kilgard, The OpenGL UtilityToolkit (GLUT) Programming Interface API,http://www.opengl.org/developers/documentation/glut/index.html .)

Die GLUT-Bibliothek (OpenGL Utility Toolkit) wur-de bereits in den vorangegangenen Beispielen und Auf-gaben verwendet. Dabei wurde diese Hilfsbibliothek imwesentlichen nur dazu benutzt, ein Fenster zu offnen,in welchem OpenGL-Zeichenoperationen durchgefuhrtwerden konnten. Dementsprechend wurde bisher nurein kleiner Teil der Funktionalitat vorgestellt.

Tatsachlich erlaubt GLUT, OpenGL-Anwendungenmit mehreren Fenstern Plattform-unabhangig zu pro-grammieren. Auch die Realisierung von Benutzerinter-aktion uber Maus, Tastatur und andere Eingabegerate istmoglich. Es konnen Menus definiert werden. Fur ein-fache Graphikelemente wie Kugel, Wurfel usw. stelltGLUT Hilfsfunktionen zur Verfugung.

Implementierungen der GLUT-Bibliothek existierenfur X Windows (UNIX), MS Windows, OS/2. Obwohlfur komplexere OpenGL-Applikationen in der Regelauf Plattform-abhangige aber machtigere Schnittstel-len zuruckgegriffen wird, reicht die Funktionalitat, dieGLUT zur Verfugung stellt, fur einfache Anwendungenvoll aus.

Die Funktionen der GLUT lassen sich in unterschied-liche Gruppen einteilen, von denen die wichtigsten imFolgenden behandelt werden:

• Initialisierung

• Start der Eventverarbeitung

• Fenster-Management

• Menu-Management

• Callback Registrierung

• Zeichnen von Text

• Zeichnen geometrischer Formen

• State-Handling

8.1 Initialisierung

Bevor irgendeine GLUT-Funktion aufgerufen werdenkann, muß zunachst die Bibliothek selbst initialisiertwerden dies erfolgt durch Aufruf der Funktion:

void glutInit(int * argc,char ** argv);

Er bewirkt, dass interne Datenstrukturen angelegt undeine Reihe von Zustandsvariablen auf sinnvolle An-fangswerte gesetzt werden. Außerdem wird uberpruft,ob OpenGL auf der aktuellen Plattform unterstutztwird. Gegebenenfalls beendet der Aufruf das Programmmit einer Fehlermeldung.

Der Aufruf erwartet als Argument die Kommando-zeile des Programms uber die beiden Variablenargcundargv . Dadurch besteht die Moglichkeit beim Starteines GLUT-Programms Optionen fur das Fenstersy-stem mit anzugegen z.B.:

-geometry WxH+X+Y Definiert die Standard-große und -position des Anwendungsfensters.

-iconic Weist das Fenstersystem an, die Anwen-dung nach dem Start statt als Fenster als Icon dar-zustellen, welches sich erst nach dem Anklickenauf die Fenstergroße entfaltet.

85

Page 86: Skript zum Kompaktkurs C mit Beispielen in OpenGL

86 KAPITEL 8. EINFUHRUNG IN DIE GLUT-BIBLIOTHEK

-gldebug Dadurch wird die Anwendung angewiesennach der Bearbeitung jedes Events den OpenGL-Fehlerstatus abzufragen und gegebenenfalls eineentsprechende Fehlermeldung auszugeben. DieseFunktion ist sehr nutzlich um Programmierfehlerzu finden.

glutInit untersucht, ob in der Kommandozeiledie entsprechenden Optionen vorkommen. Dabei wer-den bekannte Optionen aus der Kommandozeile ent-fernt. Nach dem Aufruf enthaltargv nur noch die Op-tionen, dieglutInit nicht interpretieren konnte.

8.1.1 Anfangszustand

Eine Reihe von Zustandsvariablen kontrolliert den denProgrammstart, die Anfangsgroße und -position desAnwendungsfensters sowie den Zeichenmodus. Tabel-le 8.1 zeigt die Anfangswerte fur diese Zustandsvaria-blen. Daruberhinaus sind die Namen der Funktionenenthalten, mit denen die entsprechenden Werte geandertwerden konnen.

Variable Anfangswert Anderung mitinitWindowX -1 glutInitWindowPositioninitWindowY -1 glutInitWindowPositioninitWindowWidth 300 glutWindowSizeinitWindowHeight 300 glutWindowSizeinitDisplayMode GLUTRGB glutInitDisplayMode

GLUTSINGLEGLUTDEPTH

Tabelle 8.1: Die Anfangswerte bei Initialisierung derGLUT-Bibliothek

8.1.2 Graphikmodi

Durch den Graphikmodus wird angegeben, welche Ei-genschaften der OpenGL-Kontext, der fur das Zeich-nen verwendet wird, besitzen soll. So lasst sich bestim-men, welche OpenGL-Buffer angelegt werden sollen.Der Graphikmodus wird mit der Funktion

void glutInitDisplayMode(unsigned int mode);

gesetzt. Einige der moglichen Parameter wurden bereitsaufgefuhrt. Die Tabelle 8.2 gibt einenUberblick wel-che WerteglutInitDisplayMode als Argumenteakzepiert. Die Parameter sind als Bitmaske implemen-tiert. Das bedeutet, dass eine beliebige Kombination ausden Parametern durch logisches ODER erzeugt werdenkann.

GLUTRGBBitmaske fur den RGB-Modus. Der Fra-mebuffer enthalt je einen Wert fur Rot, Grun undBlau.

GLUTRGBA|GLUTALPHA Bitmaske fur den RGBA-Modus. SieheGLUTRGB. Zusatzlich wird je-doch fur jeden Pixel ein Transparentwert (Alpha)gespeichert. Die VerODERung mit dem zweitenWert ist in diesem Fall unbedingt erforderlich, weilsonst kein Alpha-Kanal angelegt wird.

GLUTLUMINANCEBitmaske fur den Luminanz-Modus. Hierbei gibt es nur einen Farbkanal je Pi-xel. Das bedeutet, dass die Darstellung mit unter-schiedlichen Grauwerten erfolgt. Dabei wird furjedes Zeichenprimitiv nur der Rot-Kanal interpre-tiert und als Helligkeitswert gespeichert. DieserModus wird nicht von auf jeder Plattform un-terstutzt.

GLUTINDEX Bitmaske fur den Color-Index-Mode.Statt einer Farbe wird fur jedes Graphik-Primitivein Index (0 . . . 255) angegeben. Dieser wird furdas Nachschlagen in einer Fabtabelle verwendet,in der die eigentlichen Farbwerte gespeichert sind.Auf diesen Modus wird hier nicht naher eingegan-gen.

GLUTSINGLE Bitmaske fur Single-Buffer.

GLUTDOUBLEBitmaske fur Double-Buffer.

GLUTSTEREOBitmaske fur Stereo-Modus. Dabeiwerden abhangig von Single- oder Double-Buffer-Modus bis zu vier Versionen des Bildschirmspei-chers gehalten. Dies erlaubt unterschiedliche An-sichten fur das linke bzw. rechte Auge zu zeich-nen, womit sich bei Verwendung einer Stereo-Hardware (z.B. Shutter-Glasses) eine raumlicheDarstellung erzielen laßt.

GLUTACCUMBitmaske fur Accumulation-Buffer.Muss angegeben werden, wenn der Accumulation-Buffer verwendet werden soll.

GLUTDEPTHBitmaske fur den Tiefenpuffer (Z-Buffer).

GLUTSTENCIL Bitmaske fur den Stencil-Buffer.

Tabelle 8.2:Uberblick uber die Graphikmodi, die mitHilfe von glutInitDisplayMode gesetzt werdenkonnen

Page 87: Skript zum Kompaktkurs C mit Beispielen in OpenGL

8.2. EVENTVERARBEITUNG/EVENT-GETRIEBENE PROGRAMMIERUNG 87

8.2 Eventverarbeitung/Event-getriebene Programmierung

Um die Programmierungmit GLUT besser verstehen zukonnen, ist es wichtig zu erkennen, dass sich Program-me mit graphische Benutzeroberflache in ihrem Aufbaudeutlich von sequentiellen Programmen unterscheiden.

8.2.1 Sequentielle Programmierung

Oftmals weisen C-Programme eine lineare Struktur auf.Das Programm erhalt anfangs eine Menge von Eingabe-daten, die es nach einem bestimmten Algorithmus ver-arbeitet. Am Ende steht die Ausgabe des berechnetenErgebnisses:

int main(int argc, char * argv[]){

/ * Initialisierung * /int min = atoi(argv[1]);int max = atoi(argv[2]);int i;

double * ergebnis;ergebnis = (double * ) malloc(

(max-min+1) * sizeof(double));

/ * Berechnung * /for (i = min; i < max; i++)

ergebnis[i-min] = sqrt(i);

/ * Ausgabe * /for (i = min; i < max; i++)

printf("Wurzel %d = %lf\n",ergebnis[i-min]);

/ * Ende * /return 0;

}

Hier erfolgt die Eingabe der Werte beispielswei-se uber Programmparameter. Eine andere Moglichkeitstellt das Einlesen der Daten aus einer Datei dar. Nachbeendeter Berechung erfolgt die Ausgabe des Ergebnis-ses auf den Bildschirm. Diese Ausgabe kann naturlichauch innerhalb der Berechnung erfolgen.

8.2.2 Event-getriebene Programmierung

Im Gegensatz zu den sequentiellen Programmensind Anwendungen mit graphischer BenutzeroberflacheEvent-getrieben. Das bedeutet, dass diese Programmekeinem vorgegebenen Programmablauf folgen sondernlediglich auf Ereignisse reagieren, die der Benutzeroder das Fenstersystem auslosen. Ereignisse, die derAnwender auslost, sind beispielsweise ein Tastendruckoder das Klicken mit der Maus an eine Position inner-halb des Fensters. Das Fenstersystem lost unter ande-rem ein Ereignis aus, wenn das vorher verdeckte An-wendungsfenster wieder aufgedeckt wird. Das folgen-de Beispiel zeigt die typische Struktur eines Event-getriebenen Programms.

int main(int argc, char * argv[]){

/ * Initialisierung * /glutInit(&argc, argv);...

/ * Event-Schleife * /while(1){

... / * warte auf Ereignis * /

... / * bearbeite Ereignis * /}

/ * Anweisungen hier werdenniemals ausgefuehrt * /

}

Auch das Event-getriebene Programm beginnt mitder Initialisierung seiner Daten, danach betritt es je-doch die sogenannte Event-Schleife (Eventloop), in deres auf Ereignisse wartet und in geeigneter Weise rea-giert. Die Event-Schleife ist eine Endlosschleife, diedas Programm niemals verlasst. Nachdem die Grund-struktur der Event-Schleife immer gleich ist, muß sieder Anwender in der Regel nicht selbst implementieren.Auch GLUT stellt eine fertig implementierte Schleifezur Verfugung.

8.2.3 glutMainLoop

... realisiert die Event-Schleife innerhalb derGLUT-Bibliothek. Ein GLUT-Programm ruftglutMainLoop auf, nachdem alle Initalisierun-gen wie das Anlegen der Fenster und der Menus sowieInitialisierung selbst definierter Variablen erfolgt sind.

Page 88: Skript zum Kompaktkurs C mit Beispielen in OpenGL

88 KAPITEL 8. EINFUHRUNG IN DIE GLUT-BIBLIOTHEK

Der Aufruf von glutMainLoop sollte hochstenseinmal in einem GLUT-Programm enthalten sein.Da das Programm die Event-Schleife nicht verlasst,kehrt der Aufruf von glutMainLoop niemalszuruck. Anweisungen nach dieser Zeile werden nichtausgefuhrt.

int main(int argc, char * argv[]){

/ * Initialisierung * /glutInit(&argc, argv);...

/ * Event-Schleife * /glutMainLoop();

/ * Anweisungen hier werdenniemals ausgefuehrt * /

}

8.3 Fensteroperationen

Um einen Bereich zu erhalten, in den gezeichnet wer-den kann, muss ein Fenster erzeugt werden. Dies erfolgtim Rahmen der Programminitialisierung vor dem Be-treten der Event-Schleife. Die Funktion zum Erzeugeneines Fensters wurde bereits in den vorangegangenenBeispielen vorgestellt. Sie lautet

int glutCreateWindow(char * name);

Sie erzeugt ein Fenster mit der initialen Fen-stergroße, die mit Hilfe von glutInit bzw.glutInitWindowSize gesetzt wurde, in der linkenoberen Ecke des Bildschirms oder an der Position, diedurchglutInitWindowPos definiert wurde. DiesesFenster wird zum aktuellen Fenster. Als Ruckgabewertliefert sie eine Fenster-ID, mit der das Fenster identi-fiziert werden kann. Die Fenster-ID des aktuellen Fen-sters kann mit

int glutGetWindow(void);

abgefragt werden. Mit

void glutSetWindow(int win);

kann mit Hilfe der Fenster-ID ein anderes Fenster zumaktuellen Fenster bestimmt werden.

Mit Hilfe von GLUT konnen verschiedene Modifika-tionen auf dem aktuellen Fenster durchgefuhrt werden.

Diese sollen im Folgenden stichpunktartig aufgezahltwerden.

void glutPositionWindow(int x,int y); ... andert die Position des aktuellenFensters. Die Werte haben nur Vorschlagscha-rakter, im Einzelfall kann das FenstersystemAnderungen vornehmen, beispielsweise wenn diePosition außerhalb des Bildschimbereichs liegt.

void glutReshapeWindow(int width,int height); ... andert die Große des aktu-ellen Fensters. Die Werte haben nur Vorschlags-charakter, im Einzelfall kann das FenstersystemAnderungen vornehmen beispielsweise wenndie Große den verfugbaren Bildschirmplatzuberschreitet.

void glutFullScreen(void); ... andert dieGroße des Fensters so, dass es den gesamtenverfugbaren Bildschirmplatz einnimmt.

Die Modifikationen wirken sich nicht sofort aus,sondern erst dann, wenn die Event-Schleife dasnachste Mal durchlaufen wird. Dadurch konnen meh-rere glutPositionWindow ... hintereinander aus-gefuhrt werden. Ausschlaggebend ist der letzte der Auf-rufe. So uberschreiben auchglutReshapeWindow -Aufrufe nachglutFullScreen den Vollbildmoduswieder zugunsten der angeforderten Fenstergroße.

Auch die Umsetzung der folgenden Operationen istabhangig vom Fenstersystem. Die Aufrufe wirken sicherst bei der nachsten Event-Schleife aus. Der jeweilsletzte Aufruf ist ausschlaggebend.

void glutPopWindow(void); ... stellt das ak-tuelle Fenster in den Hintergrund.

void glutPushWindow(void); ... holt das ak-tuelle Fenster in den Vordergrund.

void glutShowWindow(void); ... zeigt das ak-tuelle Fenster wieder an, wenn es vorher verborgenwar. (sieheglutHideWindow )

void glutHideWindow(void); ... verstecktdas aktuelle Fenster.

void glutIconifyWindow(void); ... sorgtdafur, dass anstelle des eigentlichen Fenstersnur ein kleines Icon erscheint. Beim Klickenauf dieses Icon erscheint dann wieder dasAnwendungsfenster.

Page 89: Skript zum Kompaktkurs C mit Beispielen in OpenGL

8.4. MENUS 89

Neben diesen Funktionen, die sich auf Position,Große und Erscheinungsbild auswirken, konnen nochzwei andere Parameter gesetzt werden.

void glutSetWindowTitle(char * name);... definiert den Text, der in der Titelleiste desFensters erscheint.

void glutSetIconTitle(char * name);... definiert den Text, der unterhalb eines iconi-fizierten Fensters dargestellt wird. Dieser Textkann sich von der eigentlichen Fenstertitelleisteunterscheiden.

void glutSetCursor(int cursor);... ermoglicht es, die Gestalt des Maus-Cursorszu verandern. Die gewunschte Cursorform kannaus einer Reihe von Moglichkeiten ausgewahltwerden. An dieser Stelle soll jedoch nichtnaher auf die moglichen Parameter eingegan-gen werden. Zu diesem Zweck sei hier auf dieGLUT-Dokumentation bzw. die GLUT-man-pagesverwiesen.

Fur das eigentliche Zeichnen mit OpenGL sind nochzwei weitere Funktionen wichtig.

void glutPostRedisplay(void); ... infor-miert die GLUT-Anwendung daruber, dass derZeichenbereich neu gezeichnet werden muss.Dies kann beispielsweise dann erforderlich sein,wenn sich der Inhalt der Darstellung durcheinen Animationsschritt geandert hat und neugezeichnet werden muss. Das eigentliche Neu-zeichnen wird dann an geeigneter Stelle von derGLUT-Bibliothek ausgelost.

void glutSwapBuffers(void); Schaltetim Double-Buffer-Modus zwischen den beidenBildschirmspeichern um. Erst nach Aufruf dieserFunktion wird das gezeichnete Bild auch dar-gestellt. Er sollte deshalb immer am Ende derZeichenfunktion stehen, wenn Double-Bufferingverwendet wird.

8.4 Menus

GLUT erlaubt die Definition von Menus. Diese werdenvom Fenstersystem in der Regel als sogenannte Popup-Menus realisiert. Das bedeutet, dass das Menu nor-malerweise unsichtbar ist. Erst wenn der Benutzer mit

der rechten Maustaste in das Anwendungsfenster klickt,klappt es auf. In manchen Umgebungen (z.B. unter MSWindows) kann das Menu auch in eine Menuzeile ein-gebunden sein.

Ahnlich wie bei den Fenstern kennt GLUT das Kon-zept des aktuellen Menus. Alle Menuoperationen, dienicht explizit einen Identifikator fur ein Menu erwarten,beziehen sich auf das aktuelle Menu. Ein Beispiel ver-deutlicht die Programmierung eines Menus:

Menus werden mit

int glutCreateMenu(void ( * func)(int value));

erzeugt. Das zuletzt erzeugte Menu ist immer das aktu-elle. Es konnen beliebig viele Menus erzeugt werden.

Nach der Erzeugung konnen dem aktuellen Menu be-liebige Menupunkte hinzugefugt werden. Dies erfolgtmittels

void glutAddMenuEntry(char * name,int value);

Dieser Aufruf erhalt zwei Argumente. Der erste istein String, der den Namen des Menueintrags kenn-zeichnet. Der zweite ist ein beliebig gewahlter Inte-ger, der den Menupunkt innerhalb des Menus identifi-ziert. Menupunkte werden sequentiell durchnumeriert.Der erste Eintrag eines Menus tragt dabei die Nummer1.

Das fertige Menu muss schließlich noch aktiviertwerden. Dazu dient die Funktion

void glutAttachMenu(int button);

die als Argument denjenigen Maus-Button(GLUTLEFT BUTTON, GLUTMIDDLE BUTTONoder GLUTRIGHT BUTTON) erhalt, bei dem dasMenu erscheinen soll.

Die Reaktion auf einen ausgewalten Menupunktwird mit einer Callback-Funktion (ahnlich wie beiglutDisplayFunc ) realisiert. Der Menu-Callbackwird bei der Erzeugung des Menus als Parameter uber-geben. Jedesmal wenn der Benutzer einen Menupunktaktiviert, sorgt GLUT dafur, dass der entsprechen-de Menu-Callback aufgerufen wird. Dabei wird derMenupunkt, der den Callback ausgelost hat, als Para-meter ubergeben. In der Regel wird jedes Menu ei-ne eigene Callback-Funktion besitzen. Es konnen je-doch auch mehrere Menus dieselbe Funktion verwen-den, wenn uber die IDs der Menupunkte eine eindeutigeIdentifikation gegeben ist.

Page 90: Skript zum Kompaktkurs C mit Beispielen in OpenGL

90 KAPITEL 8. EINFUHRUNG IN DIE GLUT-BIBLIOTHEK

8.4.1 Untermenus

Untermenus werden genauso erzeugt wie ein ,,Haupt”-Menu. Sie werden mit

void glutAddSubMenu(char * name,int menu);

an das Ende des aktuellen Menus angehangt. Auffalligist, dass fur diesen Menupunkt kein Zweig in derCallback-Funktion vorgesehen ist. Dies ist nicht erfor-derlich, da GLUT die Funktionalitat fur das Aufklappeneines Untermenus selbst implementiert.

int szene = 1;

void verarbeite(int value);void zeigeSzene(int value);

void erzeugeMenue(void){

/ * Erzeuge Menue * /int menuID = glutCreateMenu(

verarbeite);glutAddMenuEntry("Neu", 7);glutAddMenuEntry("Laden", 2);glutAddMenuEntry("Speichern",

5);

/ * Erzeuge ein Untermenue * /int unterID = glutCreateMenu(

zeigeSzene);glutAddMenuEntry("Szene A", 1);glutAddMenuEntry("Szene B", 2);glutAddMenuEntry("Szene C", 3);

/ * fuege Untermenue inHauptmenue * /

glutSetMenu(menuID);glutAddSubMenu("Szene",

unterID);

/ * lege Menue auf rechteMaustaste * /

glutAttachMenu(GLUT_RIGHT_BUTTON);

}

void verarbeite(int value){

switch(value) {

case 7:/ * Anweisungen fuer

"Neu" * /break;

case 2:/ * Anweisungen fuer

"Laden" * /break;

case 5:/ * Anweisungen fuer

"Speichern" * /break;

default:...

}}

void zeigeSzene(int value){

szene = value;}

8.4.2 Sonstige Menu-Funktionen

Die ubrigen GLUT-Menu-Funktionen sind selbster-klarend und werden hier nur der Vollstandigkeit halberaufgezahlt.

void glutSetMenu(int menu); ... setztmenuals aktuelles Menu.

int glutGetMenu(void); ... liefert die ID desaktuellen Menus.

void glutDestroyMenu(int menu);... loscht das Menumenu.

void glutChangeToMenuEntry(int entry,char * name, int value); ... andert denMenueintrag mit der Nummerentry auf einenneuen Wert.

void glutChangeToSubMenu(int entry,char * name, int menu); ... andert denMenueintrag mit der Nummerentry so, dassfortan der Menupunkt das Untermenumenuaktiviert.

void glutRemoveMenuItem(int entry);... loscht den Menueintrag mit Indexentry .

Page 91: Skript zum Kompaktkurs C mit Beispielen in OpenGL

8.5. CALLBACKS 91

void glutDetachMenu(int button);... entfernt das Menu, welches mit dem Maus-Buttonbutton verbunden ist.

8.5 Callbacks

Der eigentliche Programmablauf eines GLUT-Programms ist vor dem Programmierer verborgen.Er findet im Inneren der Event-Schleife statt. Wieaber kann der Programmierer auf Ereignisse desBenutzers oder des Fenstersystems reagieren? Ein insolchen Fallen oftmals verwendetes Konzept beruhtauf dem Mechanismus der Callbacks, der auch von derGLUT-Bibliothek verwendet wird.

Fur jedes potentielle Ereignis speichert GLUT einenFunktionszeiger. Diese Zeiger kann der Programmie-rer uber spezielle GLUT-Funktionen setzen. Dieser Vor-gang wird auch als Callback-Registrierung bezeich-net. Die Funktion, welche dabei registriert wird, heißtdementsprechend Callback. Die Callback-Funktionenkonnen vom Programmierer frei gestaltet werden. Dieeinzige Randbedingung ist, dass die Signatur der Funk-tion dem entspricht, was GLUT an dieser Stelle erwar-tet.

Tritt ein Ereignis ein, so schaut GLUT nach, ob furdieses Ereignis ein Callback registiert wurde. Ist diesder Fall, wird die registrierte Funktion aufgerufen. Nachder Bearbeitung der Callback-Funktion setzt GLUT dieBearbeitung der Event-Schleife normal fort. Mit demRumpf der Callback-Funktion beschreibt der Program-mierer somit dieReaktionauf bestimmte Ereignisse.

Ereignisse konnen global sein, wie das Ablaufen ei-nes Timers, aber auch Kontext-gebunden, wie die An-forderung, dass ein bestimmtes Fenster neu gezeichnetwerden soll. Auch die Großenanderung eines Fenstersist nur fur dieses Fenster interessant. Daher speichertGLUT fur Kontext-gebundene Ereignisse unterschied-liche Callbackfunktionen fur jedes Fenster beziehungs-weise Menu.

Die meisten Callbacks konnen auch wieder entferntwerden. Dazu wird anstelle des FunktionszeigersNULLubergeben. Dies funktioniert jedoch nicht bei der Zei-chenfunktionglutDisplayFunc .

Page 92: Skript zum Kompaktkurs C mit Beispielen in OpenGL

92 KAPITEL 8. EINFUHRUNG IN DIE GLUT-BIBLIOTHEK

8.5.1 Ubersicht uber GLUT-Callbacks

Der wichtigste aller Callbacks wurde bereits vorgestellt.Es handelt sich um die Zeichenfunktion, die mitglutDisplayFunc registriert wird. Daneben gibt es noch eine ganze Reihe von anderen Callbacks.

void glutDisplayFunc( void ( * func)(void)); ... registriert eine Zeichenfunktion fur das aktuelleFenster. Der AufrufglutDisplayFunc muß auf jeden Fall in einem GLUT-Programm enthalten sein, weilsonst uberhaupt kein Fenster geoffnet wird.

Die Zeichenfunktion wird immer dann aufgerufen, wenn ein Neuzeichnen der Szene erforderlich ist. Sobeispielsweise, wenn das Fenster neu erzeugt wurde, wenn ein verdecktes Fenster aufgedeckt wird, wenn sichdie Fenstergroße andert oder wenn der Programmierer mittels glutPostRedisplay das Neuzeichnenverlangt.

void glutReshapeFunc( void ( * func)(int width, int height)); ... registriert eine Funk-tion, die aufgerufen wird, wenn sich die Große eines Fensters andert. So kann der Programmierer aufAnde-rungen reagieren.

Es existiert ein Standard-Callback fur die Großenanderung. Dieser fuhrt lediglich den OpenGL-BefehlglViewport(0,0,width,height); aus, was jedoch fur ein einfaches Zeichenfenster vollkommenausreicht.

void glutKeyboardFunc( void ( * func)(unsigned char key, int x, int y)); ... regi-striert eine Funktion, die aufgerufen wird, wenn der Anwender eine Taste betatigt. Neben dem ASCII-Codeder Taste wird auch die Position des Mauszeigers (relativ zur linken oberen Ecke des Fensters) dem Callbackals Parameter ubergeben.

Sollen zusatzlich Modifier-Keys wie CTRL, ALT oder SHIFT interpretiert werden, so konnen die ge-druckten Tasten mit Hilfe vonint glutGetModifiers(void) ermittelt werden. Diese Funktionliefert eine logische VerODERung der KonstantenGLUTACTIVE SHIFT, GLUT ACTIVE CTRL undGLUTACTIVE ALT, je nachdem welche Tasten gerade gehalten werden.

Bei den Koordinatenx undy ist zu beachten, dass das X-Koordinatensystem eine andere Ausrichtung hat alsdas OpenGL-Koordinatensystem. X-Windows hat seinen Ursprung in der linken oberen Ecke. Die Werte furdie x-Koordinate steigen nach rechts hin an, die Werte fur die y-Koordinate steigen nach unten hin an (alsogenau umgekehrt zu OpenGL, wo die y-Werte nach oben hin ansteigen).

void glutMouseFunc( void ( * func)(int button, int state, int x, int y)); ... re-gistriert eine Funktion, die aufgerufen wird, wenn der Benutzer die Maustaste im Inneren eines Fen-sters druckt oder loslaßt. Somit werden beim Drucken undanschließendem Loslassen des Maus-Buttons zwei Ereignisse ausgelost. Die gedruckte Taste wird uber den Parameterbutton ubergeben(GLUTLEFT BUTTON, GLUTMIDDLE BUTTON, GLUTRIGHT BUTTON). Der Parameterstateenthalt die Information, ob der Mausknopf gedruckt (GLUTDOWN) oder losgelassen (GLUTUP) wurde.

Bei GLUTUP-Ereignissen ist zu beachten, dass diese auch an Koordinaten auftreten konnen, die außerhalbdes Anwendungsfensters liegen.

Gedruckte Modifier-Keys konnen wiederum mitglutGetModifiers abgefragt werden (sieheglutKeyboardFunc ).

void glutMotionFunc(void ( * func)(int x, int y)); ... der hiermit registrierte Callback wirdaufgerufen, wenn die Maus bei gedruckter Taste innerhalb des Fensters bewegt wird.

void glutPassiveMotionFunc(void ( * func)(int x, int y)); ... der hiermit registrierteCallback wird aufgerufen, wenn die Maus innerhalb des Fensters bewegt wird, ohne dass eine Taste gedrucktsein muss.

Page 93: Skript zum Kompaktkurs C mit Beispielen in OpenGL

8.5. CALLBACKS 93

void glutVisibilityFunc(void ( * func)(int state)); ... der hiermit registrierte Callbackwird aufgerufen, wenn sich die Sichtbarkeit eines Fenstersandert, beispielsweise wenn das Anwendungs-fenster iconifiziert oder wieder aufgedeckt wird. Der Parameterstate gib dabei an, ob das Fenster sichtbargeworden ist (GLUTVISIBLE ) oder unsichtbar (GLUTNOTVISIBLE ).

void glutEntryFunc(void ( * func)(int state)); ... der hiermit registrierte Callback wird auf-gerufen, wenn der Mauszeiger aus dem Fenster herausbewegt wird oder hineinbewegt wird. Dabei gibtdie Variable state an, ob die Maus das Fenster gerade betreten (GLUTENTERED) oder verlassen(GLUTLEFT) wurde.

void glutSpecialFunc(void ( * func)(int key, int x, int y)); ... der hiermit regi-strierte Callback funktioniert genauso wie der Callback f¨ur die Tastatur-Ereignisse mit der Ausnah-me, dass dieser Callback ausgelost wird, wenn eine Sondertaste auf der Tastatur betatigt wurde. Zuden Sondertasten zahlen die F-TastenGLUTKEY F1 - GLUT KEY F12, aber auch CursortastenGLUTKEY LEFT, GLUT KEY RIGHT, GLUT KEY UP, GLUT KEY DOWN, GLUTKEY PAGEUP,GLUTKEY PAGEDOWN, GLUTKEY HOME, GLUTKEY END, GLUTKEY INSERT.

void glutMenuStatusFunc(void ( * func)(int status, int x, int y)); ... der hiermitregistrierte Callback wird aufgerufen, wenn ein definiertes Menu aufpopt beziehungsweise wieder ver-schwindet. Die entsprechenden Werte fur den Parameterstatus sind GLUTMENUIN USEbeziehungs-weiseGLUTMENUNOTIN USE. Mit Hilfe dieses Callbacks kann zum Beispiel eine Animation gestopptwerden, solange ein Menu angezeigt wird.

void glutIdleFunc(void ( * func)(void)); ... die hiermit registrierte Funktion wird aufgerufen,wenn das Programm sonst keine anderen Ereignisse bearbeiten muß. Dadurch eignet sich diese Funktiongut fur Prozesse oder Bearbeitungsschritte, die im Hintergrund ablaufen sollen. Allerdings sollte der Re-chenaufwand innerhalb der Idle-Funktion minimal gehaltenwerden, damit die Anwendung interaktiv bleibt.

void glutTimerFunc(unsigned int msec, void ( * func)(int value), int value);... hiermit kann ein Timer-Event generiert werden. Das Event wird nach mindestensmsec Millisekundenausgelost, es kann aber auch abhangig von der Auslastung des Systems eine großere Zeitspanne verstrichensein.

Timer-Events eignen sich besser als eine Idle-Funktion fur die Animation einer Szene, weil auf diese Weiseder Abstand der einzelnen Animationsschritte gleichmaßig ist.

Der Timer-Callback wird immer mit dem Wert als Argument aufgerufen, der bei der Registrierung alsvalueubergeben wurde. Auf diese Weise kann zwischen mehreren Timer-Ereignissen unterschieden werden.

Der Timer-Event wird nur einmal ausgelost. Wenn also eine kontinuierliche Folge von Timer-Events erzeugtwerden soll, so muß dies durch kontinuierliches Aufsetzen eines neuen Timers erfolgen.

Page 94: Skript zum Kompaktkurs C mit Beispielen in OpenGL

94 KAPITEL 8. EINFUHRUNG IN DIE GLUT-BIBLIOTHEK

8.5.2 Animationen

Mit dem IdleFunc - bzw.TimerFunc -Callback las-sen sich auf sehr einfache Weise Animationen erstel-len. Diese sind ein wesentlicher Teil der Computergra-phik, z.B. erlaubt erst die Drehung von dreidimensiona-len Objekten ein wirkliches Verstandnis der dreidimen-sionalen Struktur. Simulationen haben oft selbst einenZeitparameter, so dass sie zur Darstellung eine Anima-tion benotigen.

Ein Problem von Animationen ist die Zeit, die vomLoschen des Bildschirminhalts bis zum fertig aufgebau-ten Bild vergeht. Wenn der Bildschirminhalt in diesemZeitraum dargestellt wird, sieht der Benutzer ein nochnicht fertig aufgebautes Bild, was sich dann meistensin einem hasslichen Flackern außert. Deshalb kommthier das bereits oben beschriebene Double-Bufferingzum Einsatz, welches eigentlich recht einfach zu ver-stehen ist: Wahrend ein Bild dargestellt wird, wirdim Hintergrund (in einem zweiten Buffer) ein zweitesBild aufgebaut. Ist dieses zweite Bild fertig, wird die-ser Buffer dargestellt, und der bisher dargestellte Buf-fer kann geloscht und neu gefullt werden, ohne dassder Benutzer eine Chance hat, vom Aufbau des Bil-des etwas zu sehen, denn er sieht immer nur Buffer,in denen fertige Bilder stehen. Zum tauschen der Buf-fer muss am Ende des Zeichnens die GLUT-FunktionglutSwapBuffers() aufgerufen werden. Folgen-des Beispiel zeigt, wie eine Animierte Grafik mit HilfedesIdleFunc -Callbacks realisiert werden kann:

#include <stdio.h>#include <math.h>#include <GL/glut.h>

double winkel = 0.0;

void malen(void);void idle(void);

int main(int argc, char ** argv){

glutInit(&argc, argv);glutInitDisplayMode(

GLUT_DOUBLE | GLUT_RGB);glutInitWindowSize(400, 300);glutInitWindowPosition(100, 100);glutCreateWindow("Hallo!");glClearColor(0.0, 0.0, 0.0, 0.0);glMatrixMode(GL_PROJECTION);glLoadIdentity();

glOrtho(0.0, 400.0, 0.0, 300.0,-1.0, 1.0);

glutDisplayFunc(&malen);glutIdleFunc(&idle);glutMainLoop();return(0);

}

void malen(void){

glClear(GL_COLOR_BUFFER_BIT);glBegin(GL_LINES);

glColor3f(1.0, 1.0, 1.0);glVertex2i(200, 150);glVertex2f(200.0 + 150.0 *

cos(winkel), 150.0 -150.0 * sin(winkel));

glEnd();glFlush();glutSwapBuffers();

}

void idle(void){

winkel = winkel + 0.01;glutPostRedisplay();

}

Wichtig ist, dass die Funktionidle() nur denWinkel, um den die gezeichnete Linie gedreht werdensoll andert. Dieser wird in einer externen Variablengespeichert und ist so in allen Funktionen sichtbar.Das eigentliche Zeichnen wird dann uber die FunktionglutPostRedisplay angestoßen.

Der IdleFunc -Callback ist nur eingeschrankt furAnimationen geeignet, da der Abstand zwischen zweiAnimationsschritten nicht fest ist, sondern immer da-von abhangt, welche anderen Ereignisse noch bear-beit werden mussen. Deshalb eignet sich hier derTimerFunc -Callback besser, wobei auch hier dieZeitspanne abhangig von der Systemauslastung nichtunbedingt fest ist. Bei Verwendung desTimerFunc -Callbacks ergeben sich folgendeAnderungen in obigemProgamm:

int main(int argc, char ** argv){

.../ * Starte Timer erstmals * /glutTimerFunc(20, &tick, 0);

Page 95: Skript zum Kompaktkurs C mit Beispielen in OpenGL

8.7. ZEICHNEN GEOMETRISCHER OBJEKTE 95

}

void tick(int value){

/ * Starte Timer erneut * /glutTimerFunc(20, &tick, 0);

winkel = winkel + 0.01;glutPostRedisplay();

}

8.6 Zeichnen von Text

OpenGL stellt keine direkte Funktionalitat zurVerfugung, um einen Text in ein OpenGL-Fensterzu zeichnen. Vielmehr muss der Programmierer furjedes Zeichen des Zeichensatzes ein eigenes Bitmaperzeugen. Mit Hilfe dieser Bitmaps kann dann Textin einem OpenGL-Fenster ausgegeben werden, indemdie Bitmaps der einzelnen Zeichen mit dem korrektenAbstand hintereinander gesetzt werden.

Die GLUT-Bibliothek erleichtert diesen Vorgang, in-dem sie bereits eine Reihe solcher Bitmap-Fonts zurVerfugung stellt und auch das Zeichnen der Buchstabenmit entsprechenden Funktionen unterstutzt.

Daruberhinaus stellt GLUT auch sogenannte Stroke-Fonts zur Verfugung. Hierbei wird anstelle eines Bit-maps der Umriß jedes Buchstabens in Form eines Li-nienzuges gezeichnet. Die folgende Funktion demon-striert die Textausgabe in ein OpenGL-Fenster:

void text(void * font, int x, int y,char * string)

{int len,i;

len = (int)strlen(string);

/ * Positioniere Zeichenmarke * /glRasterPos2i(x, y);

for(i = 0; i < len; i++){

glutBitmapCharacter(font, string[i]);

}}

Zunachst muss die gewunschte Position(x, y) , ander der Text erscheinen soll, als aktuelle Zeichenpo-

sition gesetzt werden. Dies erfolgt mit dem OpenGL-KommandoglRasterPos2i . Danach wird der aus-zugebende Text zeichenweise bearbeitet und als Bit-map an die jeweils aktuelle Stelle gezeichnet. DabeiverandertglutBitmapCharacter bei jedem Auf-ruf entsprechend der Breite des aktuellen Zeichens dieRasterposition, so dass keine Neupositionierung erfor-derlich ist.

Dennoch stellt GLUT Funktionen zur Verfugung, mitdenen die Breite eines einzelnen Zeichens ermittelt wer-den kann:

int glutBitmapWidth(GLUTbitmapFont font,int character);

Analog existieren fur das Zeichnen im Stroke-Modusinnerhalb der GLUT die Funktionen:

void glutStrokeCharacter(void * font,int character);

int glutStrokeWidth(GLUTbitmapFont font,int character);

Fur das Zeichnen von Text stehen unterschiedlicheZeichensatze zur Verfugung, die mit entsprechendenKonstanten beim Aufruf der Zeichenfunktionen uberge-ben werden. Tabelle 8.3 gibt einenUberblick uber diemoglichen Fonts.

Konstante Breite× Hohe SchriftfamilieGLUTBITMAP 8 BY 13 8×13 Standard FixedGLUTBITMAP 9 BY 15 9×15 Standard FixedGLUTBITMAP TIMES ROMAN10 prop. 10pt TimesRomanGLUTBITMAP TIMES ROMAN24 prop. 24pt TimesRomanGLUTBITMAP HELVETICA 10 prop. 10pt HelveticaGLUTBITMAP HELVETICA 12 prop. 12pt HelveticaGLUTBITMAP HELVETICA 18 prop. 18pt Helvetica

Tabelle 8.3: Die von der GLUT-Bibliothek unterstutztenZeichensatze.

8.7 Zeichnen geometrischer Ob-jekte

OpenGL unterstutzt nur einfache Basisprimitive wiePunkt, Linie und Polygon, zum Zeichnen geometrischerObjekte. Sollen andere Objekte wie ein Wurfel oder ei-ne Kugel gezeichnet werden, so muss dieses Objekt auslauter Linien oder Polygonen aufgebaut werden. Dies

Page 96: Skript zum Kompaktkurs C mit Beispielen in OpenGL

96 KAPITEL 8. EINFUHRUNG IN DIE GLUT-BIBLIOTHEK

mag fur einen Wurfel noch einfach sein. Schon die Po-lygonalisierung einer Kugel erfordert jedoch einen ge-wissen Aufwand.

Die GLUT-Bibliothek unterstutzt den Programmie-rer bei der Modellierung einfacher dreidimensionalerObjekte, indem sie eine Reihe von Hilfsfunktionen zurVerfugung stellt, mit der solche Objekte gezeichnetwerden konnen. Im Folgenden sind alle unterstutztenObjekte aufgezahlt. Die Funktionen sind im wesentli-chen selbsterklarend. Die geometrischen Primitive wer-den mittig um den Nullpunkt gezeichnet.

glutSolidSphere(GLdouble radius,GLint slices,GLint stacks);

glutWireSphere(GLdouble radius,GLint slices,GLint stacks);

glutSolidCube(GLdouble size);glutWireCube(GLdouble size);

glutSolidCone(GLdouble base,GLdouble height,GLint slices,GLint stacks);

glutWireCone(GLdouble base,GLdouble height,GLint slices,GLint stacks);

glutSolidTorus(GLdouble inRadius,GLdouble outRadius,Glint nsides,GLint rings);

glutWireTorus(GLdouble inRadius,GLdouble outRadius,Glint nsides,GLint rings);

glutSolidDodecahedron(void);glutWireDodecahedron(void);

glutSolidOctahedron(void);glutWireOctahedron(void);

glutSolidTetrahedron(void);glutWireTetrahedron(void);

glutSolidIcosahedron(void);

glutWireIcosahedron(void);

glutSolidTeapot(GLdouble size);glutWireTeapot(GLdouble size);

8.8 State-Handling

Ebenso wie OpenGL speichert die GLUT-Bibliothek ei-ne ganze Reihe von internen Zustanden, um die Anzahlder Parameter fur die Funktionen moglichst gering zuhalten. Der Wert der einzelnen Zustandsvariablen kannmit der Funktion:

int glutGet(GLenum state);

ermittelt werden. Eine Aufzahlung aller Zustandsva-riablen wurde hier zu weit fuhren. Der geneigte Le-ser wird deshalb auf die GLUT-Dokumentation bezie-hungsweise die GLUT-man-pages verwiesen. Ebensosollen die beiden Funktionen

int glutDeviceGet(GLenum info);int glutExtensionSupported(

char * extension);

mit denen die angeschlossenen Eingabegerate bezie-hungsweise die von der Graphikhardware unterstutz-ten OpenGL-Erweiterungen erfragt werden konnen, nurdem Namen nach erwahnt werden.

Page 97: Skript zum Kompaktkurs C mit Beispielen in OpenGL

8.8. STATE-HANDLING 97

Ubungen zum vierten Tag

Aufgabe IV.1

Das Logo der Universitat Stuttgart besteht aus 133auf der Spitze stehenden Quadraten. Die Mittel-punkte dieser Quadrate sind als Punktpaare in derTextdatei unilogo.dat gespeichert die Sie unterhttp://www.vis.uni-stuttgart.de/ger/teaching/lecture/CKompaktkurs/unilogo.dat herunterladen konnen.Schreiben Sie ein Programm das diese Textdateieinliest und das Logo graphisch darstellt. ZeichnenSie dazu an jedem dieser Punkte ein auf der Spitzestehendes Quadrat mit der Kantenlange0.9259259.

Programmieren Sie eine animierte Darstellung desLogos, bei der sich die Große der Quadrate zyklischandert, so dass ein

”pumpender“ Effekt entsteht. Ver-

suchen Sie insbesondere durch die Verwendung vonFunktionen die Große Ihres Programms moglichst kleinzu halten.

Aufgabe IV.2

Es kommt haufig vor, dass man alten, langst vergesse-nen Code, wiederverwenden will. In dieserUbung sollder Code zur Berechnung der Mandelbrot-Menge ausAufgabe II.3 reaktiviert werden.

Offnen Sie zwei Fenster und stellen Sie in einem dieMandelbrot-Menge dar. Im zweiten soll eine sogenann-te Julia-Menge dargestellt werden, die nach folgenderVorschrift berechnet wird:

Julia-Mengen (nach ihrem Entdecker, Gaston Julia,benannt) werden ganz ahnlich wie Mandelbrotmengenberechnet, mit dem Unterschied, dasscx undcy fur dasganze Bild konstant sind (und frei vorgegeben werden

konnen) und sich der Startwert

(

x0

y0

)

aus den ska-

lierten und verschobenen Koordinaten eines Bildpunk-tes ergibt. Es soll etwa gelten:−2 < x0 < 2 und−2 < y0 < 2.

Mit Hilfe einer Callback-Funktion soll nun die Julia-Menge neu berechnet werden, sobald der Benutzer aufeinen Punkt im Fenster der Mandelbrot-Menge klickt.Die Koordinaten des angeklickten Punkts sollen dabeidie Werte(cx, cy) fur die Berechnung der Julia-Mengeangeben.

Aufgabe IV.4

Eine Beispiel fur Iterationen in zwei Dimensionen istdas sogenannteChaos Gamevon Michael F. Barnsley,bei dem zufallig eine von mehreren (linearen) Iterati-onsvorschriften angewendet wird. Und zwar mit Wahr-scheinlichkeit0.02 die Vorschrift

(

xn+1

yn+1

)

=

(

0.50.27yn

)

,

mit Wahrscheinlichkeit 0.15 die Vorschrift(

xn+1

yn+1

)

=

(

−0.139xn + 0.263yn + 0.570.246xn + 0.224yn − 0.036

)

,

mit Wahrscheinlichkeit 0.13 die Vorschrift(

xn+1

yn+1

)

=

(

0.17xn − 0.215yn + 0.4080.222xn + 0.176yn + 0.0893

)

,

und mit Wahrscheinlichkeit 0.7 die Vorschrift(

xn+1

yn+1

)

=

(

0.76 cos(φ)xn + 0.76 sin(φ)yn + 0.1075−0.76 sin(φ)xn + 0.76 cos(φ)yn + 0.27

)

,

Der Startpunkt

(

x0

y0

)

soll dabei

(

0.50.5

)

sein.φ

ist ein frei wahlbarer kleiner Winkel.Schreiben Sie ein Programm, das die Iteration10000-

mal durchfuhrt und nach jedem Iterationschritt einenPunkt an der Position(xn, yn) zeichnet. Skalieren undverschieben Sie die Fensterkoordinaten geeignet, sodass alle Punkte sichtbar sind.

Animieren Sie die Ausgabe ihres Programms, indemSie den Wert des Winkelsφ mit Hilfe der Maus einstell-bar machen.

Aufgabe IV.4

Animieren Sie 100 Punkte in einem Fenster. SpeichernSie dazu zu jedem Punkt sowohl seine zweidimensio-nalen Koordinaten~ri mit 0 ≤ i < 100, als auch sei-ne Geschwindigkeit~vi. Starten Sie mit ruhenden Punk-ten an unterschiedlichen Positionen in der Nahe des Ur-sprungs, der sich in der Mitte des Fensters befinden soll-te. Das Koordinatensystem sollte außerdem so gewahltwerden, dass die Seitenlange des Fensters in diesen Ko-ordinaten etwa 200 ist.

Berechnen Sie in in jedem Zeitschritt der Langed,die neuen Positionen aller Punkte nach den Formeln

~ri ← ~ri + ~vid +1

2~aid

2

~vi ← 0.999 · (~vi + ~aid)

Page 98: Skript zum Kompaktkurs C mit Beispielen in OpenGL

98 KAPITEL 8. EINFUHRUNG IN DIE GLUT-BIBLIOTHEK

mit

~ai = ~ri

(

1

2500sin(~r2

i /1000)− 1

10000exp(~r2

i /1000)

)

±100 · (~m− ~ri) exp(

−(~m− ~ri)2/100

)

und den Mauskoordinaten ~m (im OpenGL-Koordinatensystem!). Wahlen Sied geeignet. Fur± kann+ oder− eingesetzt werden.

Page 99: Skript zum Kompaktkurs C mit Beispielen in OpenGL

Teil V

Funfter Tag

99

Page 100: Skript zum Kompaktkurs C mit Beispielen in OpenGL
Page 101: Skript zum Kompaktkurs C mit Beispielen in OpenGL

Kapitel 9

Entwicklungswerkzeuge

In Kapitel 4.3.2 haben wir bereitsmake als einsehr hilfreiches Tool fur dieUbersetzung von C-Programmen kennengelernt. In diesem Kapitel sollennun weitere Entwicklungswerkzeuge vorgestellt wer-den, die das Leben des Programmierers erheblich ver-einfachen konnen.

9.1 Debugger

Die meisten Entwicklungsumgebungen enthalten einspezielles Programm namens Debugger zum Aufspurenvon Fehlern (

”bugs“; der Ausdruck kommt angeblich

aus der Fruhzeit des Informationszeitalters als noch al-lerhand Kleingetier fur Fehler in Computern sorgte undmuhsam von Hand entfernt werden musste). Ein De-bugger dient vor allem dazu, ein Programm kontrolliert(schrittweise) auszufuhren, so dass fehlerhafter Codeschneller lokalisiert werden kann.

Der Standard-Debugger der Gnu-Entwicklungsum-gebung heißtgdb (Gnu-DeBugger) und wird mitgdbProgrammdatei[core-Datei] aufgerufen. Die (optiona-le) core-Datei ist eine Art Bericht des Betriebssystemsnach einem Programmabsturz, der meistens in einerDatei namenscore abgespeichert wird und dem Pro-grammierer dazu dienen soll, die naheren Umstandedes Absturzes zu erforschen. (Das Schreiben dieses Be-richts, der mehr oder weniger den gesamten Systemzu-stand zum Zeitpunkts des Absturzes enthalt, wird auchals core dump bezeichnet.)

Das erste Problem beim Debuggen eines Programmsist, dass die ausfuhrbare Programmdatei normalerwei-se nur noch Informationen enthalt, die fur den Compu-ter zur Ausfuhrung des Programms relevant sind. Da-mit sind die meisten fur menschliche Programmierer in-teressanten Informationen wie Symbolnamen oder dieursprunglichen C-Befehle ausgeschlossen. Damit der

Compiler auch diese Informationen in die ausfuhrba-re Programmdatei schreibt, muss in der Regel ein ent-sprechendes Compiler-Flag gesetzt werden; beimgccist dies -ggdb . (-g alleine schreibt (meistens weni-ger) Debugging-Information im

”nativen“ Format der

jeweiligen Plattform in die Programmdatei, so dass sieauch mit dem

”nativen“ Debugger ausgewertet werden

kann.)Wenn dergdb mit einer Programmdatei aufgerufen

wird, die Debugging-Information enthalt, passiert erst-mal gar nichts, weil dergdb auf Kommandos wartet.Hier sind ein paar der nutzlichsten:

• quit beendet dengdb .

• help [gdb-Kommando oder -Thema] gibt eineBeschreibung von gdb-Kommandos bzw. -Themenaus.

• break [Dateiname:]Funktionsnameoderbreak[Dateiname:]Zeilennummer setzt einen Halte-punkt (breakpoint) an der angegebenen Zeilen-nummer bzw. beim Einsprung in die angegebeneFunktion.

• delete loscht alle Haltepunkte.

• run startet ein Programm zum Debuggen. (ImAllgemeinen sollte vorher mindestens ein Halte-punkt gesetzt worden sein, z.B. bei der Funktionmain .)

• continue lasst ein Programm nach Erreichen ei-nes Haltepunkts weiterlaufen.

• step fuhrt eine C-Anweisung aus und springt ge-gebenenfalls in Funktionen hinein.

• next fuhrt eine C-Anweisung aus, aber springtnicht in Funktionen.

101

Page 102: Skript zum Kompaktkurs C mit Beispielen in OpenGL

102 KAPITEL 9. ENTWICKLUNGSWERKZEUGE

• finish fuhrt ein Programm bis zum Rucksprungaus der aktuellen Funktion aus.

• print Ausdruckgibt einen C-Ausdruck aus, derz.B. auch Variablen enthalten darf.

• backtrace gibt eine Beschreibung des Call-stacks (also aller derzeit aktiven Unterprogramm-aufrufe) aus.

Naturlich gibt es noch viel mehr gdb-Kommandos,aber furs erste Ausprobieren sollte das reichen.

Wem das spartanische Kommandozeilen-Interfacedes gdb nicht zusagt, kann meistens auch auf De-bugger mit graphischer Benutzungsoberflache wiedddzuruckgreifen. Deren Bedienung ist meistens mehr oderweniger selbsterklarend. (Jedenfalls dann, wenn manschon mal mit einem anderen Debugger gearbeitet hat.)

9.2 Analysetools

Eine Programmiersprache wie C enthalt viele Fall-stricke, die zwar zu gultigem Code fuhren, aber nichtden vom Programmierer beabsichtigten Effekt haben.Warnungen des C-Compilers konnen auf viele derartigeProbleme hinweisen, aber es gibt Programme, die denSourcecode noch wesentlich genauer nach moglichenFehlern durchsuchen. Ein Beispiel ist das Toollint ,das z.B. mitlint .c-Dateiaufgerufen wird

Aber nicht nur der Sourcecode kann genauer analy-siert werden: Sogenannte Profiler, z.B.gprof analy-sieren den tatsachlichen Programmablauf, insbesondereum festzustellen, wo der Computer die meiste Zeit

”ver-

brat“. Damit kann bei zeitkritischen Programmen fest-gestellt werden, welche Programmteile noch optimiertwerden sollten, bzw. welche Optimierungen uberflussigsind.

9.3 Sourcecode-Management

Sobald mehrere Entwickler an einem Projekt arbei-ten wollen, bzw. ein einzelner (zerstreuter) Entwick-ler immer wieder Zugriff auf alte Programmversionenbraucht, sollte ein Sourcecode-Management-Systemwie RCS oder (das machtigere) CVS benutzt werden.Ein neueres System ist Subversion, dass immer haufigerbei Open-Source-Projekten CVS als Standardversions-verwaltung ablost.

9.4 Dokumentationssysteme

Mit der Große eines Projekts wird auch die Doku-mentation des Sourcecodes immer wichtiger. Dabeikonnen Tools zur

”automatischen“ Dokumentationser-

stellung (z.B. doxygen) gute Dienste leisten. Die mei-sten dieser Tools erwarten, dass Programmierer speziellformatierte Kommentare in den Sourcecode einfugen,die dann zusammen mit der automatischen analysier-ten Programmstruktur in nett formatierte Dokumentati-onstexte (typischerweise in HTML, LaTeX, PostScript,etc.) zusammengefasst werden. Sie biueten zudem denVorteil, dass sie automatisch einen bestimmten Stil furdie Kommentare vorgeben und so auch der Quellcodebesser lesbar wird.

9.5 Integrierter Entwicklungsum-gebungen

Wie man sieht, gibt es eine Vielzahl von unabhangi-gen Werkzeugen, die den Programmierer bei der Soft-wareentwicklung unterstutzen. Hier kann man jedochsehr leicht denUberblick verlieren und der Aufwand er-scheint oft hoher als der Nutzen. Diesen Misstand ver-suchen die sogenannten integrierten Entwicklungsum-gebugnen, kurz IDEs (Integratet Development Environ-ments), zu beheben, indem sie moglichst viele dieserTools in einer einzelnen Umgebung zusammenfassen.Ausserdem bieten Sie meist Hilfsmittel wie Syntax-highlighting und automatische Codevervollstandigungan.

Typische Vertreter dieser Programme sind z.B.KDevelop fur Linux und Dev-C++ in Kombinationmit dem MinGW-Compiler fur Windows. Diese un-terstutzen zwar schwerpunktmaßig die objektorientier-te Programmentwicklung mit C++, sind aber prinzi-piell auch fur die C-Programmierung geeignet. Auchdie klassischen Editoren wie Emacs und vi werdendurch entsprechende Einstellungen und Erweiterungenzu sehr machtigen Entwicklungswerkzeugen.

Page 103: Skript zum Kompaktkurs C mit Beispielen in OpenGL

Kapitel 10

Stilvolles C

(Dieses Kapitel ist zum Teil ein ubersetzter Auszugaus David Straker,C-Style: Standards and Guidelines,Prentice Hall, 1992.)

10.1 Was sind Kodierung-Standards?

Die Programmiersprache C hat eine definierte Syntax,die definiert, was ein gultiges C-Programm ist, und wasnicht. Trotz dieser Regeln bleibt vieles offen: Wie sollenVariablen, Funktionen, Dateien etc. genannt werden,was und wie kommentiert wird, wie Text eingerucktwird, wo Leerzeichen stehen, etc. Kodierung-Standardsenthalten Regeln, die einige dieser offenen Fragen ent-scheiden. In der Praxis werden diese Regeln aber oftnicht erzwungen; entsprechend sind es eigentlich Richt-linien (Guidelines).

10.2 Wofur sind C-Kodierung-Standards gut?

Es scheint schwierig genug zu sein, korrekten und effi-zienten C-Code zu schreiben. Der C-Compiler erscheintdabei manchmal mehr als fieser Gegner, der die Pro-grammiererin mit unverstandlichen Fehlermeldungenbombadiert, als das unglaublich machtige Tool, das ereigentlich ist. Wenn sich in diesem Kampf Program-miererin gegen Compiler der Compiler nicht um Kom-mentare, Leerzeichen und Tabulatoren, Namen von Va-riablen, etc. kummert, warum sollte es die Program-miererin tun? Wofur sollten Kodierung-Standards, dienur noch mehr Regeln zur Kodierung (zusatzlich zur C-Syntax) enthalten, gut sein?

David Straker gibt einige Ziele an, die mit Hilfe vonKodierung-Standards besser erreichbar sind:

• Produktivit at: Dazu sollte Code

– geeignet fur Entwicklungstools,

– portabel und

– wiederverwendbar sein.

• Qualit at: Code sollte Spezifikationen entspre-chen. Deshalb sollte Code

– uberprufbar und

– verlasslich sein.

• Wartbarkeit: Dazu sollte Code

– erweiterbar und

– selbst-diagnostizierend sein.

• Verstandlichkeit: Dazu sollte Code

– konsistent und

– kommunizierbar sein.

10.3 Allgemeine Grundsatze

• Denke an den Leser!

• ”Keep it Simple, stupid! ;-)“ (KISS)

• Sei explizit!

• Sei konsistent!

• Nimm den kleinstmoglichen Bereich!(Sichtbarkeits- und Gultigkeitsbereiche vonVariablen, Funktionen, etc.)

103

Page 104: Skript zum Kompaktkurs C mit Beispielen in OpenGL

104 KAPITEL 10. STILVOLLES C

• Die letzten vier Punkte lassen sich zuSECS zu-sammenfassen:Simple, Explicit, Consistent, mi-nimal Scope

• Dokumentiere den eingesetzten Standard!

• Benutze Standard-Bibliotheken!

• Benutze Entwicklungstools: Editoren mit Syntax-Highlighting, Compiler mit vielen Warnungen(gcc -Wall -ansi -pedantic ... ),Suchprogramme, language checkers (vor allemlint ), complexity checkers, pretty-printers, ...

10.4 Kommentare

Allgemein sollten Kommentare

• klar und vollstandig sein;

• kurz sein, um zu sagenwaspassiert, und langer,um zu sagenwarum;

• korrekt zein;

• vom Code unterscheidbar sein und

• in Große und Ausfuhrlichkeit der Wichtigkeit undKomplexitat des beschriebenen Codes entspre-chen.

Kopfkommentare(heading comments):

• ... sollten am Anfang jeder Datei, jeder Funktions-definition und vor großeren Datendefinitionen ste-hen.

• ... sind strukturiert und bestehen aus mehreren Zei-len.

• ... konnen auch mit manchen Entwicklungstoolsbearbeitet werden.

• ... sollten auch den Kontext (auch historisch) einerDatei oder Funktion beschreiben.

• ... sollten moglichst viel Hilfe, Warnungen, Tips,Annahmen und Grenzen des Codes enthalten.

Blockkommentare

• ... stehen nicht mit Code in einer Zeile.

• ... beschreiben den vorhergehendenund/oder nach-folgenden Code.

• ... sollten mit Hilfe von Leerzeilen Codezeilen zu-geordnet werden.

• Alle Zeilen von mehrzeiligen Blockkommentarensollten mit/ * oder* beginnen.

Zeilenkommentare

• ... stehen mit Code in einer Zeile.

• ... sollten vor allem in besonderen Situationen ver-wendet werden.

Datenkommentare:

• ... sollten bei alle Datendeklarationen stehen.

• ... sollten sehr genau sein und den Zweck von Da-ten beschreiben.

• ... sollten auch Beispiele fur die Verwendung undmogliche Werte geben.

10.5 Namen

• ... sollten auch international lesbar sein (also: eng-lisch).

• Namen sollten nicht zu lang sein.

• Um Namen zu kurzen sollten Abkurzungen ver-wendet werden.

• Abkurzungen sollten konsistent sein. Eine Listemit Standardabkurzungen ist nutzlich.

• Kurze Namen sollten nur in kleinen Bereichen ver-wendet werden, und auch dann nur, wenn ihre Be-deutung offensichtlich ist.

• Worter in Namen sollten konsistent getrennt sein(einName , ein name, ein Name).

• Namen sollten der korrekten Rechtschreibung ent-sprechen.

• Namen sollten eindeutig sein.

• Unterstriche sollten nicht in Paaren und nur inner-halb von Namen auftreten.

• Vorsicht bei Ziffern, die wie Buchstaben aussehen(l und1).

• Prozedurnamen sollten Verb-Nomen-Kombinationen sein.

Page 105: Skript zum Kompaktkurs C mit Beispielen in OpenGL

10.7. DATEI-LAYOUT 105

• Gruppen von Funktionen, Konstanten, etc. solltenein gemeinsames Prefix haben.

• Variablennamen sollten aus Nomen bestehen.

• Boolsche Variablennamen sollten mitIs oderis(ist ...?) beginnen.

• Mit #define vereinbarte Symbole sollten inGroßbuchstaben geschrieben sein.

10.6 Layout

Leerzeichen:

• Kein Leerzeichen bei den binaren Operator mithochster Prioritat:a(b) , a[b] , a.b , a->b .

• Kein Leerzeichen nach unaren Operatoren (undvor Postfix-De/Inkrementoperator).

• Leerzeichen vor und nach allen anderen binarenund ternaren Operatoren.

• Kein Leerzeichen vor, und ; , aber danach einLeerzeichen.

• Leerzeichen vor und nach Keywords.

• Wenn (ausnahmsweise) nach( ein Leerzeichen,dann auch vor dem entsprechenden) .

Zeilenumbruch:

• In jeder Zeile sollte nur eine Anweisung stehen.

• Bei mehrzeiligen Anweisungen sollte die zweiteund nachfolgende Zeilen eingeruckt werden.

• Ausdrucke sollten nach Regeln umgebrochen wer-den (z.B. nach, und; umbrechen wenn moglich).

Klammern und Blocke:

• Einruckungen von Blocken sollten konsistent sein.

• Nach if , else , while , for , switch , do im-mer einen Block, auch wenn nur eine Anweisung.

• Zu tiefe (mehr als 3) Verschachtelungen solltenvermieden werden.

10.7 Datei-Layout

Allgemein:

• Dateien sollten strukturiert sein. Dazu ist ein Tem-plate nutzlich.

• Reihenfolge von Eintragen: extern vor intern,offentlich vor privat, alphabetisch.

• Kopfkommentar mit Dateinamen, Autor(in), Kurz-beschreibung, etc.

• Lange sollte auf 1000 Zeilen (20 Seiten), Breiteauf 80 Zeichen beschrankt sein.

Header-Dateien (.h -Dateien):

• Schutz vor mehrfachen#include s mit #if!defined ... H.

• Keine Speicherplatzreservierungen in Headerda-teien. (Keine Definitionen, nur Deklarationen.)

• Moglichst pro.c -Datei eine.h -Datei.

• Funktionsprototypen von allen offentlichen Funk-tionen.

• Deklarationen von allen offentlichen Typen,struct s, ...

Code-Dateien (.c -Dateien):

• Liste der Funktionsprototypen aller privaten Funk-tionen.

• Konsistente Regeln fur Reihenfolge der Funk-tionsdefinitionen. (Z.B. zuerst offentlich (darun-ter alphabetisch), dann privat (darunter alphabe-tisch).)

• Lange von Funktionen hochstens etwa 100 Zeilen.

10.8 Sprachgebrauch

• Komplexe Ausdrucke sollten in kleiner Teile zer-legt werden (z.B. mit Hilfe von neuen Variablen.)

• Seltsame, ungewohnliche, unverstandliche undgefahrliche Konstruktionen sollten vermiedenwerden.

• Tiefe Schachtelungen sollten vermieden werden.

Page 106: Skript zum Kompaktkurs C mit Beispielen in OpenGL

106 KAPITEL 10. STILVOLLES C

• if -else statt?: .

• Keine Zuweisungen in Ausdrucken.

• Boolsche Ausdrucke sollten immer einen Ver-gleichsoperator enthalten.

• Vergleichsoperatoren sollten nur in BoolschenAusdrucken enthalten sein.

• Immer explizite Typumwandlung statt automati-scher Typumwandlung. Auch bei Argumenten vonFunktionsaufrufen.

• Zahlen sollte immer bei 0 beginnen (z.B. infor ).

• Moglichst immer asymmetrische Grenzen verwen-den (0 ≤ Index < Lange/Große/Anzahl).

• In switch vor jedemcase (außer dem ersten)einbreak .

• break undcontinue ansonsten moglichst sel-ten verwenden.

• goto moglichst gar nicht verwenden.

• return vor dem Ende des Funktionsblocks nurin Ausnahmesituationen verwenden.

• Immer den Ruckgabetyp deklarieren.

• Argumente (-Typen und -Namen) verschiedenerFunktionen moglichst ahnlich wahlen.

• Nicht zu viele Argumente.

• Nicht zu komplexe Ausdrucke bei Argumentenvon Funktionsaufrufen.

• Keine#define s um die Syntax von C auszuhe-beln.

• Vorsicht bei Makros: Kein; , Klammern von Ma-kroparametern und dem ganzen Makro, bei Ver-wendung an Seiteneffekte denken.

10.9 Datengebrauch

• Gleitkommazahlen nicht auf Gleichheit verglei-chen.

• typedef statt#define undstruct -Typ.

• Moglichst wenig globale Daten.

• Moglichst keine tiefen Schachtelungen von Zei-gern.

• Moglichst wenigunion s.

• sizeof statt Konstante.

• Magic Numbersimmer#define n. (Magic Num-berssind explizit angegebene konstante Zahlen au-ßer 0 und 1, z.B. 10.)

• static -Variablen initialisieren.

Page 107: Skript zum Kompaktkurs C mit Beispielen in OpenGL

Kapitel 11

Beliebte Fehler in C

(Dieses Kapitel ist zum Teil ein Auszug aus AndrewKoenigDer C-Experte, Addison-Wesley, 1989.)

11.1 Lexikalische Fallen

Ein C-Compiler betrachtet ein Programm nicht als ei-ne Ansammlung einzelner Buchstaben, sondern – ge-nauso wie der Mensch – liest er vielmehr die einzelnenWorter. Die Worter werden hier jedoch Tokens genannt.Unter Umstanden konnen diese Tokens ahnlich oder so-gar gleich aussehen und etwas komplett verschiedenesbedeuten.

11.1.1 = != == (oder: = ist nicht ==)

In vielen Programmiersprachen (Pascal, Ada, Algol)wird := fur die Zuweisung und= fur den Vergleich ver-wendet. In C hingegen wird= fur die Zuweisung ver-wendet und== zum Vergleichen. Hier kommt einmalmehr die Schreibfaulheit der C-Entwickler zum Aus-druck: Die Zuweisung wird namlich in der Regel vielhaufiger als der Vergleich verwendet und somit spartman sich ein paar Buchstaben Tipparbeit. Ein sinnvollerAspekt ist jedoch, dass C die Zuweisung als einen Ope-rator behandelt, so dass man leicht Mehrfachzuweisun-gen zum Beispiel wie folgt schreiben kann:a=b=c

Dies fuhrt jedoch auch zu Problemen. So ist esmoglich, Zuweisungen zu schreiben, obwohl man ei-gentlich einen Vergleich durchfuhren wollte:

if (x = y)a = x;

Hier wird in Wirklichkeit zunachst der Wert vonx mitdem vony uberschrieben und danach getestet, ob dieserWert ungleich 0 ist. Das Ergebnis ist also ein komplett

anderes als bei einem Vergleichx == y . Einige Com-piler geben daher an solchen Stellen eine Warnung aus,die meisten tun dies jedoch nicht.

Auch weniger gut sichtbare Fehler konnen auf dieseArt entstehen. Will man zum Beispiel die Leerzeichen,Tabulatoren und Zeilenvorschube in einer Datei uber-springen, so schlagt der folgende Versuch fehl:

while (c = ’ ’ ||c == ’\t’ ||c == ’\n’ )

c = getc(f);

Bei der gewahlten Anordnung fallt das falsche= sofortins Auge, aber schreibt man alles in eine Zeile, so istdas Auffinden eines solchen Fehlers schwieriger. Da derOperator= eine niedrigere Prioritat hat als|| wird derVariablenc der Wert des gesamten Ausdrucks

’ ’ || c == ’\t’ || c == ’\n’

zugewiesen. Da’ ’ immer ungleich 0 ist wird die Ab-frage immer erfullt. Ist das Einlesen von Zeichen auchnach Erreichen des Dateiendes erlaubt, so wird dieseSchleife ewig laufen.

Sollte bei Bedingungen einmal dennoch eine Zuwei-sung erfolgen, so kann man dies wie folgt umschreiben:Anstelle von

if (x = y)...

schreibt man einfach

if ((x = y) != 0)...

Dies verhindert eventuell auftretende Warnungen undverdeutlicht die Absicht des Programmierers.

107

Page 108: Skript zum Kompaktkurs C mit Beispielen in OpenGL

108 KAPITEL 11. BELIEBTE FEHLER IN C

11.1.2 & und | ist nicht &&oder ||

Auch bei diesen Operatoren treten leicht Verwechslun-gen auf:&bzw.| stellen eine bitweise Verknupfung dar,wohingegen&& bzw. || logische Verknupfungsopera-toren sind.

11.1.3 Habgierige Analyse

Wer schon einmal mit regularen Ausdrucken zu tun hat-te weiß, dass diese immer den langstmoglichen Stringermitteln, welcher dem Suchmuster entspricht. Genau-so ist ein C-Compiler: Er versucht immer zuerst dasgroßtmogliche Token zu verwenden, die lexikalischeAnalyse ist also sozusagen habgierig. Dies fuhrt zurVerwirrung bei einigen Tokens:

y = x/ * p / * p ist Pointer * /;

Der Programmierer wollte hier offensichtlich eine Di-vision durch den Wert auf den der Pointerp zeigtdurchfuhren. Wegen der Habgier wird/ * jedoch alsEinleitung eines Kommentars interpretiert. Fur denCompiler sieht diese Anweisung daher wiey = x aus!Leerzeichen oder Klammern konnen in solchen Situa-tionen Abhilfe schaffen:

y = x/ * p / * p ist Pointer * /;y = x/( * p) / * p ist Pointer * /;

11.1.4 Integerkonstanten

Ist das erste Zeichen einer Integerkonstante0, so wirddiese als Oktalzahl interpretiert.010 ist somit 8 undnicht, wie man vielleicht erwartet hatte10 . Bei Ok-talzahlen sind zudem nur Ziffern zwischen 0 und 7 er-laubt!

Hexadezimale Zahlen werden durch ein vorangestell-tes0x eingeleitet und setzen sich aus den Ziffernsym-bolen0-9 undA-F (bzw.a-f ) zusammen.

11.1.5 Strings und Zeichen

Ein Zeichen (char ) wird in der Regel wie folgt defi-niert:

char c = ’a’;

’a’ wird intern in eine Integerzahl umgewandelt. DieZuweisung ist also gleichbedeutend mit:

char c = 97;

oder

char c = 0141;

Einige Compiler lassen auch mehrere Zeichen zwischenden einfachen Hochkommas zu, da eine Integerzahlmehrere Bytes lang ist. Dies entspricht jedoch nichtANSI-C, der tatsachliche Wert von’no’ ist daher auchnicht exakt definiert.

Im Gegensatz zu den Zeichenkonstanten stellendie Stringkonstanten eine abgekurzte Schreibweise fureinen Zeiger auf das erste Zeichen eines namenlosenArrays dar. Dieses Array wird am Ende um ein Zeichenmit dem Wert0 erweitert, welches das Ende des Stringsmarkiert. Die Anweisung

printf("Hallo Welt\n");

ist also gleichbedeutend mit

char hallo[]={’H’,’a’,’l’,’l’,’o’,’ ’,’W’,’e’,’l’,’t’,’\n’,0);

printf(hallo);

Einige Compiler uberprufen nicht den Typ von Funk-tionsargumenten, so dass die folgende, falsche Anwei-sung zur Laufzeit ein interessantes Wirrwar von Zei-chen auf den Bildschirm zaubert.

printf(’\n’);

11.2 Syntaktische Fallstricke

Es reicht leider nicht, wenn man nur die Tokens einesC-Programms versteht. Man muss auch wissen, wie die-sen Tokens zu Deklarationen, Ausdrucken, Anweisun-gen und Programmen kombiniert werden. Diese Kom-binationen sind in der Regel klar festgelegt, ihre De-finition ist unter Umstanden jedoch verwirrend. Die-ser Abschnitt behandelt einige dieser undurchsichtigensyntaktischen Konstruktionen.

11.2.1 Funktionsdeklarationen

Um die Deklaration von zunachst kompliziert ausse-henden Funktionsdeklarationen wie

( * (void( * )())0)();

zu verstehen, beherzigt man am besten die folgende Re-gel: Deklarieren Sie die Funktion so, wie Sie sie ver-wenden.

Page 109: Skript zum Kompaktkurs C mit Beispielen in OpenGL

11.2. SYNTAKTISCHE FALLSTRICKE 109

Um dies zu verstehen, betrachtet man zunachst dieVariablendeklaration in C. Sie besteht aus zwei Teilen:einem Typ und den Deklaratoren. Fur zwei Fließkom-mazahlen sieht das dann zum Beispiel so aus:

float f, g;

Da ein Deklarator wie ein Ausdruck behandelt wirdkann man beliebig viele Klammern setzen und mussdies sogar in einigen Fallen, wie man gleich sehen wirdund was auch in Abschnitt 11.2.2 entsprechend be-grundet wird.

float ((f));

bedeutet daher, dass((f)) ein float ist und deshalbdarauf geschlossen werden kann, dassf ebenfalls einfloat ist.

Eine ahnliche Logik wird bei Funktionen und Zeiger-typen angewendet:

float ff();

heißt, dassff() einfloat ist und daher mussff eineFunktion sein. Ebenso gilt bei Pointern

float * pf;

dass der Ausdruck* pf einenfloat ergibt und somitpf ein Zeiger auf einenfloat reprasentiert.

Diese Formen werden in Deklarationen genauso mit-einander kombiniert wie bei Ausdrucken:

float * g(), ( * h)();

besagt also, dass* g() und ( * h)() float -Ausdrucke sind. Da() gegenuber* den Vorrang hat(siehe Abschnitt 11.2.2), bedeutet* g() dasselbe wie* (g()) . Bei g handelt es sich also um eine Funktion,die einen Zeiger auf einenfloat zuruckliefert.h istein Zeiger auf eine Funktion, die einenfloat zuruck-gibt.

Will man den Datentyp einer Variablen angeben, sogeschieht das ganz einfach, indem man den Variablen-namen weglasst und das ganze in Klammern setzt. Inobigem Beispiel isth also vom Datentyp

(float ( * )())

Wir konnen nun den Ausdruck( * (void( * )())0) –welcher ein Unterprogramm aufruft, das an der Adres-se 0 gespeichert ist – in zwei Schritten analysieren.Zunachst gehen wir von einer Variablenfp aus, dieeinen Funktionszeiger enthalt. Dabei wollen wir dieFunktion aufrufen, auf diefp zeigt. Das laßt sich fol-gendermaßen durchfuhren:

( * fp)();

Wennfp ein Zeiger auf eine Funktion ist, dann ist* fpselbst eine Funktion, so dass sie mit( * fp)() aufgeru-fen werden kann. ANSI-C erlaubt eine Abkurzung mitfp() , dies ist jedoch nur eine abgekurzte Schreibwei-se!

Die Klammern um* fp im Ausdruck( * fp) sindunbedingt notwendig, da der Funktionsoperator()starker bindet als der unare Verweis-Operator* .

Der Zeiger auf die Funktion soll auf die Adresse0 zeigen. Leider funktioniert( * 0)(); nicht, da derOperator* als Operand eine Adresse erfordert. Des-halb mussen wir0 in einen Datentyp umwandeln, derals “Adresse einer Funktion, die den Wert0 liefert” be-schrieben werden kann.

Wenn fp ein Zeiger auf eine Funktion ist, die denDatentypvoid liefert, dann ergibt( * fp)() einenvoid -Wert und die Deklaration nimmt somit folgendeGestalt an:

void ( * fp)();

Wenn wir wissen, wie eine Variable deklariert wird, sowissen wir auch, wie man eine Konstante in diesen Da-tentyp umwandelt: wir brauchen bloß den Variablen-namen weglassen und dies alscast -Anweisung zwi-schen zwei Klammern schreiben. Wir wandeln also0in einen “Zeiger auf eine Funktion, die den Datentypvoid zuruckgibt” um

(void( * )())0

und konnen nunfp durch(void( * )())0 ersetzen:

( * (void( * )())0)();

Der Strichpunkt am Ende wandelt den Ausdruck in eineAnweisung um und sorgt somit dafur, dass das Unter-programm an der Adresse0 aufgerufen wird.

11.2.2 Rangfolge bei Operatoren

Operatoren haben unterschiedliche Prioritaten, dasheißt, einige haben einen hoheren Rang als andere undwerden als erstes abgearbeitet. Um ein Beispiel aus demersten Abschnitt noch einmal aufzugreifen betrachtenwir die folgende Bedingung:

if (x = y)...

Diese hatten wir umgeschrieben in

Page 110: Skript zum Kompaktkurs C mit Beispielen in OpenGL

110 KAPITEL 11. BELIEBTE FEHLER IN C

if ((x = y) != 0)...

Die Klammern um die Anweisungx = y sind notwen-dig, da!= Vorrang gegenuber dem Zuweisungsopera-tor = hat und somit als erstes interpretiert wird. Ohnedie Klammern ware der Ausdruck also gleichbedeutendmit

if (x = (y != 0))...

Dies liefert aber nur wenny gleich 1 ist, das genaugleiche Endergebnis wieif (x = y) ...

Die nachfolgende Tabelle gibt einenUberblickuber alle Operatoren und die Rangfolge, nach der sieinterpretiert werden:

Operator Assoz.( ) [ ] -> . links! ˜ ++ -- - ( Typ) * & sizeof rechts* / % links+ - links<< >> links< <= > >= links== != links& linksˆ links| links&& links|| links?: rechtsZuweisungen rechts, links

Die Operatoren, die am starksten gebunden werden,sind diejenigen, die eigentlich gar keine Operatorendarstellen: Indizierung, Funktionsaufruf und Struktur-auswahl. Diese sind alle links-assoziativ, das heißt,a.b.c bedeutet dasselbe wie(a.b).c und nichta.(b.c) .

In der weiteren Reihenfolge kommen die einstelligenOperatoren, die binaren, die arithmetischen und danndie Shift-Operatoren. Die relationalen, logischen undZuweisungsoperatoren folgen, sowie der Bedingungs-operator. Das Schlußlicht bildet der Kommaoperator, erwird meist als Ersatz fur den Strichpunkt verwendet,wenn ein Ausdruck anstelle einer Anweisung erforder-lich ist, zum Beispiel in Makrodefinitionen undfor -Schleifen-Initialisierung.

Hier noch einmal ein Beispiel fur einen Operator mitrechter Assoziativitat: der Zuweisungsoperator. Er wirdvon rechts nach links ausgewertet.

a = b = 0;

bedeutet somit dasselbe wie

b = 0;a = b;

Ist man sich unsicher uber die Rangfolge bzw. willman das Programm moglichst verstandlich schreiben,so empfiehlt sich immer eine entsprechende Klamme-rung der Ausdrucke.

11.2.3 Kleiner; große Wirkung

Ein uberflussiger Strichpunkt kann harmlos sein. Im be-sten Fall ist es eine Null-Anweisung, die nichts bewirkt,im schlimmsten Fall steht er hinter einerif , whileoderfor -Anweisung, nach der genau eine Anweisungstehen darf. Das Ergebnis ist dann das gleiche, wiewenn diese Anweisungen uberhaupt nicht da waren.

if (x > max);max = x;

bewirkt also nicht das gleiche wie das eigentlichgewunschte

if (x > max)max = x;

Auch das Vergessen eines Strichpunkts kann Fehlerbewirken, welche der Compiler unter Umstanden nichtbeanstandet. So sieht zum Beispiel dieser Programmab-schnitt

if (n < 3)return

x = n;

fur den Compiler aus wie

if (n < 3)return (x = n);

Manche Compiler bemerken, dass diese Funktion nunein Ergebnis vom Typint zuruckgibt, obwohlvoiderwartet wird. Ist der Ruckgabewert einer Funktion je-doch nicht explizit angegeben, so wird in der Regel kei-ne Warnung ausgegeben und der Ruckgabetypint an-genommen. Dies zeigt auch, wie wichtig es ist, immer

Page 111: Skript zum Kompaktkurs C mit Beispielen in OpenGL

11.2. SYNTAKTISCHE FALLSTRICKE 111

den Ruckgabetyp einer Funktion bei der Deklarationmit anzugeben. Fur den Falln >= 3 ist es außerdemoffensichtlich, dass das Codefragment etwas komplettanderes macht, als beabsichtigt war.

Auch bei der Deklaration von Strukturen kann einvergessener Strichpunkt unerwartete Folgen haben:

struct logrec {int date;int time;int code;

}

main(){

...}

Zwischen dem ersten} undmain fehlt ein Strichpunkt.Dies fuhrt zu der Annahme, dassmain den Datentypstruct logrec zuruckgibt stattint .

11.2.4 switch immer mit break

Bei der switch Anweisung sind diecase -Markenin C als echte Sprungmarken implementiert, so dassbeim Erreichen des Endes einescase -Abschnittesdas Programm ungehindert weiterlauft. Daher mussam Ende einescase -Abschnittes immer einebreak -Anweisung folgen! Das folgende Fragment zeigt einBeispiel fur eine korrekteswitch -Anweisung:

switch (farbe) {case 1: printf("rot");

break;case 2: printf("gelb");

break;case 3: printf("gruen");

break;}

Vergißt man hier jedoch diebreak ’s, und nimmt manan, farbe hatte nun den Wert 2, so wird man als Er-gebnis die Ausgabegelbgruen erhalten.

Es gibt jedoch auch durchaus Falle, in denen es sinn-voll ist, dasbreak wegzulassen. Will man zum Bei-spiel beim Einlesen einer Datei Leerraume ubersprin-gen, so konnte man das so realisieren:

switch (...) {case ’\n’: zeilenzahl++;case ’\t’:

case ’ ’:...

}

Bei Zeilenvorschuben wird hier noch zusatzlich derZahler fur die Zeilenanzahl inkrementiert.

11.2.5 Funktionsaufrufe

Bei Funktionen, die keine Argumente benotigen mussman dennoch eine leere Argumentenliste ubergeben:

f();

Die Angabe vonf; alleine bewirkt, dass die Adresseder Funktion ermittelt, aber nicht aufgerufen wird, waskeinen Sinn macht.

11.2.6 Wohin gehort das else ?

Wie bei vielen Programmiersprachen, gibt es auch in CProbleme bei verschachteltenif -else -Konstrukten:

if (x == 0)if (y == 0)

printf("Ursprung\n");else {

z = x + y;f(&z);

}

Mit einem Editor, der die Einruckung fur C vernunftigbehandelt, hatte man den Fehler vermutlich sofort be-merkt, denn er hatte den Code so formatiert:

if (x == 0)if (y == 0)

printf("Ursprung\n");else {

z = x + y;f(&z);

}

C ordnet einen else -Teil namlich immer demnachsten, nicht abgeschlossenenif innerhalb dessel-ben Blocks zu. Betrachtet man die ursprungliche For-matierung aus dem ersten Codeabschnitt, so wollte derProgrammierer wohl zwei Hauptfalle unterscheiden:x=0 undx 6=0. Dies kann man durch eine entsprechen-de Klammerung erreichen.

Page 112: Skript zum Kompaktkurs C mit Beispielen in OpenGL

112 KAPITEL 11. BELIEBTE FEHLER IN C

if (x == 0) {if (y == 0)

printf("Ursprung\n");}else {

z = x + y;f(&z);

}

Meist ist es besser ein paar Klammern zuviel zu set-zen, als sich darauf zu verlassen, dass man (insbeson-dere bei einzeiligenif -Bodies) die Verschachtelungenrichtig beachtet hat.

11.3 Semantische Fallstricke

Ein Satz kann von der Rechtschreibung her perfekt seinund eine tadellose Grammatik aufweisen, aber er kanntrotzdem einen zweideutigen und unbeabsichtigten In-halt haben. Dieses Kapitel beschaftigt sich mit solchenProblemen in C-Programmen

11.3.1 Zeiger und Arrays

Die Notation von Zeigern und Arrays ist in C untrenn-bar miteinander verbunden. Zwei Punkte sind hierbeibesonders wichtig:

1. C hat nur eindimensionale Arrays, deren Großemit einer Konstante zur Compilationszeit festgelegt seinmuss. Das Element eines Arrays kann jedoch ein Objektmit beliebigem Typ sein, darunter auch ein anderes Ar-ray, so dass es moglich ist, mehrdimensionale Arrays zusimulieren.

2. Mit einem Array kann man nur zwei Dinge anstel-len: die Große bestimmen und den Zeiger auf das Ele-ment0 des Arrays ermitteln. Alle anderen Operationenwerden in Wahrheit nur mit Zeigern durchgefuhrt.

Deklariert wird ein eindimensionales Array wiefolgt:

int a[3];

Es sind auch Arrays von Strukturen moglich:

struct {int p[4];double x;

} b[17];

Ein zweidimensionales Array lasst sich oberflachlichsehr leicht deklarieren:

int kalender[12][31];

Dies besagt, dasskalender ein Array von 12 Arraysmit jeweils 31int -Elementen ist (und kein Array von31 Arrays mit jeweils 12int -Elementen !). Alle Ope-rationen aufkalender werden in der Regel internin Pointeroperationen umgewandelt, mit Ausnahme dessizeof -Operators, der die Große des gesamten Arraysliefert, so wie man das auch erwartet, namlich 31·12·sizeof(int) .

Will man auf das i-te Element vona zugreifen, sokann man das so machen:

elem = a[i];

Die Arrayvariablea zeigt – wie im 2. Punkt angegeben– auf das erste Elementa[0] des Arrays.a liefert alsodas gleiche wie&a[0] . Intern wird eine Indizierungdementsprechend umgeschrieben:

a[i] → * (a + i)

Dies ist naturlich gleichbedeutend mit* (i + a) ,so dass man statta[i] auch i[a] schreiben kann,wovon man aber besser Abstand nehmen sollte.

Bei zweidimensionalen Arrays zeigt

p = kalender[4];

auf das Element0 des Arrayskalender[4] .Auf den “Integer des 8. Mai’s” kann man daher auf

verschiedene Arten zugreifen:

i = kalender[4][7];i = * (kalender[4] + 7);i = * ( * (kalender + 4) + 7);

11.3.2 Zeiger sind keine Arrays

Gehen wir davon aus, wir haben zwei Stringss und t ,welche wir zu einem String zusammensetzen wollen.Hierfur stehen uns die Bibliotheksfunktionenstrcpyundstrcat zur Verfugung. Eine einfacher Ansatz ist

char * r;strcpy(r, s);strcat(r, t);

welcher jedoch falsch ist und meist zum Absturz fuhrt.Der Grund dafur ist, dassr in undefiniertes Gebiet zeigtund außerdem an dieser Stelle auch kein Speicherplatzreserviert ist. Die Losung konnte sein, die Deklarationchar * r zu ersetzen durchchar r[100]; . Dies

Page 113: Skript zum Kompaktkurs C mit Beispielen in OpenGL

11.3. SEMANTISCHE FALLSTRICKE 113

setzt aber voraus, dass die Gesamtlange vons und timmer unter 99 Zeichen sein muss (1 Zeichen wird furdie Endekennung’\0’ benotigt).

Die beste Losung ist, sich mittelsmalloc entspre-chend viel Speicher vorher zu reservieren:

char * r, malloc();r = (char * )malloc(

strlen(s)+strlen(t)+1);

if (!r) {fehlermeldung();exit(1);

}strcpy(r, s);strcat(r, t);...free(r);

11.3.3 Array-Deklarationen als Parame-ter

Es gibt in C keine Moglichkeit, ein Array an eine Funk-tion zu ubergeben. Stattdessen wird immer nur der Zei-ger auf das Anfangselement des Zeigers ubergeben

int strlen(char s[]){

...}

ist also identisch mit

int strlen(char * s){

...}

Auch bei den beiden Argumenten vonmain sieht manhaufig zwei verschiedene Implementationen:

main(int argc, char * argv[]){

...}

beziehungsweise

main(int argc, char ** argv){

...}

Diese Regel gilt jedoch nicht immer, wie das folgendezeigt:

extern char * s;

ist nicht das selbe wie

extern char s[];

11.3.4 Die Synechdoche

Synechdoche =(Oxford English Dictionary)“Ein um-fassenderer Begriff fur einen weniger umfassenden oderumgekehrt; ein Ganzes fur einen Teil oder ein Teil alsGanzes; eine Gattung fur die Spezies oder eine Speziesals Gattung”.

Dies soll den haufigen Fehler beschreiben, dass einZeiger mit den adressierten Daten verwechselt wird:

char * p, * q;p = "xyz";q = p;

Mit den beiden letzen Anweisungen wird lediglich derZeiger auf das Array’x’ , ’y’ , ’z’ , ’\0’ in p bzw.q kopiert und nicht das gesamte Array.

11.3.5 NULL-Zeiger sind keine leerenStrings

Als einzige Integerkonstante wird der Wert 0 bei Bedarfautomatisch in einen entsprechenden Zeiger umgewan-delt, der sich von jedem zulassigen Zeiger unterschei-det. Er wird daher oft symbolisch wie folgt definiert:

#define NULL 0

Dieser NULL-Zeiger darf niemals dereferenziert wer-den, wie nachfolgend zu sehen:p sei ein NULL-Zeiger,das Ergebnis von

printf("%s",p);

ist dann nicht definiert und fuhrt in der Regel zum Pro-grammabsturz.

11.3.6 Zahler und asymmetrische Gren-zen

Ein C-Array mitn Elementen hat kein Element mit demIndexn ! Die Elemente werden vielmehr von0 bisn-1numeriert. Daher ist auch dieses Programm zum Initia-lisieren des Arraysa falsch:

Page 114: Skript zum Kompaktkurs C mit Beispielen in OpenGL

114 KAPITEL 11. BELIEBTE FEHLER IN C

int i, a[10];for (i = 0; i <= 10; i++)

a[i] = 0;

Um solche Fehler zu vermeiden definiert man am be-sten den Wertebereich eines Problems immer asyme-trisch:

0 <= i < 10

Die obere Grenze der Indizes entspricht dann derGroße des Arrays. Um dies zu verdeutlichen sollte mandaher das obige Programm immer wie folgt korrigieren:

int i, a[10];for (i = 0; i < 10; i++)

a[i] = 0;

und niemals

int i, a[10];for (i = 0; i <= 9; i++)

a[i] = 0;

11.3.7 Die Reihenfolge der Auswertung

Im Gegensatz zu der in Abschnitt 11.2.2 vorgestelltenRangfolge fur Operatoren, welche festlegt, dass zumBeispiela + b * c alsa + (b * c) interpretiertwird, garantiert die Reihenfolge der Auswertung, dasses in

if (n != 0 && z / n < 5.0)...

nicht zu einem Fehler “Division durch Null” kommt.Diese Rangfolge ist fur die vier C-Operatoren&&, || ,?: und, genau festgelegt, fur die anderen ist sie com-pilerabhangig.

&& und || werten zuerst den linken Operanden ausund dann den rechten, falls dies noch erforderlich ist.

Der Operator ?: nimmt drei Operandena ? b : c , wertet zuersta aus und dann ent-wederb oderc , je nachdem, welchen Werta hatte.

Der Komma-Operator, wertet seinen linken Ope-randen aus, verwirft das Ergebnis und wertet dann denrechten aus. Achtung: der Komma-Operator hat nichtsmit dem Komma in Funktionsaufrufen zu tun, bei de-nen die Reihenfolge der Argumentubergabe nicht defi-niert ist. So bedeutet auchf(x, y) etwas anderes alsg((x, y)) . Im letzten Beispiel erhaltg nur ein Ar-gument.

11.3.8 Die Operatoren&&, || und !

C hat zwei Klasse von logischen Operatoren, die nurin Sonderfallen ausgetauscht werden konnen: die Bit-Operatoren&, | und˜ , sowie die logischen Operatoren&&, || und! .

Die Bit-Operatoren behandeln ihre Operanden – wieder Name schon andeutet – bitweise. Zum Beispiel er-gibt 10 & 12 den Wert8 (= 1000 binar), da hier dieBinardarstellungen von10 (= 1010 ) und12 (= 1100 )“ver-und-et” werden.

Im Gegensatz dazu verarbeiten die logischen Opera-toren ihre Argumente so, als ob sie entweder “falsch”(gleich0) oder “wahr” (ungleich0) sind. Sie liefern alsErgebnis1 fur “wahr” bzw. 0 fur falsch zuruck. So lie-fert also10 && 12 den Wert1 und nicht8 wie derBit-Operator&. Außerdem werten die Operatoren&&und || unter Umstanden den zweiten Operanden erstgar nicht aus, wie schon im vorangegangenen Abschnitt11.3.7 erklart wurde.

11.3.9 Integeruberlauf

C hat zwei Arten von Integerarithmetik: vorzeichenbe-haftet (signed ) und vorzeichenlos (unsigned ). Beider letzteren gibt es keinenUberlauf, alle vorzeichenlo-sen Operationen werden modulo2n durchgefuhrt, wo-bei n die Anzahl der Bits im Resultat angibt. Bei ge-mischten Operanden werden zuerst alle Operanden invorzeichenlose umgewandelt, so dass auch hier keinUberlauf auftritt.

Ein Uberlauf kann jedoch auftreten, wenn beide Ope-randen vorzeichenbehaftet sind. Das Ergebnis einesUberlaufs ist dann nicht definiert! Daher sollte man ge-gebenenfalls vor der Operation auf die Moglichkeit ei-nesUberlaufs testen. Ein plumper Versuch ware (a undb seien zwei nicht-negativeint -Variablen) :

if (a + b < 0)fehler();

Dies funktioniert naturlich nicht. Sobalda+b einenUberlauf erzeugt hat, weiß man nicht, wie das Ergeb-nis lautet. Eine korrekte Methode ware, die Operandenvor der Abfrage in vorzeichenloseint -Werte umzu-wandeln:

if ((unsigned)a + (unsigned)b> INT_MAX)

fehler();

Page 115: Skript zum Kompaktkurs C mit Beispielen in OpenGL

11.4. DER PRAPROZESSOR 115

INT_MAX reprasentiert den großten darstellbaren Inte-gerwert und ist in der Include-Dateilimits.h defi-niert. Die elegantere Methode ist jedoch diese:

if ( a > INT_MAX - b )fehler();

11.3.10 Die Ruckgabe eines Ergebnissesvon main

Das einfachste mogliche C-Programm

main() {}

enthalt einen kleinen Fehler und wird von strikten Com-pilern auch mit einer Warnung bedacht. Vonmain wirdnamlich erwartet, dass es einenint -Wert zuruckgibt.Wird kein Wert zuruckgegeben, so ist dies meist harm-los, es wird dann in der Regel ein unbestimmter Inte-gerwert zuruckgegeben und solange man diesen Wertnicht verwendet spielt es keine Rolle. Jedoch ist es so,dass das Betriebssystem anhand des Ruckgabewertesentscheidet, ob das C-Programm fehlerfrei ausgefuhrtwurde. Es kann somit zu uberraschenden Ergebnissenkommen, wenn ein anderes Programm sich auf diesenFehlerstatus verlaßt. Ein korrektes C-Programm solltealso zumindest so aussehen:

main(){

return 0; / * oder: exit(0); * /}

11.4 Der Praprozessor

Vor der eigentlichen Compilation kommt noch ein Vor-verarbeitungsschritt: der Praprozessor. Praprozessor-kommandos beginnen immer mit einem#. Der Prapro-zessor ermoglicht es, wichtige Programmabschnitte ab-zukurzen. So ermoglicht er unter anderem die globaleDefinition von konstanten Großen, welche dann auchentsprechend einfach und schnell anzupassen sind. Erermoglicht es auch, einfache und geschwindigkeitskriti-sche Funktionen zu ersetzen. Der Aufwand beim Aufrufeiner Funktion ist namlich teilweise nicht unerheblich,so dass man unter Umstanden bestrebt sein wird, etwaszu haben, das so ahnlich aussieht wie eine Funktion,aber doch weniger Aufwand nach sich zieht. Zum Bei-spiel werdengetchar undputchar normalerweiseals Makros implementiert um zu vermeiden, dass bei

jeder Zeicheneingabe oder -ausgabe eine Funktion auf-gerufen werden muss.

Beim Einsatz von Makros sollte man eines nie ver-gessen: Sie ersetzen nur Text im Programm, ein Ma-kro wird vorher nicht auf seine korrekte Syntax gepruft.Man sollte sich daher immer uberlegen, wie das Ergeb-nis im Programm aussieht, nachdem das Makro einge-pflanzt wurde.

11.4.1 Leerzeichen und Makros

Bei der Definition ist zu beachten, dass zwischen demMakronamen und seinen Parametern – welche ubri-gens auch komplett wegfallen konnen, im Gegensatzzu Funktionen, wo zumindest eine Leere Menge()ubergeben werden muss – keine Leerzeichen auftretendurfen. Das folgende Beispiel verdeutlicht dies:

#define f (x) ((x) - 1)

Man weiß nun nicht, was dieser Ausdruck darstellt.Soll f(x) ersetzt werden durch((x) - 1) , oder sollf durch (x) ((x) - 1) ersetzt werden ? Da kei-ne Leerzeichen zwischen Name und Parametern erlaubtsind, wird sich der Praprozessor fur die zweite Moglich-keit entscheiden.

Fur Makroaufrufe gilt diese Regel allerdings nicht,so dassf(3) undf (3) beide den Wert 2 liefern.

11.4.2 Makros sind keine Funktionen

Zunachsteinmal sollte man darauf achten, dass man inMakros immer alle verwendeten Argumente einklam-mert. Um dies zu verdeutlichen betrachten wir diesefalsche Implementation der Absolutfunktion:

#define abs(x) x > 0 ? x : -x

Verwendet man im Programm nun den Ausdruck

abs(a - b)

so ergibt sich nach dem Durchlauf durch den Prapro-zessor

a - b > 0 ? a - b : -a - b

was fura - b ≤ 0 naturlich ein falsches Ergebnis lie-fert, denn dieses musste-(a - b) lauten und nicht-a - b . Bei Verwendung von Makros in großerenAusdrucken ergeben sich bei fehlender Klammerungzudem eventuell unerwunschte Effekte wegen der Ein-haltung von Rangfolgen. Daher sollte das obige Makrowie folgt geschrieben werden:

Page 116: Skript zum Kompaktkurs C mit Beispielen in OpenGL

116 KAPITEL 11. BELIEBTE FEHLER IN C

#define abs(x) (((x)>0)?(x):-(x))

Auch wenn Makrofunktionen voll geklammert sind,kann es trotzdem zu Problemen kommen:

#define max(a,b) ((a)>(b)?(a):(b))nmax = x[0];i = 1;while (i < n)

nmax=max(nmax, x[i++]);

Ware max als Funktion implementiert gabe es keineProbleme, als Makro kann es jedoch passieren, dassx[i++] zweimal ausgewertet wird, namlich immerdann, wennnmax kleiner ist. i wird somit insgesamtum 2 erhoht, statt nur – wie gewunscht – um1. Solchedoppelten Aufrufe in Makros sollte man – auch in Hin-blick auf den Mehraufwand – daher bei der Makrodefi-nition vermeiden, oder wenn dies nicht moglich ist, sosollte man das Makro mit der entsprechenden Vorsichtaufrufen:

#define max(a,b) ((a)>(b)?(a):(b))nmax = x[0];for (i = 1; i < n; i++)

nmax=max(nmax, x[i]);

11.4.3 Makros sind keine Anweisungen

Man ist verleitet, Makros so zu definieren, dass siewie Anweisungen verwendet werden konnen, aber diesist erstaunlich schwierig. Ein Beispiel ist das Makroassert (siehe:man assert ). Dieses konnte manzum Beispiel versuchen wie folgt zu realisieren:

#define assert(e) if (!e) \assert_err(__FILE__,__LINE__);

Setzt man dieses Makro jedoch innerhalb vonif -Schleifen ein, so kann es zu den selben Effekten kom-men, wie sie in Abschnitt 11.2.6 besprochen wurden.In diesem Fall wird dasassert -Makro unerwunschteAuswirkungen auf den Programm-Code haben:

if (x > 0 && y > 0)assert (x > y);

elseassert (y > x);

Leider hilft in diesem Fall auch eine entsprechendeKlammerung innerhalb des Makros nichts, wie hier ge-schehen:

#define assert(e) { if (!e) \assert_err(__FILE__,__LINE__);}

Setzt man dieses Makro in obiges Beispiel ein, so wirdman leicht sehen, dass sich zwischen der Klammer desMakros} und demelse noch ein Strichpunkt befin-det, was einen Syntaxfehler zur Folge hat. Eine richtigeMethode, umassert zu definieren ist zum Beispiel,das ganze als Ausdruck umzuschreiben:

#define assert(e) ((void)((e)|| \_assert_err(__FILE__,__LINE__)));

Hierbei wird die in Abschnitt 11.3.7 erwahnte Eigen-schaft des|| -Operators ausgenutzt.

11.4.4 Makros sind keine Typdefinitionen

Sehr haufig werden Makros dazu verwendet, um denDatentyp von mehreren Variablen an einer Stelle zudefinieren, so dass man diesen spater bequem andernkann:

#define FOOTYPE struct fooFOOTYPE a;FOOTYPE b, c;

Es ist jedoch besser, eine Typdefinition zu verwendenwie man an folgendem Beispiel leicht sieht:

#define T1 struct foo *typedef struct foo * T2;

T1 a, b;T2 c, d;

Obwohl die Definitionen von T1 und T2 auf den erstenBlick das gleiche bewirken, sieht man jedoch sofort denUnterschied, wenn man die erste Deklaration nach demDurchlauf durch den Praprozessor betrachtet:

struct foo * a, b;

b wird also nicht wie beabsichtigt als Zeiger auf eineStruktur definiert.

11.5 Tips&&Tricks

Man sollte niemals ein Programm schnell hinschreiben(am besten noch kurz vor dem Schlafengehen) um dannetwas zu erhalten, das scheinbar funktioniert. Wenn esdann namlich einmal erwartungsgemaß nicht das tut,

Page 117: Skript zum Kompaktkurs C mit Beispielen in OpenGL

11.5. TIPS&&TRICKS 117

was es sollte, so wird es meist schwierig, den ver-steckten Fehler aufzuspuren. Deshalb sollte man immergrundlich nachdenken, was man haben mochte und wieman es entsprechend vollstandig losen kann, bevor manbeginnt wilden Code zu produzieren.

Es sollen nun ein paar Tips vorgestellt werden, mitderen Hilfe man einige typischen Fehler, wenn nichtvermeiden, so doch zumindest minimieren kann.

11.5.1 Ratschlage

Absichten klarmachen

Bei Vergleichen mit Konstanten sollte man diese immerauf die linke Seite schreiben. So hatte der Fehler ausAbschnitt 11.1.1 sofort zu einer Fehlermeldung gefuhrt,da Konstanten kein Wert zugewiesen werden kann:

while (’ ’ = c || / * Fehler! * /’\t’ == c ||’\n’ == c )

c = getc(f);

Außerdem sollte man lieber zuviele Klammern setzen,als zuwenige. Auch dies kann zu einer besseren Erken-nung von unbeabsichtigten Fehlern durch den Compilerfuhren.

Auch einfache Falle testen

Viele Programme sturzen ab, wenn sie eine leere Ein-gabe oder nur ein Element erhalten. Daher sollten auchimmer die einfachen Falle berucksichtigt und uberpruftwerden.

Asymmetrische Grenzen

Man muss beachten, dass Arrays in C schon bei0 be-ginnen und somit auch ein Element fruher enden, alsman dies vielleicht vielleicht erwartet hatte. Siehe dazuauch Abschnitt 11.3.6.

Bugs verstecken sich in dunklen Ecken

Man sollte sich an die gut bekannten Konstrukte derSprache halten und nicht ein spezielles Feature einesbestimmten Compilers unbedingt verwenden wollen.Dies kann bei anderen Compilern oder auf anderen Ma-schinen zu – eventuell nicht sofort sichtbaren – Fehlern

fuhren. Man sollte sich auch nicht ohne weiteres dar-auf verlassen, dass Bibliotheksfunktionen sauber imple-mentiert sind. Diese Tips gelten im ubrigen auch unein-geschrankt fur OpenGL.

Defensiv programmieren

Ein Programm solltealle Falle abdecken, die auftretenkonnen. Ereignisse die nicht erwartet werden, aber pas-sieren konnen treten meist viel zu haufig ein. Ein robu-stes Programm deckt alle Falle ab und behandelt dieseauch entsprechend.

Page 118: Skript zum Kompaktkurs C mit Beispielen in OpenGL

118 KAPITEL 11. BELIEBTE FEHLER IN C

Page 119: Skript zum Kompaktkurs C mit Beispielen in OpenGL

Kapitel 12

Ausblick auf dreidimensionales OpenGL

Die Programmierung von dreidimensionalen Graphi-ken ist im Vergleich zu zweidimensionalen Graphikenmeistens deutlich aufwendiger; vor allem wegen der(eventuell perspektivischen) Projektion auf einen zwei-dimensionalen Bildschirm, der Berechnung von Be-leuchtungseffekten und der komplizierteren geometri-schen Transformationen in drei Dimensionen. Hier einkleines Beispiel fur eine dreidimensionale Graphik inOpenGL:

#include <stdio.h>#include <stdlib.h>#include <math.h>#include <GL/glut.h>

void malen(void);

int main(int argc, char ** argv){

GLfloat light0_pos[] ={100., 100., 100., 1.0};

GLfloat light0_color[] ={1., 0., 1., 1.0};

GLfloat ambient_light[] ={0.5, 0.5, 0.5, 1.0};

glutInit(&argc, argv);glutInitDisplayMode(GLUT_RGB |

GLUT_DEPTH);glutInitWindowSize(400, 300);glutInitWindowPosition(100, 100);glutCreateWindow("Hallo!");glClearColor(0.0, 0.0, 0.0, 0.0);

glLightModelfv(GL_LIGHT_MODEL_AMBIENT,ambient_light);

glLightfv(GL_LIGHT0, GL_POSITION,light0_pos);

glLightfv(GL_LIGHT0, GL_DIFFUSE,light0_color);

glEnable(GL_LIGHTING);glEnable(GL_LIGHT0);

glEnable(GL_DEPTH_TEST);

glMatrixMode(GL_PROJECTION);glLoadIdentity();glFrustum(-2., 2., -1.5, 1.5,

1., 20.);glutDisplayFunc(&malen);glutMainLoop();return(0);

}

void malen(void){

glClear(GL_COLOR_BUFFER_BIT |GL_DEPTH_BUFFER_BIT);

glMatrixMode(GL_MODELVIEW);glLoadIdentity();glTranslatef(0., 0., -5.);glRotatef(-30., 0.0, 1.0, 0.0);glutSolidTeapot(2.);glFlush();

}

Eine wichtigeAnderung ist der Depth-Buffer, mit demVerdeckungsberechnungen sehr effizient durchgefuhrtwerden konnen. Mit

glutInitDisplayMode(GLUT_RGB |GLUT_DEPTH);

wird ein Depth-Buffer angefordert, und mit

119

Page 120: Skript zum Kompaktkurs C mit Beispielen in OpenGL

120 KAPITEL 12. AUSBLICK AUF DREIDIMENSIONALES OPENGL

glEnable(GL_DEPTH_TEST);

fur Verdeckungstests aktiviert. Inmalen() muss nunauch der Depth-Buffer geloscht werden:

glClear(GL_COLOR_BUFFER_BIT |GL_DEPTH_BUFFER_BIT);

Beleuchtungsberechnungen werden mit

glEnable(GL_LIGHTING);

aktiviert. Fur die Beleuchtung wird zum einen mit

glLightModelfv(GL_LIGHT_MODEL_AMBIENT,ambient_light);

ein allgemeines (ambientes) Licht eingeschaltet undzum anderen mit

glLightfv(GL_LIGHT0, GL_POSITION,light0_pos);

glLightfv(GL_LIGHT0, GL_DIFFUSE,light0_color);

glEnable(GL_LIGHT0);

eine Lichtquelle an die Positionlight0 pos gesetzt,die in der Farbelight0 color scheint.

Die perspektivische Projektion wird mit

glMatrixMode(GL_PROJECTION);glLoadIdentity();glFrustum(-2., 2., -1.5, 1.5,

1., 20.);

definiert. Damit wird ein Sichtfeld definiert, das inz-Richtung von−1 bis−20 reicht, und (in derz = −1-Ebene) inx-Richtung von−2 bis+2 und iny-Richtungvon −1.5 bis 1.5. Wichtig ist dabei vor allem, dassdie Standard-Blickrichtung in OpenGL die negativez-Achse ist.

In der Funktionmalen() wird schließlich mit derFunktion

glutSolidTeapot(2.);

eine Teekanne aus OpenGL-Polygonen gemalt, derenGroße mit einem Parameter eingestellt werden kann.Diese Teekanne wird mit

glRotatef(-30., 0., 1., 0.);

um 30 Grad um diey-Achse rotiert und mit

glTranslatef(0., 0., -5.);

entlang der negativenz-Achse verschoben.Wem dieser Ausblick nicht reicht, der findet im

”OpenGL Programming Guide“ (siehe Literaturli-

ste) noch wesentlich mehr Details zur OpenGL-Programmierung.

Page 121: Skript zum Kompaktkurs C mit Beispielen in OpenGL

121

Ubungen zum funften Tag

In den folgenden Aufgabe soll mit Hilfe der von GLUTzur Verfugung gestellten Funktionalitaten eine kleineinteraktive 3D-Anwendung realisiert werden.Die Auf-gaben bauen dabei auf dem Beispielprogramm aus Ka-pitel 12 auf.

Aufgabe V.1

Erweitern Sie das Beispielprogramm aus Kapitel 12 umeine einfache Maussteuerung, mit der die drei Rotati-onswinkelφx. φy undφz um die drei Koordinatenach-sen variiert werden konnen. Die drei Rotationen sollenimmer in der folgenden Reihenfolge durchgefuhrt wer-den: zuerst umφy um die y-Achse, dann umφx umdie x-Achse und schließlich umφz um die z-Achse. ImFolgenden ist genauer erlautert, wie die Mausbewegungumgesetzt werden soll:

Bei Betatigung der linken Maustaste soll die Mausbe-wegung in x-Richtung in eineAnderung vonφy und diein y-Richtung in eineAnderung vonφx umgesetzt wer-den. Bei zusatzlicher Betatigung der SHIFT-Taste sollnur die Mausbewegung in x-Richtung, bei Betatigungder ALT-Taste nur die Mausbewegung in y-Richtungberucksichtigt werden. Bei Betatigung der mittlerenMaustaste soll die Mausbewegung in x-Richtung in ei-neAnderung vonφz umgesetzt werden. Die Bewegungin y-Richtung wird ignoriert.

Ausgabe V.2

In dieser Aufgabe soll der KeyboardFunc-Callbackimplementiert werden um verschiedene Einstellungenuber Tastendruck zu ermoglichen. Realisiern Sie fol-gende Funktionalitaten:

1. Uber eine Taste soll zwischen solider undGitternetz-Darstellung des Teekessels gewechseltwerden konnen.

2. Eine weitere Taste soll das Ein- bzw. Ausschaltender Beleuchtung ermoglichen.

3. Mit einer weiteren Taste, soll die Position des Ob-jekts auf den Ausgangzustand zuruckgesetzt wer-den.

4. Durch gleichzeitiges Betatigen von CTRL und Qsoll das Programm beendet werden.

5. Uber eine geeignete Taste (z.B.h) soll auf derStandardausgabe eine Hilfe zur obigen Tastenbe-legung ausgegeben werden.

Ausgabe V.3

Erweitern Sie ihr Programm um die Moglichkeit einherAnimation des Teekessels, bei der der Teekessel fort-laufend um die y-Achse rotiert wird. Die Animation solldabei uber eine geeignete Taste gestartet bzw. gestopptwerden konnen. Zusatzlich soll uber die PfeiltastenUP und DOWN, die Rotationsgeschwindigkeit erhohtbzw. gesenkt werden konnen. Außerdem soll mit einerweiteren Taste die Rotationsrichtung geandert werdenkonnen. Nehmen Sie diese Animationssteuerung in dieHilfebeschreibung aus Aufgabe V.2 auf.

Ausgabe V.3

Fugen Sie zu Ihrem Programm ein Menu hinzu. Die-ses Menu soll fur jedes geometrische Objekt, dass vonder GLUT-Bibliothek zur Verfugung gestellt wird, al-so Teekessel, Kugel, usw. (s. Kapitel 8.7), einen Ein-trag enthalten. Beim Anklicken eines Menueintrags solldie Darstellung des bisherigen Objektes durch die desneu ausgewahlten ersetzt werden. Die sonstigen Ein-stellmoglichkeiten aus den vorangegangenen Aufgabensollen fur alle auswahlbaren Objekte weiterhin funktio-nieren.

Fugen Sie außerdem in einer Ecke des OpenGL-Fensters eine Textausgabe des aktuellen Systemzu-stands ein. Also z.B. welches Objekt dargestellt wird,ob die Beleuchtung eingeschaltet ist, ob die Animationlauft, ...

Page 122: Skript zum Kompaktkurs C mit Beispielen in OpenGL

122 KAPITEL 12. AUSBLICK AUF DREIDIMENSIONALES OPENGL

Literatur

• Brian W. Kernighan, Dennis M. Ritchie,The CProgramming Language, Prentice Hall, 1977.

• Andrew Koenig,C Traps and Pitfalls, Addison-Wesley, 1989.

• David Straker,C Style : Standards and Guidelines,Prentice Hall, 1992.

• Mason Woo, Jackie Neider, Tom David, DaveShriner, Tom Davis,OpenGL 1.2 ProgrammingGuide, 3rd ed., Addison Wesley, 1999.