FT DR A Notizen zum C-Kurs am Joint College in Shanghai V 0.2 Prof. Dr. Bernd Kahlbrandt 26. Februar 2014 FT DR A Alle in diesem Dokument enthaltenen Informationen, Verfahren und Darstellungen wurden nach bestem Wissen zusammengestellt und sorgfältig überprüft. Dennoch sind Fehler nicht auszuschließen. Aus diesem Grund sind die im vorliegenden Dokument enthaltenen Informationen mit keiner Verpflichtung oder Garantie irgendeiner Art verbunden. Weder der Autor noch die Hochschule übernehmen infolgedessen irgendwelche juristische Verantwortung und werden keine daraus folgende oder sonstige Haftung übernehmen, die auf irgendeine Art aus der Benutzung dieser Informationen oder Teilen davon ensteht. c 2004–2005 Bernd Kahlbrandt FT Inhaltsverzeichnis 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1 2 3 6 6 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 7 7 7 7 10 11 13 15 18 19 20 20 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 23 23 23 27 32 33 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35 35 35 35 38 39 40 41 42 42 44 2 3 4 Einführung 1.1 Übersicht . . . . . . . . . . . . . . . . . . . . . . 1.2 Lernziele . . . . . . . . . . . . . . . . . . . . . . 1.3 C Programme schreiben, umwandeln und ausführen 1.4 Historische Anmerkungen . . . . . . . . . . . . . 1.5 Aufgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . Grundlagen 2.1 Übersicht . . . . . . . . . . . . 2.2 Lernziele . . . . . . . . . . . . 2.3 Namen . . . . . . . . . . . . . . 2.4 Struktur von C-Programmen . . 2.5 Datentypen . . . . . . . . . . . 2.6 Funktionen . . . . . . . . . . . 2.7 printf und putchar . . . . . . . . 2.8 scanf und getchar . . . . . . . . 2.9 Umlenken von Ein- und Ausgabe 2.10 Trigraphs . . . . . . . . . . . . 2.11 Historische Anmerkungen . . . 2.12 Aufgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Formattierte Ein- und Ausgabe 3.1 Übersicht . . . . . . . . . 3.2 Lernziele . . . . . . . . . 3.3 Ausgabe . . . . . . . . . . 3.4 Eingabe . . . . . . . . . . 3.5 Datentypenkonvertierung . 3.6 Aufgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Operatoren 4.1 Übersicht . . . . . . . . . . . . . 4.2 Lernziele . . . . . . . . . . . . . 4.3 Rechenoperatoren . . . . . . . . . 4.4 Zusammengesetzte Zuweisungen . 4.5 Vergleichoperatoren . . . . . . . . 4.6 Logische Operatoren . . . . . . . 4.7 Der ternäre Operator ?: . . . . . . 4.8 Referenzieren und Dereferenzieren 4.9 Bit-Manipulationen . . . . . . . . 4.10 Aufgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . iv DR A Abbildungsverzeichnis . . . . . . . . . . . . . . . . . . i . . . . . . . . . . . . . . . INHALTSVERZEICHNIS ii 6 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45 45 45 45 51 53 54 54 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57 57 57 57 59 59 59 61 62 Funktionen 7.1 Übersicht . . . . . . . . . . . . . . 7.2 Lernziele . . . . . . . . . . . . . . 7.3 Modularisierung in C . . . . . . . . 7.4 Wert- und Referenzsemantik . . . . 7.5 Rekursive Funktionen . . . . . . . . 7.6 Einige wichtige Standardfunktionen 7.6.1 Funktionen mit Strings . . . 7.6.2 Funktionen für Zeichen . . . 7.6.3 Mathematische Funktionen . 7.6.4 Datum und Uhrzeit . . . . . 7.6.5 Hilfsfunktionen . . . . . . . 7.7 Aufruf-Parameter . . . . . . . . . . 7.8 Historische Amerkungen . . . . . . 7.9 Aufgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63 63 63 63 67 69 71 71 71 71 72 72 72 73 73 Der C Präprozessor 8.1 Übersicht . . . . . . . . . . . . . . . 8.2 Lernziele . . . . . . . . . . . . . . . 8.3 Einbinden von Code . . . . . . . . . 8.4 Definieren von Eigenschaften . . . . . 8.5 Bedingte Befehle bzw. Verzweigungen 8.6 Einige Tücken . . . . . . . . . . . . . 8.7 Historische Anmerkungen . . . . . . 8.8 Aufgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75 75 75 75 75 77 77 78 78 Zeiger (Pointer) 9.1 Übersicht . . . . . . . . . 9.2 Lernziele . . . . . . . . . 9.3 Definition von Zeigern . . 9.4 Zeigerarithmetik . . . . . 9.5 Historische Anmerkungen 9.6 Aufgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79 79 79 79 81 82 82 . . . . . . . Vektoren und Strings 6.1 Übersicht . . . . . . . . . . 6.2 Lernziele . . . . . . . . . . 6.3 Definition von Vektoren . . . 6.4 Initialisierung von Vektoren 6.5 Strings . . . . . . . . . . . . 6.6 Vektoren und Adressen . . . 6.7 Mehrdimensionale Vektoren 6.8 Aufgaben . . . . . . . . . . DR A 7 Kontrollstrukturen 5.1 Übersicht . . . . . . . . . 5.2 Lernziele . . . . . . . . . 5.3 Schleifen . . . . . . . . . 5.4 Verzweigungen . . . . . . 5.5 Sprünge . . . . . . . . . . 5.6 Historische Anmerkungen 5.7 Aufgaben . . . . . . . . . FT 5 8 9 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . INHALTSVERZEICHNIS iii . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83 83 83 83 85 86 89 89 11 Umwandlung von Datentypen 11.1 Übersicht . . . . . . . . . 11.2 Lernziele . . . . . . . . . 11.3 . . . . . . . . . . . . . . 11.4 Historische Anmerkungen 11.5 Aufgaben . . . . . . . . . . . . . . 12 Bit-Manipulationen 12.1 Übersicht . . . . . . . . . 12.2 Lernziele . . . . . . . . . 12.3 . . . . . . . . . . . . . . 12.4 Historische Anmerkungen 12.5 Aufgaben . . . . . . . . . . . . . . FT 10 Strukturierte Datentypen 10.1 Übersicht . . . . . . . . . . . . . . . . . . . . . 10.2 Lernziele . . . . . . . . . . . . . . . . . . . . . 10.3 Deklaration strukturierter Datentypen . . . . . . 10.4 Verwendung von strukturierten Datentypen . . . 10.5 Strukturierte Datentypen, Funktionen und Zeiger 10.6 Historische Anmerkungen . . . . . . . . . . . . 10.7 Aufgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91 91 91 91 91 91 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93 93 93 93 93 93 13 Dynamische Speicherplatzververwaltung 13.1 Übersicht . . . . . . . . . . . . . . . 13.2 Lernziele . . . . . . . . . . . . . . . 13.3 Verschiedene Arten von Speicher . . . 13.4 Speicherreservierung und Freigabe . . 13.5 Historische Anmerkungen . . . . . . 13.6 Aufgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95 . 95 . 95 . 95 . 98 . 100 . 100 14 Dateizugriff 14.1 Übersicht . . . . . . . . . . 14.2 Lernziele . . . . . . . . . . 14.3 Streams . . . . . . . . . . . 14.4 Elementare Dateioperationen 14.5 . . . . . . . . . . . . . . . 14.6 Historische Anmerkungen . 14.7 Aufgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101 101 101 101 101 101 101 101 15 Anwendungen: Listen 15.1 Übersicht . . . . . . . . . 15.2 Lernziele . . . . . . . . . 15.3 . . . . . . . . . . . . . . 15.4 Historische Anmerkungen 15.5 Aufgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103 103 103 103 103 103 A Code Konventionen für C A.1 Übersicht . . . . . . . . . A.2 Lernziele . . . . . . . . . A.3 Programmstruktur . . . . . A.4 Namen . . . . . . . . . . . A.5 . . . . . . . . . . . . . . A.6 Historische Anmerkungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105 105 105 105 105 105 105 DR A . . . . . INHALTSVERZEICHNIS iv FT B Aufgaben 107 B.1 Rechner . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107 C Lösungsvorschläge 109 C.1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111 111 111 111 111 112 112 112 113 113 114 115 115 117 117 117 117 118 118 118 119 119 120 121 122 122 123 123 124 124 124 124 126 127 E IOCCC E.1 1984 . . . . . . . . . . . . E.1.1 anonymous . . . . E.1.2 decot . . . . . . . E.1.3 Laman . . . . . . E.1.4 mullender . . . . . E.2 2001 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129 129 129 131 131 132 133 DR A D Lösungsvorschläge für ausgewählte Aufgaben D.1 Kapitel 1 . . . . . . . . . . . . . . . . . . . . . . . . . D.1.1 tunix.c . . . . . . . . . . . . . . . . . . . . . D.2 Kapitel 2 . . . . . . . . . . . . . . . . . . . . . . . . . D.2.1 Namensdeklarationen . . . . . . . . . . . . . . D.2.2 signed, unsigned char . . . . . . . . . . . . . . D.2.3 int bzw. unsigned int . . . . . . . . . . D.2.4 Fehler in Summenfunktion . . . . . . . . . . . D.2.5 happybirthday1 verb+>+ berndbirth.out . . . . D.2.6 printf mit ungültigen Esacpe-Sequenzen . . . . D.2.7 Fahrenheit - Celsius . . . . . . . . . . . . . . D.2.8 Body-Mass-Index . . . . . . . . . . . . . . . . D.2.9 Deklarationen und Definitionen . . . . . . . . D.2.10 Ceiling und Floor . . . . . . . . . . . . . . . . D.3 Kapitel 3 . . . . . . . . . . . . . . . . . . . . . . . . . D.3.1 Falscheingaben bei formio11.c . . . . . . . . . D.3.2 Typkonvertierung char to int und zurück . . . . D.3.3 . . . . . . . . . . . . . . . . . . . . . . . . . D.3.4 Dreistellige Zahl in unterschiedlichen Formaten D.4 Artikel, Stückzahl, Preis, Gesamtpreis . . . . . . . . . D.5 Kapitel 4 . . . . . . . . . . . . . . . . . . . . . . . . . D.5.1 Zusammengesetzte Zuweisungen . . . . . . . D.5.2 Tabellen 4.5–4.9 . . . . . . . . . . . . . . . . D.5.3 Speicherbedarf der elementaren Datentypen . . D.6 Kapitel 5 . . . . . . . . . . . . . . . . . . . . . . . . . D.6.1 Matrix der ASCII-Zeichen . . . . . . . . . . . D.6.2 BMI mit erläuternden Ausgaben . . . . . . . . D.6.3 for Schleife mit Präfix-Inkrementoperator . . . D.7 Kapitel 6 . . . . . . . . . . . . . . . . . . . . . . . . . D.8 Kapitel 7 . . . . . . . . . . . . . . . . . . . . . . . . . D.8.1 stringcopy . . . . . . . . . . . . . . . . . . . . D.8.2 Palindromerkennung . . . . . . . . . . . . . . D.8.3 Textverschlüsselung . . . . . . . . . . . . . . D.8.4 Body-Mass Index . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . F Development Environments 135 F.1 Microsoft Visual Studio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135 F.2 Erläuterungen zu Fehlermeldungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135 Index 138 FT Abbildungsverzeichnis 2.1 2.2 2.3 2.4 2.5 2.6 Namensbildung in C . . . . . . . . . . . . . . . limits.h für VC++ .NET 2003 . . . . . . . . . . . C Datentypen, Teil 1: Ganze Zahlen und Zeichen C Datentypen, Teil 2: Fließkommazahlen . . . . Escape- und Format-Zeichen in ANSI C . . . . . Variablen und Adressen in C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 9 10 10 14 16 3.1 3.2 3.3 3.4 3.5 Escape-Sequenzen in ANSI C . . . . . . . . Ausgabe von Zeichen und ganzen Zahlen . . Ausgabe von Gleitkommazahlen und Strings . Datentyp double nach IEEE (64 Bit) . . . . . Datentypen für Gleitkommazahlen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24 25 26 28 29 4.1 4.2 4.3 4.4 4.5 4.6 4.7 4.8 4.9 4.10 4.11 4.12 4.13 4.14 4.15 Binäre arithmetische Operatoren . . . . . . . . . . . . . . . Typkonvertierungsregeln für binäre arithmetische Operatoren Unäre arithmetische Operatoren . . . . . . . . . . . . . . . Zusammengesetzte Zuweisungen . . . . . . . . . . . . . . . Vergleichsoperatoren . . . . . . . . . . . . . . . . . . . . . Logische Operatoren . . . . . . . . . . . . . . . . . . . . . Wahrheitstafel für logische Operatoren Bool . . . . . . . . . Wahrheitswerte Boolesch und in C . . . . . . . . . . . . . . Wahrheitstafel für logische Operatoren im C-Stil . . . . . . Unäre boolesche Operatoren . . . . . . . . . . . . . . . . . Der Auswahloperator ?: . . . . . . . . . . . . . . . . . . . . Bit-Operatoren . . . . . . . . . . . . . . . . . . . . . . . . Wirkungsweise der Bit-Operatoren . . . . . . . . . . . . . . Bit-Masken . . . . . . . . . . . . . . . . . . . . . . . . . . Bit-Manipulationen, Beispiele . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36 36 37 39 39 40 40 40 40 40 41 42 42 43 43 5.1 5.2 5.3 5.4 5.5 While und do while-Schleife for-Schleife . . . . . . . . if-else-Konstrukt . . . . . . Geschachtelte if-Konstrukte switch-Konstrukt . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46 48 51 51 52 8.1 #include . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75 13.1 Speicherstruktur eines C-Programms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96 . . . . . DR A . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . v . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ABBILDUNGSVERZEICHNIS vi 1. VC etwas ausarbeiten deferred 2. Index überprüfen. skipped FT Todo 3. Kapitel Bitmanipulationen in einer neuen Auflage wieder einblenden. 4. Kapitel Umwandlung von Datentypen in einer neuen Auflage wieder einblenden. 5. In jedem Kapitel nochmal historische Anmerkungen durchsehen, ggf. einiges schreiben oder Abschn. streichen. 6. Analogie Camino de Santiago 2001 und C-Kurs ausarbeiten. Ein bischen wie in [Blo71]. 7. IOCCC wieder einblenden. 8. << MUSS rein! put to -Operator. Done 1. αnachV ersionentf ernen DR A 2. Badness in Lösungsvorschlägen beseitigen. 3. Entscheiden, ob Lösungsvorschläge gleich mit rausgeben! 4. Lösungsvorschlag Prüfziffer streichen 5. Kapitel Dateizugriff ausarbeiten oder streichen. 6. Kapitel Dynamische Speicherverwaltung weiter schreiben. 7. Kapitel Sort ausarbeiten bzw. kürzen (Java raus). 8. Glossar Reihenfolge überprüfen (nur beim Einfügen kursorisch) 9. Glossar auffüllen (bis j erledigt.) 10. Kapitel Dateizugriff über Streams und Elementare Dateioperationen zusammenfassen. 11. Kapitel Zur Auflockerung etc. ausblenden, eventuell Teile drin lassen. 12. Kapitel Pointer ausarbeiten. 13. Kapitel strukturierte Datentypen schreiben. 14. IOCCC ausblenden. Höchstens zum Schluss mal zeigen. 15. gcc streichen 16. make streichen 17. Anwendungen Binärbäume ausblenden. 18. Anwendungen Listen ausblenden. 19. Kapitel Bitmanipulationen ausblenden 20. Kapitel Umwandlung von Datentypen ausblenden 21. Übungsaufgaben mit Lösungen. Es sind inzwischen viele drin. ABBILDUNGSVERZEICHNIS vii FT 22. Entscheiden ob es in Text- und oder Index „Addition“ oder „Additionsoperator“ heißen soll etc., mit oder ohne Bindestrich. 23. Lösungsvorschläge für die bereits formulierten Aufgaben ausarbeiten. 24. Gliederung ausarbeiten 25. Für jede neu formulierte Aufgabe gleich einen Lösungsvorschlag schreiben. (Seit diesem Eintrag fast durchgehalten, nur nicht bei einige Fleißaufgaben). 26. Einführung gestalten 27. Comics für Shanghai einscannen 28. putchar und getchar in den Abschn. 2.7 bzw. 2.8 erläutern, damit man früher mehr sinnvolles machen kann. 29. Abb. 2.6 auf angemessenes Format bringen. 30. Herausfinden, warum die Ausgabe in formio10.c nicht rechtsbündig erscheint. Die Antwort ist ganz einfach: Bündige Ausgabe erfolgt immer im Rahmen der DR A Feldbreite. Wird keine angegeben, so ist dies die Länge des Feldes. Im Beispiel wurde als auf jeder Zeile das Feld rechtsbündig entsprechend seiner Länge ausgegeben. Bei Eingabe von 1, 11, 111 also 1 rechtsbüdig in einem Feld der Länge 1, 11 in einem Feld der Länge 2 usw. Daher die linksbündige Ausgabe. Die Lösung ist also ganz einfach: Überall die gleiche Feldbreite angeben. 31. Email an Wang Nan ([email protected]) mit aktueller Literaturliste. 32. look up, how to get & etc. displaid correctly in index: Solution: put = (in german index style) or @ resp. at every level where it’s needed. ABBILDUNGSVERZEICHNIS viii Vorlesungsplan Soll Inhalt 27.02.2014 02 28.02.2014 03 26.08.2004 04 31.08.2004 05 31.08.2004 02.09.2004 02.09.2004 06 07 08 07.09.2004 09 07.09.2004 10 Kontrollstrukturen 09.09.2004 11 Vektoren und Strings 09.09.2004 12 Vektoren und Strings 14.09.2004 13 Vektoren und Strings 14.09.2004 14 Vektoren und Strings 16.09.2004 15 Funktionen 16.09.2004 16 Funktionen 21.09.2004 21.09.2004 23.09.2004 23.09.2004 17 18 19 20 28.09.2004 28.09.2004 30.09.2004 30.09.2004 10.10.2004 21 22 23 24 25 Funktionen Präprozessor Zeiger Zeiger Test 2 Zeiger Zeiger Zeiger Dynamische Speicherplatzverwaltung 10.10.2004 12.10.2004 12.10.2004 26 27 28 FT 25.02.2014 Doppel stunde 01 Vorstellung, Organisatorisches, Einführung Kapitel 1, Folien Intro Grundlagen: Struktur von C-Programmen Datentypen, Funktionen Abschn. 2.4–2.6, Folien: Wiederholung, Klärung summenfunktion printf, scanf, Abschn. 2.7- 2.8, Folien: 02, Nr. 6 Umlenken von Ein- und Ausgabe 2.9 Wiederholung, Fragen, Diskussion Informationen zum Praktikum Formattierte Ein- und Ausgabe Kap. 3, Folien: Operatoren Operatoren Test 1 Kontrollstrukturen DR A Datum Test 5 Dynamische Speicherplatzverwaltung Dateizugriff Dateizugriff ABBILDUNGSVERZEICHNIS ix Datum Vorlesungsplan Ist Termin 25.02.2014 26.08.2004 31.08.2004 02.09.2004 07.09.2004 09.09.2004 Dateizugriff Dateizugriff Klausur Inhalt Kap 1, Abschn. 2.1–2.5 Bis Folie 27, S. 8 Handout Kap. 2.7–2.9 Kap. 3 Operatoren Kontrollstrukturen bis auf if/switch Test 1 Besprechung des Tests Grundlagen Rechner if/switch Vektoren und Strings, Anfang Besprechung des Tests Vektoren und String Besprechung Praktikum, FUnktionen Funktionen, Wiederholung, Rekursion, main Präprozessor Test 2 Besprechung des Tests, Teil 1 Pointer, Anfang Besprechung des Praktikums Noch etwas zum Praktikum, Hausaufgaben Zeiger und Zeiger auf Funktionen Sieb des Eratosthenes, Neue Hausaufgaben Besprechung Hausaufgaben Insertion Sort und Funktionszeiger Wiederholung Wiederholung, Erläuterung von deutschen Worten in typischen Aufgaben. Klausur DR A 09.09.2004 14.09.2004 16.09.2004 21.09.2004 Inhalt FT 14.10.2004 14.10.2004 19.10.2004 Doppel stunde 29 30 23.09.2004 28.09.2004 30.09.2004 10.10.2004 12.10.2004 14.10.2004 ABBILDUNGSVERZEICHNIS DR A FT x FT Vorwort DR A Dieses Skript entstand für die Teilnehmer der Vorlesung Programmierung I am Joint College der USST und der HAW in Shanghai im Short Semester vom 23.08.-22.10.2004. Es ist Version 0.1 und wird so noch mehr Fehler jeder Art enthalten, als mir lieb sein kann. Allerdings sind alle Programmbeispiele ausprobiert und mit insert file in die LATEX-Source übernommen worden. Aber auch dabei sind natürlich Fehler möglich. Um das Skript möglichst schnell fertig zu bekommen, habe ich auf einige Feinarbeiten verzichtet. Es gibt also Seiten (z. B. . Seite 96) die zu leer sind, an einigen Stellen wird nach oben oder unten um mehr als 9pt oder nach rechts und linke um mehr als 6pt über den Satzspiegel hinausgeschrieben. Der Stil ist persönlich gehalten und sicher nicht immer ganz sprödes Schriftdeutsch. Dieses Dokument wäre ohne die aktive Mitwirkung der Studierenden im verkürzten Semester im Herbst 2004 an der USST nicht möglich gewesen. Da diese Studierenden nicht nur „gegen“ den Stoff kämpfen, sondern auch gegen die Deutsche Sprache, habe ich begonnen, dieses Skript zu schreiben. Da es schon hunderte oder tausende Bücher über C gibt, muss man fragen, ob sich dieser Aufwand „lohnt“! Aber angesichts der Schwierigkeiten mit denen die Studierenden hier zu kämpfen haben, und angesichts der Resonanz scheint es sich zu lohnen. Viele Studierende haben durch ihre konstruktive Kritik zur Verbesserung dieses Dokuments beigetragen. Ohne diese aktive Rückkopplung hätte dieser Kurs nicht erfolgreich werden können. Ich bedanke mich bei den Teilnehmern dieses Kurses für die konstruktive Zusammenarbeit und die Beiträge, die sie zu diesem Kurs und zu diesem Skript geliefert haben. Die Anregungen werden in die nächste Auflage eingehen. Wenn die Qualität der Beiträge und Anregungen auch unterschiedlich war, so war doch jede Äußerung eine für den Dozenten nützliche Rückkopplung. Beiträge haben die folgenden Teilnehmer geleistet: Cao Bin, Chen Hao, Chen Yingjie, Dou Xiao, Du Hongbo, Fu Xiangyun, Gao Cheng, Ge Di, Geng Xiaoming, Hu Chenjun, Jiang Chengyong, Jiang Yanfei, Li Feng, Liu Songlin, Luo Liren, Pan Enyu, Pan Xinyuan, Pan Yiwen, Peng Bo, Qian Lingyun, Qin Chuan, Shen Zhongyuan, Shi Zheng, Sun Xiaojun, Wang Bin, Wang Dongpeng, Wang Jiyuan, Wang Liming, Wang Liyan, Wang You, Wu Fangqi, Wu Jiejie, Xu Chao, Xu Renyi, Yang Yifan, Yao Fusheng, Ye Weiqiang, Yu Zhongwei, Yuan Zhaoxing, Zhang Chenglei, Zhang Fanjun, Zhang Jie, Zhang Lin, Zhang Meiyan, Zheng Haijiao, Zhou Ying, Zhu Da. Für die immer freundliche Unterstützung in allen Lebenslagen danke ich meinen chinesischen Kollegen und Kolleginnen, Xu Fang, Xu Jingwei, Weikang Qian, Ma Xiating, Fang Zongda, Lu Ling. Mein besonderer Dank gilt meiner Familie, vor allem meiner Frau Katja, die sofort sagte, ich solle eine solche Chance nutzen, auch wenn wir dadurch mal wieder fast ein viertel Jahr getrennt seien. Bernd Kahlbrandt, Shanghai, August 2004. xi Einführung FT Kapitel 1 You Oughta Know Alanis Morisette 1.1 Übersicht DR A Es ist egal, an welcher Programmiersprache Sie das Programmieren lernen: • Man kann in der besten Programmiersprache schlechte Programme schreiben. • Guten Programmierstil kann man auch an der schlechtesten Programmiersprache lernen. Warum also gerade C? Dafür gibt es eine Reihe guter Gründe: • C gibt es fast überall. • C ist relativ einfach. • C Programme können sehr effizient sein. • Sie können mit C fast alles machen. • In C können Sie Vieles lernen, was Sie dann in C++, C# oder Java auch anwenden können. • Irgendwer muss auch die Programme schreiben, die für alles die Basis bilden: Betriebssysteme, Utilities etc. Gerade Ingenieure benötigen zumindest ab und zu Dinge, die etwa in Java nicht (so einfach) zu machen sind. In diesem Kapitel möchte ich Ihnen zeigen, wie Sie auf möglichst einfache Weise drei Dinge tun können, nämlich ein C-Programm: 1. schreiben, 2. umwandeln, 3. ausführen Was dabei genau abläuft, was dahinter steckt und was man sonst noch alles machen kann, lernen Sie im Laufe dieses Kurses. Es kann durchaus sein, dass Sie im Laufe des Kurse Schwierigkeiten zu überwinden haben. Das könnten z. B. folgende sein: • Sie verstehen mein Deutsch nicht gut genug. • Sie verstehen den fachlichen Inhalt nicht oder nicht sofort. 1 KAPITEL 1. EINFÜHRUNG 2 FT • Sie meinen, alles verstanden zu haben, aber sie schaffen es nicht ein Programm zu schreiben, in dem Sie den Stoff an einer Aufgabe anwenden sollen. • Sie schaffen es, ein Programm zu schreiben, dass Sie für richtig halten, aber der Compiler ist anderer Ansicht. • Sie schreiben ein Programm, das Sie für richtig halten, der Compiler findet auch keine Fehler, das Ergebnis ist aber trotzdem falsch. • ... Denken Sie dann bitte an Folgendes: • Fragen Sie unbedingt nach, wenn Sie einen deutschen Satz oder auch nur einen Begriff nicht verstanden haben. Kennt etwa jeder Chinese jedes Schriftzeichen oder versteht jeden andern Chinesen, der vielleicht etwas Dialekt spricht? DR A • Es ist Ihre Aufgabe sich ernsthaft anzustrengen, den Stoff zu verstehen. Es ist meine Aufgabe, den Stoff so darzustellen, dass Sie ihn verstehen. Wenn Sie nicht nachfragen ist das unhöflich, weil Sie mir keine Chance geben, die Darstellung zu verbessern. Denken Sie bitte nicht, weil die Anderen nicht fragen, hätten alle Anderen das verstanden, nur Sie nicht. Wahrscheinlich trauen die Anderen sich nur nicht, zu fragen. Haben Sie keine Angst vor Fehlern, wenn Sie Deutsch sprechen. Sie können daraus nur lernen. • Schaffen Sie es nicht, ein Programm zu schreiben, dass eine Aufgabe löst, so ist dies ein sicheres Zeichen, dass Sie den Stoff doch nicht ganz oder nicht ganz richtig verstanden haben. Also müssen Sie den Dozenten fragen! • Auch ein C-Compiler mag einen Fehler haben. Bekommen Sie beim Umwandeln eines Programmes aber Warnungen oder gar Fehlermeldungen, so ist es aber sehr, sehr wahrscheinlich, dass der Fehler bei Ihnen liegt. Dabei kommen zwei Dinge besonders häufig vor: 1. Sie haben irdendwelche Systemeinstellungen nicht korrekt vorgenommen. Das „sollte“ an der Hochschule nicht vorkommen. Diese Dinge sind mit Unterstützung eines fachkundigen Mitarbeiters oder Mitarbeiterin meistens leicht lösbar. 2. Der Compiler hat etwas entdeckt, das ihm eine Warnung oder eine Fehlermeldung Wert ist. Ihre Erfahrung reicht aber nicht aus, um die Fehlermeldung richtig zu verstehen und auf den Teil des Codes zu schließen, der die Meldung verursacht. Hierzu werden wir gemeinsam eine Sammlung lehrreicher Beispiele zusammenstellen. Zur Erläuterung werde ich den Code, den ich Ihnen zur Verfügung stelle ausführlich kommentieren. Ein nützliches Werkzeug dafür ist doygen. Das ist sozusagen „javadoc“ für C und C++. Und noch ein ganz wichtiger Hinweis: Programme, die ein Rechner versteht kann jeder Idiot schreiben. Gute Programmierer schreiben Programme, die Menschen verstehen! 1.2 Lernziele • Einen Editor benutzen können (um C-Programme zu schreiben) • Einen C-Compiler nutzen können, z. B. MinGW. • Wissen, was Sie tun sollten, um diesen Kurs mit Erfolg abzuschließen. 1.3. C PROGRAMME SCHREIBEN, UMWANDELN UND AUSFÜHREN 3 1.3 C Programme schreiben, umwandeln und ausführen FT Nehmen Sie sich Ihren Lieblingseditor her. Über Geschmack soll man nicht streiten, Sie können also irgend einen nehmen, hier nur eine kleine Auswahl: emacs Dies ist ein Lisp Interpreter, mit dem Sie auch einen Editor erhalten. An emacs ihm scheiden sich die Geister. Es gibt nur begeisterte Fans und fanatische Gegner. Wohl für kaum ein Akronym gibt es so viele verschiedene „Lesarten“. Dieser Editor hat für fast alle Programmiersprachen Modi, die das Schreiben des entsprechenden Codes erleichtern. vi In Bezug auf vi sind die Rollen der Gegner und Befürworter im Vergleich zu emacs genau vertauscht. Windows Editor Einfaches Zubehör zu MS-Windows, das früher „notepad“ hieß. notepad++ Einfach freier Editor für Windows, der für viele Zwecke eine gute Unterstützung bietet, z. B. für die Programmierung in C. wordpad Ebenfalls ein einfaches Zubehör zu MS-Windows. Dieser Editor kann aber Unicode. Sie haben so die Chance (aber nicht die Garantie) ohne große Schwierigkeiten Unix-Dateien unter Windows zu editieren und umgekehrt. Netbeans Entwicklungsumgebung von Oracle für C, C++ und Java. DR A Visual Studio Entwicklungsumgebung für Microsoft für C, C++,C# und Java. Eclipse Open Source Entwicklungsumgebung für C, C++ und Java. Ich werde hier notepad++ verwenden. Natürlich können Sie auch die Entwicklungsumgebung von Microsoft verwenden. Die bringt aber so viele Möglichkeiten und Einstellungen mit, dass es für den Anfang vielleicht etwas zu schwierig für Sie sein könnte. Hier nun der Code für das denkbar einfachste C-Programm, dass einfach gar nichts tut: 01 02 03 04 05 06 07 08 /** * One of the most simple C programs. * @author Bernd Kahlbrandt */ int main(void) return 42; Um das Programm umwandeln zu können, müssen Sie wissen, wie der Compiler heißt, d. h. welches Programm sie ausführen müssen, welche Parameter es erwartet etc. MinGW (Minimal GNU for Windows) ist ein Open Source Compiler, der auch von Netbeans verwendet wird. MinGW hat auch einen C-Compiler (außer dem C++, Fortran . . . ). Am einfachsten ist es, wenn Sie sich eine einfache Batch-Datei (.bat) machen, die etwa so aussieht wie die folgende buildc.bat: Mingw32-gcc ./src/%1.c -std=c99 -o ./bin/%1.exe Diese Zeile erläutere ich nun Schritt für Schritt: 1. Mingw32-gcc: Dies ist der Name des C-Compilers von MinGW. Es muss in einem Verzeichnis liegen, das in Ihrem gültigen Pfad liegt. Ich empfehle, den Source-Code, also die Dateien mit der Endung .c, in einem Verzeichnis zu speichern, hier src/. vor dem Dateinamen. Das ./c davor heißt einfach, dass der Name relativ zum aktuellen Verzeichnis ist. 2. ./src/%1.c: %1 ist das Symbol für den ersten Parameter. Hier steht der Name Ihres Programmes. Damit Sie weniger tippen müssen, geben Sie die Erweiterung .c nicht mit, sondern sie wird automatisch ergänzt (.c). KAPITEL 1. EINFÜHRUNG 4 FT 3. -std=c99: Dies ist eine Option für den Compiler. Sie besagt, das der Standard (-std) C99 verwendet werden soll. Das ist die aktuelle Version von C. 4. -o Dieser Parameter gibt an, wie die Ausgabedatei heißen soll und was für eine Ausgabe Sie erstellen wollen. 5. ./bin/%1.exe %1 ist wieder der Name des Programms, nun aber mit der Endung .exe. In Windows ist das eine ausführbare Datei und eine solche wird deshalb auch von Mingw32-gcc erstellt. Ich empfehle, die ausführbaren Programme in einem eigenen Verzeichnis, hier bin zu speichern. Nun zur Erklärung des Programms, Zeile für Zeile: 1. Zeilen 01-04 enthalten einen mehrzeiligen Kommentar. Ein mehrzeiliger Kommentar beginnt in C mit /* und endet mit */. In Zeile 01 beginnt mit /**. Dies bedeutet, das es sich um einen doxygenKommentar handelt. Sie müssen im Praktikum keine umfangreichen Kommentare dieser Art schreiben: Ich erwarte nur so viel, wie Sie in diesem Beispiel sehen. Die Syntax von doxygen entspricht weitgehend der des entsprechenden Werkzeugs javadoc für Java. Zeile 02 enthält eine Beschreibung des Programms. Ich werde hier englische Kommentare verwenden. Zeile 03 enthält den Autor des Programms. Dies wird durch @author gefolgt von dem Namen angegeben. Für jeden Autor ein @author. Zeile 04 beendet den Kommentar. DR A 2. In Zeile 05 beginnt eine Funktion, die main heißt. Jedes ausführbare C-Programm muss eine Funktion dieses Namens haben. Als Ergebnis liefert main immer eine ganze Zahl zurück. Ganze Zahlen heißen in C int. Die Funktion main kann Argumente haben, muss dies aber nicht. Da dieses Beispiel ganz einfach sein soll, bekommt diese main-Funktion keine Parameter. Das wird hier durch das Wort void angegeben. 3. Die öffnende geschweifte Klammer { in Zeile 06 kennzeichnet den Beginn eines Blocks. Dieser enthält den Code der Methode und geht bis Zeile 08, in der er durch eine schließende geschweifte Klammer } beendet wird. Es ist in C üblich, solche Klammerpaare so zu setzen wie hier: DIe Schließende Klammer steht in der gleichen Zeile wie die öffnende. 4. In Zeile 07 wird 42 zurückgegeben, nach [Ada81] die Antwort auf die „Große Frage nach dem Leben, dem Universum und allem?“1 Mit Visual C++.NET kommt ein Programm cl.exe das dies leistet. Für unseren einfachen Fall brauchen Sie nur eine DOS-Box zu öffnen. Damit alle Umgebungsvariablen richtig gesetzt sind, machen Sie das am einfachsten so: Wählen Sie aus dem Startmenü Programme →Microsoft Visual Studio .NET 2003 →Visual Studio .NET Tools →Visual Studio .NET Command Prompt aus. Wechseln Sie dann in das Verzeichnis, in dem Sie das Programm tunix.c gespeichert haben. Dort geben Sie dann Folgendes ein: cl tunix.c Das Ergebnis sehen Sie hier: Microsoft (R) 32-Bit C/C++-Optimierungscompiler Version 13.10.3077 für 80x86 Copyright (C) Microsoft Corporation 1984-2002. All rights reserved. tunix.c Microsoft (R) Incremental Linker Version 7.10.3077 Copyright (C) Microsoft Corporation. All rights reserved. 1 Für diese Antwort hat Deep Thought 7,5 Millionen Jahre benötigt. /out:tunix.exe tunix.obj 5 FT 1.3. C PROGRAMME SCHREIBEN, UMWANDELN UND AUSFÜHREN Lassen Sie das „.c“ weg, so passiert auch nichts Schlimmes, Sie bekommen lediglich zusätzlich die Warnung: cl: Befehlszeile warning D4024: Typ der Quelldatei ’tunix’ nicht erkannt, Objekt datei wird angenommen Geben Sie nun den Befehl dir ein. Im Ergebnis sollten Sie etwa dieses Ergebnis sehen: (Die Letzte Spalte enthält von mir ergänzte Erläuterungen) 22.06.2004 21.06.2004 22.06.2004 22.06.2004 11:11 120 tunix.c 13:51 37 tunix.c~ 12:06 22.016 tunix.exe 12:06 479 tunix.obj C Source Code Sicherungsdatei von emacs Die ausführbare Datei Die Objekt Datei Sehen wir uns nun einmal an, was wir aus diesem einfachen Beispiel alles lernen können. Das Programm tut zwar nichts (daher der Name), aber einige der Elemente werden Sie immer wieder verwenden. DR A 1. „/*. . . */“: Ein Schrägstrich gefolgt von einem Stern kennzeichnet die folgenden Zeichen als Kommentare bis ein Stern gefolgt von einem Schrägstrich den Kommentar abschließt. Diese Art der Kommentare heißen „Kommentare im C-Stil“, da sie insbesondere in C üblich sind. 2. „int main(void)“: Sie können ein C-Programm in viele Programmteile zerlegen. Wie wir später sehen werden, ist die sinnvolle Zerlegung zentraler Bestandteil guten Programmierens. Ein Teil muss aber eine Funktion enthalten, die „main“ heißt. In dem folgenden Klammerpaar „(void)“ steht eigentlich nichts. Das Schlüsselwort void bedeutet, dass die Funktion nichts übergeben bekommt (keine Parameter). Das Schlüsselwort „int“ vor dem Funktionsnamen bedeutet, dass die Funktion nach erfolgreicher Ausführung eine ganze Zahl zurückliefert. Ganze Zahlen heißen in C „int“ vom englischen Wort „integer“. 3. „{ . . . }“: Geschweifte Klammern schließen einen Programmblock ein. Sie müssen ein geschweiftes Klammerpaar setzen, um den Code einer Funktion einzuschließen. Wenn es die Lesbarkeit verbessert können Sie beliebig viele weitere Klammerpaare setzen. 4. „return 47“: In dieser Zeile wird die Funktion beendet und die ganze Zahl 47 zurückgegeben. Üblich ist es, beim erfolgreichen Ende eines Programmes „0“ zurückzugeben, Im Fehlerfall, wird oft eine andere Zahl zurückgeliefert, die Informationen darüber enthält, wie schwer der Fehler war. 5. Ich habe für jede geschweifte Klammer eine Zeile „spendiert“. Ich finde Code übersichtlicher, wenn die Klammerpaare jeweils in einer Spalte übereinanderstehen. Es gibt etwa genauso viele Programmierer, die das ebenfalls so machen wie die, die es anders machen. Damit haben wir einige Basisbestandteile von C-Programmen gesehen. Wenden wir uns nun den Dateien in unserem Verzeichnis zu: tunix.c Der Sourcecode, auf meinem Rechner 120 Bytes lang. tunix.c˜ Die Sicherungsdatei die emacs für mich angelegt hat. Ich hatte zunächst den Code geschrieben und dann den Kommentar ergänzt. „Netto“ ist der Code also nur 37 Bytes lang. tunix.exe Das ist die ausführbare Datei, die der Compiler erzeugt hat. Achten Sie auf die Größe von 22.016 Bytes; dies erscheint auf den ersten Blick viel für ein Programm, dass nichts tut. tunix.obj Was zum Teufel ist das? Im Englischen heißt es object deck, im Deutschen ist Objektdatei eine mögliche Übersetzung. Diese Datei enthält die wesentlichen Teile des Programms, allerdings sind die Adressen, die verwendet werden, noch relativ zum Beginn des Programms. Durch den sogenannten Linker, wird diese Objektdatei mit den notwendigen Laufzeitbibliotheken zu einer ausführbaren Datei „zusammengebaut“, die hier „tunix.exe“ heißt. KAPITEL 1. EINFÜHRUNG 6 Was macht nun das Programm cl? Dazu betrachten wir die obige Ausgabe etwas genauer: FT cl Der Programmname ist eine Abkürzung für Compiler und Linker. Der Compiler prüft die Syntax und erstellt den ausführbaren Code, der sich aus den angegebenen Source-Dateien erstellen lässt. Der Linker zieht Module aus Bibliotheken heran, prüft, ob die Funktionen in der Bibliothek denen entsprechen, die Sie in Ihrem Programm verwenden und packt alles mit den Modulen zusammen, die benötigt werden, um im jeweiligen Betriebssystem ein Programm zu laden und die Ausführung zu beginnen. C/C++ Optimierungscompiler In der Vergangenheit waren Compiler „verbesserungsfähig“: Man konnte den erzeugten Code durch geeignete Optimierungsprogramme drastisch beschleunigen. Heute optimieren die meisten Compiler. Manche Unzulänglichkeiten in der Programmierung werden dadurch „ausgebügelt“. tunix.c Dieser Name gibt uns die Eingabedatei für den Compiler an. Incremental Linker Nach dem Compile-Schritt kommt der Link-Schritt (s. o.). DR A /out:tunix.exe Die Ausgabedatei des Linkers heißt tunix.exe. Die Voreinstellung ist: Heißt die SourceCode Datei xxxx.c, so heißt die ausführbare Datei xxxx.exe. Sie können ihr aber auch jeden anderen Namen geben. Geben Sie etwa cl tunix.c /Fenihao.exe ein, so wird die Ausgabedatei nihao.exe erzeugt. /Fe ist dabei ein Parameter für cl dem der Name der Ausgabedatei folgen muss. tunix.obj In dieser Zeile wird die Eingabedatei des Linkers ausgegeben. Führen Sie das Programm aus, indem Sie „tunix.exe“ eintippen und die „Enter“-Taste drücken, so sehen Sie keinen Effekt. Kein Wunder, das Programm tut ja auch nichts. 1.4 Historische Anmerkungen C wurde ursprünglich von Dennis Ritchie für und auf dem UNIX Betriebssystem der DEC PDP-11 entworfen und implementiert. Erwähnenswerte Vorläufer sind BCPL und die im Folgenden genannten. Viele der maßgeblich Beteiligten an der Entwicklung von C und C++ hatten Erfahrungen mit BCPL und deren Vorläufer, CPL. Die Erfahrungen, die er mit BCPL gewonnen hatte, gingen in Ken Thompsens Entwicklung der Sprache B für die DEC PDP-7 im Jahre 1970 ein. Böse Zungen behaupten, Dennis Ritchie habe sich mit der Entwicklung dieser Sprache einen Scherz erlauben wollen. Er habe sich nicht vorstellen können, dass irgend jemand ernsthaft in einer solchen Sprache würde programmieren wollen. Dies ist wohl eine boshafte Unterstellung. Der Erfolg von C führte zur Entwicklung vieler aktueller Sprachen. Die Entwicklung von C++ wird von Bjarne Stroustrup sehr anschaulich in [Str94b] beschrieben. Java versucht einige Schwächen von C zu beseitigen. Dabei entstand die Sprache aus einem völlig andersartigen Projekt. Die Firma Sun kämpfte immer an zwei Fronten: Gegen Microsoft und gegen IBM. Als sich in den USA der Trend andeutete, Internetzugang über die vorhandenen Kabelnetze für Fernsehempfang zur Verfügung zu stellen, witterte der CEO von Sun eine Chance. Sun begann ein Betriebssystem für die dazu benötigten Set-Top Boxen zu entwickeln. Das Projekt scheiterte, aber Java wurde ein Riesenerfolg. Microsoft bietet im Rahmen von .NET die Weiterentwicklung C# (gesprochen: C sharp) an. 1.5 Aufgaben 1. Schreiben Sie das Programm tunix.c und versuchen Sie die Ergebnisse der Vorlesung selbst zu erzeugen. 2. Wandeln Sie das Programm tunix.c mit allen C-Compilern um, auf die Sie Zugriff haben. Dokumentieren Sie bitte die Ergebnisse. Grundlagen 2.1 Übersicht FT Kapitel 2 DR A Dieses Kapitel gibt Ihnen eine erste Übersicht über einige wichtige Dinge in C. So können Sie gleich (ganz) einfache Programme schreiben, umwandeln und ausführen. Ich werde Ihnen aber nicht gleiche alle Details „verraten“ und nicht alles genau erklären können. Das hole ich dann in den folgenden Kapiteln nach. Insbesondere lernen Sie einige Datentypen und Funktionen kennen, die Sie immer wieder verwenden werden. 2.2 Lernziele • Gültige Namen in C-Programmen bilden können. • Die verschiedenen Komponenten beschreiben können, die ein C-Programm ausmachen. • Den Präprozessor-Befehl #include kennen. • Einige Funktionen der • Etwas auf der Konsole ausgeben können. Bibliothek stdio kennen. • Variablen von der Konsole einlesen und auswerten können. 2.3 Namen Namen für die Bezeichnung von Variablen, Funktionen, Konstanten etc. können in C aus den Buchstaben A–Z, a–z, dem Ubnterstrich _ sowie den Ziffern 0–9 gebildet werden, wobei das erste Zeichen keine Ziffer sein darf. In Syntaxdiagrammen dargestellt sieht das so aus: 2.4 Struktur von C-Programmen Ein Programm ist ein in einer Programmiersprache formulierter Algorithmus. Ein Algorithmus ist eine präzise, d. h. in einer genau definierten Sprache abgefasste, endliche Beschreibung eines allgemeinen Verfahrens unter Verwendung ausführbarer elementarer Verarbeitungsschritte. Die Bezeichnung wird aus dem Namen des persischen Lehrbuchautors Abu Ja’far Mohammed ibn Mûsâ al-Khowârizmî hergeleitet. Genauer kann man einen Algorithmus so charakterisieren: Eindeutigkeit Er ist eindeutig und unmissverständlich formuliert. Endlichkeit Er ist in endlich vielen Schritten beschrieben. 7 KAPITEL 2. GRUNDLAGEN DR A FT 8 Abbildung 2.1: Namensbildung in C 2.4. STRUKTUR VON C-PROGRAMMEN 9 Terminiertheit Er endet in endlichen vielen Schritten. FT Reproduzierbarkeit Er ist aus wohldefinierten elementaren Schritten aufgebaut. Ein Programm kann in einer Datei stehen oder auf mehrere Dateien verteilt werden. Bisher haben wir nur Dateien (genaugenommen nur eine) mit der Endung .c kennengelernt. Diese sind Eingaben für den Compiler. Weitere Dateien definieren Schnittstellen. Diese werden Header-Dateien genannt und haben die Endung .h. Einen Beispielausschnitt einer solchen Datei, limits.h zeigt Abb. 2.2 Sie zeigt die wichtigsten /*limits.h - implementation dependent values */ #if _MSC_VER > 1000 #pragma once #endif #ifndef _INC_LIMITS #define _INC_LIMITS CHAR_BIT SCHAR_MIN SCHAR_MAX UCHAR_MAX 8 (-128) 127 0xff #ifndef #define #define #else #define #define #endif _CHAR_UNSIGNED CHAR_MIN SCHAR_MIN CHAR_MAX SCHAR_MAX CHAR_MIN 0 CHAR_MAX UCHAR_MAX /* _CHAR_UNSIGNED */ #define #define #define #define #define #define #define #define #define #define MB_LEN_MAX SHRT_MIN SHRT_MAX USHRT_MAX INT_MIN INT_MAX UINT_MAX LONG_MIN LONG_MAX ULONG_MAX #endif /* _INC_LIMITS */ /* /* /* /* number of bits in a char */ minimum signed char value */ maximum signed char value */ maximum unsigned char value */ DR A #define #define #define #define /* mimimum char value */ /* maximum char value */ 5 /* max. # bytes in multibyte char */ (-32768) /* minimum (signed) short value */ 32767 /* maximum (signed) short value */ 0xffff /* maximum unsigned short value */ (-2147483647 - 1) /* minimum (signed) int value */ 2147483647 /* maximum (signed) int value */ 0xffffffff /* maximum unsigned int value */ (-2147483647L - 1) /* minimum (signed) long value */ 2147483647L /* maximum (signed) long value */ 0xffffffffUL /* maximum unsigned long value */ Abbildung 2.2: limits.h für VC++ .NET 2003 Längen der ganzzahligen Datentypen. Sie finden diese Dateien im Verzeichmis include Ihrer Entwicklungsumgebung oder Ihres Compilers. Die Fließkomma Datentypen in C sind float, double und long double. Sie werden es vor Allem mit zwei Arten von Header-Dateien zu tun haben. Da sind zunächst die Header-Dateien, die mit den Bibliotheken von C kommen. Es ist guter Stil, C-Programme modular aufzubauen. Auch Sie werden also Deklarationen von Schnittstellen in eigenen Header-Dateien vornehmen. So können Sie Programme, die sie bereits geschrieben haben beim Entwickeln neuer Programme leicht heranziehen. Für fast jedes Ihrer Programme werden Sie die C Standard-Bibliotheken heranziehen. Die ersten davon werden Sie in diesem Kapitel kennenlernen. KAPITEL 2. GRUNDLAGEN 10 2.5 Datentypen Typ unsigned char signed char int unsigned int unsigned short signed short unsigned long signed long FT Für den Anfang beschränken wir uns auf Zahlen und Zeichen. Die folgende Abb. 2.3 zeigt die verschieSpeicherplatz (Byte) 1 1 2 oder 4 2 oder 4 2 2 4 4 Wertebereich von bis 0 255 −128 127 −32.768 32.767 −2.147.483.648 2.147.483.647 0 65.535 0 4.294.967.295 0 65.535 −32.768 32.767 0 4.294.967.295 −2.147.483.648 2.147.483.647 Abbildung 2.3: C Datentypen, Teil 1: Ganze Zahlen und Zeichen DR A denen ganzzahligen Datentypen in ANSI C. Die Längen in Abb. 2.3 sind die, die der Standard garantiert. limits.h enthält die Realität „Ihres“ Compilers. Die Unterscheide in den Längen von int ergeben sich daraus, dass die Länge eines int immer die eines Maschinenregisters ist. Auf 16-Bit Maschinen also 2 Byte und auf 32-Bit Maschinen 4 Byte. Die ersten beiden Zeilen von Abb. 2.4 zeigt die Fließkomma-Datentypen in ANSI C. Die dritte Ziele Typ float double long double Speicherplatz (Byte) 32 64 80 Wertebereich von bis ±1, 7 · 10−38 ±3, 4 · 1038 ±2, 2 · 10−308 ±1, 7 · 10308 −4932 ±1, 7 · 10 ±3, 4 · 104932 Abbildung 2.4: C Datentypen, Teil 2: Fließkommazahlen mit long double ist kein Bestandteil des ANSI C99 Standards, wird aber von eingen Compilern auf einigen Betriebssystemen unterstützt. Variablen werden deklariert, indem Sie den Typ angeben, z. B. „int“, gefolgt von dem Namen. Sie werden definiert, in dem die Deklaration mit der Zuweisung eines Wertes endet. Hier einige Beispiele: /****************************************** datatypes02.c Deklaration von numerischen Datentypen *******************************************/ int main(void) { { int a; long b; } { int c =5; long d = 888888L; } return 0; } Variablendeklarationen und -definitionen können fast überall verwendet werden. Es ist aber üblich diese am Anfang eines Blocks zu tun. Ein Block ist dabei alles, was zwischen zwei geschweiften Klammer steht: 2.6. FUNKTIONEN 11 FT {...}. Diese können die Klammern der main Funktion sein, die wir bereits in tunix.c gesehen haben oder ein anderes von Ihnen gesetztes Klammerpaar. Für ganzzahlige Konstanten gelten folgende Regeln: • Eine dezimale Konstante beginnt nie mit einer 0. • Eine oktale Konstante (Zahl zur Basis 8 beginnt immer mit einer 0 und darf nur die Ziffern 0 bis 7 enthalten. • Eine hexadezimale Konstante (Zahl zur Basis 16 beginnt mit 0x oder 0X4. Sie darf die Ziffern 0–9 und die Buchstaben a–f oder A–F enthalten. • Die Suffix U bzw L groß oder klein geschrieben und in beliebiger Reihenfolge definieren long bzw. unsigned. • Ein Minus- oder Plus Zeichen (−,+) gilt formal nicht als Bestandteil einer ganzzahligen Konstante (siehe 4, unäre Operatoren). Fließkommakonstanten sind double, es sei denn Sie geben durch den Suffix f bzw. F explit float an. Aufzählungstypen definieren Namen für ganzzahlige Konstanten, wie in enum boolean {FALSE, TRUE}; DR A Der erste Name in der Liste hat den Wert 0, der zweite den Wert 1 usw. Die enum Konstanten wie FALSE oder TRUE werden ohne enum-Namen wie boolean verwendet. Sie müssen innerhalb eines enum-Namens eindeutig sein. Verwenden Sie in einem Programm mehrere enums, so müssen die Namen der Konstanten innerhalb aller verwendeten enums eindeutig sein. Ein weiterer Datentyp ,den Sie oft verwenden werden, ist String. Ein String ist eine Zeichenkette. 2.6 Funktionen Funktionen dienen zur Strukturierung von Programmen. Sie sollen dabei helfen, komplexe Abläufe in einzelne übersichtliche Schritte zu zerlegen. Um eine Funktionen verwenden zu können, müssen Sie Folgendes wissen: • Den Namen der Funktion. • Die Parameter, die sie übergeben können oder müssen und deren Typ. • Den Rückgabewert, den Sie von der Funktion erwarten können. Das sind allerdings „nur“ formale Dinge. Ohne diese zu kennen, werden Sie ein Programm aber nicht erfolgreich umwandeln können. Es ist guter Stil, die drei genannten Dinge in einer Header-Datei zu deklarieren. Dies geschieht in der Form eines sog. Prototyps: [rückgabe typ] funktionsname([typ1 parameter1, typ2 parameter2, ...]); Für Funktionen die aus einer Standard-Bibliothek oder einer von Ihnen geschriebenen Bibliothek stammen, können Sie die Prototypen dann einfach durch einen #include-Befehl für Ihr Programm verfügbar machen. Außer diesen Informationen müssen Sie natürlich unbedingt wissen, was die Funktion eigentlich leistet. Hier zunächst ein Beispiel für einen Prototyp int summe(int a, int b); Der Name „summe“ legt nahe, dass die Funktion die Summe der beiden Parameter a und b berechnet und zurückliefert. Die gesamte Funktion kann dann so aussehen: KAPITEL 2. GRUNDLAGEN 12 Vollständig kann das so aussehen: FT int summe(int a, int b); { return (a + b); } DR A /****************************************** summe.c berechnet die Summe zweier ganzer Zahlen ******************************************/ #include <stdio.h> int summe(int a, int b) { return (a + b); } int main(void) { int pa = 9; int pb = 16; printf("%d%s%d%s%d%s",pa," + ",pb," = ",summe(pa,pb),"."); return 0; } Als Ergebnis sollten Sie auf der Konsole folgendes sehen: 9 + 16 = 25. Sehen wir uns, wie schon beim Programm tunix.c an, was wir daraus lernen können. 1. In der ersten Zeile wird, wie in fast jedem C Programm, die Header-Datei stdio.h deklariert, d. h. durch eine #include Direktive eingebunden. Dies wird so häufig verwendet, dass Sie dies in Visual Studio auch weglassen können. Es ist aber nicht garantiert, dass jeder andere Compiler das Programm dann auch umwandeln kann. 2. Die folgenden vier Zeilen enthalten die Deklaration und die Definition der Funktion summe. Beachten Sie bitte, dass hier das Semikolon am Ende der ersten Zeile fehlt, während es bei der Deklaration des Prototypen in der Header-Datei notwendig ist. 3. Die geschweiften Klammern eines Blocks — hier der Programmcode für summe und main — setze ich immer so, dass sie paarweise in einer Spalte stehen. Eine andere übliche Konvention ist es, die öffnende geschweifte Klammer { in die gleiche Zeile, wie die Deklaration der Funktion zu schreiben. Die schließenden Klammern } werden aber genauso eingerückt, wie ich es mache. 4. Die einzige Codezeile der Funktion summe liefert den Inhalt des eingeklammerten Ausdrucks zurück. Es werden zunächst die Werte der übergebenen Parameter a und b addiert und dieser Wert dann an Programm zurückgegeben, dass die Funktion summe aufgerufen hatte. In diesem Fall können Sie die Klammern auch weglassen, da der Additionsoperator stärker bindet. Ich empfehle Ihnen dringend, Klammern immer dann zu setzen, wenn Sie dadurch die Lesbarkeit des Programms verbessern. 5. Die Funktion main(void) kennen wir bereits. 6. In ihren ersten beiden „richtigen“ Codezeilen werden zwei Variablen (pa und pb) als int deklariert und auch sofort definiert; d. h. es werden ihnen die Werte 9 bzw. 16 zugewiesen. 7. Es folgt dann der Aufruf von printf mit einer etwas längeren Parameterliste. printf stammt aus stdio. Diese beginnt mit einer Reihe von Zeichen, die in "-Zeichen eingeschlossen ist, dem sogenannten Formattierungsstring. Der Übersichtlichkeit halber stelle ich die entsprechenden Teile des Formattierungstrings den zu formattierenden Elementen in der folgenden Tabelle gegenüber: 2.7. PRINTF UND PUTCHAR 13 Variable/ Konstante pa " + " pb " = " summe(pa,pb) "." Ergebnis 9 + 16 = 25 . FT Formattierung %d %s %d %s %d %s Ganz analog könnten Sie Funktionen schreiben die multiplizieren („*“ statt („+“), dividieren ((„/“ statt („+“) oder subtrahieren ((„-“ statt („+“). 2.7 printf und putchar Eine Basisfunktion in C ist printf. Der Prototyp von printf ist: int printf(const char *, ...); printf ist Bestandteil der Standard-Bibliothek und wird durch include von stdio.h verfügbar gemacht. Diese Funktion leistet folgendes: DR A • Sie erhält als ersten (eventuell einzigen) Parameter einen String mit Formattierungsangaben. • Für jede Formattierungsangabe folgt dann eine Variable, z. B. eine Zahl, ein String oder eine Konstante. Die Formattierungsangabe bestimmt, wie die Variable etc. auf der Console ausgegeben wird. • Die Ellipse (. . . ) gibt an, dass weitere Parameter folgen können. • char* bezeichnet einen String, siehe hierzu später mehr in Kap. 6 und 9. • Das Schlüsselwort const bedeutet, dass die übergebenen Parameter in der Funktion printf nicht verändert werden können. Sie können also ganz sicher sein, dass „Ihre“ Variablen nach der Ausgabe mit printf genauso „aussehen“, wie vorher. • Als Rückgabewert liefert printf die Anzahl ausgegebener Zeichen. Als einfaches Beispiel dient hier das Programm „Guten Morgen Shanghai“. /****************************************** moinmoin01.c Gibt Guten Morgen Shanghai auf der Console aus *******************************************/ #include <stdio.h> int main() { int i = printf("%s","Guten Morgen Shanghai"); printf("%s%d%s","\nDas sind ",i," Zeichen"); return i; } Die Ausgabe ist: Guten Morgen Shanghai Das sind 21 Zeichen Sehen wir uns, wie schon beim Programm tunix.c an, was wir daraus lernen können. 1. Das wichtigste zu erst: „Moin“, oder „Moinmoin“ ist einfach „guten Morgen“ auf Hamburgisch. 2. Sie können void beim Aufurf von main auch weglassen. KAPITEL 2. GRUNDLAGEN 14 FT 3. Das Zeichen \n am Anfang des zweiten Aufrufs von printf definiert einen Zeilenumbruch und ist ein Zeichen, eine sogenannte Escape-Sequenz 4. Zu den Zeichen, die bei der Ausgabe gezählt werden, gehören alle Zeichen, also auch Leerstellen, Zeilenumbruch etc. Die Sprache C ist so flexibel, dass man die Funktion printf hier auch etwas anders schreiben kann, z. B. so: /****************************************** moinmoin02.c Gibt Guten Morgen Shanghai auf der Console aus *******************************************/ #include <stdio.h> int main() { int i = printf("Guten Morgen Shanghai"); printf("\nDas sind %d Zeichen",i); return i; } Was lernen wir daraus? Zumindest dieses: DR A 1. Man kann auf „%s“ verzichten, wenn man nur einen String ausgeben will. 2. Man kann Strings im Formatstring unterbringen Warum braucht man denn überhaupt „%s“? Es ist kein guter Stil Stringkonstanten direkt in ein Programm hineinzuschreiben. So muss man ja immer das Programm ändern, wenn der String sich ändert, z. B. weil das Programm in einer anderen Sprache verwendet werden soll. Besser ist es dann, Variablen aus einer Datei einzulesen, so dass man nur die Datei austauschen muss. Zum Abschluss dieser Kurzvorstellung von printf nun noch einige weitere Formattierungssymbole und Escape-Sequenzen, damit Sie sofort etwas mehr an Ihren Ausgaben gestalten können. Eine weitgehend vollständige Übersicht erhalten Sie in Kap. 3. In vielen Fällen wollen Sie aber Zeichen einzeln und Einzelzeichen bzw. Formatzeichen \a \t \n %c %d %f Bedeutung bzw. Typ alert(BELL) horizontal tab(HT) line feed(LF) char oder int(≤ 255) int float/double ASCII-Wert bzw. Bedeutung dezimal hex 7 7 9 9 10 A Einzelnes Zeichen dezimal Gleitpunktzahl: [−]ddd.dd Abbildung 2.5: Escape- und Format-Zeichen in ANSI C unmittelbar ausgeben. Dazu gibt es die Funktion int putchar(char c); die das Zeichen in der Variablen c ausgibt, oft auf die Konsole, aber dazu mehr in Abschn. 2.9. putchar liefert im Erfolgsfall das Zeichen und sonst EOF. EOF ist ein Symbol für einen Wert, der angibt, dass das Ende einer Datei erreicht ist. Das Programm moinmoin01 können wir natürlich auch mit putchar schreiben, es ist dann aber nicht mehr so gut lesbar und wir bekommen die Anzahl Zeiochen nicht geliefert: /****************************************** moinmoin03.c Gibt Guten Morgen Shanghai mit putchar auf der Console aus 2.8. SCANF UND GETCHAR 15 DR A FT *******************************************/ #include <stdio.h> void main(void) { putchar(’G’); putchar(’u’); putchar(’t’); putchar(’e’); putchar(’n’); putchar(’ ’); putchar(’M’); putchar(’o’); putchar(’r’); putchar(’g’); putchar(’e’); putchar(’n’); putchar(’ ’); putchar(’S’); putchar(’h’); putchar(’a’); putchar(’n’); putchar(’g’); putchar(’h’); putchar(’a’); putchar(’i’); } Natürlich hätte ich dass auch alles in weniger Zeilen anordnen können. Das wäre dann aber noch unübersichtlicher. Die Funktion putchar stelle ich Ihnen bereits jetzt vor, damit Sie bald sinnvolle Programme schreiben können. 2.8 scanf und getchar Das Gegenstück zu printf für das Einlesen von Variablen über die Tastatur ist die Funktion scanf. Als einfaches Beispiel betrachten wir ein Programm, dass aus Ihrem Geburtsdatum eine „Glückszahl“ erzeugt. /****************************************** happybirthday1.c liest ein Geburtsdatum über die Tastatur ein und "errechnet" eine Glückszahl ******************************************/ #include <stdio.h> int main() { int tag; int monat; int jahr; printf("\nGeben Sie bitte Ihr Geburtsdatum ein!"); printf("\nTag: "); scanf("%d", &tag); printf("\nMonat: "); scanf("%d", &monat); printf("\nJahr: "); KAPITEL 2. GRUNDLAGEN 16 } FT scanf("%d", &jahr); printf("%s%d%s%d%s%d%s","Ist Ihr Geburtsdatum wirklich der: "\ ,tag,".",monat,".",jahr,"?"); srand(tag + monat + jahr); printf("\nDann ist Ihre Glückszahl heute: %d\n",rand()); return 0; Wie schon in früheren Abschnitten erläutere ich, was sich in diesem Programm „Neues tut“. Ich beginne mit der ersten, Ihnen unbekannten Funktion, zumindest soweit es diesen Kurs angeht. 1. Die Funktion scanf wird gleich genauer erläutert. Hier nur so viel: Der erste Parameter enthält einen Formattierungsstring, hier %d. scanf erwartet also eine int Eingabe. Der zweite Parameter enthält den Namen einer Variablen. Beachten Sie aber, dass davor ein &-Zeichen steht. Dies ist der Adressoperator. scanf erhält hier also als zweiten Parameter die Adresse der Variablen tag. Hierzu mehr unten. 2. Die vorletzte printf-Zeile ist zu lang. Sie wird deshalb auf einer zweiten Zeile fortgesetzt. Das wird dem Compiler durch den \ am Ende des ersten Teils des Befehls signalisiert. 3. Die Funktion srand() initialisiert einen Zufallszahlengenerator. DR A 4. In der vorletzen Zeile wird eine Zufallszahl ausgegeben und als Glückszahl „verkauft“. Das für Sie zunächst geheimnisvolle ist der Adressoperator &. Um ihn zuverstehen betrachten Sie bitte die Abb. 2.6. Abbildung 2.6: Variablen und Adressen in C Der Name einer Variablen bezeichnet ein Stück Speicherplatz in dem Sinne, dass Sie den Namen verwenden können, um den entsprechenden Wert in Ihrem Programm zu verwenden. Diese Variable belegt ein Stück des Speichers. Ein Teil der Flexibilität von C kommt daher, dass Sie in Ihrem Programm Zugang zu allen wichtigen Eigenschaften einer Variablen haben. So ermöglicht es Ihnen der Adressoperator &, die Adresse einer Variablen abzufragen. Die Funktion scanf ist nun so „primitiv“, dass sie von Ihnen nicht den Namen der Variablen erwartet, sondern die Adresse, an der diese Variable steht. In C können Sie aber auch Namen für Adressen vergeben. Das geht so: int *i; char *name; Hier werden durch datentyp* Namen für Adressen deklariert. Hier ist i keine Variable, die eine ganze Zahl enthält, sondern eine Variable, die eine Adresse enthält, an der eine ganze Zahl steht. Ganz analog ist der Inhalt von name nicht ein Zeichen, sondern eine Adresse an der ein Zeichen steht. Hier haben Sie also Zeiger, englisch pointer, die auf Variablen „zeigen“. Haben Sie eine solche Variable, so benötigen Sie die Umkehrung des Adressoperators. Die ist der Operator *. *i bzw. *name liefern also die Inhalte, die an der jeweiligen Adresse stehen. Zum Beispiel liefert: 2.8. SCANF UND GETCHAR 17 } diese Ausgabe Var 1000 12FEDC a 12FEDB *Var 1000 &Var 12FEDC 12FED4 12FEDB 12FEE0 DR A i zi c zc FT /* zeiger01.c Elementares Beispiel für * und & Status: ok*/ #include <stdio.h> void main(void) { int i = 1000; int* zi = &i; char c = ’a’; char* zc = &c; printf(" \tVar\t*Var\t&Var\n"); printf("i \t%d\t\t%X\n",i,&i); printf("zi\t%X\t%d\t%X\n",zi,*zi,&zi); printf("c \t%5c\t\t%X\n",c,&c); printf("zc\t%X\t%c\t%X\n",zc,*zc,&zc); a Nach Definition enthalten zi bzw. zc die Adresse von zi bzw. zc, hier 12FEDC bzw. 12FEDB. Umgekehrt erhalten wir mit dem Operator * den Inhalt, der an der jeweiligen Adresse steht (1000 bzw. a). Die beiden leeren Felder in der Spalte *Var sind nicht gefüllt, da der *-Operator nur bei Variablen einen Sinn macht, die eine Adresse enthalten; dies ist bei i und c aber nicht der Fall. Der Addressoperator ist immer anwendbar. Auch eine Variable, die eine Adresse enthält ist ja an einer Adresse gespeichert. Wir werden diese Operatoren an einigen Stellen benötigen, damit Sie frühzeitig sinnvolle Programme schreiben können. Die Einzelheiten des Umgangs mit Adressen in C lernen Sie aber erst im weiteren Verauf der Vorlesung. Für das Einlesen von Variablen über die Tastatur verwenden Sie bei scanf die bereits von printf bekannten Formattierungszeichen. Wollen Sie aber in ihrem Programm nicht auf das Drücken der Eingabetaste am Ende einer Zeile warten oder lesen Sie aus einer Datei, so werden Sie in vielen Fällen die Zeichen einzeln behandeln wollen. Dazu gibt es die Funktion int getchar(void); Diese Funktion liest ein Zeichen ein. Der Rückgabewert ist das gelesene Zeichen oder EOF, wenn ein Fehler aufgetreten ist. Ich erspare Ihnen an dieser Stelle dass unsinnige Wiederholen von getchar-Aufrufen und nutze die Chance Ihnen eine einfache Schleifenkonstruktion vorzuführen. Wir wollen eine Reihe von Zeichen kopieren. Im nächsten Abschnitt werden Sie lernen, wie man damit eine Datei Zeichen für Zeichen einlesen und in eine neue Datei ausgeben kann. Natürlich hat jedes Betriebssystem für so etwas eine Systemfunktion, aber die muss ja auch mal jemand geschrieben haben. Was wollen wir also machen: Wir lesen ein Zeichen aus einer Datei und geben es aus. Das machen wir solange, bis das Ende der Datei erricht ist. In C liest sich das so: /********************************** copy01.c Einfachstes Kopierprogramm KAPITEL 2. GRUNDLAGEN 18 FT Status: incomplete **********************************/ #include <stdio.h> int main(void) { int c; c = getchar(); while(c!=EOF) { putchar(c); c = getchar(); } } Wie bereits mehrfach erkläre ich Ihnen nun was hier abläuft, vor allem was neu ist. Wer etwas nicht versteht, was bereits früher erklärt wurde, frage bitte unbedingt jetzt nach. 1. Die ersten Zeilen sind bereits bekannt. Der Status ist „incomplete“, weil jede Fehlerbehandlung fehlt. DR A 2. Die Variable c wird als int deklariert. Dies überrascht zunächst. Allerdings werden char und int in C gleichartig und soweit möglich austauschbar gehandhabt. Da es mehr ints als chars gibt, ist das Programm so etwas allgemeiner. 3. c = getchar();Hieraus lernen wir zwei Dinge. 3.1. Weiter oben haben Sie gelesen, dass getchar das Zeichen oder EOF zurückliefert. Der Wert von EOF ist implementierungsabhängig. Sie finden ihn in <stdio.h>, in MS VC7 ist er −1. Daher also die Deklaration von c als int: Auch der Wert von EOF muss aufgenommen werden können. 3.2. Wir stehen vor dem Beginn einer Schleife. Sehr oft müssen vor dem ersten Durchlauf einige Dinge erledigt werden, hier eben das Lesen des ersten Zeichens. 4. In der nächsten Zeile beginnt eine Schleife. while (Bedingung) sorgt dafür, dass der auf dieses Kommando folgende Block ausgeführt wird, wenn die Bedingung wahr ist und die Schleife verlasse wird, wenn Sie falsch ist. Die Bedingung lautet hier c!=EOF. „!=“ bedeutet „nicht gleich“, d. h. es wird geprüft, dass nicht das Zeichen „Dateiende“ gelesen wurd. 5. In dem folgenden Paar geschweifter Klammern steht zunächst ein Aufruf putchar(c). Im ersten Durchlauf der Schleife wird also das vor Schleifenbeginn gelesene Zeichen ausgegeben. Anschließend wird das nächste Zeichen gelesen usw. Wenn Sie das Programm so aufrufen, werden Sie nicht viel Freude damit haben. Probieren Sie mal aus, wie das Programm auf Eingaben reagiert. Für Weiteres warten Sie bitte bis zum nächsten Abschnitt ab, dann werden Sie hiermit richtig umgehen können. 2.9 Umlenken von Ein- und Ausgabe Betrachten wir einmal die Progrämmchen, die wir bisher kennengelernt haben, unter dem Gesichtspunkt der Benutzerfreundlichkeit und Testbarkeit. Da stört es (zumindest mich), die Eingaben — und sei es nur zum Testen — immer wieder eintippen zu müssen. Insofern hätte es einen gewissen Charme, wenn man die Eingabedaten aus einer Datei einlesen könnte. Das ist gar nicht so abstrus, wie es auf den ersten Blick erscheinen mag: • Es erleichtert das Testen: Testdaten einmal erfassen, immer wieder verwenden. • Auch große Mengen von Daten können übernommen bzw. ausgegeben werden. 2.10. TRIGRAPHS 19 Jedes C-Programm kennt drei sogenannte „file descriptors“ oder „file handle“ FT 0 Steht für die Standard-Eingabeinheit, kurz stdin 1 Steht für die Standard-Ausgabeinheit, kurz stdout 2 Steht für die Standard-Ausgabeinheit für Fehlermeldungen, kurz stdout. Nehmen Sie keine Änderungen vor, so ist stdin mit der Tastatur und stdout, stderr mit der Konsole (dem Bildschirm) verbunden. Das kann man aber ändern. Bei stdin und stdout kommt das häufig vor und geht ganz einfach. Für stderr bevorzugt man oft die Konsole, da man so direkt über Fehler informiert wird. Möchte man den Verlauf aber protokollieren, so wird man auch stderr gerne einer Datei zuordnen. Dazu gibt es aber keine derartige eingebaute Möglichkeit. Sehen wir uns das beim Programm happybirthday1.c einmal an. Dies erwartet drei Eingaben, jede einzelne durch Drücken der Enter-Taste beendet. Diese Eingaben kann ich nun in eine Datei schreiben, die in berndbirth.in nenne: 29 11 1954 Um die Eingabe von der Tastatur auf die Datei umzulenken, rufe ich das Programm nun so auf: happybirthday1 < berndbirth.in DR A Das Zeichen < bewirkt, dass die Eingabe umgeleitet wird. Entsprechend bewirkt das Zeichen >, dass die Ausgabe in eine Datei umgleitet wird. Rufe ich das Programm mit dem Befehl happybirthday1 < berndbirth.in > berndbirth.out auf, so steht in der Datei berndbirth.out Geben Sie bitte Ihr Geburtsdatum ein! Tag: Monat: Jahr: Ist Ihr Geburtsdatum wirklich der: 29.11.1954? Ihre Glückszahl ist heute: 6550 2.10 Trigraphs Trigraphs sind Folgen von drei Zeichen, die in C-Quelltext bestimmte Sonderzeichen ersetzen, welche auf manchen Tastaturen nicht vorhanden sind. Die folgende Tabelle zeigt die verfügbaren Trigraph-Zeichenketten: Trigraph ??= ??( ??) ??/ ??’ ??! ??< ??> ??- Zeichen # [ ] \\ ^ | { } ˜ Beispiel: ??=define arraycheck(a,b) a??(b??) ??!??! b??(a??) bedeutet #define arraycheck(a,b) a[b] || b[a]. Manche Compiler verarbeiten Trigraphs nicht direkt. Grund: Rechner, auf denen diese Compiler laufen, erlauben die Eingabe aller Zeichen des C-Zeichensatzes. Ein Beispiel dafür sind Borland-Compiler. Trigraphs enthaltende Quelltextdateien müssen mit einem Hilfsprogramm (TRIGRAPH) vorverarbeitet werden. Bei der Neuprogrammierung sollte - von extremen Ausnahmen abgesehen, auf den Einsatz von Trigraphs verzichtet werden. Eine der Ausnahmen könnte die Teilnahme am IOCCC sein. KAPITEL 2. GRUNDLAGEN 20 2.11 Historische Anmerkungen 2.12 Aufgaben FT Für viele erscheint es auf den ersten Blick unverständlich, dass C so elementare Dinge, wie einen Druckbefehl (u. v.˙ m) nicht direkt in der Sprache enthält, sondern dass diese Funktionalität über Bibliotheken extra zur Verfügung gestellt wird. Durch diesen modularen Aufbau ist die Sprache im Kern aber sehr schlank und einfach1 . Auf der anderen Seite ist sie durch Bibliotheken praktisch beliebig erweiterbar. Gerade deshalb erfreut sie sich einer großen Verbreitung und war der Anstoß für viele weitere Entwicklungen, wie C++ und letztendlich auch Java. 1. Hier folgen einige Deklarationen für Variablen in C. Geben Sie bitte an, ob die Namen in diesen Deklarationen gültig sind! Begründen Sie bitte Ihre Antwort! 1.1. int 1zweidrei; 1.2. int einszwei3; 1.3. char Eins_zwei_drei; 1.4. char Eins zwei drei; 1.5. char _; DR A 1.6. char [] void; 1.7. double pi; 1.8. Double E; 2. Finden Sie bitte heraus, ob char von Ihrem Compiler als signed oder unsigned interpretiert wird und belegen Sie Ihr Ergebnis! 3. Finden Sie limits.h in Ihrer Installation und vergleichen Sie mit dem hier präsentierten Ausschnitt! 4. Welche Wertebereiche haben int bzw. unsigned int auf einer 64-Bit Maschine? 5. Was passiert, wenn Sie in dem Programm summe.c die Zeile printf("%d%s%d%s%d%s",pa," + ",pb," = ",summe(pa,pb),"."); durch printf("%d%s%d%s%d%s",pa," + ",pb," = ",summe,"."); ersetzen? Versuchen Sie, dass Ergebnis zu erklären! 6. Was passiert, wenn Sie das Programm happybirthday1 mit dem Befehl happybirthday1 > berndbirth.out aufrufen? 7. Versuchen Sie bitte herauszufinden, was passiert, wenn Sie printf mit einem Formattierungszeichen %c aufrufen, wenn c nicht aus diesem Kapitel bekannt ist. Dokumentieren Sie das Ergebnis. 8. Schreiben Sie ein Programm, das eine Temperatur in Celsius als Eingabe erhält, in Fahrenheit umrechnet, und das Ergebnis ausgibt. 1 Sie ist wirklich viel einfacher, als manche andere Sprachen, auch wenn sie Ihnen zunächst als schwierig genug erscheinen mag. 2.12. AUFGABEN 21 FT 9. Schreiben Sie bitte nach dem Beispiel der Funktion summe aus Abschn. 2.6 Funktionen für Division, Multiplikation und Subtraktion und probieren Sie diese aus! 10. Schreiben Sie ein Programm, dass eine Körpergröße h in Meter und ein Gewicht w in Kilogramm erhält und den Body-Mass-Index (BMI) berechnet und ausgibt: BM I := w h2 11. Schreiben Sie Programme, die angelsächsischen Maßeinheiten in ihr Äquivalent im Meter-Kilogramm System umrechnen. Angelsächsische Einheiten sind u. a. Gewichte pound, ounce, dram, pennyweight, grain Hohlmaße gallon, quart, pint, gill, fluidounce, fluidram, minim, barrel US Trockenmaße bushel, peck, quart, pint Längen mile, rod, yard, foot, inch Die Umrechnungskoeffizienten versuchen Sie bitte zunächst selbst in Erfahrung zu bringen. 12. Schreiben Sie Funktionen, die Einheiten, die von TEX verwendet werden, in die Basiseinheit „scaled point (sp)“ umrechnen. Die Einheiten, die in TEX verwendet werden sind: Bezeichnung point pica inch big point Zentimeter Millimeter didot point cicero scaled point Wert 72.27 pt = 1 inch 1 pc = 12 pt 1 inch = 72.27 pt = 2.54 cm 72 bp = 1 inch 2.54 cm = 1 inch 10 mm = 1 cm 1157 dd = 1238 pt 1 cc = 12 dd 65536 sp = 1 pt DR A Abk. pt pc in bp cm mm dd cc sp Rufen Sie diese Funktionen in einer main Funktion auf. 13. Welche der folgenden Variablendefinitionen ist nicht zulässig oder nicht sinnvoll? 13.1. int a = 2.5; 13.2. nt b = ’?’; 13.3. char z = 500; 13.4. int big = 40000; 13.5. double fläche = 1.23E+5 13.6. long count = 0 13.7. char c = ’\’’; 13.8. unsigned char ch = ’\201’; 13.9. unsigned int size = 40000; 13.10. float value = 12345.06789; Wenn etwas falsch oder unsinnig ist, erklären Sie bitte warum. 14. Welche der folgenden Variablendeklarationen und -definitionen ist korrekt, welche falsch? Was liefert die Ausgabe mit printf ? 14.1. KAPITEL 2. GRUNDLAGEN 22 15.1. ⌈3, 99⌉ = 15.2. ⌊3, 99⌋ = 15.3. ⌈−3, 11⌉ = DR A 15.4. ⌊−3, 11⌋ = FT 15. In der C-Bibliothek gibt es eine Reihe mathematischer Funktionen. Sie finden Sie in der HeaderDatei math.h. Die Funktion ceil(double d) (Decke) liefert die kleinste ganze Zahl ⌈d⌉, die größer oder gleich d ist. Entsprechend liefert floor (Fußboden) die größte ganze Zahl ⌊d⌋, die kleiner oder gleich d ist. Beantworten Sie bitte die folgenden Fragen und bestätigen Sie durch ein C-Programm, dass die Funktionen aus der C-Bibliothek dieses Ergebnis liefern! FT Kapitel 3 Formattierte Ein- und Ausgabe 3.1 Übersicht DR A Die Ein- und Ausgaben waren sehr einfach. Auch für die kleinen Programme, die Sie hier zur Übung schreiben sollen, wollen wir uns nun die Eingabe von Werten etwas einfacher machen und die Ausgaben ansprechender gestalten. Das werden Sie auch an anderen Stellen verwenden können. Bei dieser Gelegenheit ergibt es sich auch, etwas zur Konvertierung von Zeichen in Zahlen und zurück zu erläutern. 3.2 Lernziele • Die Escape-Sequenzen in C sicher verwenden können. • Ausgabe von Zahlen und Strings sicher auf der Konsole gestalten können. • Zahlen und Strings sicher von der Konsole einlesen können. • Die Formattierungszeichen in C zur Ausgabe und Eingabe der verschiedenen C-Datentypen sicher einsetzen können. 3.3 Ausgabe Für die Ausgabe stehen uns zum Einen weitere Escape-Sequenzen zur Verfügung. Die folgende Abbildung 3.1 enthält eine vollständige Übersicht. Wollen wir etwa drei Zahlen eingeben und die Summe berechnen, so können wir das wie folgt machen. Dabei werden die bereits in Kap. 2 eingeführte Escape-Sequenz \n und die Escape-Sequenz \t verwendet: /*formio10.c Summe von drei Status: incomplete*/ #include <stdio.h> int main() { int a,b,c; printf("\nBitte geben Sie scanf("%d",&a); printf("\nBitte geben Sie scanf("%d",&b); printf("\nBitte geben Sie scanf("%d",&c); printf("\n\t%d",a); printf("\n\t%d",b); Zahlen den ersten Summanden ein:"); den zweiten Summanden ein:"); den dritten Summanden ein:"); 23 KAPITEL 3. FORMATTIERTE EIN- UND AUSGABE 24 Bedeutung \a \b \t \n \v \f \r \" \’ \? \\ \0 \ooo (max. 3 Oktalziffern \xhh (max. 2 Hex Ziffern hh alert(BELL) backspace(BS) horizontal tab(HT) line feed(LF) vertical tab(VT) form feed(FF) carriage return(CR) \" ’ ? \ NUL-Zeichen Bitmuster Bit-Muster ASCII-Wert dezimal hex 7 7 8 8 9 9 10 A 11 B 12 C 13 D 34 22 39 27 63 3F 92 5C 0 0 ooo hh FT Einzelzeichen Abbildung 3.1: Escape-Sequenzen in ANSI C DR A printf("\n\t%d",c); printf("\n\t--------"); printf("\n\t%d",a+b+c); return 0; } Geben wir nach und nach 1, 10, 100 ein, so erhalten wir: 1 10 100 -------111 Das Ergebnis ist zwar schon spaltenweise angeordnet, mindestens zwei Dinge gefallen mir an der Ausgabe aber noch nicht: 1. Ich hätte die Spalten gerne rechtsbündig, wie es bei Zahlenkolonnen üblich ist. 2. Ich möchte soviele −-Zeichen als Trennstriche haben, wie die Zahl lang ist. Beides bekommen wir teilweise hin, wenn Sie noch Folgendes wissen: Sie können zusätzlich noch die Breite angeben, die ein Feld haben soll, indem vor dem Prozentzeichen die Anzahl Stellen angeben wird. Standardmäßig wird die Ausgabe rechtsbündig angeordnet, wenn Sie noch ein −-Zeichen vor die Länge setzen, erfolgt linksbündige Ausgabe. Überzählige Stellen werden mit Blanks aufgefüllt. Geben Sie keine Feldbreite an, so ist das Feld so lang wie der jeweilige Wert, eine bündige Ausgabe kann also nicht erwartet werden. Mit diesen Änderungen sieht das Programm nun so aus /*formio11.c Summe von drei Zahlen, bessere Ausgabe Status: incomplete*/ #include <stdio.h> int main() { int a,b,c; printf("\nBitte geben Sie den ersten Summanden ein:"); scanf("%d",&a); printf("\nBitte geben Sie den zweiten Summanden ein:"); 3.3. AUSGABE 25 FT scanf("%d",&b); printf("\nBitte geben Sie den dritten Summanden ein:"); scanf("%d",&c); printf("\n\t%5d",a); printf("\n\t%5d",b); printf("\n\t%5d",c); printf("\n\t%5s","---"); printf("\n\t%5d",a+b+c); return 0; } Das Ergebnis 1 11 111 --123 entspricht weitgehend meinen Wünschen. Folgendes fehlt aber noch: DR A 1. Die Anzahl −-Zeichen wird noch nicht an die längste Zahl angepasst. Wie man das macht, kann ich Ihnen erst später erklären. 2. Die Eingabe ist völlig unsicher. Probieren Sie doch mal, eine Zahl mit Punkt oder Komma einzugeben! Deshalb ist der Status auch dieses Programmes (natürlich) „incomplete“. Die Tabelle in Abb. 3.2 zeigt alle Formattierungsbuchstaben für ganze Zahlen, dezimal, oktal und hexadezimal. Im Formattierungsstring werden diese Buchstaben durch ein vorgestelltes %-Zeichen gekennzeichnet. Beachten Sie bitte, dass Zeichen hier wie ganze Zahlen behandelt werden und deshalb ebenfalls in Typ c d ld u o x X Argumenttyp char oder int(≤ 255) int long int unsigned int unsigned int unsigned int unsigned int Darstellung Einzelnes Zeichen dezimal dezimal dezimal oktal hexadezimal mit a,b,c,d,e,f hexadezimal mit A,B,C,D,E,F Abbildung 3.2: Ausgabe von Zeichen und ganzen Zahlen dieser Tabelle erscheinen. Das folgende Programm gibt die Eingaben, die wir bereits in dem Programm formio11.c gemacht haben in einigen dieser Formate zum Vergleich aus: /*formio12.c Summe von drei Status: incomplete*/ #include <stdio.h> int main() { int a,b,c; printf("\nBitte geben Sie scanf("%d",&a); printf("\nBitte geben Sie scanf("%d",&b); printf("\nBitte geben Sie scanf("%d",&c); Zahlen, in vier Formaten den ersten Summanden ein:"); den zweiten Summanden ein:"); den dritten Summanden ein:"); KAPITEL 3. FORMATTIERTE EIN- UND AUSGABE 26 } FT printf("\n\tZeichen\tdez.\tokt.\thex."); printf("\n\t%3c\t%3d\t%3o\t%3X",a,a,a,a); printf("\n\t%3c\t%3d\t%3o\t%3X",b,b,b,b); printf("\n\t%3c\t%3d\t%3o\t%3X",c,c,c,c); printf("\n\t%3s\t%3s\t%3s\t%3s","-","---","---","--"); printf("\n\t%3c\t%3d\t%3o\t%3X",a+b+c,a+b+c,a+b+c,a+b+c); return 0; Das Ergebnis sieht im wesentlichen so aus: Bitte geben Sie den ersten Summanden ein:1 Bitte geben Sie den zweiten Summanden ein:11 Bitte geben Sie den dritten Summanden ein:111 dez. 1 11 111 --123 okt. 1 13 157 --173 hex. 1 B 6F -7B DR A Zeichen ? ? o { Aus der Ausgabe sehen wir u. a., dass der ASCII Code für den Buchstaben „o“ decimal 111 und hexadezimal 6F ist. Die beiden „?“ stehen für Zeichen, die ich nicht zur Verfügung hatte. Ganz analog werden Gleitkommazahlen eingegeben und ausgegeben. Wie bei ganzen Zahlen kann auch Typ f lf e/le E/lE g/lg G/lG s Argumenttyp float/double double float/double float/double float/double float/double String Darstellung Gleitpunktzahl: [−]ddd.dd Gleitpunktzahl: [−]ddd.dd Exponentenschreibweise: [−]d.dde ± dd Exponentenschreibweise: [−]d.ddE ± dd wie e, f das jeweils kürzere Analog zu E Ausgabe bis ’\0’ Abbildung 3.3: Ausgabe von Gleitkommazahlen und Strings bei Gleitkommazahlen und Strings eine Feldbreite angegeben werden. Nach der Feldbreite bei Gleitkommazahlen kann ein Punkt und dann die Anzahl Nachkommastellen folgen. Die Anzahl Nachkommastellen und der Dezimalpunkt sind Teil der Feldbreite! Bei Genauigkeit „0“ wird kein Dezimalpunkt ausgegeben. Sehen wir uns auch hierfür ein Beispiel an: /*formio13.c Summe von drei Status: incomplete*/ #include <stdio.h> int main() { float a,b,c; printf("\nBitte geben Sie scanf("%f",&a); printf("\nBitte geben Sie scanf("%f",&b); printf("\nBitte geben Sie scanf("%f",&c); Gleitkommazahlen, den ersten Summanden ein:"); den zweiten Summanden ein:"); den dritten Summanden ein:"); 3.4. EINGABE 27 } FT printf("\n\t%10f",a); printf("\n\t%10f",b); printf("\n\t%10f",c); printf("\n\t%10s","----------"); printf("\n\t%10f",a+b+c); return 0; Dieses Programm unterscheidet sich bis auf den Datentyp float, die Typangabe f für scanf bzw. printf und die Feldbreite 10 bei der Ausgabe nicht von dem früheren Programm formio11.c. Es liefert die Ausgabe: Bitte geben Sie den ersten Summanden ein:1.1 Bitte geben Sie den zweiten Summanden ein:10.10 Bitte geben Sie den dritten Summanden ein:100.100 DR A 1.100000 10.100000 100.099998 ---------111.299999 Das sollte fast alles für Sie nachvollziehbar sein. Aber warum steht in der Ausgabe 100.099998, wo ich doch 100.1 eingegeben habe? Das ist etwas, auf dass Sie bei einem Rechner immer gefasst sein müssen. Rechner stellen Gleitkommazahlen immer nur näherungsweise dar. Sie haben also immer Rundungsfehler. Bei wichtigen Rechnungen müssen Sie sich überlegen, ob das Ergebnis hinreichend genau ist, um ihm „trauen“ zu können. Aber das ist ein Thema der Mathematik. Ich werde es in diesem Kurs nur dann behandeln, wenn wir zu Themen kommen, bei denen Rundungsfehler eine Rolle spielen. 3.4 Eingabe Auch bei der Eingabe können wir noch etwas lernen. Ersetzen Sie doch mal in dem Programm formio13.c in der ersten „richtigen“ Codezeile float durch double. Vielleicht erreichen wir mit einer höheren Rechengenauigkeit einen Wert der näher an 100.100 liegt. Die Ausgabe ist dann: Bitte geben Sie den ersten Summanden ein:1.1 Bitte geben Sie den zweiten Summanden ein:10.10 Bitte geben Sie den dritten Summanden ein:100.100 0.000000 0.000000 -32012345804664886000000000000000000000000000000000 0000000000000000000000000000000000000000.000000 ----------32012345804664886000000000000000000000000000000000 0000000000000000000000000000000000000000.000000 Lediglich aus satztechnischen Gründen habe ich einige Nullen entfernt, aber das Ergebnis hat natürlich nichts mit dem zu tun was erreicht werden sollte. Die Erklärung hierfür liegt in dere Funktion scanf. Der müssen wir durch ein geeignetes Zeichen den Typ double angeben. Dies ist sozusagen „long float“ und deshalb müssen wir statt %f hier %lf bei scanf angeben. Das neue Programm sieht dann so aus: KAPITEL 3. FORMATTIERTE EIN- UND AUSGABE 28 FT /*formio15.c Summe von drei double Gleitkommazahlen, Status: incomplete*/ #include <stdio.h> int main() { double a,b,c; printf("\nBitte geben Sie den ersten Summanden ein:"); scanf("%lf",&a); printf("\nBitte geben Sie den zweiten Summanden ein:"); scanf("%lf",&b); printf("\nBitte geben Sie den dritten Summanden ein:"); scanf("%lf",&c); printf("\n\t%10f",a); printf("\n\t%10f",b); printf("\n\t%10f",c); printf("\n\t%10s","----------"); printf("\n\t%10f",a+b+c); return 0; } Das Ergebnis sieht nun schon viel besser aus: DR A Bitte geben Sie den ersten Summanden ein:1.1 Bitte geben Sie den zweiten Summanden ein:10.10 Bitte geben Sie den dritten Summanden ein:100.100 1.100000 10.100000 100.100000 ---------111.300000 Sehen wir uns aus diesem Anlass die Gleitkommazahlen etwas genauer an. Die Abb. 3.4 zeigt den grund- Bitposition v 63 Exponent 62 53 Mantisse 52 0 Abbildung 3.4: Datentyp double nach IEEE (64 Bit) sätzlichen Aufbau einer Gleitkommazahl. Der Wert g einer Gleitkommazahl ergibt sich dann so: g = v · M antisse · 2Exponent In float.h finden Sie die Längen aller dieser Dinge in Ihrer Installation. Die Abb. 3.5 zeigt typische Eigenschaften von Gleitkommazahlen. Das erklärt nun auch den Effekt mit 100.099998 bei unserem ersten Versuch, Gleitkommazahlen von der Tastatur einzulesen (Programm formio13.c). Es ist ja 100.100 = ≈ = 1.5640625 · 26 1.564062 · 26 100.099968 Wie bereits bei den ganzen Zahlen sind die Werte in Abb. 3.5 die, die der Standard garantiert. Die Werte Ihrer Installation finden Sie in float.h. Die Zahlen in Abb. 3.5 ergeben sich z. B. so: Auf einem 32 Bit 3.4. EINGABE float double long double Speicherplatz Wertbereich dezimal, ohne Vorzeichen kleinstmöglich größmöglich 1.2 · 10−38 3.4 · 1038 −308 2.3 · 10 1.7 · 10308 −4932 3.4 · 10 1.1 · 104932 Genauigkeit dezimal FT Typ 29 4 Bytes 8 Bytes 10 Bytes 6 Stellen 15 Stellen 19 Stellen Abbildung 3.5: Datentypen für Gleitkommazahlen Rechner belegt eine Variable vom Typ double 64 Bit. Eines für das Vorzeichen, 10 für den Exponenten und 53 für die Mantisse 10 22 = 21024 ≈ 10308 Mit dem Operator sizeof können Sie jederzeit feststellen, wie lang Variablen bzw. Typen sind, z. B. sizeof(short). Sehen wir uns nun einmal an, wie scanf genauer arbeitet. Auch diese Funktion kann mehrere Zeichen im Formattierungsstring haben. Ein Beispiel: DR A /*formio16.c Eingabe mehrerer Elemente in einer Zeile Status: incomplete*/ #include <stdio.h> int main() { char c; int i; float f; double d; printf("\nGeben Sie bitte c i f d ein:"); scanf("%c %d %f %lf",&c, &i, &f, &d); printf("\nc = %c",c); printf("\ni = %d",i); printf("\nf = %12.10f",f); printf("\nd = %12.10f",d); return 0; } Dieses Programm liefert z. B. Folgendes: Geben Sie bitte c i f d ein:A 8 8.8 8.888888888 c i f d = = = = A 8 8.8000001907 8.8888888880 Wie schon üblich hier wieder die Erläuterungen dazu. 1. In den ersten vier Zeilen im Block der main-Funktion werden vier Variablen deklariert, die später mit Inhalt gefüllt werden sollen. Das hätte man auch in einer Zeile machen können. Es ist aber in C++ und Java üblich, nur eine Variable pro Zeile zu deklarieren oder zumindest nicht Variablen verschiedener Typen. Außerdem haben Sie im Ausdruck so mehr Platz für eigene Notizen. 2. In der nächsten Zeile werden Sie zur Eingabe von c i f d aufgefordert. Nehmen Sie das wörtlich, so erhalten Sie z. B. folgende Ausgabe: KAPITEL 3. FORMATTIERTE EIN- UND AUSGABE 30 Geben Sie bitte c i f d ein:c i f d = = = = c 1245120 0.0000000000 -66350771293251242000000000000000000000000000000000.0000000000 FT c i f d Sie sehen: Man muss einem Benutzer (neudeutsch: User) schon genau sagen, was er machen soll. Rechnen Sie immer mit dem DAU, dem Dümmsten Anzunehmenden User. Das sonderbare Ergebnis werde ich Ihnen im Abschn. 3.6 erläutern. 3. Anschließend wird scanf aufgerufen, um die vier Elemente einzulesen. scanf überliest die Zwischenraumzeichen (Leer-, Tabulator oder Newline-Zeichen). Ein Eingabefeld endet mit dem ersten Zwischenraumzeichen oder dem ersten Zeichen, das nicht mehr verarbeitet werden kann. Das ist z. B. der Fall wenn eine numerische Variable eingelesen werden soll und ein Buchstabe direkt auf die Ziffern folgt. DR A 4. Bei der Ausgabe der Gleitkommazahlen sehen Sie eine Anwendung der Angaben für Feldbreite und Genauigkeit. Die Zahl vor dem Punkt gibt die Gesamtlänge an, die Zahl nach dem Punkt die Genauigkeit, d. h. bei numerischen Elementen die Anzahl Nachkommastellen, bei Strings die Anzahl Stellen des Strings, die maximal ausgegeben wird. 8 . ←− 8 8 8 8 8 8 8 12 8 8 0 −→ Der Dezimalpunkt wird dabei mitgezählt. Was passiert aber nun, wenn eine zu lange Eingabe getätigt wird? Hier ein Beispiel: /*formio17.c Fehlerhafte Eingabe bei scanf Status: incomplete*/ #include <stdio.h> int main() { long konto, blz; printf("\nGeben Sie bitte die Kontonummer ein:"); scanf("%7ld",&konto); printf("\nGeben Sie bitte die Bankleitzahl ein:"); scanf("%8ld",&blz); printf("\nKonto = \t%8ld",konto); printf("\nBLZ = \t%8ld",blz); return 0; } Geben Sie bitte die Kontonummer ein:12345678 Geben Sie bitte die Bankleitzahl ein: Konto = 1234567 BLZ = 8 Was ist hier schiefgelaufen? 1. Zunächsteinmal habe ich eine zu lange Kontonummer eingegeben: 8-stellig, während im Programm eine 7-stellige erwartet wird, in scanf steht ja "%7ld". 3.4. EINGABE 31 FT 2. scanf verarbeitet also die ersten sieben Zeichen der Eingabe. Anschließend steht aber noch das achte Zeichen, hier eine 8 im Eingabepuffer zur Verfügung. Dies wird nun von scanf gleich mitverarbeitet, so dass wir gar keine Chance mehr haben eine Bankleitzahl (BLZ) einzugeben. In der Variablen blz steht nun als 8. Wie löst man dieses Problem? Dazu gibt es die Funktion fflush. Diese Funktion erwartet als Parameter einen Zeiger (siehe Kap. 9) auf eine Datei, in diesem Fall auf die Standardeingabe stdin. Diese Funktion leert den Eingabepuffer. Fügen wir einen Aufruf dieser Funktion nach jedem Aufruf von scanf ein, so haben wir zumindest diese eine Fehlermöglichkeit erfolgreich beseitigt. DR A /*formio18.c Eingabe bei scanf und fflush Status: incomplete*/ #include <stdio.h> int main() { long konto, blz; printf("\nGeben Sie bitte die Kontonummer ein:"); scanf("%7ld",&konto); fflush(stdin); printf("\nGeben Sie bitte die Bankleitzahl ein:"); scanf("%8ld",&blz); printf("\nKonto = \t%8ld",konto); printf("\nBLZ = \t%8ld",blz); return 0; } Geben Sie bitte die Kontonummer ein:12345678 Geben Sie bitte die Bankleitzahl ein:20070024 Konto = 1234567 BLZ = 20070024 Auf den Aufruf von fflush nach dem zweiten scanf habe ich verzichtet, da keine weitere EIngabe beabsichtigt ist. Das Ergebnis kann natürlich noch nicht ganz befriedigen. Der Benutzer sollte bei derartigen Fehleingaben eine Nachricht bekommen und das Programm nicht einfach „eigenmächtig“ die Eingabe abschneiden. Aber hier geht es uns ja zunächst um die Grundlagen der Syntax von C. Wie können wir nun überprüfen, ob wir alle Felder gelesen haben? scanf liefert als Rückgabewert die Anzahl eingelesener Felder oder den Wert EOF. Dieser Wert wird auch zurückgeliefert, wenn ein Dateiende durch Drücken von Ctrl-Z (DOS) oder Ctrl-D (UNIX). Wie sich das auswirkt, zeigt das folgende Beispiel: /*formio19.c Rückgabewert von scanf und Ctrl z Status: incomplete*/ #include <stdio.h> int main() { long konto; long rueckgabe; printf("\nGeben Sie bitte die Kontonummer ein:"); rueckgabe = scanf("%7ld",&konto); printf("Der Rueckgabewert von scanf ist: %ld",rueckgabe); printf("\nKonto = \t%8ld",konto); return 0; } KAPITEL 3. FORMATTIERTE EIN- UND AUSGABE 32 FT Geben Sie bitte die Kontonummer ein:^Z Der Rueckgabewert von scanf ist: -1 Konto = 29657673 ^Z steht dabei für „Ctrl-Z“. In der Kontonummer steht nun „Müll“, aber Sie könnten das erkennen, indem Sie den Rückgabewert von scanf abfragen. Die Angabe einer Genauigkeit der Eingabe ist bei scanf nicht möglich. 3.5 Datentypenkonvertierung Variablen können in C mit großer Freiheit von einem Datentyp in einen anderen konvertiert werden. Das ist mal notwendig, mal nützlich, aber nicht unbedingt notwendig und manchmal sehr gefährlich. Sehen wir uns dochmal an, was wir bisher kennen. So haben wir im Zusammenhang mit dem Programm formio16.c auf Seite 29 bei einer Fehleingabe ein zunächst unverständliches Ergebnis erhalten. Dies sehen wir uns zunächst näher an. 1. Das erste Zeichen ist typgerecht ein Zeichen und wird korrekt eingelesen. DR A 2. Geben wir uns wie in formio19 vorgeführt den Rückgabewert von scanf aus, so sehen wir, dass 1 Eingabefeld gelesen wurde. Die anderen drei Felder wurden also gar nicht eingelesen. In den Variablen steht also irgenwelcher Müll, der zufällig gerade an den Speicherplätzen dieser Variablen stand. Sie sollten daraus lernen, dass es sehr sinnvoll ist, Variablen nur dann ohne Definition zu deklarieren, wenn man sich ganz sicher ist, dass sie vor der ersten Verwendung noch einen sinnvollen Wert zugewiesen bekommen. Hier hat also keine Konvertierung stattgefunden. scanf hat einfach eingelesen oder nicht. Wenn eingelesen wurde, stand auch ein sinnvolles Ergebnis in den Variablen. Etwas anders sieht es aus, wenn man mit Variablen in einem Programm hantiert. Beispiel /****************************************** datatypes04.c Konvertierungen int2char etc. Status: ok *******************************************/ int main(void) { char ca = ’a’; int ia = 88; char cb = ia; int ib = ca; printf("\nVar. Zeichen\tDez.\tHex"); printf("\nca = \t%c\t%d\t%x",ca,ca,ca); printf("\nia = \t%c\t%d\t%X",ia,ia,ia); printf("\ncb = \t%c\t%d\t%X",cb,cb,cb); printf("\nib = \t%c\t%d\t%x",ib,ib,ib); return 0; } Var. Zeichen ca = a ia = X cb = X ib = a Dez. 97 88 88 97 Hex 61 58 58 61 In diesem Programm werden einfach je zwei int und zwei char Variablen deklariert, je einer ein Wert zugewiesen und dann „über Kreuz“ zugewiesen. Der Compiler nimmt das ohne Warnung hin. Verwendet man aber den Schalter /Wall, so bringt er eine korrekte Warnung für Zeile 10. 3.6. AUFGABEN 33 FT warning C4242: ’Initialisierung’: Konvertierung von ’char’, möglicher Datenverlust und ich erläutere Ihnen jetzt die Ausgabe: 1. In der ersten Zeile sehen Sie den Inhalt der Variablen ca als Zeichen und in Dezimal- bzw. Hexadezimaler Darstellung. Obwohl es ein Zeichen ist, wird es ohne Probleme als Zahl ausgegeben. Das ist die ASCII-Codierung des Zeichens. 2. Umgekehrt wird die Variable ia, der der Wert 88 zugewiesen wurde ohne Probleme als Zeichen interpretiert. Wir sehen: Dezimal 88 ist der ASCII-Code für den Buchstaben „X“. 3. Auch durch die Zuweisungen einer int Variablen zu einer char bleibt alles ok. Generell gilt: Weisen Sie einer int Variablen eine char Variable zu und anschließend letzterer wieder die ganzzahlige, so geht keine Information verloren. Anders herum kann aber Information verloren gehen, siehe hierzu Aufgabe 2. Durch den Cast-Operator erzwingen Sie eine Datentypumwandlung: typ1 variable1; typ2 variable2; variable1 = (typ1) variable2; DR A Wozu das brauchen, werden Sie später lernen. Hier nur noch der kleine Hinweis, dass dieser Programmausschnitt eine Umwandlungswarnung bringen würde, egal welche Typen ich für typ1 oder typ2 einsetze und ob die Datentypumwandlung zulässig ist: Ich verwende die Variable variable2 vor einer Initialisierung. 3.6 Aufgaben 1. Führen Sie das Programm formio11 aus und geben Sie für einen oder mehrere Summanden ungültige Werte ein. 2. Probieren Sie aus, was passiert, wenn Sie im Programm datatypes04.c der Variablen ia statt 88 die Zahl 888 zuweisen und Erklären Sie, was passiert! 3. Schreiben Sie ein Programm, dass folgendes tut: • Es gibt die Zahl 0.123456 als Gleitpunktzahl linksbündig mit Feldbreite 15 aus. • Es gibt die Zahl 123.456 in exponentieller Schreibweise rechtsbündig mit Feldbreite 10 ausgegeben werden. • Die Zahl 12.345678 soll auf zwei Stellen hinter dem Dezimalpunkt gerundet rechtsbündig mit der Feldbreite 12 ausgegeben werden. 4. Schreiben Sie ein C-Programm, dass eine dreistellige ganze Zahl von der Tastatur einliest und diese als Zeichen, dezimal, oktal und hexadezimal ausgibt. 5. Schreiben Sie ein C-Programm, dass eine achtstellige Artikelnummer, eine Stückzahl und einen Stückpreis von der Tastatur einliest und diese Werte ergänzt um den Gesamtpreis (Stückzahl * Stückpreis) auf dem Bildschirm ausgibt. 6. In dieser Aufgabe sollen Sie lernen, welcher Speicherplatz für Variablen reserviert wird. Mit dem sizeof-Operator kann die Anzahl der Bytes ermittelt werden, die Variablen eines bestimmten Datentyps im Hauptspeicher belegen. Beispiel: sizeof(short) hat den Wert 2. Sie können bei sizeof aber auch einen Variablennamen oder eine Konstante angeben. Schreiben Sie bitte ein C-Programm, das für jeden elementaren Datentyp mittels des sizeofOperators die Größe des benötigten Speicherplatzes auf dem Bildschirm ausgibt. KAPITEL 3. FORMATTIERTE EIN- UND AUSGABE DR A FT 34 Operatoren 4.1 Übersicht FT Kapitel 4 DR A In diesem Kapitel lernen Sie die grundlegenden Operatoren für Rechenoperationen, Vergleiche, etc. kennen, die Sie brauchen, um in Kap. 5 den Programmablauf zu steuern. Diese Operatoren realisieren die aus der Schulmathematik bekannten elementaren Rechenoperationen oder logische Operationen ebenso, wie die eher C-spezifischen Prä- und Postfix-Inkrementoperationen. In C sind darüberhinaus zusammengesetzte Zuweisungen beliebt. Außerdem gibt es in C++ Operatoren für die Manipulation auf Bit-Ebene. Da dies für Technik-Studierende interessant sein kann, präsentiere ich Sie bereits in diesem Kapitel. Zum Abschluss dieser Übersicht noch eine Warnung: Ich weigere mich, die Vorrangfolge von Operatoren in irgendeinem System auswendig zu lernen. Im Zweifel kann man immer Klammern setzen und meistens ist die Aussage dann auch besser verständlich. Denken Sie also bitte auch — aber nicht nicht nur — bei der Lösung von Aufgaben in Tests, Praktikum und Klausur darauf sorgfältig Klammern zu setzen. Lösungen, die auf die Standardreihenfolge der Auswertung setzen und in denen keine Klammern gesetzt werden, werden als falsch bewertet werden! 4.2 Lernziele • Elementare Rechenoperationen mit numerischen Datentypen in C sicher durchführen können. • Den C-Stil der Verwendung von Inkrement-Operatoren und zusammengesetzten Zuweisungen kennen. • Vergleichsoperationen sicher anwenden können. • Logische Operatoren sicher verwenden können. • Operationen für Bit-Manipulation kennen. 4.3 Rechenoperatoren Die elementaren Rechenoperationen in Abb. 4.1 sollten unmittelbar verständlich sein. Schließlich kennen Sie die meisten aus der Schule Wenn nicht: Fragen Sie bitte! Ungewohnt könnte der Stern (*) als Zeichen für die Multiplikation sein, wo in handschriftlichen Rechnungen eher ·, × verwendet oder die Zahlen oder Variablen einfach nebeneinander geschrieben werden. Unbekannt könnte die Moduldivision % sein. a%b liefert den Rest, der bleibt, wenn a durch b dividiert wird. Deshalb hierzu ein einfaches Beispielprogramm. /*op01.c Beispielprogramm zur Moduldivision Status: incomplete*/ 35 KAPITEL 4. OPERATOREN 36 Bedeutung Addition Subtraktion Multiplikation Division Modulodivision Mathematisches Äquivalent a+b a−b a·b a/b Rest von a/b FT Operator + * / % Abbildung 4.1: Binäre arithmetische Operatoren #include <stdio.h> int main() { int a, b; printf("Geben Sie bitte zwei Ganze Zahlen durch ein oder mehrere\ Leerzeichen getrennt, ein:"); scanf("%d%d",&a,&b); printf("%d = %d mod %d",a,b,a%b); return 0; } DR A Wie das Ergebnis aussieht, probieren Sie bitte selber aus! Damit haben wir die binären arithmetischen Operatoren fast vollständig behandelt. Es bleibt nur noch zu klären, welchen Typ das Ergebnis hat, wenn Elemente mit unterschiedlichen Typen verknüpft werden. Werden Operanden unterschiedlichen Typs verknüpft, so wird der jeweils „kleinere“ Oeprator in den größeren Typ konvertiert. Es gelten für diese impliziten Typkonvertierungen in C also die in der Tabelle in Abb. 4.2 dargestellten Konvertierungsregeln. op1 long double numerisch≤long double float numerisch≤ float unsigned long numerisch ≤ unsigned long long numerisch ≤ long unsigned int numerisch ≤ int op2 numerisch≤long double long double numerisch≤ float float ≤unsigned long unsigned long numerisch ≤ long long numerisch ≤ int unsigned int Umwandlung in long double long double float float unsigned long unsigned long long long unsigned int unsigned int Abbildung 4.2: Typkonvertierungsregeln für binäre arithmetische Operatoren Sie sehen daraus: Der „größere“ Datentyp gewinnt. Dies sind hoffentlich alle Fälle, bis auf den in dem geprüft werden muss, ob long int alles darstellen kann, was unsigned int kann. Für die Einzelheiten siehe die in [KR88], S. 198, zusammengestellten Regeln. Explizite Typkonvertierungen sind ganz knapp in Abschn. 3.5 vorgestellt worden. Alle Konvertierungen, die zu Informationsverlusten führen sollten zu Warnungen des Compilers führen. Dies ist aber nicht gewährleistet. Es hängt davon ab, welches Niveau Sie für Warnungen einstellen. Mit dem Befehl cl /? erhalten Sie eine Liste der Compiler-Optionen. EInige davon sind in Abschn. F.1 beschrieben. Nun wenden wir uns den unären arithmetischen Operatoren zu, die sie bisher wahrscheinlich noch nicht alle kennen. Die Tabelle in Abb. 4.3 zeigt die Grundzüge, lässt aber einen wichtigen Unterschied zwischen den Präfix- un Postfix-Inkrementoperatoren nicht erkennen. Lesen Sie also auch den übernächsten Absatz! Es gibt drei unäre Operatoren in C. De Vorzeichen Operator (−/+) kennen Sie wahrscheinlich zumindest von Ihrem Taschenrechner. Er + liefert lediglich den Wert des Operanden. Der Vorzeichenoperator - erhält den Wert des Operanden und ändert das Vorzeichen. Interessanter sind da schon die Inkrement- und Dekrement-Operatoren. Von diesen gibt es jeweils eine Präfix- und eine Postfix-Variante. Steht der Operator vor der Variablen (Präfix) so wird er auf die Variable 4.3. RECHENOPERATOREN Bedeutung Vorzeichenoperator Inkrement-Operator Dekrement-Operator Mathematisches Äquivalent +a, -a ++a, a++ (a =a + 1) --a, a-- (a = a -1) FT Operator +, ++ -- 37 Abbildung 4.3: Unäre arithmetische Operatoren angewandt und anschließend wird die veränderte Variable im jeweilige Ausdruck verwendet. Steht der Operator hinter der Variablen (Postfix) so wird die Variable zunächst mit ihrem alten Wert im jeweiligen Ausdruck verwendet und anschließend der Operator verwendet. Zunächst ein ein ganz einfaches Beispiel hierzu: DR A /*op02.c Beispielprogramm zu Inkrementoperatoren Status: incomplete*/ #include <stdio.h> int main() { int a = 8; int b = 88; printf("\na++ = %d",a++); printf("\na = %d",a); printf("\n++b = %d",++b); printf("\nb = %d",b); return 0; } a++ = 8 a = 9 ++b = 89 b = 89 Spätestens jetzt sollten Sie die Wirkung nachvollziehen können: In der ersten „printf“-Zeile wird a++ ausgegeben. Dies Ausgabe lautet, wie nach Definition zu erwarten, 8, da erst der Ausdruck ausgewertet und dann der Postfix-Inkrementoperator angewendet wird. In der nächsten Zeile, also nach Auswertung des Ausdrucks, hat er aber den um eins erhöhten Wert 9. Die Anwendung des Präfix-Inkrementoperators führt aber gleich zur Ausgabe von 89, da dieser vor der Ausführung des Ausdrucks ausgewertet wird. Sehen wir uns das an einem weiteren Beispiel aus [KR88] an: Die Funktion squeeze(s,c) entfernt alle Exemplare des Zeichens c aus dem String s. /*squeeze delete all c from a */ void squeeze(char *s, int c) { int i, j; for(i = j = 0; s[i] != ’\0’;i++) if(s[i] != c) s[j++]= s[i]; s[j] = ’\0’; } Immer, wenn ein nicht-c Zeichen vorkommt, wird es an die j-te Stelle kopiert und erst anschließend wird j um eins erhöht. Dieser Teil ist also äquivalent zu folgendem Code: if(s[i]!= c) { s[j] = s[i]; KAPITEL 4. OPERATOREN 38 j++; FT } Die erstere Variante entspricht dem in C (und C++) üblichen Stil. Hier zur Illustration eine einfache Anwendung: DR A /*op03.c Einfaches Beispiel Postfix-Inkrement Status: incomplete*/ #include <stdio.h> /*squeeze delete all c from a */ void squeeze(char *s, int c) { int i, j; for(i = j = 0; s[i] != ’\0’;i++) if(s[i] != c) s[j++]= s[i]; s[j] = ’\0’; } int main() { char* s; char c; printf("\nGeben Sie bitte einen String ein:"); scanf("%s",s); fflush(stdin); printf("\nGeben Sie bitte ein Zeichen ein:"); scanf("%c",&c); squeeze(s,c); printf("\n%s",s); return 0; } Dieses Programm wenden wir nun auf ein bekanntes Palindrom an: Geben Sie bitte einen String ein:EINNEGERMITGAZELLEZAGTIMREGENNIE Geben Sie bitte ein Zeichen ein:E INNGRMITGAZLLZAGTIMRGNNI Soviel zu den arithmetischen Operatoren und einer ihrer häufigsten Anwendungen, in Schleifen, loops. Die Einzelheiten der verschiedenen Schleifenkonstruktionen in C werden in Kap. 5 besprochen. 4.4 Zusammengesetzte Zuweisungen Außer der bereits in den ersten Kapiteln eingeführten Zuweisung, z. B. int a = 1, werden in C gerne zusammengesetzte Zuweisungen verwendet. Einige davon und ihre „einfachen“ äquivalenten Formulierungen habe ich in Abb. 4.4 zusammengestellt. Das spart Leerzeichen und die Wiederholung des Variablennamens. Studien darüber, ob die geringere Anzahl Zeichen, es ProgrammiererInnen ermöglicht, den Code schneller zu verstehen sind mir nicht bekannt. Nach allen meinen Beobachtungen ist es dem Compiler egal, ob sie einfache Zuweisungen und binäre arithmetische Operationen verwenden oder die zusammengesetzte Form. Der erzeugte Code ist der gleiche. Derartige zusammengesetzte Zuweisungen kann man mit jedem binären arithmetischen Operator und auch den Bitoperatoren bilden. Es gibt also +=, -=, *=, /=, %=. Achten Sie bei den zusammengesetzten Zuweisungen bitte drauf, dass hier implizit Klammern gesetzt werden, wie Sie es in den Zeilen 3 und 4 der Abb. 4.4 auf Seite 39 sehen. 4.5. VERGLEICHOPERATOREN 39 Äquivalente Formulierung i=i+3 i=i-5 i = i * (j + 2) i = i / (k - 2) FT Zusammengesetze Zuweisung i+=3 i-=5 i*= j + 2 i/= k-2 Abbildung 4.4: Zusammengesetzte Zuweisungen 4.5 Vergleichoperatoren In diesem Abschnitt erhalten Sie eine Übersicht über die Vergleichsoperatoren. Um diese halbwegs sinnvoll erläutern zu können, benötige ich bereits eine Kontrollstruktur if - then - else, die erst im nächsten Kapitel systematisch eingeführt werden wird. Ich hoffe, sie ist auch so verständlich. Abbildung 4.6 enthält eine Tabelle der (binären) Vergleichsoperatoren. Bedeutung kleiner kleiner gleich größer größer gleich gleich ungleich DR A Operator < <= > >= == != Abbildung 4.5: Vergleichsoperatoren Das folgende Programm zeigt einige mehr oder weniger abstruse Anwendungsbeispiele. Es läuft in einer Endlosschleife, bis es durch Ctrl-c abgebrochen wird. /*op04.c Beispielprogramm zu Vergleichsoperatoren Status: incomplete*/ #include <stdio.h> int main() { int a; int b; srand(6); for(;;) { b = rand(); printf("\nGeben Sie bitte eine ganze Zahl ein: "); scanf("%d",&a); if (a == b) printf("\nTreffer"); else if( a <= b) printf("\nTiefflieger"); else printf("\nUeberflieger"); } return 0; } Versuchen Sie mal einen Treffer zu landen und erklären Sie, warum das so schwierig ist!. KAPITEL 4. OPERATOREN 40 4.6 Logische Operatoren FT Aussagenlogik sollte einfach sein. Aber in vielen Sprachen weicht man von den mathematischen oder logischen Regeln gravierend ab. So wird im Spanischen oft eine doppelte Negation verwendet, die dann als Verstärkung gedacht ist. Das geht schon soweit, das eine einfache Negation gar nicht mehr wahrgenommen wird. In C ist das eigentlich ganz einfach. Für mich als Mathematiker ist nur unverständlich, warum es keine booleschen Variablen gibt und man immer entsprechend der Tabelle in Abb. 4.8 auf Seite 40 umdenken muss. Mit diesen Operatoren können Sie verschiedene Aussagen verknüpfen. Achten Sie bitte darauf, dass Operator && || ! Bedeutung und oder nicht, Negation Abbildung 4.6: Logische Operatoren „oder“ etwas anderes ist, als „entweder oder“. Genaueres entnehmen Sie bitte der Tabelle in Abb. 4.7. In y W F W F x&&y W F F F x||y W W W F !x F F W W DR A x W W F F Abbildung 4.7: Wahrheitstafel für logische Operatoren Bool C ist das im Prinzip genauso einfach. Sie müssen aber Folgendes berücksichtigen: „Wahr“ heißt „ungleich 0“, „Falsch“ heißt „0“. In tabellarischer Form sehen Sie dies in Abb. 4.8 Mit diesen „Ersetzungen“ sieht die Boolesch Wahr (true) Falsch (false) C !=0 =0 Abbildung 4.8: Wahrheitswerte Boolesch und in C Tabelle aus Abb. 4.7 aus, wie in 4.9. Statt 1 könnte dort jeder von 0 verschiedene Wert stehen. Das sollte x !=0 !=0 0 0 y !=0 0 !=0 0 x&&y 1 0 0 0 x||y 1 1 1 0 !x 0 0 1 1 Abbildung 4.9: Wahrheitstafel für logische Operatoren im C-Stil mit zu den Dingen gehören, die sie genau und ohne Nachschlagen wissen. Den einzigen unären, logischen oder booleschen Operator in C sehen Sie in der Abb. 4.10. Operator ! Bedeutung nicht Abbildung 4.10: Unäre boolesche Operatoren 4.7. DER TERNÄRE OPERATOR ?: 41 Der ternäre Operator ?: FT 4.7 Der ternäre Operator ?: Ausdruck ? Ausdruck1 : Ausdruck2 heißt Auswahloperator und dient zum kompakten Forumlieren von Entscheidungen und arbeitet wie in Abb. 4.11 visualisiert. Der Ausdruck wird ausgewertet. Ist er wahr, so wird Ausdruck1 ausgeführt, ist er H HH HH Ausdruck HH HH wahr falsch HH H Ausdruck2 DR A Ausdruck1 Abbildung 4.11: Der Auswahloperator ?: falsch, so wird Ausdruck2 ausgeführt. Auch hierzu ein ganz einfaches Beispiel: /*op06.c Einfaches Beispiel Auswahloperator Status: incomplete*/ #include <stdio.h> void main(void) { float x, y; printf("\nGeben Sie bitte zwei verschiedene Zahlen ein:"); scanf("%f %f",&x, &y); x == y ? printf("Sie sollten doch verschiedene Zahlen eingeben!"): printf("\nDie groessere Zahl ist: %.2f.",x > y ? x : y); } In diesem Programm wird der Auswahloperator gleich zweimal verwendet. 1. Zunächst wird geprüft, ob die beiden Zahlen wirklich verschieden sind. Wurden zwei gleiche Zahlen eingegeben, so wird eine Fehlermeldung ausgegeben. 2. Waren die Zahlen verschieden, so wird mittels des Auswahloperators die größere der beiden ausgewählt und ausgegeben. Ein anderes gutes Beispiel ist eine Ausgabe, bei der ein Wort im Singular oder im Plural benötigt wird, z. B. „Zahl“. Soll der Benutzer eine Zahl eingeben, so möchte man ihm auch sagen „Bitte geben Sie 1 Zahl ein!“ Soll er mehrere eingeben, z. B. 5, so möchte man ihm sagen „Bitte geben Sie 5 Zahlen ein“. Das geht ganz einfach so: /*op07.c Einfaches Beispiel Auswahloperator Status: incomplete*/ #include <stdio.h> KAPITEL 4. OPERATOREN 42 FT void main(void) { int i; printf("\nGeben Sie bitte eine positive ganze Zahl ein:"); scanf("%d",&i); printf("\nGeben Sie bitte %d Zahl%s ein!",i,i==1? "":"en"); } Gibt man zunächst 1 ein, so lautet die Ausgabe: Geben Sie bitte 1 Zahl ein Gibt man eine Zahl > 1 ein so lautet die Ausgabe z. B. : Geben Sie bitte 1 Zahl ein 4.8 Referenzieren und Dereferenzieren DR A Bereits in Kap. 2 habe ich Ihnen & und * vorgestellt. Auch das sind unäre Operatoren. Beide werden auf Variablen oder Konstanten angewandt. & liefert in jedem Fall die Adresse, an der das Element im Hauptspeicher abgelegt ist. Den Adressoperator & nennt man manchmal auch Referenzierungsoperator, da er zu einer Variablen eine Referenz auf diese bzw. ihren Speicherplatz liefert. Umgekehrt nennt man den Operator * auch Indirektions- oder Dereferenzierungsoperator. Wir werden uns mit beiden ausführlich in den Kap. 6 und 9 beschäftigen. 4.9 Bit-Manipulationen C kennt vier logische Bit-Operatoren, zwei Shift-Operatoren und Bit-Masken. Abbildung 4.12 Die folgenOperator & | ^ ˜ << >> Bedeutung und oder entweder oder nicht (Einer-Komplement) Links-Shift Rechts-Shift Abbildung 4.12: Bit-Operatoren den vier Tabellen in Abb. 4.13 zeigen die Wirkungsweise der logischen Bit-Operatoren. Die Operanden der und 0&0 0&1 1&0 1&1 Ergebnis 0 0 0 1 oder 0|0 0|1 1|0 1|1 Ergebnis 0 1 1 1 entw. oder 0^0 0^1 1^0 1^1 Ergebnis 0 1 1 0 nicht ˜0 ˜1 Ergebnis 1 0 Abbildung 4.13: Wirkungsweise der Bit-Operatoren Bit-Operatoren müssen einen ganzzahligen Datentyp haben. Das folgende Beispiel aus [KPP96] illustriert die Arbeitsweise dieser Operatoren. Sie werden entsprechend der Tabellen in Abb. 4.13 auf jedes einzelne Bit der Darstellung angewandt. Die Shift-Operatoren verschieben die Bits um die angegebene Anzahl nach links bzw. rechts. Beim Links-Shift << werden immer 0-Bits nachgeschoben. Beim Rechts-Shift hängt es vom Compiler ab, ob bei 4.9. BIT-MANIPULATIONEN 43 FT Werten mit Vorzeichen mit 0 oder dem Vorzeichenbit (1) aufgefüllt wird. Bei Feldern ohne Vorzeichen, wird in jedem Fall auch beim Rechts-Shift mit 0 aufgefüllt. Von daher ist man bei den Bit-Operatoren nur bei der Anwendung auf vorzeichenlose Felder auf der sicheren Seite. unsigned char a, b, c a =5; b = 12; c = a & b; c = a | b; c = a ^ b; c = ~a; c = b << 3; c = b >> 2; Bitmuster 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 1 1 0 0 0 0 0 0 0 0 1 0 0 0 1 0 1 1 1 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 1 0 1 1 0 0 1 1 0 0 1 Spaßeshalber geben wir die Werte auch nochmal dezimal und hex auf der Konsole aus: DR A a = 5, 5 b = 12, C a & b = 4, 4 a | b = 13, D a ^ b = 9, 9 ~a = -6, FFFFFFFA a << 3 = 96 Wie verwendet man nun diese Operatoren? Fangen wir mit einigen einfachen Dingen an. Was macht, z. B. das folgende Codefragment? int a; a=<<1; Alle Bits werden um eine Stelle nach links geschoben steht in der Definition. Aber was heißt das nun konkret? Alle 0- und alle 1-Bits „wandern“ nach links. Wo also vorher eine 1 an Stelle i stand, von rechts, mit 0 beginnend gezählt, steht sie nun an Stelle i + 1. Vorher stand sie also bei 2i und nun bei 2i+1 . Solange wir nicht aus dem Zahlenbereich hinauslaufen, ist dass also einfach eine Multiplikation mit 2. Ganz entsprechend ist ein Rechts-Shift bei vorzeichenlosen Feldern einfach eine Division durch 2. Man verwendet in diesem Zusammenhang oft sogenannte Bit-Masken. Diese werden meistens oktal oder definiert. Dazu beachten Sie am einfachsten die Tabelle in Abb. 4.14 Die Liste lässt sich beliebig Dezimal 0 7 15 255 oktal 0 7 17 377 Hexadezimal 0 7 F FF Erläuterung Alle Bits sind 0 Bits 0–3 des rechtesten Bytes sind 1 Das rechteste Byte hatte alle Bits auf 1 Die rechtesten beiden Bytes haben alle Bits auf 1 Abbildung 4.14: Bit-Masken fortsetzen. Natürlich werden die besonders einprägsamen Werte am häufigsten verwendet. So kann man sehr einfach bestimmte Bits in einem Feld ändern, wie die Abb. 4.15 Das Ganze funktioniert entsprechend Operation x = x | 077 x = x & ~077 Erläuerung o’77’=X’3F’, also werden die rechtesten 6 Bit auf 1 gesetzt. Setzt entsprechend auf 0. Abbildung 4.15: Bit-Manipulationen, Beispiele der Tabellen in Abb. 4.13: Ist eines der Bits 1 (wahr) so wird das entsprechende Ergebnisbit auf 1 gesetzt. In der zweiten Zeile, wo der und-Operator verwendet wird werden entsprechend die Bits auf 0 gesetzt, bei KAPITEL 4. OPERATOREN 44 4.10 Aufgaben FT denen in der Maske auf der rechten Seite des Operators eine 0 steht. Ob man derartige Dinge lieber oktal oder hexadezimal angibt, ist Geschmackssache, ich neige zu hexadezimaler Angabe. Allerdings macht mich doch stutzig, dass ich Dateiattribute in Unix manchmal (wenn auch sellten) durch Angabe des oktalen Werts ändere. Auch die binären Bit-Operatoren können in zusammengesetzten Zuweisungen verwendet werden, es gibt also auch &=, |=, ^=, <<= und >>= 1. Finden Sie bitte heraus, ob Ihr Compiler Unterschiede zwischen einer zusammengesetzten Zuweisung im Vergleich zur äqivalenten Form macht! Wie können Sie das herausfinden und Ihre Aussage belegen? 2. Schreiben Sie bitte eines (oder mehrere) C-Programme, die die Tabellen in den Abb. 4.5–4.9 korrekt ausgerichtet auf der Konsole ausgibt! Sie können dabei alle Möglichkeiten von C verwenden, die Sie bisher in dieser Veranstaltung kennen gelernt haben. 3. Erklären Sie, warum es so schwierig ist, in dem Programm op04.ceinen „Treffer“ zu landen! Was könnte man tun, um es dem „Spieler“ etwas einfacher zu machen? DR A 4. Geben Sie bitte an, welchen Typ und welchen Wert das Ergebnis in den folgenden Beispielen hat und begründen Sie Ihre Ansicht: 4.1. int i = 888; long l; l = i + 50; 4.2. long l = 0x654321; short s; s = l; 4.3. int i = -2; unsigned int ui = 2; i = i*ui; 4.4. double d = -2.345; int i; unsigned int ui; i = d; ui = d; 4.5. double d = 1.23456789012345; float f; f = d; FT Kapitel 5 Kontrollstrukturen 5.1 Übersicht In diesem Kapitel lernen Sie, wie man ein Programm wirklich schreibt. bisher kennen Sie einige Befehle der Sprache C, die nacheinander abgearbeitet werden. Nun lernen Sie Befehle kennen, für DR A • Schleifen (siehe loop im Glossar). • Verzweigungen (if-then-else, switch, ?:) • Sprünge (goto, continue, break) 5.2 Lernziele • Die grundlegenden Befehle zur Steuerung des Programmablaufs in C beherrschen. • Schleifen mit for sicher verwenden können. • while und do while sicher verwenden können. 5.3 Schleifen Das Beispielprogramm happybirthday1.c aus Abschn. 2.8 muss jedesmal neu aufgerufen werden, wenn man eine anderes Geburtsdatum eingeben will. Um das zu vermeiden, können wir eine Schleife einbauen. Am einfachsten zu schreiben ist eine Endlosschleife: for(;;); Das braucht man eher selten. Meistens entstehen Endlosschleifen ungeplant durch Programmierfehler. Verwenden wir es aber hier mal einfach: /****************************************** happybirthday2.c liest in einer Endlosschleife ein Geburtsdaten über die Tastatur ein und "errechnet" jeweils eine Glückszahl Status: incomplete ******************************************/ #include <stdio.h> #include <stdlib.h> void main(void) { 45 KAPITEL 5. KONTROLLSTRUKTUREN 46 FT int tag; int monat; int jahr; for(;;) { printf("\nGeben Sie bitte Ihr Geburtsdatum ein!"); printf("\nTag: "); scanf("%d", &tag); printf("\nMonat: "); scanf("%d", &monat); printf("\nJahr: "); scanf("%d", &jahr); printf("%s%d%s%d%s%d%s","Ist Ihr Geburtsdatum wirklich der: "\ ,tag,".",monat,".",jahr,"?"); srand(tag + monat + jahr); printf("\nDann ist Ihre Glückszahl heute: %d\n",rand()); } } DR A Beendet werden kann das Programm nun allerdings nur durch Ctrl-C oder eine äquivalente Tastenkombination, was nicht gerade professionell ist. In solchen Fällen verwendet man besser eine Abfrage an den Benutzer, ob er noch weiter machen möchte oder nicht. Dazu gibt es zwei Möglichkeiten, die while- und die do while-Schleife. Die Grundprinzipien beider zeigen der linke bzw. der rechte Teil der Abb. 5.1 Bei der while-Schleife wird vor Eintritt in den Schleifenkörper ein Ausdruck ausgewertet und geprüft Solange Ausdruck wahr ist Anweisung Anweisung Solange Ausdruck wahr ist Abbildung 5.1: While und do while-Schleife ob er wahr (d. h. ungleich 0) oder falsch (d. h. gleich 0) ist. Ist er wahr, so werden die Anweisungen im Schleifenkörper ausgeführt. Anschließend wird dann wieder der Ausdruck ausgewertet usw. Bei der do while-Schleife wird zunächst der Schleifenkörper ausgeführt und anschließend der Ausdruck überprüft. Dazu bauen wir eine weitere Variable in das Programm ein: weiter und fragen den Benutzer, ob er weiter machen möchte oder nicht. In der while Variante sieht das so aus: /****************************************** happybirthday3.c liest in einer while Schleife ein Geburtsdatum über die Tastatur ein und "errechnet" jeweils eine Glückszahl bis der Benutzer etwas anderes, als J eingibt Status: incomplete ******************************************/ #include <stdio.h> #include <stdlib.h> void main(void) { 5.3. SCHLEIFEN 47 DR A } FT int tag; int monat; int jahr; char weiter = ’J’; while(weiter==’J’) { printf("\nGeben Sie bitte Ihr Geburtsdatum ein!"); printf("\nTag: "); scanf("%d", &tag); printf("\nMonat: "); scanf("%d", &monat); printf("\nJahr: "); scanf("%d", &jahr); printf("%s%d%s%d%s%d%s","Ist Ihr Geburtsdatum wirklich der: "\ ,tag,".",monat,".",jahr,"?"); srand(tag + monat + jahr); printf("\nDann ist Ihre Glueckszahl heute: %d\n",rand()); printf("\nMöchten Sie weitere Glueckszahlen berechnen?\ J: Ja, alles andere: Nein. "); scanf("%s",&weiter); } Gegenüber den bisherigen Varianten habe ich hier folgende Veränderungen vorgenommen: 1. Ich habe einen #include <stdlib.h> Präprozessorbefehl ergänzt. 2. Die main-Funktion hat nun den Rückgabewert void und einen vollständigen Prototyp (mit void in den Klammern). Dadurch entfällt jetzt auch der return-Befehl. 3. Ich habe eine zusätzliche char-Variable, weiter eingebaut und sie mit ’J’ initialisiert. 4. Nach dem Berechnen der Glückszahl wird der Benutzer gefragt, ob er/sie weiter machen möchte. Anschließend wird die Antwort mit der Funktion scanf in das Feld weiter eingelesen, das ja in der Schleifenbedingung abgeprüft wird. Ganz ähnlich sieht es mit der do while Schleife aus: /****************************************** happybirthday4.c liest in einer do while Schleife ein Geburtsdatum über die Tastatur ein und "errechnet" jeweils eine Glückszahl bis der Benutzer etwas anderes, als J eingibt Status: incomplete ******************************************/ #include <stdio.h> #include <stdlib.h> void main(void) { int tag; int monat; int jahr; char weiter; do { printf("\nGeben Sie bitte Ihr Geburtsdatum ein!"); KAPITEL 5. KONTROLLSTRUKTUREN 48 } while(weiter==’J’); } FT printf("\nTag: "); scanf("%d", &tag); printf("\nMonat: "); scanf("%d", &monat); printf("\nJahr: "); scanf("%d", &jahr); printf("%s%d%s%d%s%d%s","Ist Ihr Geburtsdatum wirklich der: "\ ,tag,".",monat,".",jahr,"?"); srand(tag + monat + jahr); printf("\nDann ist Ihre Glueckszahl heute: %d\n",rand()); printf("\nMöchten Sie weitere Glueckszahlen berechnen?\ J: Ja, alles andere: Nein. "); scanf("%s",&weiter); DR A Beachte Sie bitte, dass die Schleife in diesem Fall auf jeden Fall einmal durchlaufen wird. Ich habe das zusätzlich dadurch verdeutlicht, dass ich die Variable weiter nicht initialisiert habe. Ich hätte ihr auch den Wert N zuweisen können, dass würde nichts ändern. Alle diese Programm liefern auch mit der CompilerOption /Wall mit nur einer einzigen Warnung durch, die sich auf eine in stdlib.h stehende Deklaration bezieht. Sehen wir uns nun die for Schleife etwas näher an. Den Ablauf in einer for-Schleife for(Ausdruck1, Ausdruck2, Ausdruck3) { Anweisung } visualisiert die Abb. 5.2. Das heißt: Ausdruck1 Solange Ausdruck2 wahr ist Anweisung Ausdruck3 Abbildung 5.2: for-Schleife 1. Bei Eintritt in die for-Schleife wird Audruck1 ausgeführt. 2. Ist Ausdruck2 wahr, so werden die Anweisungen im Schleifenkörper ausgeführt und anschließend Ausdruck3 ausgewertet. 3. Das geht solange weiter, bis Ausdruck2 falsch ist, dann wird die Schleife verlassen. Dabei „zäume ich das Pferd zur Abwechslung von hinten auf“. In diesem Beispiel könnte man den gleichen Efekt wie bei der while-Schleife so mit for erreichen. Allerdings werde ich Ihnen hinterher detailliert erläutern, warum dies eine abschreckende Verwendung der for-Schleife ist! 5.3. SCHLEIFEN 49 DR A FT /****************************************** happybirthday5.c liest in einer for Schleife ein Geburtsdatum über die Tastatur ein und "errechnet" jeweils eine Glückszahl bis der Benutzer etwas anderes, als J eingibt Status: bad ******************************************/ #include <stdio.h> #include <stdlib.h> void main(void) { int tag; int monat; int jahr; char weiter=’J’; for(;weiter==’J’;scanf("%s",&weiter)) { printf("\nGeben Sie bitte Ihr Geburtsdatum ein!"); printf("\nTag: "); scanf("%d", &tag); printf("\nMonat: "); scanf("%d", &monat); printf("\nJahr: "); scanf("%d", &jahr); printf("%s%d%s%d%s%d%s","Ist Ihr Geburtsdatum wirklich der: "\ ,tag,".",monat,".",jahr,"?"); srand(tag + monat + jahr); printf("\nDann ist Ihre Glueckszahl heute: %d\n",rand()); printf("\nMöchten Sie weitere Glueckszahlen berechnen?\ J: Ja, alles andere: Nein. "); } } Der for-Befehl erwartet drei Parameter, die alle fehlen können. Im dritten Parameter steht irgendein gültiger Ausdruck in C, der am Ende der Schleife ausgewertet oder „berechnet“ wird. In diesem Fall habe ich also die Abfrage zum Weitermachen in der for-Schleife untergebracht. Vor einem Schleifendurchlauf wird der zweite Parameter ausgewertet. Ausdruck zwei muss eine ganze Zahl liefern. Liefert er „0“, also „falsch“, siehe Abb. 4.8 auf 40, so wird die for-Schleife beendet, liefert er !=0, so wird der Schleifenkörper durchlaufen. Dies ist aber ein Beispiel für ganz schlechte Programmierung, bei der gleich gegen eine ganze Reihe von Grundprinzipien guten Programmierstils verstoßen wird. Es reicht noch nicht für einen Sieg am International Obfuscated C Code Contest (IOCCC), könnte aber einen ersten Baustein bilden. Was ist also so schlecht an happybirthday5.c? 1. Es ist eine unübliche Verwendung der for-Schleife, aber dazu unten mehr. 2. Die Frage „Möchten Sie weitere Glueckszahlen berechnen?“ ist von der Eingabe soweit getrennt wie nur irgend möglich. Diese sollten im Interesse einen hohen Zusammenhalts aber mindestens in einem Block stehen. Hier sind sie auf den Block des for-Befehl und den Block des Schleifenkörpers verteilt. 3. Damit wird gleichzeitig eine unnötige Kopplung zwischen den genannten Blöcken eingeführt. 4. Da die Bedingung am Ende des Schleifendurchlaufs abgeprüft wird, handelt es sich eigentlich um eine do while-Schleife, und die sollte man dann auch verwenden. Sie steht aber am Anfang, also KAPITEL 5. KONTROLLSTRUKTUREN 50 FT an einer ganz anderen Stelle. Wollen Sie den Codeverlauf nachvollziehen, so müssen Sie ziemlich hin- und herspringen. Die vollständige Syntax der for-Schleife ist wie folgt: for(Ausdruck1;Ausdruck2;Ausdruck3) { ... } Ausdruck1 wird zur Initialisierung genau einmal, nämlich beim Eintritt in die Schleife ausgeführt. Anschließend wird geprüft, ob Audruck2 als Resultat „wahr“, d. h. „!=0“ liefert. In diesem Fall wird der Schleifenkörper durchlaufen. Handelt es sich nur um eine Zeile, besteht der Block also nur aus einer Anweisung, so kann auf die geschweiften Klammern verzichtet werden. Nach Ausführung des Schleifenkörpers wird der Ausdruck3 ausgewertet. Anschließend wird wirder der Ausdruck2 ausgewertet usw. Das folgende Programm zeigt eine typische Verwendung der for-Schleife. Dabei wird eine Tabelle der ersten 10 natürlichen Zahlen und deren Quadrate ausgegeben. DR A /*for01.c Die ersten 10 Quadratzahlen Status: ok*/ #include <stdio.h> void main(void) { int i; printf(" i\ti*i\n"); for(i = 1;i<=10;i++) printf("%3d\t%3d\n",i,i*i); } C-Programmierer bevorzugen einen knappen Stil und so werden viele dieses Programm wie folgt schreiben: /*for02.c Die ersten 10 Quadratzahlen Status: ok*/ #include <stdio.h> void main(void) { int i; printf(" i\ti*i\n"); for(i = 1;i<=10;printf("%3d\t%3d\n",i++,i*i)); } Ich halte die erstere Variante aber für die bessere, weil Schleifen-Definition und Schleifenkörper klar getrennt sind. Beachten Sie die Verwendung des Postfix-Inkremenoperators im letzten Ausdruck der forAnweisung. Was würde passieren, wenn man dort stattdessen den Präfix-Inkrementoperator einsetzen würde? Da ich davon ausgehe, dass Sie die Grundrechenarten beherrschen, habe ich darauf verzichtet, die Ausgabe des Programms mit abzudrucken. Um vorzeitig aus einer ganzen Schleife oder einem Durchlauf des Schleifenkörpers auszusteigen gibt es die Befehle break und continue. break bewirkt das Verlassen des Schleifenkonstrukts; die Programmausführung wird mit dem nächsten Kommando fortgesetzt. Das kann natürlich auch ein übergeordnetes Schleifenkonstrukt sein. continue bewirkt, dass der Rest der laufenden Iteration übersprungen und direkt die nächste begonnen wird. Das heißt • In einer for-Schleife wird die Kontrolle an den letzten Parameter des for-Konstrukts übergeben (z. B. i++) • Für while und do while bedeutet es, das der Test-Teil, d. h. die Auswertung der Bedingung, sofort vorgenommen wird. In einem switch-Befehl können Sie continue nicht verwenden. 5.4. VERZWEIGUNGEN 51 5.4 Verzweigungen FT In C gibt es neben dem Auswahl-Operator, der bereits in Abschn. 4.7 vorgestellt wurde zwei Möglichkeiten, den Programmablauf in Abhängigkeit von Bedingungen zu steuern. Als Erste stelle ich Ihnen das if-else-Konstrukt vor, if (Bedingung) Anweisung1 else Anweisung2 das in Abb. 5.3 Ist die Bedingung wahr, d. h. der Ausdruck Bedingung liefert bei Auswertung mit HH Bedingung HH HH HH falsch wahr HH H H Anweisung2 DR A Anweisung1 Abbildung 5.3: if-else-Konstrukt den aktuellen Parameterwerten einen Wert ungleich 0, so wird der Block Anweisungen1ausgeführt, andernfalls der Block Anweisungen2. Natürlich können if-Konstrukte beliebig tief geschachtelt werden, wie es Abb. 5.4 exemplarisch zeigt. H HH Bedingung1 wahr HH H H HH Bedingung2 wahr H H Anweisung1 H falsch falsch Anweisung2 ... H HHBedingungx wahr HH falsch H Anweisungx Anweisungz Abbildung 5.4: Geschachtelte if-Konstrukte Achten Sie aber auf die geschweiften Klammerpaare. Das folgende Beispiel ist syntaktisch korrekt, aber nicht auf den ersten Blick einfach zu verstehen: if (a == 1) if (b == 1) a = 42; else b = 42; KAPITEL 5. KONTROLLSTRUKTUREN 52 switch (Ausdruck) case Konstante1: [Anweisungen] [break] ... case Konstantek [Anweisungen] [break] [default Anweisungen] FT Das else bezieht sich auf das zweite if, das erste hat kein else. Fehler, die durch dieses „dangling else problem“ entstehen, sind manchmal schwer zu finden. Für Mehrwegentscheidungen gibt es das switch-Konstrukt. Es ist besonders für Menüauswahlen geeignet. Hier eine Zahl innerhalb eines endlichen Bereichs ganzer Zahlen die mögliche Eingabe. Je nach Eingabe wird entsprechend verzweigt. Visualisiert wird dies Abb. 5.5 Wichtig ist beim switch ist, dass Ausdruck als Ergebnis int liefert und DR A hhh hhhh hhh switch(Ausdruck) hhhh hhh case Konst1: hhhh hhhh case Konst2: hhh hhhh hh h default: ... Anweisungen Anweisungen Anweisungen Anweisungen Abbildung 5.5: switch-Konstrukt die Konstanten ebenfalls ganze Zahlen sind. Sehen wir uns das Ganze nun mal an einigen Beispielen an: Für eine Auswahl von Menüpunkten wird man ein switch-Konstrukt bevorzugen, etwa wie in folgendem Code-Fragment: /*switch01.c Eine einfache Menüauswahl Status: ok /sumindest für die Funktionm nicht main*/ #include <stdio.h> void main(void) { int auswahl; printf("\nGeben Sie bitte eine Zahl zwischen 1 und 10 ein: "); scanf("%d",&auswahl); switch (auswahl) { case 1: { printf("\nSie haben File Open gewaehlt"); break; } 5.5. SPRÜNGE 53 FT case 10: { printf("\nSie haben Programm verlassen gewaehlt"); break; } default: { printf("\nSie haben eine ungueltige Auswahl getroffen"); } } } DR A Achten Sie bitte auf die break Befehle. „Vergessen“ Sie diese, so wird nach einem „Treffer“, d. h. wenn der Wert des Ausdrucks mit einer der Konstanten in den case-Befehlen übereinstimmt, einfach weiter gearbeitet und insbesondere der default-Zweig durchlaufen. Die case Anweisungen sind dannnur noch Marken ohne Bedeutung und es wird der ganze Code hinter ihnen ausgeführt. Das wird in vielen Fällen nicht beabsichtigt sein. break sorgt dafür, dass das ganze switch-Konstrukt verlassen wird. Allgemeine Regeln aufzustellen, wann man besser if-else und wann man besser switch verwendet, halte ich für problematisch. Da spielen auch viele persönliche Vorlieben eine Rolle. Sicher ist aber, das gerade Entscheidungen, egal ob sie mit if-else, switch oder ?: getroffen werden, zu den fehleranfälligsten Programmteilen gehören. Achten Sie also genau auf die Aufgabenstellung, analysieren Sie sie sorgfältig und setzen in Zweifelsfällen hinreichend viele Klammerpaare. 5.5 Sprünge Diese Thema — Sprünge, Gotos, etc. — möchte viele gerne aussparen. Es gibt aber immer mal wieder Situationen, in denen diese Konstrukte den Code viel einfacher machen, als die viel gerühmten „strukturierten Anweisungen“. Zu letzteren gehören z. B. alle Schleifen- und Entscheidungskonstrukte, die wir bisher kennengelernt haben.Diese wurden deshalb auch mittels sogenannter Struktogramme visualisiert. Hier also nun die Erläuterung der goto- und label-Befehle. Ein label-Befehl definiert einen Punkt in einem C-Programm. Mit dem goto-Befehl kann man dann direkt an diese Stelle springen, so dass die Programmausführung dort fortgesetzt wird. So etwas ist in verschiedenen Situationen nützlich, die hier ohne Anspruch auf Vollständigkeit genannt seien. 1. Sie wollen aus dem innersten Konstrukt einer verschachtelten Struktur heraus, z. B. um Fehler zu behandeln. Der break-Befehl, den wir beim switch-Konstrukt kennen gelernt haben, kann auch bei allen Schleifen verwendet werden. Er bringt einen aber nur aus dem Konstrukt heraus, in dem man sich gerade befindet. Oft will man aber „ganz raus“. Dazu kann ein goto-Befehl wie in folgendem Code-Fragment aus [KR88] sehr nützlich sein: /*goto01 Ein sinnvoller Einsatz von goto Status: Fragment*/ void main(void) { int desaster = 1; for(;;) for(;;) { if (desaster) goto error; } error: { /*Betreibe Schadensbegrenzung*/ KAPITEL 5. KONTROLLSTRUKTUREN 54 } FT } Code, der goto-Befehle enthält kann immer auch ohne diese geschrieben werden. In manchen Fällen kostet das zusätzliche Vergleiche oder zusätzliche Variablen. Wenn der Code mit dem gotoBefehl aber einfacher ist als der ohne, sollte man ihn trotz vieler Bedenken (siehe Abschn. 5.6) einsetzen- 5.6 Historische Anmerkungen Der Befehl goto und die ähnlichen continue und break galten seit Dijkstras Bemerkungen in [Dij68] als unanständig. Dies war die große Zeit der strukturierten Programmierung. Dieses Konzept verlangte, dass ein Programm nur aus Sequenzen, Schleifen und Entscheidungen bestehen durfte. Mir ist in meinem ersten Programmierkurs 1973 (Algol 60) sogar erzählt worden, „gotos“ seien schlecht für die Performance. Die Programmierer bei der damaligen Esso AG in Hamburg, wo ich damals jobte, konnten darüber nur den Kopf schütteln. Insgesamt gab es fünf Forderungen an ein Programm das als strukturiert gelten durfte. Der Kollege Volker Claus aus Stuttgart hat auf dem Kolloquium zur Feier des 25. Geburtstag der Technischen Informatik an der HAW Hamburg (damals noch FH Hamburg) einen sehr launigen Vortrag gehalten, der viele meiner Vorurteile bestätigt hat. Ich gebe hier einiges aus dem Gedächtnis wieder, habe aber keine weiteren Belege dafür. DR A Wie müsste eigentlich ein Programm aussehen, dass gegen alle guten Prinzipien der strukturierten Programmierung verstößt? Ich habe versucht so ein Programm zu konstruieren und es ist mir gelungen. Dann habe ich mich gefragt, ob jemand wirklich so ein Programm schreiben würde. Und ich bin in einem meiner eigenen Bücher fündig geworden! Nun dachte ich zunächst, nur ich sei so blöd, habe aber noch weiter gesucht. Da bin ich dann auch noch in [Knu73] auf das Programm gestoßen, das den Algorithmus für Garbage Collection implementiert. Auch dieses verstößt gegen alle Prinzipien der strukturierten Programmierung. Da habe ich dann den Begriff des „TUP“ erfunden. Ein Total Unstrukturiertes Programm. In C finden Sie viele (alle?) Elemente der strukturierten Programmierung, aber Sie haben die Freiheit sie zu verwenden oder nicht. Es gibt einige Situationen, in denen Code mit nicht-strukturierten Elementen sehr viel einfacher ist als sog. strukturierter Code. Trotzdem sollten Sie goto und verwandte Befehle sparsam einsetzen. Meine Erfahrung hat gezeigt, dass es gerade schlechte Programmierer sind, die häufiger zu diesen Befehlen greifen und die Programme dann auch deutlich schlechter sind. Das betrifft alle Aspekte, z. B. Performance, Wartbarkeit, Funktionserfüllung . . . . Gerade dem goto-Befehl wird viel Spaghetti-Code angelastet. Das ist sicher richtig. Aber es sei auch auf den eben in diesem Abschn. zitierten Vortrag verwiesen. Meine Erfahrung ist, das man auf gotos sehr oft verzichten kann. So habe ich aus den Schulungsunterlagen für die Programmiersprache CA-IDEAL stets die beiden Folien mit dem goto-Befehl, der dort quit transfer to hieß, herausgenommen. Alle schlechten Programme, die ich in CA-IDEAL gesehen habe, machten ausgiebig und unnötig Verwendung von diesem Befehl. 5.7 Aufgaben 1. Schreiben Sie ein Programm, dass die 256 ASCII Zeichen in einer 16 × 16-Matrix ausgibt. Geben Sie als Zeilen- bzw. Spaltenköpfe den ersten bzw. zweiten Teil der hexadezimalen Darstellung des entsprechenden Bytes aus. 2. Schreiben Sie ein Programm, dass eine Körpergröße h in Meter und ein Gewicht w in Kilogramm erhält und den Body-Mass-Index (BMI) w BM I := 2 h berechnet, ausgibt und je nach Wert folgende Bewertung abgibt: 5.7. AUFGABEN 55 0 < BM I < 18 : Sie sind auffällig untergewichtig und sollten mehr qualitativ hochwertige Nahrung zu sich nehmen. FT 18 ≤ BM I ≤ 25 Ihr Gewicht entspricht Ihrer Körpergröße. Essen Sie weiter wie gewohnt. 25 ≤ BM I Sie sind übergewichtig. Reduzieren Sie Ihre Nahrungsaufnahme und versuchen Sie auf möglichst frische und kalorienarme Kost umzustellen. 3. Was ändert sich an Programm for02.c, wenn Sie i++ ++i ersetzen? Wie erreichen Sie, das trotzdem die Tabelle aus Programm for01.c ausgegeben wird? 4. Schreiben Sie ein C-Programm, dass die sogenannte Collatz-Folge berechnet 3cn + 1, cn gerade cn+1 : cn cn gerade 2 , 5. Schreiben Sie ein C-Programm, dass die Prüfziffer Modulo 11 berechnet! Testen Sie das Programm mit einigen ISBN Nummern der Bücher, die Sie für diesen Kurs verwenden bzw. empfohlen bekommen haben. 6. Schreiben Sie ein Programm, dass eine Unter- und eine Obergrenze in Grad Fahrenheit als Eingabe erhält und eine Tabelle in 10er-Schritten der Fahrenheit und entsprechenden Celsius Werte ausgibt. DR A 7. Schreiben Sie ein Programm das Unter- und Obergrenzen für Größen in Meter und Gewichte in Kilogramm erhält und eine Tabelle der Body-Mass-Indexe ausgibt. KAPITEL 5. KONTROLLSTRUKTUREN DR A FT 56 FT Kapitel 6 Vektoren und Strings 6.1 Übersicht DR A Vektoren und Strings gehören zu den ganz oft verwendeten Elementen in Programmiersprachen. Man kann Sie als Konstruktionen auffassen, die auf den elementaren Datentypen aufbauen. So ist ein Vektor eine Struktur, die viele Elemente eines Typs enthalten kann. Ein String kann als ein Vektor von Zeichen (char) betrachtet werden. In C hängen Vektoren und Zeiger eng zusammen. Es ist deshalb eine nicht unwichtige didaktische Frage, was man erst einführt. Ich habe mich hier an [KPP96] orientiert, aber einige Informationen über Zeiger schon an vielen Stellen eingestreut, bevor Zeiger in Kap. 9 systematisch behandelt werden. 6.2 Lernziele • Vektoren definieren und initialisieren können. • Strings definieren und initialisieren können. • Vektoren und Strings einlesen und auslesen können. • Mit ein- und mehrdimensionalen Vektoren rechnen können. 6.3 Definition von Vektoren Ein Vektor wird durch Angabe eines Typs, eines Namens und einer Anzahl von Elementen deklariert: typ name[anzahl]; Hier einige Beispiele: int zeile[100]; char hallo[]; char moin[5]; Hier sehe Sie bereits, warum Vektoren und Strings in einem Kapitel behandelt werden: Ein String ist im Wesentlichen einfach ein Vektor von Zeichen (chars). Die Anzahl Elemente muss eine Konstante sein oder ein Ausdruck, der nur Konstanten enthält. Als ganz einfaches Beispiel lesen wir eine Zeile ein und geben Sie rückwärts aus: /* palindrom01.c Zeile Einlesen und rückwärts ausgeben Status: incomplete*/ #include <stdio.h> 57 KAPITEL 6. VEKTOREN UND STRINGS 58 FT #define MAXLEN 100 void main(void) { int i, c; int puffer[MAXLEN]; printf("\nGeben Sie bitte eine Zeile Text ein: \n"); for (i=0; (i < MAXLEN) && ((c = getchar()) != ’\n’);i++) puffer[i]= c; putchar(’\n’); while(--i >= 0) putchar(puffer[i]); } Ich ziehe Konstantendeklarationen Präprozessor-Direktiven vor. Warum, erfahren Sie in Kap. 8. Hier „mault“ aber der C-Compiler, wenn ich #define MAXLEN 100 durch DR A const int MAXLEN = 100; ersetze. Das ist aber nicht weiter tragisch, denn so etwas sollte man in eine Konfigurationsdatei schreiben, als Parameter übergeben, aber nicht so in ein Programm schreiben. Um zu C kompatibel zu bleiben, finden Sie solche Deklarationen aber auch noch sehr viel in C++. Sehen wir uns nun das Programm palindrom01.c etwas genauer an. So wie es hier steht, wandelt es der Compiler auch mit /Wall ohne Warnungen um. Aber an diesem Beispiel sehen wir sehr gut, dass es Unterschiede zwischen den Zielen gibt, die C Syntax peinlich genau einzuhalten und lesbare Programme zu schreiben. Der Vektor puffer[MAXLEN] soll Zeichen vom Typ char enthalten. Deklariert man ihn auch so, so bringt der Compiler die Warnung palindrom01.c(11) : warning C4244: ’=’: Konvertierung von ’int’ in ’char’, möglicher Datenverlust für den Teil der for-Anweisung, in dem c = getchar() steht. Für dieses Problem gibt es keine einfache Lösung. Der Rückgabetyp von getchar() ist nun einmal int. Das lässt sich auch nicht ohne weiteres ändern, denn es muss a auch EOF zurückgegeben werden können, und das ist nicht notwendig ein char. So wie das Programm hier abgedruckt ist, wird es vom Compiler ohne Warnungen /auch mit /Wall) „verdaut“. Der Lesbarkeit wäre aber sicher mit einer Deklaration char puffer[MAXLEN] besser gedient. Sehen wir uns nun an, was wir aus dem obigen Programm palindrom01.c und insgesamt über Vektoren lernen können oder müssen. 1. Die Elemente eines Vektor belegen immer einen zusammenhängenden Speicherbereich. 2. Auf die einzelnen Elemente kann über vektorname[i] zugegriffen werden, wie dies an zwei Stellen um Beispielprogramm geschieht. 3. Die Indexwerte eines Vektors der Länge n laufen von 0–n − 1. Achten Sie nicht darauf, so sind typische Anfängerfehler nach dem Motto „off by one“ oder „leicht daneben ist auch vorbei“ die unweigerliche Folge. 4. Vektoren können mit jedem Datentyp gebildet werden. 6.4. INITIALISIERUNG VON VEKTOREN 59 6.4 Initialisierung von Vektoren FT Im Programm palindrom01.c wurde der Vektor aus Zeichen Element für Element durch Tastatureingaben gefüllt. Sie können Vektoren aber auch gleich bei der Definition initialisieren. int zahlen[3] = {1, 2, 3}; Geben Sie die Länge nicht an, so ermittelt der Compiler bei der Definition aus der Länge des Initialisierungsvektors die Länge. Wir hätten als auch dies schreiben können: int zahlen[] = {1, 2, 3}; Aber aufgepasst: Deklarieren Sie einen Vektor ohne Längenangabe und ohne Initialisierung, so erhalten Sie die Compiler-Fehlermeldung „C2133 unbekannte Größe“. Das ist auch verständlich, denn wie sollte der Compiler hier auf eine Größe des Vektors schließen? 6.5 Strings DR A Nun kommen wir endlich dazu, uns die schon oft ohne Erklärung verwendeten Strings genauer anzusehen. Ein String ist eine Zeichenfolge, die mit dem String-Endezeichen ’\0’ abschließt. Eine zeichenweise Definition, wie oben für ganze Zahlen vorgeführt sähe also so aus: char str[] ={’M’,’o’,’i’,’n’,’\0’}; Einfacher liest sich aber die Initialisierung mit dem Wert als String-Konstante char str[] = "Moin"; Hier ist das String-Endezeichen Bestandteil der String-Konstante. Betrachtet man einen String also als Vektor, so belegt ein String der Länge n die n + 1 Elemente von 0 bis n eines Vektors der Länge n + 1. Zum Beleg dafür ein kleines Programm und seine Ausgabe. /* vect02.c Deklaration und Definition von Vektoren*/ #include <stdio.h> void main(void) { char str1[] ={’M’,’o’,’i’,’n’,’\0’}; char str2[] = "Moin, Moin"; printf("\nDie Laenge von str1 ist %d.",sizeof(str1)); printf("\nDie Laenge von str2 ist %d.",sizeof(str2)); } Die Laenge von str1 ist 5. Die Laenge von str2 ist 11. 6.6 Vektoren und Adressen In C ist der Name eines Vektor ein Zeiger. Unser String str1 enhält die Adresse, an der das erste Zeichen gespeichert ist. Es ist also egal, ob wir str1 schreiben oder &str[0], wir erhalten in jedem Fall die Adresse des ersten Zeichens des Strings. Ist int i; so ist es ebenso egal, ob wir schreiben str1 + i oder&str1[i]. Das heißt, addieren wir auf die Anfangsadresse eines Vektors eine ganze Zahl i, so erhalten wir die Adresse des i-ten Vektorelements. Auf den ersten Blick mag des etwas verwirrend erscheinen. Es ist aber in C üblich eher mit den Zeigern als den Vektoren zu hantieren. Dies hat Konsequenzen: Nehmen wir etwa einen String, der ja (auch) ein Vektor von chars ist. Hier wieder der Hamburgische Gruß KAPITEL 6. VEKTOREN UND STRINGS 60 FT /*vect12.c Was passiert bei strings */ #include <stdio.h> #include <string.h> void main(void) { char *str1 = "Moin, Moin"; char *str2 = " "; printf("\n%s",str1); printf("\n%x",str1); printf("\n%s",str2); printf("\n%x",str2); str2 = str1; printf("\n%s",str2); printf("\n%x",str2); str1[2]=’j’; printf("\n%s",str2); printf("\n%x",str2); } DR A in str1 und ein leerer String in str2. Anschließend geben wir sowohl die Strings, als auch die jeweilige Adresse, an der sie beginnen aus. Was passiert nun aber bei der Zuweisung str2 = str1;? Beides sind Adressen. Also enthält str2 anschließend die Adresse, die in str1 seht. Beide verweisen nun also auf den gleichen Hauptspeicherbereich. Was das bedeutet macht die nächste Zuweisung deutlich: str1[2]=’j’;, die das erste „i“ durch ein „j“ ersetzt. Dadurch wird aber auch str2 geändert, wie die folgende Ausgabe zeigt. Das ist auch nachvollziehbar, denn beide beziehen sich auf den gleiche Hauptspeicherbereich. Dies mag in manchen Fällen gewollt sein, ist es hier aber nicht. Um einen String einem anderen zuzuweisen können Sie keine Zuweisung verwenden, sondern müssen die Funktion strcpy aus der String Bbiliothek, deklariert in string.h. Ebenso sieht es mit dem Vergleichoperator == aus. Mit diesen Erläuterungen ist es nun auch klar, wie man Zeichenketten mittels scanf einliest, etwa so: scanf("%s",str1); Wie Sie aus Kap. 2, Abschn. 2.8 vielleicht noch erinnern, erwartet scanf Adressen von Variablen und geanu das kriegt sie hier ja auch. Die Funktion scanf hat noch ein paar weitere Möglichkeiten, die sie im Formattierungsstring ausnutzen können: Feldbreite Geben Sie eine Feldbreite bei %s an, etwa %88s, so werden maximal 88 Zeichen gelesen. Suchzeichen Statt der Typangabe s kann hinter dem %-Zeichen auch eine Liste von Suchzeichen in eckigen Klammern erscheinen. Ist auf eine Ausgabe nur die Möglichkeit gegeben, mit J für Ja oder N für nein zu antworten, so kann man das etwa so angeben: scanf("%1[JjNn]",antwort); Dann werden nur diese vier Zeichen akzeptiert, anderfalls bricht scanf das Einlesen ab. Damit haben wir gleich kleine bzw. große Eingabezeichen mit behandelt. Mehrer Suchzeichen können auch einfach so angegeben werden: a-z erlaubt etwa nur die Eingabe von Kleinbuchstaben. Ausschlusszeichen Wird dem Suchzeichen das Zeichen ^ vorangestellt, so wird die Eingabe beendet, sobald eines der Suchzeichen eingegeben wird. Nun können wir bereits eine Sache erklären, die Ihnen bisher geheimnisvoll erschienen Sein muss. Bereits in Abschn. 2.8 haben wir gesehen, wie man Variablen deklariert, die eine Adresse enthalten. Haben wir mittels char moin[] = "Moin"; 6.7. MEHRDIMENSIONALE VEKTOREN 61 char *moin = "Moin"; FT einen String deklariert und definiert, so wissen wir aus diesem Abschnitt, dass moin eine Adresse enthält. Dies ist die Adresse des ersten Zeichens des Strings. Das hätten wir aber auch so schreiben können: Beide Schreibweisen sind äquivalent. Die letztere ist in C aber üblich. 6.7 Mehrdimensionale Vektoren Bisher haben wir „eindimensionale“ Vektoren betrachtet. „Eindimensional“ in dem Sinne, dass es 1 × nMatrizen, eben vektoren. Bei vielen numerischen Rechnungen benötigt man aber etwa Matrizen, etwa n×n Matrizen. In C sind nach der Definition des ANSI Standards bis zu 31 Dimensionen möglich. Sehen wir uns das an einem einfachen Beispiel an. DR A /*vect04.c Ein einfaches lineares Gleichungssystem */ #include <stdio.h> void main(void) { int matrix[2][2]={{1,2},{3,4}}; int b[2] = {1,1}; int x[2]; int i,j; for(i=0;i<2;i++) { printf("\n"); for(j=0;j<2;j++) { printf("%d ",matrix[i][j]); } } } Aus der Definition der Matrix in der ersten Code-Zeile von main sehen sie vielleicht schon wie mehrdimensionale Vektoren in C aufgebaut sind: Ein n + 1-dimensionaler Vektor ist ein 1-dimensionaler Vektor, dessen Elemente n-dimensionale Vektoren sind. So weisen wir hier der ersten Zeile der Matrix die Werte 1 und 2 zu und der zweiten die Werte 3 und 4 und kontrollieren dies durch eine einfache Ausgabe. Ein lineares Gleichungssystem löst man natürlich in vielen Fällen durch das Gausssche Eliminiationsverfahren. Hier tut es aber auch eine bekannte Formel. /*vect05.c Ein einfaches lineares Gleichungssystem */ #include <stdio.h> void main(void) { int matrix[2][2]={{1,2},{3,4}}; int b[2] = {1,1}; int x[2]; int i,j; int determinante = matrix[0][0]*matrix[1][1]-matrix[1][0]*matrix[0][1]; printf("Die Determinante ist: %d",determinante); x[0] = (b[0]*matrix[1][1]-b[1]*matrix[0][1])/determinante; x[1] = (matrix[0][0]*b[1] - matrix[1][0]*b[0])/determinante; for(i=0;i<2;i++) { printf("\n"); for(j=0;j<2;j++) KAPITEL 6. VEKTOREN UND STRINGS 62 { } for(i=0;i<2;i++) printf("\n%3d",x[i]); } FT printf("%d ",matrix[i][j]); } Zur Lösung numerischer Aufgaben aber auch noch eine Warnung: Verlassen Sie sich nicht einfach auf das Ergebnis. Dies wäre leichtsinnig, wie Sie schon bei der Berechnung der Fibonacci-Zahlen in Abschn. 7.5 gesehen haben. Auch bei der Lösung selbst kleiner linearer Gleichungssysteme kann es zu abstrusen Ergebnissen kommen. Ursache hierfür ist dann meistens eine sogenannte schlechte Kondition der Matrix. Hier geht noch alles gut, aber wenn Sie mit floats oder doubles rechnen, sollten Sie immer überlegen, ob das Ergebnis gültig ist oder nicht. 6.8 Aufgaben 1. Schreiben Sie ein Programm, dass einen Vektor von Zeichen über die Tastatur einliest und prüft, ob es sich um ein Palindrom handelt. DR A 2. Schreiben Sie ein Programm, das als Eingabe eine Zahl, eine Einheit für diese Zahl und eine Einheit, in diese umgerechnet werden soll. Es liefert als Ausgabe die Größe in der neuen Einheit. Funktionen 7.1 Übersicht FT Kapitel 7 DR A Die Zerlegung von Programmen gehört zu den elementaren Aufgaben um in einem großen Programmsystem den Überblick zu behalten. Einzelne Funktionen oder Gruppen von Funktionen fasst man deshalb zu eigenen Modulen zusammen. In diesem Zusammenhang werde ich Ihnen deshalb auch einige Bewährte Prinzipien des Software-Engineering vorstellen, so wie sie in C angewendet werden. In der Standardbibliothek von C werden ihnen viele Funktionen mitgeliefert, die sie laufend verwenden. In diesem Kapitel werden Sie lernen, was dabei alles zu beachten ist. Außerdem werden Sie erfahren, wie man einem CProgramm bereits beim Aufruf Parameter mitgeben kann. 7.2 Lernziele • Wissen, wie man Funktionen in separate Dateien ausgliedert. • Wissen, wie man mehrere Dateien gleichzeitig umwandelt. • Einige Grundprinzipien der Modularisierung kennen. • Den Unterschied zwischen Wertsemantik und Referenzsemantik kennen. • Einige wichtige Standardfunktionen besser verstehen und anwenden können. • Aufrufparameter an ein Programm übergeben können. 7.3 Modularisierung in C Ich beginne diesen Abschnitt, in dem ich ein Beispiel aus Abschn. 2.6 aufgreife. Dort hatte ich Ihnen das Programm summe.c gezeigt. Für dieses Programm ist es natürlich nicht notwendig, eine Zerlegung vorzunehmen. Aber bereits an diesem einfachen Beispiel kann man die Grundtechnik zeigen, wie sie CProgramme in einzeln compilierbare Einheiten zerlegen. In diesem Fall habe ich das Programm summe.c zur Erläuterung in drei Teile zerlegt: 1. summe02.c: Das Hauptprogramm, in dem die Funktion main steht und das die Funktion summe aufruft. 2. summe03.h: Die Header-Datei, in der die Funktion summe deklariert wird. 3. summe03.c: Die C-Datei, in der die Funktion summe implementiert wird. Wie bereits in den vorangehenden Kapiteln sehen Sie nun die Dateien und anschließend die Erläuterungen dazu. 63 KAPITEL 7. FUNKTIONEN 64 FT /****************************************** summe.c berechnet die Summe zweier ganzer Zahlen ******************************************/ #include <stdio.h> #include "summe03.h" int main() { int pa = 9; int pb = 16; printf("%d%s%d%s%d%s",pa," + ",pb," = ",summe(pa,pb),"."); return 0; } /*summe03.h Header Datei für Summenfunktion*/ int summe(int a, int b); DR A /* summe03.c Implemtierung der Summenfunktion*/ #include "summe03.h" int summe(int a, int b) { return (a + b); } Nun zu den Erläuterungen • Das Programm summe02.c kennen Sie bereits. Es sollte nun keiner weiteren Erläuterung bedürfen. • In der Header-Datei summe03.h steht nut die Deklaration der Summenfunktion. Achten Sie bitte darauf, das diese mit einem „;“ (Semikolon) abgeschlossen wird. • In der Datei summe03 finden Sie die bereits aus Abschn. 2.6 bekannte Funktion. Nun müssen wir das Ganze auch noch „zum Laufen“ bringen. Versuchen wir zunächst einmal, dass Programm summe02.c umzuwandeln. cl summe02.c bringt uns das aus der Vorlesung bereits bekannte Ergebnis: Microsoft (R) 32-Bit C/C++-Optimierungscompiler Version 13.10.3077 für 80x86 Copyright (C) Microsoft Corporation 1984-2002. All rights reserved. summe02.c Microsoft (R) Incremental Linker Version 7.10.3077 Copyright (C) Microsoft Corporation. All rights reserved. /out:summe02.exe summe02.obj summe02.obj : error LNK2019: Nicht aufgelöstes externes Symbol ’_summe’, verwiesen in Funktion ’_main’ summe02.exe : fatal error LNK1120: 1 unaufgelöste externe VerweiseMicrosoft (R) \\ 32-Bit C/C++-Optimierungscompiler Version 13.10.3077 für 80x86 Copyright (C) Microsoft Corporation 1984-2002. All rights reserved. summe02.c Microsoft (R) Incremental Linker Version 7.10.3077 Copyright (C) Microsoft Corporation. All rights reserved. 7.3. MODULARISIERUNG IN C 65 FT /out:summe02.exe summe02.obj summe02.obj : error LNK2019: Nicht aufgelöstes externes Symbol ’_summe’, verwiesen in Funktion ’_main’ summe02.exe : fatal error LNK1120: 1 unaufgelöste externe Verweise Das war’s wohl nicht! Was ist schiefgelaufen? Sie können in C Funktionen separat umwandeln. das geht z. B. so. cl /c summe03.c Die Ausgabe lautet nun: Microsoft (R) 32-Bit C/C++-Optimierungscompiler Version 13.10.3077 für 80x86 Copyright (C) Microsoft Corporation 1984-2002. All rights reserved. summe03.c DR A Der Schalter „/c“ sagt dem Compiler (genauer: dem Programm cl) das er nur umwandeln, aber nicht linken soll. Hier erscheint, kein Fehler, also schient alles in Ordnung zu sein. In der Ausgabe (output) wird auf ein „Nicht aufgelöstes externes Symbol ’_summe’“ verwiesen. Irgendwie scheint der Compiler trotz des #include Befehls unsere Funktion summe nicht zu kennen. Um diesen „Problem zu lösen“ müssen wir noch etwas mehr über (den Microsoft) Compiler wissen. Wenn wir ein Programm umwandeln wollen, dass mehrere Dateien verwendet, müssen wir das dem Compiler auch sagen!. Das geschieht z. B. so: cl summe02.c summe03.c Die Ausgabe lautet nun (ohne die bereits hinlänglich bekannten Copyright Aussagen) /out:summe02.exe summe02.obj summe03.obj Achten Sie bitte darauf, dass zwischen den Dateinamen, die der Compiler bekommt, kein Komma steht! Danach läuft alles wie gewohnt ab (und funktioniert). Sie sollen sich bemühen, Ihre Programme nach folgenden Prinzipien zu strukturieren: Hoher Zusammenhalt Alles, was Sie in ein Paar von Header- und Implementierungs-Dateien zusammenfassen, sollte auch wirklich zusammengehören. Wenn Sie etwas weglassen würden, würde etwas fehlen. Geringe Kopplung Sie sollten nicht zu viele verschiedene Dateien heranziehen müssen. Um so mehr dies sind, um so anfälliger ist Ihr eigenes Programm für Probleme, wenn sich in den anderen Dateien etwas ändert. Zwischen den genannten Zielen — „Hoher Zusammenhalt“ und „Geringe Kopplung“ — besteht ein Konflikt! Schreiben Sie alles in eine Datei — Deklarationen, die eigentlich in Header-Dateien gehören und Implementierungen, die eigentlich in .c-Dateien gehören — so haben sie minimale Kopplung aber meist „lausigen“ Zusammenhalt. Schreiben Sie für jede Funktion eine Header-Datei und eine ImplementierungsDatei, so haben sie maximalen Zusammenhalt, aber maximale Kopplung. Den Weg aus diesem Konflikt müssen Sie selber finden. Mein Motto hierzu ist der Titel eines Films von Alexander Kluge In Gefahr und größter Not bringt der Mittelweg den Tod. Auf das tägliche Leben und das Programmieren angewendet heißt das: Es ist besser eine eindeutige Entscheidung zu treffen, als immer nur abzuwarten. Sollte die Entscheidung falsch sein, so kann man sie (hoffentlich) noch korrigieren. Ein erstes Beispiel können Sie sich an der Aufteilung der C Standardbibliotheken nehmen. Aber achten Sie bitte darauf, dass diese über Jahrzehnte gewachsen sind und viele Altlasten mit sich schleppen. Manches würde man heute anders machen! KAPITEL 7. FUNKTIONEN 66 FT Für weitergehende Erläuterungen dazu, wie man C-Programme technisch in verschiedene Teile zerlegt, muss ich Sie auf den Anhang F verweisen, die wir in diesem Semester wohl nicht beide mehr behandeln werden können und die nur einige rudimentäre Informationen enthalten. Fassen wir zusammen: Funktionen in C sind das was in anderen Programmiersprachen Unterprogramm, Subroutine etc. genannt wird. Eine Funktion sollte alle Elemente enthalten, die sie zur Erledigung ihrer Aufgabe benötigt und keine weiteren. So erreichen Sie einen guten funktionalen Zusammenhalt Ihrer Funktionen. Haben Funktionen jeweils eine wohldefinierte Aufgabe, so werden sie auch nur eine begrenzte und übersichtliche Anzahl anderer Funktionen verwenden. Die Kopplung mit anderen Elementen wird sich also in Grenzen halten. Eine Art der Kopplung ist in C aber sehr häufig: Die sogenannte globale Kopplung. Durch die Definition von Konstanten oder sogar Variablen außerhalb einer Funktion stehen diese global zur Verfügung. Die Gefahren hierdurch liegen auf der Hand: Ändert ein Programm den Wert einer globalen Variablen, so können davon sehr viele andere Programme betroffen sein. In C ist diese Gefahr aber dadruch eng begrenzt, dass fast nur Konstanten auf diese Art definiert werden. Beispiele davon haben wir bereits in limits.h gesehen. Und Konstanten ändern ihren Wert nicht, wie ja schon der Begriff angibt. Sehen wir uns zum Schluss dieses Abschnitts noch ein einfaches Beispiel einer Funktion an. Aufgabe ist es, aus einem Radius die Fläche eines Kreises zu berechnen. Ist r der Radius, so kennen Sie die Formel f = π · r2 DR A aus der Schulmathematik. Zunächstmal müssen wir den Wert von π finden. So etwas wird es sicher in C geben. Aber wo? Das ist ein mathematisches Symbol so dass wir mal in math.h nachsehen wollen. Tatsächlich, dort finden wir eine Zeile #define M_PI 3.14159265358979323846 Wenn ich Ihnen nun noch sage, dass der Radius über die Tastatur eingegeben und mit hoher Genauigkeit berechnet werden soll, so sollte das folgende kleine Programmbeispiel auch für Sie leicht zu schreiben sein. /*radius01.c Berechnet aus Radius die Kreisfläche Status: incomplete*/ #include <stdio.h> #include <math.h> double kreisflaeche(double radius); void main(void) { double radius; printf("Geben Sie bitte einen Radius als Gleitpunktzahl ein:"); scanf("%lf",&radius); printf("\nDie Flaeche dieses Kreises ist: %lf",kreisflaeche(radius)); } double kreisflaeche(double radius) { return (M_PI*radius*radius); } Eine kleine Überraschung hat der Compiler aber doch noch für uns. Obwohl wir den #include-Befehl für math.h hingeschrieben haben, erhalten wir die Fehlermeldung C2065: ’M_PI’: nichtdeklarierter Bezeichner. Was haben wir falsch gemacht? Ein erneuter Blick in math.h lässt uns dies erkennen: Über dem Teil mit den Definitionen der mathematischen Konstanten und Funktionen steht folgendes: #ifdef _USE_MATH_DEFINES 7.4. WERT- UND REFERENZSEMANTIK 67 FT /* Define _USE_MATH_DEFINES before including math.h to expose these * macro definitions for common math constants. These are placed * under an #ifdef since these commonly-defined names are not part * of the C/C++ standards. */ Wir müssen also irgendwo vor dem Präprozessor-Befehl #include <math.h> eine Zeile einnfügen, in der steht: #define _USE_MATH_DEFINES Was man mit dem Präprozessor sonst noch so anstellen kann erfahren Sie in Kap. 8. Mit dieser kleinen Ergänzung läuft das Programm nun aber wirklich. 7.4 Wert- und Referenzsemantik DR A Rufen Sie eine Funktion mit Parametern auf, so verwendet C die sogenannte „Wertsemantik“, im Englischen „call by value“ genannt. Übergeben wird nicht die Variable, sondern der Wert, den sie enthält, daher der Begriff Wertsemantik. Die aufgerufene Funktion erhält also einen Wert, der in einem Parameter gespeichert wird, der wie eine lokale Variable betrachtet werden kann. Insbesondere haben Änderungen, die innerhalb einer Funktion an einem Parameter-Wert vorgenommen werden, keine Auswirkung auf den Wert der Variablen außerhalb der Funktion. Wollen Sie, dass eine Funktion den Wert einer übergebenen Variablen so ändern kann, dass dies tatsächlich den Wert der außerhalb der Funktion definierten Variablen ändert, so müssen Sie das anders machen. C ermöglicht Ihnen die in gewissem Sinne indirekt zu tun. Statt eine Variable als Parameter zu deklarieren, deklarieren Sie eine Adresse einer Variablen, einen sogenannten Zeiger oder Pointer. Über diese Adresse (deren Änderung nach draußen natürlich keine Wirkung hat), kann die Funktion auf die Variable zugreifen und ihren Wert ändern. Dies nennt man Referenzsemantik oder „call by reference“. Zur Wert- und Referenzsemantik gibt es eine nette Anekdote. Nicklaus Wirth ist einer der bekanntesten Informatiker und hat viel auf dem Gebiet Compiler und Programmiersprachen gearbeitet. Wirth beklagte sich scherzhaft darüber, dass die Europäer seinen Namen als „Niklaus Wirt“, Amerikaner aber als „Nickles Worth“ aussprechen würden. Dies heiße, die Europäer würden ihn beim Namen (by name) rufen und die Amerikaner nach seinem Wert (by value). Die Wertsemantik hat einige Vorteile, trotzdem kann man in vielen Fällen nur unter Verwendung der Referenzsemantik gut modularisieren. Die wichtigsten Vorteile der Wertsemantik sind: • Die aufgerufene Funktion kann keinen unbeabsichtigten oder ungewollten Änderungen an den Argumenten der aufrufenden Funktion vornehmen. • Argumente einer Funktion können beliebige Ausdrücke sein. • Die Werte der Parameter stehen sofort wie Werte lokaler Variablen zur Verfügung. Das man auch Adressen übergeben kann hat einige interessante Konsequenzen. So kann man auch Funktionen übergeben. Sie können also ein Programm schreiben, dass eine vom Anwender angegebene Funktion auf andere Elemente anwendet. Manchmal haben Sie keine Wahl zwischen Wert- und Referenzsemantik. Definieren Sie eine Funktion, die als Parameter einen Vektor erhält, so wissen wir bereits, dass tatsächlich eine Adresse übergeben wird, die Adresse des ersten Elements. Als einfaches Beispiel hierfür lassen wir den Anwender eine Textzeile eingeben und geben sie in der umgekehrten Reihenfolge wieder aus. Vielleicht gelingt es uns ja so neue Palindrome zu finden. /* palindrom02.c Wort umdrehen mit Funktion/Vektor Status: incomplete*/ #include <stdio.h> #include <stdlib.h> KAPITEL 7. FUNKTIONEN 68 DR A FT #define MAXLEN 100 void reverse(char s[]); void main(void) { int i, c; char puffer[MAXLEN]; printf("\nGeben Sie bitte eine Zeile Text ein: \n"); for (i=0; (i < MAXLEN-1) && ((c = getchar()) != ’\n’);i++) puffer[i]= c; puffer[i]=’\0’; reverse(puffer); printf("\n%s",puffer); } void reverse(char s[]) { int c, i, j; for(i = 0, j = strlen(s)-1;i < j; i++, j--) { c = s[i]; s[i] = s[j]; s[j] = c; } } Sezieren wir dieses Program einmal, um zu sehen, was hier so alles passiert: 1. Wie schon in einem anderen Programm, wird stdlib.h verwendet, diesmla um die Funktion strlen zur bestimmung der länge eines Strings verwenden zu können. 2. Als erstes wird eine Konstante MAXLEN mit 100 definiert. Nicht schön, aber wir müssen irgendwie den Speicherplatz begrenzen, solange Sie noch nicht in Kap. 13 gerlent haben, wie man sich dynamisch Speicher beschafft (solange noch welcher da ist). 3. Außerdem deklarieren wir vor der main-Funktion die Funktion reverse, die wir verwenden wollen und weiter unten implementieren. 4. Als erste Aktionen wird der Benutzer aufgefordert, eine Zeile Text einzugeben. 5. Diese wird dann in einer for-Schleife eingelesen. Die Schleife wird beendet wenn Enter gedrückt (\n, Zeilenumbruch) wird oder die maximale Anzahl Zeichen MAXLEN erreicht ist. Dabei müssen wir den Platz für das String-Ende Symbol mit berücksichtigen. Deshalb endet die Schleife schon bei MAXLEN-2. 6. Nach dem Verlassen der Schleife hat der Postfix-Inkrement Operator seine Aufgabe ein letztes Mal erledigt. Die Variable i ist nun um 1 größer als die Position des Vektors an der das letzte Zeichen unseres Textstrings steht. Da schreiben wir nun das String-Endezeichen ’\0’. Durch Ausdruck des Strings im hexadezimalen Format können sie sich vergewissern, dass dort nun wirklich 0 steht. Denken Sie dran: puffer[100] ist ungültig! 7. Nun wird die Funktion reverse aufgerufen und das Ergebnis ausgegeben. 8. Zum Schluss der Clou: Da Platz knapp ist drehen wir den String „in place“ um, also ohne einen weiteren String gleicher Länge zu belegen. Das ist ein sehr typisches C-Konstrukt und Sie sollten es sicher verstehen. 8.1. Die beiden Laufvariablen i und j brauchen wir sowieso, wie Sie in [KPP96] nachlesen können, wo das ganze mit einem zusätzlichen String gemacht wird. Wir brauchen so nur eine zusätzliche Hilfsvariable c. 7.5. REKURSIVE FUNKTIONEN 69 FT 8.2. In der for-Schleife verwenden wir zwei Laufvariablen. Die eine, i, wird mit 0, die andere, j, wird mit strlen(s)-1 initialisiert. i „zeigt“ also zu Beginn der Schleife auf das erste Zeichen und j auf das letzte Zeichen des Strings. 8.3. Die Begrenzung der Schleife erfolgt durch i < j. 8.4. Am Ende eines Schleifendurchlauf wird i inkrementiert und j dekrementiert, jeweils postfix. 8.5. Im Schleifen Körper wird als erstes s[i] in die Hilfsvariable c „gerettet“. 8.6. Anschließend wird s[i] durch s[j] ersetzt. 8.7. Als Letztes wird der gerettete Wert von s[i], der uns ja in c noch zur Verfügung steht s[j] zugewiesen. 8.8. Das ganze ist eigentlich ein „swap“-Funktion, wir vertauschen jeweils zwei Werte. Im ersten Schleifendurchlauf den ersten mit dem letzten, im zweiten den zweiten mit dem vorletzten usw. Dieses Konstrukt braucht man laufend, z. B. bei Sortierverfahren. 8.9. Das String-Endezeichen fassen wir gar nicht an, es bleibt auf seiner Position. 7.5 Rekursive Funktionen Zur Einführung in diesen Abschnitt ein altbekanntes Beispiel. Die nach der Formel DR A n0 = n1 = 1 ni+2 = ni+1 + ni gebildeten Zahlen heißen Fibonacci-Zahlen oder Fibonacci-Folge. Diese kann man natürlich sehr einfach mit einem C-Programm berechnen: Der Algorithmus sieht im Pseudocode ganz harmlos aus: fib(n) if (n=0 oder n =1) return 1 else return (fib(n-1) + fib(n-2)) Das kann man in C genauso umsetzen /*fibonacci02.c Berechnung der Fibonacci-Zahlen bis MAX rekursiv. Status: incomplete*/ #include <stdio.h> const long int MAX = 40L; long int fib(long int n); void main(void) { long int i, obergrenze; printf("Geben Sie bitte an bis zu welcher Fibonacci-Zahl Sie \ rechnen lassen wollen:"); scanf("%ld",&obergrenze); printf("\nBerechnung der ersten %ld Fibonacci-Zahlen rekursiv",obergrenze); if(obergrenze > MAX) printf("\nDas haetten Sie besser nicht getan!"); printf("\n%15s\t%15s\n","n","fn"); for(i = 0;i<=obergrenze;i++) { printf("%15ld\t%15ld\n",i,fib(i)); } KAPITEL 7. FUNKTIONEN 70 FT } long int fib(long int n) { if(n == 0 || n==1) { return 1; } else return (fib(n-1) + fib(n-2)); } Die kleine Bemerkung in der einen Ausgabe sollte Sie aber etwas skeptisch machen. Man kann zeigen, das diese Implementierung des Algorithmus exponentiellen Aufwand verursacht. Die Anzahl auszuführender Rechenoperationen ist so ca. 2n . Lassen Sie sich aber davon nicht ins Bockshorn jagen, eingige der effizientesten Algorithmen arbeiten rekursiv, wie wir später in dieser Veranstaltung sehen werden. Hier nun noch eine effizientiere Implementierung mit einer for-Schleife. DR A /*fibonacci01.c Berechnung der Fibonacci-Zahlen bis obergrenze in einer for Schleife. Status: started*/ #include <stdio.h> const long int EINS = 1L; const long int ZWEI = 1L; void main(void) { long int i; long int j = EINS; long int k = ZWEI; long int f; long int obergrenze; printf("Geben Sie bitte an bis zu welcher Fibonacci-Zahl Sie \ rechnen lassen wollen:"); scanf("%ld",&obergrenze); printf("\nBerechnung der ersten %d Fibonacci-Zahlen\n",obergrenze); printf("%15s\t%15s\n","n","fn"); printf("%15d\t%15d\n",0,EINS); printf("%15d\t%15d\n",1,ZWEI); for(i = 2;i<=obergrenze;i++) { f = j + k; j = k; k = f; printf("%15ld\t%15ld\n",i,f); } } Hier laufen wir einfach durch eine for-Schleife, bilden eine Summe und tauschen die beteiligten Variablen nach oben. Das Laufzeitverhalten ist nun linear, d. h. wir brauchen für jede Zahl nur eine Addition und drei Zuweisungen. Das Ergebnis überzeugt, was die Laufzeit angeht. Auch die ersten 1000 werden auf meinem Rechner in ca. 0.8 Sekunden ausgegeben, die meiste Zeit erfordert dabei die Bildschirmausgabe. Nicht überzeugen können die berechneten Werte, um es mal vorsichtig zu formulieren. Ab der sechsundvierzigsten Fibonacci Zahl haben wir den durch long int definierten Bereich verlassen. Wir könnten noch einen Versuch mit long unsigned machen, aber das können Sie selber ausprobieren. Ändert man die entsprechenden Datenttypen und Ausgabefunktionen auf long double ab, so kommt man um einiges weiter. Dieses Programm sollte ihnen aber auch zeigen, dass man einem Ergebnis eines Rechners nicht blind trauen darf. 7.6. EINIGE WICHTIGE STANDARDFUNKTIONEN 71 FT Natürlich können Funktionen alle arithmetische Datentypen als Rückgabewert haben. Außerdem sind die folgenden Typen zulässig: struct, union, pointer, oder void. Nicht zulässig als Rückgabewerte sind Funktionen oder Vektoren. 7.6 Einige wichtige Standardfunktionen Einige Funktionen werden Sie immer wieder verwenden. Damit Sie nicht die Header-Dateien durchsuchen müssen, habe ich Ihnen hier einige zusammengestellt. 7.6.1 Funktionen mit Strings char *strcpy(char* s, const char *ct) Kopiert den String ct, einschließlich String-Endezeichen auf den String s und liefert s zurück. char *strcat(char* s, const char* ct) Hängt den String ct an das Ende von s an und liefert das s veränderte s zurück. DR A int strcmp(const char *cs, const char *ct) Vergleicht den String cs mit dem String ct und liefert ein Ergebnis < 0, wenn gilt cs < ct, 0, wenn cs==ct und > 0, wenn gilt cs > ct size_t strlen(const char *cs) Liefert die Länge des Strings cs zurück. size_t ist dabei ein unsigned integer Typ, der von sizeof zurückgeliefert wird. Diese Funktionen und weitere sind in string.h deklariert und Bestandteil des Standards. 7.6.2 Funktionen für Zeichen int isalnum(int c) isalpha(c) oder isdigit(c) ist wahr. int isalpha(int c) islower oder isupper ist wahr. int isctrl(int c) c enthält ein Control Zeichen (0–0x1F, 0x7F). int isdigit(int c) c enthält eine Ziffer 0 − 9. int islower(int c) c enthält einen Kleinbuchstaben. int isupper(int c) c enthält einen Großbuchstaben. 7.6.3 Mathematische Funktionen double sin(double x) sin(x) double cos(double x) cos(x) double exp(double x) ex double log(x) ln x double log10(double x) log10 x double pow(double x, double y) xy √ double sqrt(double x) x KAPITEL 7. FUNKTIONEN 72 7.6.4 Datum und Uhrzeit FT clock_t clock(void) Liefert die Prozessorzeit seit start des Programms. time_t time(time_t *tp Liefert die aktuelle Zeit und Datum. size_t strftime(char* s, size_t smax, const char *fmt, const struct tm *tp) Konvertiert nach ähnlichen Regeln wie printf Datum und Uhrzeit in einen lesbaren String, wie z. B. Montag, 30.08.2004. 7.6.5 Hilfsfunktionen double atof(const char *s) int atoi(const char *s) int rand(void) void srand(unsigned int seed) 7.7 Aufruf-Parameter DR A Sie können einem Programm auch schon beim Aufruf Parameter mitgeben. Das geht aber anders als mit „normalen“ Funktionen. Der naive Ansatz würde einfach ein Programm mit einer main-Funktionen schreiben, in der Parameter deklariert sind. Versuchen wir doch einfach mal ein Programm zu schreiben, das mit einem Parameter aufgerufen wird und diesen auf dem Bildschirm ausgibt („echoed“). /* func01.c zum Umgang mit Parametern in der main Funktion Status: Wrong*/ #include <stdio.h> void main(char *echo) { printf("\n%s", echo); } Das lässt sich zwar prima umwandeln, das Ergebnis entspricht aber nicht unseren Vorstellungen: Statt das Gewünschte zu tun bricht das Programm gnadenlos ab. Das ist auch verständlich, denn schließlich haben wir es hier mit einem Teil der Schnittstelle zum Betriebssystem zu tun und da kann man sich schon vorstellen, dass dies etwas komplizierte ist. Dazu müssen wir in der main-Funktion zwei Parameter verwenden, die üblicherweise argc (argument count) und argv (argument vector) genannt werden. Im ersten steht dann die Anzahl Parameter und in dem anderen stehen die übergebenen Parameter zur Verfügung. Unser Programm echo.c sieht damit nun so aus: /* echo01.c zum Umgang mit Parametern in der main Funktion Status: incomplete*/ #include <stdio.h> void main(int argc, char *argv[]) { int i; for(i=1;i<argc;i++) printf("%s%s",argv[i],(i < argc-1)?" " : ""); printf("\n"); } Nun läuft alles wie gewünscht. Sie werden noch Gelegenheit haben, diese Sache auszuprobieren. 7.8. HISTORISCHE AMERKUNGEN 73 7.8 Historische Amerkungen 7.9 Aufgaben FT Funktionen gibt es in vielen Programmiersprachen. Manchmal heißen Sie Unterprogram, manchmal Routine, Procedure usw. Die Ansichten darüber, wie stark man ein Programmsystem durch Funktionen oder Unterprogramme modularisieren sollte, haben sich im Laufe der Zeit gewandelt. In C sehen Sie aber häufig große System, die aus vielen relativ kleinen Funktionen bestehen. Jede einzelne Funktion kann dabei relativ einfach sein. Trotzdem verliert man angesichts der vielen Funktionen in einem System dann auch leicht den Überblick. 1. Schreiben Sie eine Funktion char *stringcopy(char *str2, char *str1), die einen String „str1“ auf einen String „str2“ kopiert. 2. Schreiben Sie eine Funktion, die alle Großbuchstaben eines Strings in Kleinbuchstaben konvertiert! 3. Schreiben Sie eine Funktion, die in einem String nach einem Zeichen sucht und die erste Stelle, an der das Zeichen auftritt zurückliefert. DR A 4. Schreiben Sie eine Funktion encode, die einen String nach folgendem einfachen Schema verschlüsselt: Jeder Buchstabe wird durch den n-ten auf ihn folgenden ersetzt. Schreiben Sie auch eine Funktion decode, die den Ursprungsstring wieder herstellt. 5. Hier sollen sie üben, wie ein Wert eingelesen werden kann beziehungsweise (bzw.) wie ein Wert mit der Tastatur dem Programm übergeben werden kann. Schreiben Sie ein Programm, dass eine Körpergröße h in Meter und ein Gewicht w in Kilogramm erhält und den Body-Mass-Index (BMI) berechnet und ausgibt: w BM I := 2 h Fragen Sie nach der Ausgabe des Ergebnisses den Benutzer, ob er weitermachen möchte. Versuchen Sie bitte, dies in einer Funktion zu tun, die Sie auch für andere Aufgaben verwenden können (wie gesagt: Gute ProgrammiererInnen sind faul). KAPITEL 7. FUNKTIONEN DR A FT 74 FT Kapitel 8 Der C Präprozessor 8.1 Übersicht Was Sie in diesem Kapitel lernen, werden Sie an einigen Stellen dringend benötigen. Anderes sollten Sie vergessen, sobald sie keinen Code mehr pflegen müssen, der diese Konstrukte verwendet. DR A 8.2 Lernziele Dieses Kapitel hat nur drei Lernziele: • Die Präprozessor-Befehle anwenden können, die Sie immer wieder benötigen werden. • Die Gefahren des Präprozessors kennen. • Den Präprozessor gezielt da nutzen können, wo es nicht anders geht und ihn ansonsten nicht benutzen. 8.3 Einbinden von Code Den Präprozessor-Befehl #include haben wir bereits in Abschnitt 2.6 kennengelernt. Es gibe ihn in zwei Varianten, die in der folgenden Abb. erläutert sind. Damit ist eigentlich schon fast alles über #include #include <hugo.h> \#include "hugo.h" Die Datei wird implementierungsspezifisch gesucht, z. B. im Standardpfad Das aktuelle Verzeichnis wird nach hugo.h durchsucht Wird sie nicht gefunden, so wird implementierungsspezifisch weitergesucht Abbildung 8.1: #include gesagt. Nur noch eine kleine Anmerkung: Ob eine .h-Datei oder etwas anderes verwendet wird, um Bibliotheken zur Verfügung zu stellen ist nicht festgelegt, sondern wird dem Hersteller des Compilers überlassen. Achten Sie ferner darauf, dass Präprozessor-Befehle nicht mit einem Semicolon beendet werden. 8.4 Definieren von Eigenschaften Betrachten wir ein Programm, das Temperaturen in Fahrenheit in Celsius umrechnet ([KR88], p. 13) 75 KAPITEL 8. DER C PRÄPROZESSOR 76 FT /* fahrenheit01.c Konvertieren von Temperaturen in Fahrenheit in Celsius */ #include <stdio.h> main() { int fahr; for(fahr = 0;fahr <= 300;fahr = fahr + 20) printf("%d %6.1f\n",fahr,(5.0/9.0)*(fahr-32)); } Es ist kein guter Stil, Dinge wie die untere Grenze (0), die obere Grenze (300) und die Schrittweite (20) fest im Programm zu „verdrahten“. Eine Möglichkeit, dies etwas flexibler zu gestalten bietet der PräprozessorBefehle #define. Er ermöglichte es (möglichst sinnvolle) Namen für irgendwelche Strings zu vergeben. Hier werden die Namen LOWER, UPPER, STEP verwendet. Die Konventionen solche symbolische Namen oder symbolische Konstanten in Großbuchstaben zuschreiben ist sehr weit verbreitet. DR A /* fahrenheit02.c Konvertieren von Temperaturen in Fahrenheit in Celsius, #define */ #include <stdio.h> #define LOWER 0 #define UPPER 300 #define STEP 20 main() { int fahr; for(fahr = LOWER;fahr <= UPPER;fahr = fahr + STEP) printf("%d %6.1f\n",fahr,(5.0/9.0)*(fahr-32)); } Obwohl dies weit verbreitet ist, gibt es an diesem Vorgehen Kritik, wie wir weiter unten sehen werden. Manche sehen es als besser an, für diese Zwecke Konstanten zu verwenden, wie im folgenden Beispiel fahrenheit03.c /* fahrenheit03.c Konvertieren von Temperaturen in Fahrenheit in Celsius, const */ #include <stdio.h> const int LOWER = 0; const int UPPER =300; const int STEP =20; main() { int fahr; for(fahr = LOWER;fahr <= UPPER;fahr = fahr + STEP) printf("%d %6.1f\n",fahr,(5.0/9.0)*(fahr-32)); } Ihren Vorteil haben die durch #define definierten symbolischen Namen aber auch: So können Sie ganze Funktionen etc. „umbenennen“. Ein einfaches Beispiel hierfür ist: #define forever for(;;) Solche Makros können auch Parameter erhalten: #define max(A, B) ((A) > (B)? (A) : (B)) Dieses Makro wird inline aufgelöst: An jeder Stelle, an der max(A, B) wird ((A) > (B)? (A) : (B)) ersetzt. Das kann böse Folgen haben: Sehen wir uns mal an, was passiert, wenn wir folgendes Programm ausführen: 8.5. BEDINGTE BEFEHLE BZW. VERZWEIGUNGEN 77 FT /* preproz01 Gefahren des Präprozessors */ #include <stdio.h> #define max(A, B) ((A) > (B)? (A) : (B)) main() { int i =1; int j =2; printf("%d ist das Maximum von %d und %d",max(i++,j++),i,j); } Dieses Programm liefert die Ausgabe: 3 ist das Maximum von 2 und 4 Das Maximum wird korrekt berechnet, aber die größere Variable wird zweimal inkrementiert. Dies kommt dadurch zu Stande, dass wirklich immer die Variable ersetzt wird. Das Gegenstück zu #define ist #undef. Damit wird der Name wieder frei und kann anderweitig verwendet werden. 8.5 Bedingte Befehle bzw. Verzweigungen DR A In vielen Fällen kann man nicht einfach Makros definieren oder Dateien inkludieren, sondern muss dass von Bedingungen abhängig machen. Dazu gibt es die Befehle #if, #elif (else if), #else, #endif Soweit #define betroffen ist, verwendet man noch #ifdef(if defined) und #ifndef (if not defined). Wollen Sie z. B. sicher stellen, dass eine Header-Datei nur einmal eingebunden wird, so erreichen Sie das so #if !defined(HDR) #define HDR /* Inhalt von hdr.h */ #endif Eine alternative Schreibweise dafür ist #ifndef HDR #define HDR /* Inhalt von hdr.h */ #endif 8.6 Einige Tücken Sehen Sie sich bitte folgendes Programm genau an: /****************************************** summe04.c berechnet die Summe zweier ganzer Zahlen Wirklich? ******************************************/ #include <stdio.h> #define summe differenz int differenz(int a, int b); /*int summe(int a, int b);*/ KAPITEL 8. DER C PRÄPROZESSOR 78 FT int main(void) { int pa = 9; int pb = 16; printf("%d%s%d%s%d%s",pa," + ",pb," = ",summe(pa,pb),"."); return 0; } int differenz(int a, int b) { return (a - b); } Sind Sie sicher, dass Sie solche Dinge in einem größeren System mit hunderten von Header- und Implementierungsdateien sicher und schnell erkennen? Wenn nicht, sollten Sie mit dem Präprozessor vorsichtig und sorgfältig umgehen. Wenn doch, sollten Sie Ihre Ansicht nochmal überdenken. 8.7 Historische Anmerkungen DR A Der Präprozessor ist nach meiner Ansicht das (vielleicht) ärgerlichste Relikt aus der Steinzeit der Datenverarbeitung in C, C++ und letzendlich sogar in Java. 8.8 Aufgaben 1. Definieren Sie ein Macro swap(t,x,y, das zwei Argumente vom Typ t vertauscht. FT Kapitel 9 Zeiger (Pointer) 9.1 Übersicht DR A An Zeigern, Neudeutsch Pointern scheiden sich die Geister. Für die Einen sind sie eine der wichtigsten Ursachen für Programmierfehler, für die Anderen eine unverzichtbare Möglichkeit gut lesbaren und performanten Code zu schreiben. Ich rechne mich eher dem zweiten Lager zu. Der Einsatz von Zeigern ist keine strategische Frage sondern lediglich eine taktische oder operationelle Frage. Ein guter Programmierer wird Zeiger dort verwenden, wo die notwendig oder sinnvoll ist. Zeiger sind Variablen, wie andere auch. Entscheidend ist, dass es für jede Deklaration eines Datentyps in C eine entsprechende Deklaration eines Zeigers gibt. Sie können von einer Variablen, die mit einem Datentyp deklariert ist, zum Zeiger kommen und umgekehrt. Die Operatoren hierzu haben Sie bereits in Kap. 4 gesehen. In diesem Kapitel werden sie deren praktischen Einsatz kennenlernen. 9.2 Lernziele • Wissen, was ein Zeiger ist. • Den Unterschied zwischen Wert und Adresse einer „Variablen“ erläutern können. • Mit „Zeigern rechnen“ können. • Den Adress- und den Dereferenzierungsoperator sicher anwenden können. 9.3 Definition von Zeigern In Abschn. 2.8 haben Sie gesehen, wie eine Variable in C „aussieht“. Hier eine kurze Wiederholung. Durch int eineGanzeZahl = 5; wird eine Variable deklariert die den Typ int hat und es wird ihr der Wert 5 zugewiesen. Durch den Adressoperator & & eineGanzeZahl erhalte ich die Adresse der Variablen eineGanzeZahl. Eine Adresse vom Typ int wird durch int *adresseEinerGanzenZahl; wird ein Zeiger deklariert. In diesem Beispiel könnte man sich so einen Überblick über die Lage machen /********************************** pointer01.c integer pointer and adress Status: ok. **********************************/ 79 KAPITEL 9. ZEIGER (POINTER) 80 Dieses Programm liefert die Ausgabe; eineGanzeZahl: &eineGanzeZahl: adresseEinerGanzeZahl: *adresseEinerGanzenZahl: FT #include <stdio.h> void main(void) { int eineGanzeZahl = 5; int *adresseEinerGanzenZahl =&eineGanzeZahl; printf("\neineGanzeZahl: \t\t\t%d", eineGanzeZahl); printf("\n&eineGanzeZahl: \t\t%X", &eineGanzeZahl); printf("\nadresseEinerGanzeZahl: \t\t%X", adresseEinerGanzenZahl); printf("\n*adresseEinerGanzenZahl: \t%d", *adresseEinerGanzenZahl); } 5 12FEE0 12FEE0 5 DR A Auf diese Weise haben wir gleich die Formatangabe %X wiederholt, die die hexadezimale Ausgabe bewirkt. Ich war es gewohnt, Dumps in einem Format zu lesen, in dem nur Großbuchstaben erscheinen. Es ist wahrscheinlich, dass Sie so etwas nicht mehr werden machen müssen. Falls doch: Ihr Taschenrechner oder PC kann Hex Rechnen oder Sie können sich ein kleines C-Programm schreiben, dass dies erledigt. Vielleicht ergibt sich das sogar in diesem Kurs. Fassen wir zusammen: In C können Sie sowohl Variablen mit einem bestimmten Datentyp deklarieren als auch Variablen, die einen Zeiger auf einen Wert enthalten. Aus diesen Definitionen ergeben sich sofort einige offene Punkte bzw. Fragen: 1. Ok, wenn ich eine Variable deklariere und definiere reserviere ich mir Speicher und weise diesem bei Definition auch einen Wert zu. Was ist, wenn ich einen Zeiger deklariere? Woher weiß der Compiler, wieviel Platz er braucht? Bei einem Zeiger auf ein int mag das offensichtlich sein, aber was ist bei char *s oder char *s[] 2. Wie weise ich einem Zeiger einen Wert zu? 3. Wie stelle ich sicher, dass die Adresse, die ich einem Zeiger zuweise • Frei ist? • Einen Datentyp des gewünschten Typs aufnehmen kann? Um die erste Frage zu beantworten müsste ich Ihnen zunächst erklären, wie Sie Speicher reservieren, sonst würde Alles schief gehen. Ich beschränke mich deshalb in diesem Abschnitt auf den Umgang mit festem Speicher, der wärend der Laufzeit des Programms nicht wächst oder schrumpft. Den Rest hole in Kap. 13 nach. Die zweite Frage ist da schon einfacher zu beantworten, muss aber zunächst präzisiert werden. Soll dem Zeiger selber ein Wert zugewiesen werden? Das muss man machen, wenn ein Zeiger deklariert aber noch nicht definiert wurde. Wie das geht erfahren Sie in Kap. 13. Einem bereits initialisierten Zeiger einen neuen Wert zuweisen? Er zeigt dann auf einen anderen Teil des Speichers. Sie müssen also sicherstellen, dass er weiter auf etwas zu seinem Typ kompatibles zeigt. So etwas kann sehr wohl nützlich und sinnvoll sein, aber Sie sollten dann immer genau wissen, was Sie tun, damit nichts schief geht. Wollen Sie dem Speicherbereich, auf den der Zeiger verweist einen neuen Wert zuweisen? Nichts leichter als das. Nehmen Sie den *-Operator und ab geht die Post. Das haben wir ja gerade schon gemacht, hier noch ein kleines Beispiel: /* zeiger02.c Elementares Beispiel für * Operator und Zuweisung zu Zeigern Status: ok*/ #include <stdio.h> 9.4. ZEIGERARITHMETIK 81 9.4 Zeigerarithmetik FT void main(void) { char *palindrom = "RELIEFPFEILER"; printf("\n%s",palindrom); } In C gibt es einen engen Zusammenhang zwischen Zeigern und Vektoren. Jede Operation, die mittels der Indexe in einem Vektor durchgeführt werden kann, kann auch mit Zeigern ausgeführt werden. Sehen wir uns das mal im Vergleich an: DR A int a[] = {0,1,2,3,4,5,6,7,8,9}; int *za; int x, y, z; int i = 5; za = &a[0]; za = a; x = *za; y = *(za+1); z = *(a+i); Was passier hier alles? 1. In der ersten Zeile wird ein Vektor von 10 ganzen Zahlen deklariert und mit den Zahlen von 0 bis 9 initialisiert. 2. In der nächsten Zeile wird zur Abwechslung ein Zeiger za auf int deklariert. Ein solcher Zeiger zeigt auf eine Adresse im Speicher, ab wo ganze Zahlen gespeichert werden können. 3. Die nächsten beiden enthalten nur Deklarationen und eine Definition von später verwendeten Hilfsvariablen. 4. In der nächsten Zeile weise ich dem Zeiger za die Adresse des ersten Vektorelements a[0] zu. 5. Wer sich noch an Kap. 6 erinnert, sollte wissen, dass das auch einfacher geht, Ich hätte es einfach so wie der nächsten Zeile machen können. Der Name eines Vektors ist ja ein Zeiger auf das erste Element des Vektors. 6. Was bewirkt nun die Zuweisung x = *za? Daduch wird x = a[0], denn so haben wir ja gerade eben zugewiesen. 7. Hier wird es jetzt spannender: za+1 setzt den Zeiger um eine Adresse weiter. Da der Zeiger typisiert ist, „weiß“ er, wie lang die Daten sind, auf die er verweist. Daher kann er mit dieser Anweisung auf das nächste Element verweisen. Das ist nach der erfolgten Zuweisung gerade a[1] und das wird jetzt y zugewiesen. 8. Ganz analog wird in der nächsten Zeile das i-te (hier das 5-te) Element von a der Variablen z zugewiesen. Der erfahrene C-Programmierer bevorzugt nach meinem Eindruck immer die Arbeit mit Zeigern gegenüber der Arbeit mit Vektoren. Auch Sie sollten sich daran gewöhnen. Ich bin zwar überzeugt davon, dass die Vektorschreibweise in vielen Fällen • besser lesbar und einfacher verständlich ist. • ein Compiler aus beiden Schreibweisen den gleichen ausführbaren Code erzeugen kann. KAPITEL 9. ZEIGER (POINTER) 82 FT Das würde mich dazu bewegen immer, die leichter lesbare Variante zu schreiben, aber ich bin ja auch nicht in einer C-Umgebung aufgewachsen, sondern mit anderen Sprachen. Denken Sie bei Ihren eigenen Programmen bitte an das, was ich Ihnen schon früher gesagt habe: Programme, die ein Computer versteht, kann jeder Idiot schreiben. Gute Programmierer schreiben Programme, die Menschen verstehen. Auch C-Programmierer sind Menschen und man sollte ihre Marotten ernstnehmen. Betrachten wir zum Abschluss einmal die folgende Code-Zeile, die ein wohlbekannte Funktion implementiert (vgl. [KR88], S. 105, [Str97]). while (*s++ = *t++); s und t seien dabei char *. Dies erscheint zunächst etwas kryptisch, ist aber ein Stil, den Sie in CProgrammen häufig sehen werden. Meine Ansicht zu diesem Code verrate ich Ihnen am Ende dieses Abschnitts. Sehen wir uns nun an, was dieser Code eigentlich tut. 1. Die while-Schleife wird so lange durchlaufen, bis der Ausdruck, der dort steht, ungleich 0 ist. Ist er 0, so wird die Schleife verlassen. Etwas redundant, aber für Anfänger leichter zu lesen könnte man also auch schreiben: while ((*s++ = *t++)!= 0); DR A 2. Das bringt uns auf eine weitere Idee: Was hier abgefragt wird, ist ja offenbar das Stringendesymbol. Wir können also auch schreiben while ((*s++ = *t++) != ’\0’); Damit haben wir das „Geheimnis“ dieses Codes fast schon gelüftet. 3. Die Postfix-Inkrementoperatoren werden erst nach Auswertung des Ausdrucks wirksam. Es wird also der Wert an der alten Position von t auf die entsprechende Position von s gesetzt. Dieser Wert ist auch der,m der mit ’\0’ verglichen wird. Also läuft das Ganze, bis der String t auf den String s kopiert worden ist, einschließlich des Endesymbols. Was Sie oben gesehen haben, ist also einfach die Implementierung der strcpy-Funktion. Der Stil ist in C üblich. Sie sollten ihn aber nicht einfach kopieren. Diese Code-Zeile brauchen Sie nie zu schreiben, dafür gibt es die Funktion strcpy, die Sie einfach aufrufen können. 9.5 Historische Anmerkungen 9.6 Aufgaben FT Kapitel 10 Strukturierte Datentypen 10.1 Übersicht Die strukturierten Datentypen in C stellen einen ersten Schritt zu Klassen in objektorientierten Systemen dar. Die wesentlichen Unterschiede zwischen einer struct in C und einer class in C++ sind: • Eine class kann auch Funktionen als Elemente haben. DR A • Die Sichtbarkeit von Elementen ist bei einer struct public und bei einer class private, wenn nicht explizit etwas anderes angegeben wird. Dieses Kapitel soll Ihnen auch helfen, Ihr Abstraktionvermögen zu schärfen. Viele Dinge lassen sich einfacher erledigen, wenn man einige der Dinge, die zu erledigen sind, systematisch strukturiert.1 Die wesentliche Anwendung von structs wird für Sie sein, den Inhalt von Datensätzen, die Sie mit einem Programm aus einer Datei lesen einfach und mit einfacher Fehlerbehandlung lesen zu können. 10.2 Lernziele • structs definieren und verwenden können. • Mit structs als Parameter in Funktionen arbeiten können (call by value). • Mit Zeigern auf structs umgehen können. 10.3 Deklaration strukturierter Datentypen Eine struct ist eine Zusammenfassung einer oder mehrerer Variablen unter einem gemeinsamen Namen. Sie definieren also so eine Art neuen Typs in C. Hier eine Reihe von Beispielen: 1. Um mit Punkte auf einem Bildschirm zu hantieren können Sie eine Struktur „Punkt“ definieren: struct punkt { int x; int y; }; 2. In der Elektrotechnik (natürlich nicht nur dort) benötigen Sie ab und zu komplexe Zahlen. Die könnten sie so definieren 1 Das ist eine grundlegende Einsicht der Erkenntnistheorie, es sprengt aber den Rahmen einer Programmiervorlesung, dies genau auszuführen. 83 KAPITEL 10. STRUKTURIERTE DATENTYPEN struct complex { double re; double im; }; FT 84 3. Adressen bestehen in vielen Ländern aus Straße, Hausnummer, Postleitzahl und Ort. Das könnte man so in C nachbilden. struct { char char char char }; adresse *strasse; *nummer; *plz; *ort; DR A Die Deklaration einer struct definiert einen neuen Typ. Die Variablen in den geschweiften Klammern heißen Elemente (englisch member oder data memmber) der struct. In den obigen drei Beispielen hat etwa punkt die Elemente x und y, complex die Elemente re und im und adresse die vier Elemente strasse, nummer, plz und ort. Da structs Typen definieren, können Sie nach einer Deklaration einer struct Variablen mit diesem Typ definieren und auch mit konstanten Ausdrücken initialisieren. Dies machen wir nun mal für die drei eben deklarierte Beispiele. 1. Durch die Punkte anfang und ende wird eine Strecke definiert, die vom Punkt mit den Koordinaten (0,0) zu (10,10) geht. struct punkt anfang = {0,0}; struct punkt ende = {10,10}; 2. Durch struct complex i = {0, 1}; definieren wir eine Variable i, deren Wert die „imaginäre Einheit“ ist. 3. Das folgende ist die Adresse des Fuxing Campus der USST. struct adresse usst = {"Fuxing Zhonglu","1195","200031","Shanghai"}; Da structs Typen definieren, können Sie auch wieder in struct Deklarationen verwendet werden. So kann man aufbauend auf der struct punkt Strukturen für Strecken oder Kreise aufbauen. struct strecke { struct punkt anfang; struct punkt ende; }; struct kreis { struct punkt mittelpunkt; int radius; }; Bei Variablen, wie wir sie bisher kennengelernt haben, können Sie die Initialisierung so machen, wie hier beschrieben. Sie können dazu auch eine Funktion aufrufen, die diesen strukturierten Datentyp zurückliefert. 10.4. VERWENDUNG VON STRUKTURIERTEN DATENTYPEN 85 10.4 Verwendung von strukturierten Datentypen FT Haben Sie strukturierte Datentypen definiert, so können Sie mit ihnen arbeiten. Wie man lokale Variable initialisiert haben Sie bereits im vorstehenden Abschnitt gesehen. Dazu wird einfach für jedes Element ein konstanter Ausruck mitgeliefert. Anschließend können wir mit den Variablen arbeiten. An ein Element einer Struktur kommen wir mit dem Operator „.“, dem structure member operator, heran. So können wir dann einfach die eben initialisierte Werte in gewohner Weise ausgeben, um uns persönlich davon zu überzeugen, dass die Variablen so initialisiert wurden, wie wir es beabsichtigten. printf("\nanfang = (%d,%d).",anfang.x,anfang.y); printf("\nende = (%d,%d).",ende.x,ende.y); printf("\ni = %lf + i%lf).",i.re,i.im); printf("\nAnschrift USST:\n%s %s\n%s %s",\ usst.nummer,usst.strasse,usst.ort,usst.plz); Und tatsächlich erhalten wir das gewünschte Ergebnis: DR A anfang = (0,0). ende = (10,10). i = 0.000000 + i1.000000. Anschrift USST: 1195 Fuxing Zhonglu Shanghai 200031 Allerdings sehen wir nun, dass die dritte Zeile etwas irritierend zu lesen ist. Einerseits habe ich die Variable i genannt. Das erschien auf den ersten Blick sinnvoll, weil der Wert ja die imaginäre Einheit i sein sollte. So liest sich die Zeile etwas sonderbar. Ferner ist es in C üblich, mit Buchstaben wie i, j etc. int-Variablen zu bezeichnen. Dies alles zeigt, dass ich die Varaible besser imag für imaginäre Einheit oder ähnlich hätte nennen sollen. Gerade in größeren Projekten ist es wichtig gute Namenskonventionen konsequent umzusetzen, damit die Programme lesbar bleiben. Wie anderen Variablen können Sie auch Variablen strukturierter Datentypen andere Varaiblen des gleichen Typs zuweisen: struct punkt a = ende, e=anfang; struct complex imag = i; struct adresse usstfuxing = usst; Mit dem sizeof-Operator können Sie feststellen, wie viele Bytes die von Ihnen definierten strukturierten Datentypen belegen. printf("\n\t\tBytes"); printf("\npunkt:\t\t%d",sizeof(struct printf("\ncomplex:\t%d",sizeof(struct printf("\nadresse:\t%d",sizeof(struct printf("\nstrecke:\t%d",sizeof(struct printf("\nkreis:\t\t%d",sizeof(struct Hier das Ergebnis: punkt: complex: adresse: strecke: kreis: Bytes 8 16 16 16 12 punkt)); complex)); adresse)); strecke)); kreis)); 86 KAPITEL 10. STRUKTURIERTE DATENTYPEN FT Dieses Ergebnis ist bei den Typen punkt, complex, strecke und Kreis unmittelbar einleuchtend. Die Länge ergibt sich durch Addition der Längen der Elemente. Bei adresse stutzen Sie vielleicht zunächst: Die Strings der Variablen usst sind doch viel länger als 16 Bytes. Hier müssen Sie sich an die Kap. 6 und 9 zurückerinnern. Stringvariablen sind Zeiger. Ein Zeiger enthält eine Adresse. Adressen sind auf einem 32-Bit-Rechner 4 Bytes lang und damit ist auch dieses Ergebnis klar. Außer den bisher vorgeführten Operationen können Sie von einer Variablen eines strukturierten Datentyps nur noch die Adresse über den Adressoperator & bekommen. Sie kann nicht in Vergleichsoperationen verwendet werden. Solche Variablen können aber auf verschiedene Arten an Funktionen übergeben werden, wie Sie im nächsten Abschnitt sehen werden. 10.5 Strukturierte Datentypen, Funktionen und Zeiger Haben wir zwei Punkte a = (a1 , a2 ) und b = (b1 , b2 ), so können wir nach dem Lehrsatz des Pythagoras ihren Abstand leicht ermitteln: p Abstand = ((b1 − a1 )2 + (b2 − a2 )2 ) DR A In Abschn. 7.6 haben Sie die C-Funktion sqrt gesehen. Mittels dieser Funktion können wir die Abstandsberechnung für zwei Punkte leicht implementieren. Ich werde das zwar nicht ganz ausführen, aber im Ansatz so machen, wie Sie bei ernsthaften C-Projekten vorgehen würden. Die Deklaration des strukturierten Datentyps Punkt schreibe ich in eine Header-Datei punkt.h. In diese Datei schreibe ich auch alle Deklarationen der Funktionen, die ich für die Arbeit mit diesem Datentyp schreibe. Die Implementierungen dieser Funktionen kommen in eine Datei punkt.c. In einem weiteren Programm teste ich dann, was ich dort geschrieben habe. Außer der Funktion abstand schreibe ich mir gleich eine Funktion machpunkt, die mir aus zwei ganzen Zahlen einen Punkt erstellt. Hier das Zwischenergebnis: #ifndef PUNKT #define PUNKT /*punkt.h Deklarationen für die struct punkt*/ struct punkt { int x; int y; }; double abstand(struct punkt a, struct punkt b); struct punkt machpunkt(int x, int y); #endif /*punkt.c Implementierung von Funktionen für struct punkt Status: incomplete */ #include "punkt.h" #include <math.h> double abstand(struct punkt a, struct punkt b) { return sqrt((b.x-a.x)*(b.x-a.x)+(b.y-a.y)*(b.y-a.y)); } struct punkt machpunkt(int x, int y) { struct punkt temp; temp.x = x; temp.y = y; return temp; } 10.5. STRUKTURIERTE DATENTYPEN, FUNKTIONEN UND ZEIGER 87 Hier wieder wie gewohnt einige Erläuterungen dazu. FT 1. Die Header-Datei punkt.h beginnt mit einem Präprozessor Befehl #ifndef PUNKT. Dieser Befehl überprüft, ob Konstant PUNKT bereits definiert wurde. Ist dies des Fall, so geht es nach dem #endif weiter, es werden also gar keine Deklarationen aus punkt.h übernommen, wenn irgendwo #include s"punkt.h" steht. Ist PUNKT noch nicht definiert, so wird als erstes PUNKT definiert und dann alle Zeilen in die includende Datei übernommen. Auf diese Weise wird verhindert, das punkt.h mehrfach includet wird. 2. Anschließend wird der strukturierte Datentyp punkt deklariert und die beiden Funktionen abstand und machpunkt. 3. abstand gibt double zurück, denn obwohl wir Punkte mit ganzzahligen Koordinaten betrachten, ist der Abstand zweier Punkte nicht notwendig ganzzahlig. 4. In machpunkt wird eine temporäre Variable vom Typ punkt deklariert, ihren Elementen werden die übergebenen Parameter zugewiesen. Anschließend wird die so nun auch definierte Variable zurückgegeben. DR A In dem Beispiel der Abstandsberechnung habe ich der Funktion die Strukturen übergeben. Das ist aber nur eine der Möglichkeiten. Eine andere Möglichkeit wäre es, die einzelnen Elemente des strukturierten Datentyps separat zu übergeben. Dies bringt uns aber etwas in Konflikt mit dem eigentlichen Ziel der strukturierten Datentypen. Wir wollen mit diesen ja gerade die Komponenten zusammen behandeln können und gerade nicht jede einzelne. Außerdem steht es in Widerspruch zum bewährten Prinzip der Kapselung. Diese ist zwar bei structs noch nicht besonders ausgeprägt, wohl aber bei der Weiterentwicklung des Konzepts zu Klassen z. B. in C++. Die einzelnen eines strukturierten Datentyps zu übergeben, kann meines Erachtens höchstens in Implementierungsdateien für structs sinnvoll sein, nicht in Programmen, die diese structs nutzen. Eine weitere Möglichkeit besteht darin einen Zeiger auf die struct zu übergeben. Dies ist insbesondere für große Strukturen oft effizienter. Ich zeige Ihnen as an einer einfachen Funktion für den strukturierten Datentyp adresse. Muss man in mehreren Programmen eine Adresse drucken, so schreibt man sich dafür besser eine Funktion. Die macht das dann einheitlich und Sie müssen nicht jedesmal eine mehr oder weniger komplexe printf-Konstruktion eintippen oder kopieren. Hier das Ergebnis /*struct03.c Einige Beispiele für structs*/ #include <stdio.h> struct adresse { char *strasse; char *nummer; char *plz; char *ort; }; int print(const struct adresse* adr); void main(void) { struct adresse usst = {"Fuxing Zhonglu","1195","200031","Shanghai"}; print(&usst); } int print(const struct adresse *adr) { return printf("\n%s %s\n%s %s",\ (*adr).nummer,(*adr).strasse,(*adr).ort,(*adr).plz); } und die Erläuterungen dazu. 88 KAPITEL 10. STRUKTURIERTE DATENTYPEN 1. Die Deklaration der Struktur kennen wir ja bereits. FT 2. Danach folgt die Deklaration der Funktion print. Ich habe hier einen englischen Namen gewählt, um die Ähnlichkeit mit der C-Funktion printf zu betonen. Wie printf gibt auch print die Anzahl ausgegebener Zeichen zurück. 3. Als einzigen Parameter erhält die Funktion einen Zeiger auf eine adresse. 4. Der Parameter ist als const deklariert. Auch dies entspricht der Deklaration in printf. Sie können sich höchstens fragen, ob die Adresse, oder der ab dieser Adresse gespeicherte Inhalt durch print nicht geändert werden kann. Die Adresse zu ändern ist nicht sinnvoll, also kann wohl nur der Inhalt vor irrtümlicher Verwendung geschützt sein. Ausprobieren liefert folgendes Ergebnis: Versuchen Sie in print die übergebene Adresse zu verändern, etwa durch eine neue Zuweisung, so bekommen Sie die Compiler-Fehlermeldung. error C2166: L-Wert gibt ein konstantes Objekt an DR A Das ist eher eine Auskunft für Informatiker als für Programmierer. Mit L-Wert ist die linke Seite einer Zuweisung gemeint. Steht dort ein Parameter der mit const deklariert wurde, so weist der Compiler diesen Versuch entrüstet zurück. 5. In der main-Funktion initialisieren wir eine Variable mit einer bekannten Adresse und rufen dann print auf. 6. Da print eine Adresse erwartet, müssen wir vor die zu übergebende Varaible den Adress-Operator & schreiben. 7. Die Implementierung der Funktion print enthält nur einen Befehl return printf( .... 8. Dadurch wird zunächsteinmal die Anzahl ausgegebene Zeichen zurückgeliefert, wie es oben beschriebne wurde. 9. Der Formattierungsstring von printf sieht genauso aus wie in dem obigen Beispiel. 10. Stolpern werden Sie über die Angaben der Elemente einer Adresse. Aber auch das ist halb so schlimm. Der Parameter heißt adr und ist ein Zeiger. *adr ist also das was an der Stelle steht, auf die der Zeiger verweist. Sicherheitshalber klammer ich so etwas immer ein: (*adr). Die Klammern sind in diesem Fall auch nötig, ohne die Klammern würde das nicht funktionieren, da der Punkt-Operator stärker bindet als der *-Operator. Aber dafpr gibt es ja gerade die Klammerpaare. Dann folgt der Punkt-Operator mit dem wir auf die Elemente einer Struktur zugreifen können und der Name des Elements und das war’s dann auch schon. Zugriffe auf die Elemente einer Struktur, auf die man einen Zeiger übergeben bekommen hat, sind so häufig, das es dafür einen eigenen Operator gibt, den „structure pointer operator“, oder „Stuktur Zeiger Operator“ „->“. Mit Hilfe dieses Operators schreibe ich nun den Befehl in print in der in C üblichen Form um: int print(const struct adresse *adr) { return printf("\n%s %s\n%s %s",\ adr->nummer,adr->strasse,adr->ort,adr->plz); } 10.6. HISTORISCHE ANMERKUNGEN 89 10.6 Historische Anmerkungen FT Ich sehe struct als das Konstrukt an, das den Übergang von C su objektorientierten Sprachen, wie C++ maßgeblich vorbereitet hat. Wenn Sie von C auf C++ umsteigen, gibt es meines Wissens nur zwei (kleine) Unterschiede gegenüber Klassen zu beachten: 1. Eine Klasse (class) kann im Unterschied zu einer struct auch eigene Funktionen, nun Operationen oder in C++ member functions genannt. 2. Die Default-Sichtbarkeit der Elemente ( Attribute, Operationen und Oberklassen) in C++ ist public. Für eine Klasse ist die Default-Sichtbarkeit privat. Zwischen structs in C und C++ gibt es aber einen deutlichen Unterschied: In C können Sie die Operatoren überladen. So könnten Sie viele der Operationen, die wir auf structs als Funktionen realisiert haben, durch Operatoren, wie „+“, „+=“ etc. realisieren. DR A 10.7 Aufgaben KAPITEL 10. STRUKTURIERTE DATENTYPEN DR A FT 90 FT Kapitel 11 Umwandlung von Datentypen 11.1 Übersicht 11.2 Lernziele DR A 11.3 11.4 Historische Anmerkungen 11.5 Aufgaben 91 KAPITEL 11. UMWANDLUNG VON DATENTYPEN DR A FT 92 FT Kapitel 12 Bit-Manipulationen 12.1 Übersicht 12.2 Lernziele DR A 12.3 12.4 Historische Anmerkungen 12.5 Aufgaben 1. Schreben Sie bitte Funktionen isEven(int i) und isOdd(int i), die möglichst effizient ermitteln, ob eine ganze Zahl vom Typ int gerade (even) bzw. ungerade (odd) ist! 93 KAPITEL 12. BIT-MANIPULATIONEN DR A FT 94 FT Kapitel 13 Dynamische Speicherplatzververwaltung 13.1 Übersicht DR A Bisher haben wir Speicher statisch reservert. Eine Variable wurde zu Beginn eines Blocks vollständig deklariert und definiert. In vielen Fällen ist das aber nicht möglich: • Wir wissen nicht, wieviele Zeichen oder Zeilen der Benutzer eingeben will. • Wir wissen nicht, wie groß eine Datei ist, die wir mit dem Programm einlesen müssen. In beiden Situationen ist es unsinnig dafür große Speicherbereiche statisch zu reservieren: • In vielen Fällen belegt man so viel zu viel Speicher, der eigentlich gar nicht benötigt wird und so anderer Nutzung entzogen wird. • Irgendwann wird diese statische Grenze doch überschritten werden. Sie lernen in diesem Kapitel Speicher dynamisch zu reservieren und wieder freizugeben. Das erfordert ein bischen Disziplin, aber das haben Sie an früheren Beispielen ja auch schon erlebt. Um diesw tun zu können, werden wir aber sehr viele der in den früheren Kapiteln eingeführten Konzepte benötigen. Darüber hinaus enthält dieses Kap. in Abschn. 13.3 eine Übersicht über die Speicherarten und -klassen von. 13.2 Lernziele • Die verschiedenen Arten von Speicher kennen, die in C-Programmen vorkommen. • Speicher dynamischen reservieren können. • Speicher verwalten können. • Speicher wieder freigeben können. • Mit möglichst wenigen Ressource-Leaks programmieren können. 13.3 Verschiedene Arten von Speicher Sehen wir uns mal an, wie ein C-Programm intern aufgebaut ist. Bisher haben wir ja nur Variablen am Anfang eines Blocks und einige Deklarationen und Definitionen in Header-Dateien kennengelernt, die der Compiler mitliefert. Aber wo werden diese Daten nun tatsächlich abgelegt. Ich will Ihnen hier keine ganz 95 KAPITEL 13. DYNAMISCHE SPEICHERPLATZVERVERWALTUNG FT 96 Gesamtprogramm Modul 1 lokal modulglobal lokal Funktion DR A Funktion programmglobal Modul 2 Funktion lokal modulglobal Dynamischer Speicher Abbildung 13.1: Speicherstruktur eines C-Programms 13.3. VERSCHIEDENE ARTEN VON SPEICHER 97 FT genaue Darstellung mit vielen technischen Details des Rechneraufbaus geben. Der Einfachheit halber belasse ich es bei einer stark vereinfachten Darstellung. Schematisch zeigt dies Abb. 13.1. Denken Sie bei einem Modul bitte an an eine .c-Datei die zu einer .obj umgewandelt wird und bei dem Gesamtprogramm an die durch den Linker erstellte .exe-Datei. Auf Englisch heißt das, was in dieser Abbildung als Modul bezeichnet wird „translation unit“, Umwandlungseinheit. Das ist eine Menge von .c- und .hDateien, die alleine umgewandelt werden kann. In einem solchen Programm kommen verschiedene Arten von Speicher vor. Je nach Art hat der Speicher eine unterschiedlich lange „Lebensdauer“. Es gibt auch Unterschiede, von welchen Teilen des Programms auf welche Speicherteile zugegriffen werden kann und ob die Speicherinhalte und ggf. von woher aus, geändert werden können. Diese verschiedene Arten von Speicherplatz, werde ich Ihnen nun von von „innen“ (lokal) nach „außen“ (global) vorstellen. Lokale Variablen Dies sind die Variablen, die Sie am Anfang eines Blocks innerhalb einer C-Funktion, z. B. der main-Funktion deklarieren. Ohne weitere Vorkehrungen werden sie und ihre Werte werden am Anfang des Blocks angelegt und beim Verlassen des Blocks wieder zerstört. Modulglobale Variable Das sind Variablen, die in einem Modul, z. B. einer .c oder .h-Datei außerhalb einer Funktion deklariert werden. Mit diesen Variablen können alle Funktionen des eines Moduls arbeiten. Globale Variable Diese Variablen stehen allen Funktionen eines Programmsystems zur Verfügung. DR A Sowohl globale als auch modulglobale Variablen sind schon bei Programmstart vorhanden und bleiben bis Programmende bestehen. Wie lange eine Variable „lebt“, wer darauf zugreifen darf wird durch die Schlüsselworte const, volatile, external, static, auto und register festgelegt, die wir nun im einzelnen diskutieren. const Wird der Deklaration einer Variablen const vorangestellt, so kann der Wert der Variablen vom Programm nicht verändert werden. So könnte man die Definition von PI auch so schreiben const double PI = 3.14159265358979323846; volatile Wird der Deklaration einer Variablen volatile vorangestellt, so kann der Wert der Variable von außen geändert werden. Das kann z. B. durch Interrupts geschehen. Die Deklarationen volatile und const können auch zusammen eingesetzt werden. external Externe Variablen sind zunächst einmal solche, die außerhalb einer Funktion deklariert oder definiert werden. Sie stehen ohne weitere Maßnahmen nicht für andere Quelldateien zur verfügung sondern nur für die, die sie definiert haben. Sollen solche Variablen aber nach ihrer Deklaration und vor ihrer Definition verwendet werden, so muss der Deklaration das Schlüsselwort external vorangestellt werden. In einem solchen Fall wäre es möglich, dass die die Deklaration in einer Datei und die Definition in einer anderen steht. Externe Variablen können nur mit Konstanten initialisiert werden. static Wieder etwas ganz anderes erledigt die Deklaration static für uns. Diese sorgt dafür, dass die Variable einer anderen Speicherklasse angehört. Geben Sie nichts an, so ist die Speicherklasse automatic und es handelt sich einfach um Variablen, wie wir sie bisher kennengelernt haben. Das Schlüsselwort static kann bei lokalen Variablen innerhalb eines Blocks und bei globalen Variablen außerhalb eines Blocks verwendet werden. Innerhalb eines Blocks bewirkt das Schlüsselwort static, das der Speicher beim Verlassen des Blocks nicht freigegeben wird. Er steht so lange zur Verfügung, bis das Gesamtprogramm beendet wird. Variablen, die außerhalb eines Block deklariert werden, sind automatisch „static“, der Speicher wird also erst dann freigegeben, wenn das Gesamtprogramm oder das Modul beendet wird. Bei derartigen Variablen und auch Funktionen bewirkt das Schlüsselwort static, dass die Elemente nur für Funktionen innerhalb der Datei, in denen ihre Deklaration steht, sichtbar und damit ggf. bearbeitbar sind. KAPITEL 13. DYNAMISCHE SPEICHERPLATZVERVERWALTUNG 98 FT auto Elemente, die innerhalb eines Blocks deklariert werden sind automatic, wenn nichts anderes angegeben wird, d. h. sie werden automatisch beim Verlassen des Blocks wieder vernichtet. register Objekte, die als register deklariert werden sind automatic und der Compiler sollte das Schlüsselwort als Anregung nehmen, die Objekte in schnellen Maschinenregistern zu speichern. Wie auch Sie, ist der Compiler frei diesen gut gemeinten Rat in den Wind zu schlagen.1 13.4 Speicherreservierung und Freigabe C stellt in stdlib.h einige Funktionen zur Verfügung, um vom Betriebssystem Speicher anzufordern, zu verwalten und wieder freizugeben. Dies geschieht in einem Speicherbereich der Stack2 genannt wird. Da ist zunächst die Funktion malloc, „memory allocation“, mit dem folgenden Prototyp: void *malloc(unsigned size); DR A Diese Funktion reserviert einen zusammenhängenden Speicherbereich und liefert einen Zeiger auf dessen Anfang zurück. Der Typ der Daten die dort gespeichert werden können ist void. Sie können dort also beliebige Daten ablegen. Achten Sie darauf, dass ein zusammenhängender Speicherbereich angefordert wird. Selbst wenn insgesamt noch soviel Speicher verfügbar ist, wie Sie benötigen, kann es sein, dass es kein hinreichend großes zusammenhängendes Stück mehr gibt. In diesem Fall kann Ihnen auch malloc nicht helfen und liefert den Zeiger 0 (NULL) zurück. In C ist garantiert, dass 0 nie eine gültige Adresse sein kann. Nach einem Aufruf von malloc müssen Sie also immer überprüfen, ob er auch erfolgreich war. Achten Sie bitte außerdem darauf, dass dieser Speicher nicht initialisiert ist. Wenn Sie also nicht selber in Ihrem Programm sinnvolle Werte zuweisen, so steht dort irgendwelcher „Müll“! Nun endlich ein kleines Beispiel. /* alloc01.c Ein erstes Beispiel für dynamische Speicherreservierung Status: incomplete*/ #include <stdio.h> #include <stdlib.h> char *stringToRead; void main(void) { int i; printf("Wieviele Zeichen wollen Sie eingeben?"); scanf("%d",&i); stringToRead = malloc((++i)*sizeof(char)); if (stringToRead!=0) { printf("\nBitte geben Sie jetzt Ihren String ein."); scanf("%s",stringToRead); printf(stringToRead); free(stringToRead); } else printf("\n Nicht mehr genug Speicher vorhanden.\nKaufen Sie sich einen\ groesseren Rechner\n oder beenden Sie einige Programme."); } Hier wieder die gewohnten Erläuterungen zu diesem Programm. 1. Zunächst werden wie schon oft die Standard Header-Dateien stdio und stdlib eingebunden. Letztere für die Funktionen zur dynamischen Speicherverwaltung (malloc und free). 1 Vielleicht 2 Auf wusstes Sie dass noch nicht: Ratschläge sind immer auch Schläge. Deutsch heißt ein Stack „Keller“. Ich weigere mich diese Übersetzung zu verwenden. 13.4. SPEICHERRESERVIERUNG UND FREIGABE 99 FT 2. Anschließend definiere ich einen Zeiger auf einen String stringToRead. Derartig lange Namen sind in C nicht sehr verbreitet, sondern erst mit C++ und Java populär geworden. Ich versuche aber immer Namen zu nehmen, die klar sagen, wozu die Variable oder die Funktion da ist. Das spart viele Kommentare, die dann noch viel länger wären. 3. Dann zeige ich grenzenloses Vertrauen in den Anwender und bitte ihn, anzugeben, wieviele Zeichen er oder sie eingeben möchte. 4. Sträflicherweise prüfe ich überhaupt nicht ab, ob eine sinnvolle, zumindest, numerisch positive, Eingabe gemacht wurde. 5. In der nächsten Zeile reserviere ich Speicher für die einzugebenden Zeichen. 6. In der nächsten Zeile finden Sie (vielleicht zum ersten Mal) eine Fehlerbehandlung. Die ist hier so wichtig, dass ich nicht einmal Programme zur Erläuterung der Speicherverwaltung ohne diese zeige! Wenn genug Speicher auf dem Stack vorhanden war, geht die Verarbeitung weiter, anderfalls wird das Programm mit feundlichem Bedauern und einem (guten?) Rat beendet. 7. Im Erfolgsfall haben wir genug Speicher zugeteilt bekommen und können die Eingabe wie gewohnt einlesen. DR A 8. Anschließend gebe ich den String zur Kontrolle aus. Dabei mache ich mir zu Nutze, dass printf als Parameter nur const char * erwartet. Ich kann also einfach den namen des Strings angeben. Nach den Erläuterungen aus Abschn. 6.6 auf Seite 59 ist das ja gerade ein char *. 9. Anschließend gebe ich mit der Funktion free den reservierten Speicher wieder frei. Viel mehr kann man zu diesem kleinen Programm wohl kaum sagen. Verwenden Sie malloc so müssen Sie wissen, wieviel Speicher Sie brauchen. Haben Sie einige Objekte, für die Sie Speicher brauchen, so ist es manchmal praktischer, die Funktion calloc zuverwenden. void *calloc(size_t n, size_t size); Wie schon an anderer Stelle erwähnt ist size_t einfach ein unsigned Typ, der mit Größen von Speicherbereichen zu tun hat und deshalb quasi als Synonym für unsigned erfunden wurde, um diese Tatsache auch explizit zum Ausdruck zu bringen. calloc reserviert untypisierten Speicher für n Objekte der Größe size, also insgesamt n·size Bytes. Im Unterschied zu malloc initialisiert calloc den Speicher mit binär 0. Hier ein ganz kleines Beispiel: /* alloc02.c Ein weiteres Beispiel für dynamische Speicherreservierung Status: incomplete*/ #include <stdio.h> #include <stdlib.h> char *stringToRead; void main(void) { int i; printf("Wieviele Zeichen wollen Sie eingeben?"); scanf("%d",&i); stringToRead = calloc(i,sizeof(char)); if(stringToRead) { int j; for(j=0;j<i;j++) printf("%X",*(stringToRead+j)); printf("\nBitte geben Sie jetzt Ihren String ein."); scanf("%s",stringToRead); printf(stringToRead); 100 KAPITEL 13. DYNAMISCHE SPEICHERPLATZVERVERWALTUNG } FT free(stringToRead); } else printf("\n Nicht mehr genug Speicher vorhanden.\n Kaufen Sie sich einen\ groesseren Rechner\n oder beenden Sie einige Programme."); Die Ausgabe zeigt, dass der Speicher nun tatsächlich mit Nullen initialisiert wird. 13.5 Historische Anmerkungen Die Deklaration static finden Sie auch in den objektorientierten Programmiersprachen C++ und Java. Dort bezeichnet sie sogenannte Klassenelemente. Für diese gibt es nur einen gemeinsamen Wert („Instanz“) für alle Objekte der Klasse. Dies ist eine konsistente Erweiterung des Konzepts aus C: static deklarierte Elemente sind ebenfalls in einem gewissen Sinne global. In einem objektorientierten Kontext ist die zu betrachtende Einheit nicht mehr die Programm-Quellcodedatei, wie in C, sondern die Klasse. Eine Klasse wird in Java ja auch in jeweils einer Datei definiert. Insofern hat static hier auch in diesem Sinne die gleiche Bedeutung, wie in C. DR A 13.6 Aufgaben 1. Schreiben Sie eine Funktion, die zwei Strings zu einem neuen zusammenfügt, indem Sie neuen Speicherplatz reserviert, die beiden Strings zu einem neuen String in dem neuen Speicherbereich zusammenkopiert. Dateizugriff 14.1 Übersicht 14.2 Lernziele DR A 14.3 Streams FT Kapitel 14 14.4 Elementare Dateioperationen 14.5 14.6 Historische Anmerkungen 14.7 Aufgaben 101 KAPITEL 14. DATEIZUGRIFF DR A FT 102 FT Kapitel 15 Anwendungen: Listen 15.1 Übersicht DR A Listen sind eine ganz wichtige und grundlegende Datenstruktur, die es ermöglicht viele wichtige andere Strukturen, wie Stacks (LIFO — last in first out Speicher), Queues (FIFO — first in last out Speicher) und viele weitere effizient zu implementieren. 15.2 Lernziele • Einfach und doppelt verkettete Listen implementieren können. • Listen mit antizipativer Indizierung implementieren können. 15.3 15.4 Historische Anmerkungen 15.5 Aufgaben 103 KAPITEL 15. ANWENDUNGEN: LISTEN DR A FT 104 FT Anhang A Code Konventionen für C A.1 Übersicht DR A Dieses Kapitel enthält bewährte Regeln, nach denen Code in der Programmiersprache C geschrieben wird. Viele davon sind nicht C-spezifisch sondern gelten sinngemäß auch für andere Programmiersprachen. Diese Regeln haben nicht nur eine formale Bedeutung, sondern fördern auch die Qualität des Codes. A.2 Lernziele • C Programme strukturieren können. • Regeln zur Namensbildung in C beherrschen. A.3 Programmstruktur Ganz feste Regeln für die Strukturierung von C Programmen gibt es nicht. A.4 Namen Variablennamen: Kleinbuchstaben Kein _ am Anfang von Variablennamen und Funktionsnamen, wegen Verwechslungsgefahr mit Systemvariablen und Bibliotheksfunktionen, die oft mit diesem Zeichen beginnen. Konstanten: Großbuchstaben A.5 A.6 Historische Anmerkungen 105 ANHANG A. CODE KONVENTIONEN FÜR C DR A FT 106 Aufgaben FT Anhang B 1. In welchen Header-Dateien werden die Grenzen für die numerischen Datentypen definiert? 2. Schreiben Sie bitte ein C-Programm, dass die minimalen und maximalen Werte für die numerischen Datentypen aus den Header-Dateien ausgibt! DR A 3. Schreiben Sie bitte ein C-Programm, dass die minimalen und maximalen Werte für die numerischen Datentypen in der jeweiligen Installation durch geeignete Operationen ermittelt! B.1 Rechner In [KR88] finden Sie Code für einen einfachen Rechner, der arithmetische Ausdrücke in rechtspolnischer Notation auswertet (main.c, calc.h,stack.c, getop.c, getch.c). Hierzu die folgenden Aufgaben: 1. Modifizieren Sie bitte getop so, das es ungetch nicht benutzt! Hinweis: Verwenden Sie eine geeignete statische Variable. ([KR88], ex 4-11) 2. Ergänzen Sie bitte den Rechner so, dass er auch den modulo-Operator „%“ verwenden kann. Achten Sie auf positive und negative Zahlen! ([KR88], ex 4-3) 3. Ergänzen Sie bitte den Stack um Funktionen um das erste Elemente auszugeben ohne es zu entfernen, es zu duplizieren und eine um die beiden obersten Elemente zu vertauschen! ([KR88], ex 4-4) 4. Ergänzen Sie bitte den Rechner um die Funktionen sin, cos, exp, log und pow! ([KR88], ex 4-5) 5. Die Funktionen getch und ungetch behandeln EOF korrekt. Warum? Ändern Sie die Funktionen bitte so, dass sie sicehr funktionieren! ([KR88], ex 4-9) 6. Statt zeichenweise kann der arithmetische Ausdruck auch als eine Zeile eingelesen werden. Ändern Sie den Code bitte auf diese Vorgehensweise um! ([KR88], ex 4-10) 107 ANHANG B. AUFGABEN DR A FT 108 FT Anhang C Lösungsvorschläge C.1 1. In welchen Header-Dateien werden die Grenzen für die numerischen Datentypen definiert? Antwort: Zumindest in der MinGW Installation sind dies die Dateien limits.h, float.h und stdint.h. DR A 2. Schreiben Sie bitte ein C-Programm, dass die minimalen und maximalen Werte für die numerischen Datentypen aus den Header-Dateien ausgibt! Lösung: Siehe limits01.c. 3. Schreiben Sie bitte ein C-Programm, dass die minimalen und maximalen Werte für die numerischen Datentypen in der jeweiligen Installation durch geeignete Operationen ermittelt! Lösung: Siehe limits02.c. 4. 109 ANHANG C. LÖSUNGSVORSCHLÄGE DR A FT 110 FT Anhang D Lösungsvorschläge für ausgewählte Aufgaben D.1 Kapitel 1 DR A D.1.1 tunix.c Die Lösung dieser Aufgabe ist Bestandteil des Scripts. D.2 Kapitel 2 D.2.1 Namensdeklarationen Hier folgen einige Deklarationen für Variablen in C. Geben Sie bitte an, ob die Namen in diesen Deklarationen gültig sind! Begründen Sie bitte Ihre Antwort! 1. int 1zweidrei; 2. int einszwei3; 3. char Eins_zwei_drei; 4. char Eins zwei drei; 5. char _; 6. char [] void; 7. double pi; 8. Double E; Lösungsvorschlag: 1. Der Name „1zweidrei“ ist ungültig, da er mit einer Ziffer („1“) beginnt. 2. Der Name „einszwei3“ ist gültig, da er nur aus Buchstaben aus dem Bereich a–z und einer Ziffer am Ende besteht, also nicht an erster Stelle, wo eine Ziffer nicht zulässig ist. 3. Der Name „Eins_zwei_drei“ ist gülti, da er nur aus Buchstaben a–z, A–Z, sowie _ besteht, die alle zulässig sind 4. Der Name „Eins zwei drei“ ist nicht gültig, da er zwei Leerzeichen 111 enthält. 112 ANHANG D. LÖSUNGSVORSCHLÄGE FÜR AUSGEWÄHLTE AUFGABEN 5. Der Name „_“ ist FT 6. Der Name „void“ ist ungültig, da „void“ ein reserviertes Wort ist. 7. Der Name „pi“ ist gültig, da er nur aus Zeichen aus dem Bereich a–z besteht. 8. Der Name „E“ istgültig, da er nur aus einem Zeichen aus dem Bereich a–z besteht. Die Deklaration ist aber fehlerhaft, da es den Typ Double nicht in C gibt. D.2.2 signed, unsigned char Zur Beantworung dieser Frage schreiben wir das Programm DR A /****************************************** datatypes.c Testen der Läge eingebauter Datentypen *******************************************/ #include <stdio.h> #include <limits.h> int main() { { char lowChar = CHAR_MIN; char highChar= CHAR_MAX; printf("%s%d%s%d","Untere Grenze char: \ ",lowChar,", obere Grenze char: ",highChar); } { int lowInt = INT_MIN; int highInt= INT_MAX; printf("%s%d%s%d","\nUntere Grenze int: \ ", lowInt, ", obere Grenze int: ", highInt); } return 0; } In meiner Umgebung (MinGW bzw. Visual Studio .NET 2003) liefert dieses Programm die Ausgabe: Untere Grenze char: -128, obere Grenze char: 127 Untere Grenze int: -2147483648, obere Grenze int: 2147483647 Also wird char i dieser Umgebung als signed char interpretiert. D.2.3 int bzw. unsigned int Die Länge einer int-Variablen ist die eines Registers, auf einer 64 Bit Maschine also 8 Byte. Die Wertebereiche sind also in diesem Fall 0 bis 264 − 1 = 18.446.744.073.709.551.616 − 1 bzw. −263 + 1 = −9.223.372.036.854.775.808 + 1 bis 263 − 1 = 9.223.372.036.854.775.808 − 1. D.2.4 Fehler in Summenfunktion Das Ergebnis lautet nun 9 + 16 = 4198400. Was bedeutet das nun? In der geänderten Zeile steht, dass ein Funktion im dezimalen Format (als ganze Zahl) ausgegeben werden soll. Geben wir das Ergebnis im Hex-Format aus mittels: printf("%d%s%d%s%x%s",pa," + ",pb," = ",summe,"."); D.2. KAPITEL 2 113 9 + 16 = 401000. FT So erhalten wir: Rufen wir den Compiler mit der Option /Fm auf, die dafür sorgt, das er eine sog. map-Datei erzeugt, so finden wir dort die Zeilen: Address 0001:00000000 0001:00000010 0001:0000007c Publics by Value _summe _main _printf Rva+Base 00401000 f 00401010 f 0040107c f Lib:Object summefalsch.obj summefalsch.obj LIBC:printf.obj Auf Adresse 401000 steht also unsere kleine Summenfunktion. Wir lernen hieraus: Dezimal oder Hexadezimal dargestellt erhalten wir mittels printf die Adresse einer Funktion. D.2.5 happybirthday1 verb+>+ berndbirth.out Die Befehlszeile bewirkt, dass alle Ausgaben, die wir zuvor auf der Konsole gesehen haben in die Datei berndbirth.out geschrieben werden. Insbesondere bekommen wir die Zeilen: DR A Geben Sie bitte Ihr Geburtsdatum ein! Tag: und die folgenden nicht zu sehen. Allerdings wartet das Programm auf eine Eingabe von der Tastatur. Wir geben also ein Datum ein in der Form tt ENTER mm ENTER jjjj ENTER und können anschließend alle Ausgaben in der Datei berndbirth.out ansehen. D.2.6 printf mit ungültigen Esacpe-Sequenzen Ein ganz naiver Versuch nimmt einfach einige Zeichen her und protokolliert die Ergebnisse: /* printf01 Was passiert bei printf mit falschen Escape-Sequenzen Naiver Ansatz ohne Argumente Status: incomplete*/ void main(void) { int ai = 6; int bi = 9; char ac =’6’; char bc = ’9’; printf(); } Der Microsoft Compiler wandelt dies bedenkenlos um. Gibt man allerdings /Wall an, so erhält man eine Reihe Warnungen, die einen schon skeptisch machen sollten. Bei Ausführung stürzt das Programm aber gnadenlos ab. Mir sagten die Debug-Infos wenig, ich erspare sie Ihnen deshalb an dieser Stelle. Der cc Compiler auf einem meiner Linux-Rechner, genauer gcc version egcs-2.91.66 19990314/Linux (egcs-1.1.2 release) brachte keine Warnung wegen print. Bei Ausführung stürzte das Programm aber mit der Meldung „Speicherzugriffsfehler“ ab. Einen leeren String „verdaut“ der Microsoft Compiler schon besser: 114 ANHANG D. LÖSUNGSVORSCHLÄGE FÜR AUSGEWÄHLTE AUFGABEN FT /* printf02 Was passiert bei printf mit falschen Escape-Sequenzen Naiver Ansatz mit leerem String Status: incomplete*/ void main(void) { int ai = 6; int bi = 9; char ac =’6’; char bc = ’9’; printf(" "); } Hier haben wir ein „Erfolgserlebnis“, Compiler läuft durch und zur Laufzeit gibt es keinen Fehler. Kein Wunder, das Programm tut ja auch nicht mehr als das altbekannte tunix.c. Gibt man nun eine ungültige Escape-Sequenz ein, so erhält man eine Warnung, die da sinngemäß lautet: „ungültige Escape-Sequenz“. D.2.7 Fahrenheit - Celsius DR A /* fahrenheit04.c Konvertieren von Temperaturen * in Fahrenheit in Celsius, const * Status: incomplete */ #include <stdio.h> #include "funktionen.h" void main(void) { double fahr, celsius; do { printf("\nGeben Sie bitte eine Temperatur in Fahrenheit ein:\n"); scanf("%lf",&fahr); fflush(stdin); celsius = (5.0/9.0)*(fahr-32); celsius >= -274.1 ? printf("Das sind %lf Grad Celsius.",celsius):printf("Temper } while (weiter()); } Der Vollständigkeit hier noch funktionen.h und .c #ifndef FUNKTIONEN #define FUNKTIONEN /*funktionen.h Header für eine Sammlung von mehr oder weniger nützlicher Hilfsfunktionen*/ /*Abfrage, ob eine Schleife nochmals durchlaufen werden soll, *am besten in einer do while Schleife zuu verwenden.*/ int weiter(void); #endif /*funktionen.c Sammlung von mehr oder weniger nützlichen Hilfsfunktionen Status: tested*/ #include "funktionen.h" #include <stdio.h> #include <stdlib.h> int weiter(void) { D.2. KAPITEL 2 115 } D.2.8 Body-Mass-Index FT char w=’N’; printf("\nMoechten Sie weitermachen? Dann geben Sie bitte J ein."); scanf("%c",&w); fflush(stdin); return (toupper(w)==’J’)? 1:0; DR A /* bmi01.c Errechung des Body-Mass-Index Status: incomplete*/ #include <stdio.h> int main(void) { float h,w; printf("Geben Sie bitte Ihre Körpergröße in Metern ein:"); scanf("%f",&h); printf("Geben Sie bitte Ihr Gewicht in Kilogramm ein:"); scanf("%f",&w); printf("\nIhr Body-Mass-Index ist: %f\n",w/(h*h)); return 0; } D.2.9 Deklarationen und Definitionen Gibt man diese Deklarationen ein, wobei ich diesmal zur besseren Übersicht die Zeilennummern angebe 01 02 03 04 05 06 07 08 09 10 11 12 13 } #include <stdio.h> void main(void) { int a =2.5; nt b = ’?’; char z = 500; int big = 40000; double fläche = 1.23+5 long count = 0 char c = ’\’’; unsigned char ch = ’\201’; unsigned int size = 40000; float value = 12345.0679; so erhält man das folgende Ergebnis test.c(4) : warning C4244: ’Initialisierung’: Konvertierung von ’double’ in ’int ’, möglicher Datenverlust test.c(5) : error C2065: ’nt’: nichtdeklarierter Bezeichner test.c(5) : error C2146: Syntaxfehler: Fehlendes ’;’ vor Bezeichner ’b’ test.c(5) : error C2144: Syntaxfehler: ’<Unbekannt>’ sollte auf ’<Unbekannt>’ fo lgen test.c(5) : error C2144: Syntaxfehler: ’<Unbekannt>’ sollte auf ’<Unbekannt>’ fo lgen test.c(5) : error C2143: Syntaxfehler: Es fehlt ’;’ vor ’Bezeichner’ 116 ANHANG D. LÖSUNGSVORSCHLÄGE FÜR AUSGEWÄHLTE AUFGABEN FT test.c(5) : warning C4555: Der Ausdruck hat keine Auswirkungen; Ausdruck mit Neb eneffekt erwartet test.c(5) : error C2065: ’b’: nichtdeklarierter Bezeichner test.c(6) : error C2143: Syntaxfehler: Es fehlt ’;’ vor ’eingeben’ test.c(7) : error C2143: Syntaxfehler: Es fehlt ’;’ vor ’eingeben’ test.c(8) : error C2143: Syntaxfehler: Es fehlt ’;’ vor ’eingeben’ test.c(11) : error C2143: Syntaxfehler: Es fehlt ’;’ vor ’eingeben’ test.c(12) : error C2143: Syntaxfehler: Es fehlt ’;’ vor ’eingeben’ Zunächst korrigiere ich die auf der Hand liegenden Fehler, wie fehlende Semikolons am Zeilenende und mache aus nt den gültigen Typ int. Dann verbleiben noch folgende Warnungen A16corrected.c(4) : warning C4244: ’Initialisierung’: Konvertierung von ’double’ in ’int’, möglicher Datenverlust A16corrected.c(11) : warning C4245: ’Initialisierung’: Konvertierung von ’int’ i n ’unsigned char’, signed/unsigned-Konflikt A16corrected.c(13) : warning C4305: ’Initialisierung’: Verkürzung von ’double’ i n ’float’ Das erläutere ich nun im Einzelnen. DR A int a =2.5; Hier wird eine Gleitpunktzahl (float) einer int Variablen zugewiesen. Dabei geht der gebrochene Anteil verloren und der COmpiler weist völlig lorrekt darauf hin. nt b = ’?’; Hier sollte es offenbar int heißen. Das ist dann ganz und gar korrekt. in b steht dann ’?’ bzw. der entsprechende dezimale Wert 63 =X’3F’. char z = 500; Dies mag zwar nicht sinnvoll sein, ist aber syntaktisch korrekt und in z steht hinterher ’c’ = -12. Die erfordert eine Erklärung: Die 256 Zeichen werden – da nichts von unsigned hier steht – durch -128 –127 verschlüsselt. Nun kommen wir aber mit 500 an und weisen diese Zahl einem char zu. Bei dieser Zuweisung wird nun in den angegeben Bereich transformiert, d.h. es wird modulo 256 hier hinheinverschoben. Ziehen wir einmal 256 ab, so erhalten wir 244, immer noch größer, als 127. Also müssen wir nochmals 256 abziehen und landen nun bei dem angegeben Wert -12. Eine Warnung des Compilers vor dem möglichen Datenverlust wäre aber am Platze gewesen. int big = 40000; Die Antwort hängt von Ihrem Rechner ab: Auf 16-Bit Rechnern müsste der Compiler eine Warnung, bringen, da dort MAXINT= 32767 ist. In MS VS .Net 2003 und auch schon in in VC98 haben wir es mit 32-Bit-Systemen zu tun. double fläche = 1.23+5; Bis auf das ursprünglich fehlende Semikolon am Ende der Zeile ist dies in Ordnung. long count = 0; Bis auf das ursprünglich fehlende Semikolon am Ende der Zeile ist dies in Ordnung. char c = ’\”; Dies ist völlig in Ordung so weisen Sie c das Zeichen ’ zu. Dezimal ist das der Wert 39. unsigned char ch = ’\201’; So weisen Sie ch den oktalen Wert o’201’ = X’81’ = 129 zu. Dies liegt außerhalb des 7-Bit ASCII Codes und entspricht dem deutschen Buchstaben ü. Die rechte Seite ist aber eine Konstante, die ein Vorzeichen haben könnte, während die linke Seite unsigned ist. Die Warnung des Compilers ist also angebracht. unsigned int size = 40000; Dies ist zur Abwechslung mal in Ordnung. float value = 12345.0679; Die Konstante ist länger, als float darstellen kann. Es können also Stellen abgeschnitten werden und der Compiler warnt zu Recht. Und tatsächlich: es ist value = 12345.068359. D.3. KAPITEL 3 117 D.2.10 Ceiling und Floor 1. ⌈3, 99⌉ = 4,00 2. ⌊3, 99⌋ = 3,00 3. ⌈−3, 11⌉ = -3,00 4. ⌊−3, 11⌋ = -4,00 Programm in chap3/floorceil.c. D.3 Kapitel 3 FT In der C-Bibliothek gibt es eine Reihe mathematischer Funktionen. Sie finden Sie in der Header-Datei math.h. Die Funktion ceil(double d) (Decke) liefert die kleinste ganze Zahl ⌈d⌉, die größer oder gleich d ist. Entsprechend liefert floor (Fußboden) die größte ganze Zahl ⌊d⌋, die kleiner oder gleich d ist. Beantworten Sie bitte die folgenden Fragen und bestätigen Sie durch ein C-Programm, dass die Funktionen aus der C-Bibliothek dieses Ergebnis liefern! D.3.1 Falscheingaben bei formio11.c DR A Hier eine Tabelle mit einige Eingaben und deren Wirkung: Summand 1 1,1 Eingaben Summand 2 Wirkung Summand 3 1.1 a 1 29650297 666772048 ----696422346 1 29650297 1487151696 ----1516801994 1245120 29650297 -1726560832 -----1695665415 Sie sehen, nach derartigen Fehleingaben haben Sie gar keine Chance mehr, weitere Summanden einzugeben. Damit wollen wir es für’s Erste genug sein lassen. D.3.2 Typkonvertierung char to int und zurück Hier das Programm /****************************************** datatypes07.c Konvertierungen int2char etc. Status: ok *******************************************/ #include <stdio.h> int main(void) { char ca = ’a’; int ia = 888; char cb = ia; int ib = ca; printf("\nVar. printf("\nca = printf("\nia = printf("\ncb = printf("\nib = return 0; ANHANG D. LÖSUNGSVORSCHLÄGE FÜR AUSGEWÄHLTE AUFGABEN FT 118 Zeichen\tDez.\tHex"); \t%c\t%d\t%x",ca,ca,ca); \t%c\t%d\t%X",ia,ia,ia); \t%c\t%d\t%X",cb,cb,cb); \t%c\t%d\t%x",ib,ib,ib); } und die Fehlermeldung des Compilers DR A datatypes07.c(10) : warning C4242: ’Initialisierung’: Konvertierung von ’int’ in ’char’, möglicher Datenverlust end{verbatim} und die Ausgabe \begin{verbatim} Var. Zeichen Dez. Hex ca = a 97 61 ia = x 888 378 cb = x 120 78 ib = a 97 61 Hier nun die Erklärung: char ohne weitere Qualifikation ist in MS C signed character und belegt den Bereich von −128 bis 127. Die Variable ia hat bei ihrer Initialisierung den Wert 888 weit jenseits dieses Bereichs. Bei der Deklaration und Definition char cb = ia; wird dieser Wert in den Bereich von char transformiert: Durch dreimaliges Subtrahieren von 256 landen wir gerade bei 78. Damit ist auch geklärt, warum printf hier als Zeichen „x“ ausgibt. D.3.3 Yet to be done D.3.4 Dreistellige Zahl in unterschiedlichen Formaten Die Aufgabenstellung ist vage. Das folgende Programm interpretiert sie so, dass eine Zahl eingegeben werden soll. /*datatypes08.c Eingabe einer dezimal dreistelligen Zahl *Ausgabe dezimal, oktal und hexadezimal *Status: incomplete */ #include <stdio.h> void main(void) { int i; printf("\nGeben Sie bitte eine dreistellige ganze Zahl dezimal ein:"); scanf("%3d", &i); printf("Dezimal\tOktal\tHexadezimal"); printf("\n%d\t%o\t%X",i,i,i); } D.4 Artikel, Stückzahl, Preis, Gesamtpreis /****************************************** D.5. KAPITEL 4 119 D.5 Kapitel 4 FT datatypes09.c Eingaben und Rechnungen. Status: inclomplete *******************************************/ #include <stdio.h> void main(void) { long int artikelnummer; long int stueckzahl; double stueckpreis; printf("\nGeben Sie bitte eine achtstellige Artikelnummer ein:"); scanf("%8d",&artikelnummer); printf("\nGeben Sie bitte eine ganzzahlige Stueckzahl ein:"); scanf("%d",&stueckzahl); printf("\nGeben Sie bitte eine Stueckpreis ein:"); scanf("%lf",&stueckpreis); printf("Sie haben % ld Stueck für insgesamt %lf eingekauft.",stueckzahl, stueckzah } DR A D.5.1 Zusammengesetzte Zuweisungen Ich vergleiche einfach zwei Programme, die die Beispiele enthalten. /* zuweisung1.c*/ int main() { int i = 0; int j = 0; int k = 5; i+=3; i-=5; i*= j +1; i/= k-2; } /* zuweisung2.c*/ int main() { int i = 0; int j = 0; int k = 5; i = i + 3; i = i - 5; i = i * (j + 2); i = i / (k - 2); } Ein einfaches dir zuweisung*.* /OE /ON-Kommando zeigt folgende Ausgabe: 10.07.2004 10.07.2004 10.07.2004 10.07.2004 12:32 12:32 12:32 12:32 125 148 22.016 22.016 zuweisung1.c zuweisung2.c zuweisung1.exe zuweisung2.exe 10.07.2004 10.07.2004 ANHANG D. LÖSUNGSVORSCHLÄGE FÜR AUSGEWÄHLTE AUFGABEN 12:32 12:32 556 zuweisung1.obj 556 zuweisung2.obj FT 120 Sowohl die Objekt- als auch die ausführbaren Dateien sind genau gleich groß. Eine einfacher Vergleich mit dem diff Befehl im Unix oder emacs zeigt als einzigen Unterschied die „1“ bzw. „2“ im Dateinamen. Ziehen Sie hieraus bitte folgende Konsequenz: machen Sie sich mehr Gedanken über die Verständlichkeit Ihrer Programme als darüber, wie effizient der Code sein mag, den den Compiler daraus macht. D.5.2 Tabellen 4.5–4.9 Da Ihnen das Skript (hoffentlich) im pdf-Format vorliegt, können Sie einfach damit beginnen, den Text der Tabellen in Ihr C-Programm zu kopieren und „printf darum zu schreiben.“ Das sieht dann ungefähr so aus: DR A /*formio01.c Formattierte Ausgabe in C Start mit einem langen String Status: ok*/ #include <stdio.h> int main() { printf("Operator Bedeutung<kleiner<=kleiner gleich>\ größer>=größer gleich==gleich!=ungleich"); return 0; } Die Ausgabe Operator Bedeutung<kleiner<=kleiner gleich>größer >=größer gleich==gleich!=ungleich kann aber ebenfalls nnoch nicht überzeugen. Wir müssen noch einige newlines etc. einfügen. Das Ergebnis ist natürlich noch völlig ungenügend. Im nächsten Schritt fügen wir nun eine \t Escape-Sequenz (Tabulator) zwischen den Spalten und eine \n Escape-Sequenz für „neue Zeile“ ein. Gleichzeitig ersetzen wir die Deutschen Sonderzeichen durch ihre Ersatzausdrücke /*formio02.c Formattierte Ausgabe in C Nun spaltenweise Status: ok*/ #include <stdio.h> int main() { printf("Operator \tBedeutung\n<\tkleiner\n<=\tkleiner gleich\n>\ \tgroesser\n>=\tgroesser gleich\n==\tgleich\n!=\tungleich"); return 0; } Die Ausgabe gefällt nun schon besser: Operator Bedeutung < kleiner <= kleiner gleich > groesser >= groesser gleich == gleich != ungleich D.5. KAPITEL 4 121 FT Hierbei fällt aber auf, dass die Spalten nicht genau ausgerichtet sind, \t verschiebt um einen konstanten Wert. Fügen wir einfach ab Zeile 2 eine zusätzliche \t Escape-Sequenz ein, so ist „alles im Lot“. Das Programm sieht nun so aus: /*formio03.c Formattierte Ausgabe in C Nun spaltenweise und korrekt ausgerichtet Status: ok*/ #include <stdio.h> int main() { printf("Operator \tBedeutung\n<\t\tkleiner\n<=\t\tkleiner gleich\n>\ \t\tgroesser\n>=\t\tgroesser gleich\n==\t\tgleich\n!=\t\tungleich"); return 0; } und die Ausgabe ist Bedeutung kleiner kleiner gleich groesser groesser gleich gleich ungleich DR A Operator < <= > >= == != korrekt ausgerichtet. Nach dem gleichen Schema können wir die restlichen Teile der Aufgabe erledigen. Das sollte jetzt nur noch eine Fleißaufgabe sein. Einige können wir aber auch eleganter lösen, z. B. die Reproduktion von der Tabelle in Abb. 4.9. Da brauchen wir eine Überschrift und eine geschachtelte for-Schleife. /*op10.c Wahrheitstafel für Boolesche Operatoren*/ #include <stdio.h> void main(void) { int x, y; printf("\nx\ty\tx&&y\tx||y\t!x"); for(x=1;x>=0;x--) { for(y=1;y>=0;y--) { printf("\n%d\t%d\t%d\t%d\t%d",x,y,x&&y,x||y,!x); } } } D.5.3 Speicherbedarf der elementaren Datentypen Die Lösung ist geradeaus: /*datatypes06.c Speicherbedarf der elementaren Datentypen*/ #include <stdio.h> void main(void) { printf("\nType\t\tByte"); printf("\nchar\t\t%d",sizeof(char)); printf("\nunsigned char\t%d",sizeof(unsigned char)); 122 ANHANG D. LÖSUNGSVORSCHLÄGE FÜR AUSGEWÄHLTE AUFGABEN } FT printf("\nshort\t\t%d",sizeof(short)); printf("\nint\t\t%d",sizeof(int)); printf("\nlong\t\t%d",sizeof(long)); printf("\nfloat\t\t%d",sizeof(float)); printf("\ndouble\t\t%d",sizeof(double)); printf("\nlong double\t%d",sizeof(long double)); und liefert im Fall von MS VS .NET 2003 das folgende Ergebnis. Type char unsigned char short int long float double long double DR A D.6 Kapitel 5 Byte 1 1 2 4 4 4 8 8 D.6.1 Matrix der ASCII-Zeichen Ich löse diese Aufgabe wie folgt: Eine äußere Laufvariable i für die Zeilen, eine innere Laufvariable j für die Spalten. In einer ersten Schleife werden die Spaltenköpfe ausgegeben. Dann folgen die beiden geschachtelten Schleife. In der äußeren Schleife wird nur der Zeilenkopf ausgegeben, in der inneren die Zeichen„16*i + j“. Zur „Kontrolle“ folgt dann noch die entsprechende Matrix mit den hexadecimalen Darstellungen. Insgesamt sieht das dann so aus: /* asciimatrix01.c Ausgabe einer Matrix der ASCII-Zeichen mit hexadezimalen Zeilen und Spaltenköpfen Status: ok*/ #include <stdio.h> void main(void) { int i=0; int j=0; printf("\n "); for(i=0;i<16;i++) printf("%2X ",i); for(i=0;i<16;i++) { printf("\n%2X ",i); for(j=0;j<16;j++) { printf("%2c ",16*i + j); } printf("\n\t "); } printf("\n "); for(i=0;i<16;i++) printf("%2X ",i); for(i=0;i<16;i++) D.6. KAPITEL 5 123 { } } FT printf("\n%2X ",i); for(j=0;j<16;j++) { printf("%2X ",j+16*i); } printf("\n\t "); Aus satztechnischen Gründen verzichte ich darauf, das Ergebnis hier mit auszudrucken. D.6.2 BMI mit erläuternden Ausgaben DR A /* bmi02.c Errechung des Body-Mass-Index Status: incomplete*/ #include <stdio.h> #include <string.h> #include "funktionen.h" void main(void) { double h,w, bmi; char message[200]; do { printf("\nGeben Sie bitte Ihre Koerpergroesse in Metern ein:"); scanf("%lf",&h); fflush(stdin); printf("Geben Sie bitte Ihr Gewicht in Kilogramm ein:"); scanf("%lf",&w); fflush(stdin); bmi = w/(h*h); if((0 < bmi) && (bmi < 18)) strcpy(message,"Sie sind auffaellig untergewichtig und sollten mehr qualitati if((18 < bmi) && (bmi < 25)) strcpy(message,"Ihr Gewicht entspricht Ihrer Koerpergroesse. Essen Sie weiter if(25 < bmi) strcpy(message,"Sie sind uebergewichtig. Reduzieren Sie Ihre Nahrungsaufnahme printf("\nIhr Body-Mass-Index ist: %lf\n%s",bmi, message); fflush(stdin); } while(weiter()); } D.6.3 for Schleife mit Präfix-Inkrementoperator Ich zeige gleich alle drei Varianten in einem Programm: /*for03.c Die ersten 10 Quadratzahlen? Status: ok*/ #include <stdio.h> void main(void) { int i; printf(" i\ti*i\t i\ti*i\t i\ti*i\n"); ANHANG D. LÖSUNGSVORSCHLÄGE FÜR AUSGEWÄHLTE AUFGABEN 124 } FT for(i = 1;i<=10;i++) { int k = i + 1; printf("%3d\t%3d\t",i,i*i); printf("%3d\t%3d\t",k,k*k); printf("%3d\t%3d\n",i,i*i); } Die Ausgabe gebe ich der Kürze halber in drei Spalten nebeneinander an: i*i 1 4 9 16 25 36 49 64 81 100 i 2 3 4 5 6 7 8 9 10 11 i*i 4 9 16 25 36 49 64 81 100 121 i 1 2 3 4 5 6 7 8 9 10 i*i 1 4 9 16 25 36 49 64 81 100 DR A i 1 2 3 4 5 6 7 8 9 10 D.7 Kapitel 6 D.8 Kapitel 7 D.8.1 stringcopy Nichts leichter als das: /*funktionen.c Sammlung von mehr oder weniger nützlichen Hilfsfunktionen Status: tested*/ #include "funktionen.h" #include <stdio.h> #include <stdlib.h> ... char *stringcopy(char* str2, const char *str1) { int i=0; while(str1[i]!=’\0’) str2[i++] = str1[i]; str2[i]=’\0’; return str2; } D.8.2 Palindromerkennung Die Basis für die Lösung dieser Aufgabe bildet das Programm palindrom01.c aus Abschn. 6.3 oder palindrom02.c aus Abschn. 7.4. Wir können auf zwei Arten an die Lösung herangehen: 1. Den String nicht „in place“ umdrehen sondern die Zeichen in der anderen Reihenfolge in einem neuen String speichern. Anschließend strcmp verwenden. D.8. KAPITEL 7 125 2. Zu versuchen, den Vergleich Zeichen für Zeichen am String selber vorzunehmen. FT Die erste Variante zeigt palindrom03.c: DR A /* palindrom03.c String auf Palindromeigenschaft überprüfen Status: incomplete*/ #include <stdio.h> #include <string.h> /*#include <stdlib.h>*/ #define MAXLEN 100 void reverse(char *s, char *t); void main(void) { int i; char c; char puffer[MAXLEN]; char reversed[MAXLEN]; printf("\nGeben Sie bitte eine Zeile Text ein: \n"); for (i=0; (i < MAXLEN-1) && ((c = getchar()) != ’\n’);i++) puffer[i]= c; puffer[i]=’\0’; printf("\nSie gaben ein: %s", puffer); reverse(puffer, reversed); printf("\nRueckwaerts: %s", reversed); strcmp(puffer,reversed)?printf("\nDas ist kein Palindrom"):printf("\nDas ist ein Pa } void reverse(char *s, char* t) { unsigned int i; unsigned int laenge = strlen(s)-1; printf("\nLaenge =%d",laenge); for(i = 0;i <= laenge; i++) { t[i] = s[laenge - i]; } t[i] = ’\0’; } Wie schon bei der Diskussion von palindrom01.c sei jetzt nochmals auf die obere Grenze in der forSchleife hingewiesen: Wenn der String in einem Vektor der Länge n gespeichert ist, belegen seine Zeichen die Stellen 0 bis n − 1. An der Stelle n steht ’\0’. Die Funktion strcmp liefert 0 zurück, wenn beide Strings gleich sind, sonst etwas von 0 verschiedenes. Daher die Anordnung der beiden Ausdrücke nach dem Fragezeichen. Versuchen wir nun auch noch die andere Lösungsstrategie umzusetzen. Als Basis nehme ich wieder palindrom02.c und versuche die Funktion reverse so zu verändern, dass sie vergleicht und nicht invertiert. Ich deklariere sie mit dem Rückgabetyp int und versuche 1 zurück zu liefern, wenn es sich um einPalindorm handelt, sonst 0. Dazu muss ich im Wesentlichen die Zuweisung s[i]=s[j] durch einen Vergleich s[i]!=s[j] ersetzen. Beim ersten ungleich, weiß ich, dass es sich nicht um ein Palindrom handelt und kann 0 zurückliefern. Läuft die Schleife bis zum Ende durch, so ist es ein Palindrom und ich kann 1 zurückliefern. /* palindrom04.c String auf Palindromeigenschaft prüfen, in place Status: incomplete*/ #include <stdio.h> #include <string.h> 126 ANHANG D. LÖSUNGSVORSCHLÄGE FÜR AUSGEWÄHLTE AUFGABEN DR A FT #define MAXLEN 100 int palindrom(char s[]); void main(void) { int i, c; char puffer[MAXLEN]; printf("\nGeben Sie bitte eine Zeile Text ein: \n"); for (i=0; (i < MAXLEN-1) && ((c = getchar()) != ’\n’);i++) puffer[i]= c; puffer[i]=’\0’; palindrom(puffer)?printf("\nDas ist ein Palindrom"):printf("\nDas ist kein Palindro } int palindrom(char s[]) { int i, j; for(i = 0, j = strlen(s)-1;i < j; i++, j--) { if (s[i] != s[j]) return 0; } return 1; } D.8.3 Textverschlüsselung Die Idee zu der Lösung dieser Aufgabe macht Gebrauch von Modulodivision % und der Tabelle der ASCII Codes für Großbuchstaben im Glossar. Zunächst einmal stellen wir fest, dass es 26 Großbuchstaben gibt. Währen die von 0 bis 25 durchnummeriert, so könnten wir einfach jeden Buchstaben wie folgt ersetzen c = (c + n)%26; Da die Großbuchstaben im ASCII Code aber von X’41 bis 5A. Daher müssen wir erst in den Bereich von 0 bis 25 hineintransformieren: c = ’A’ + (s[i]-’A’ + n)%26; Für die Funktion zum Decodieren bemerken wir zunächst, dass die eine Verschiebung um 26 − n nach vorne ist. Damit ist dies dann auf das Codieren zurückgeführt: c = ’A’ + (s[i]-’A’ + (26-n))%26; Damit sind wir praktisch fertig: void encode(char *s) { unsigned i=0; while(s[i]!=’\0’) { if(s[i]!= ’ ’) s[i++] = ’A’ + (s[i]-’A’ + 5)%26; } } void decode(char *s) { unsigned i=0; while(s[i]!=’\0’) D.8. KAPITEL 7 127 { } } DR A D.8.4 Body-Mass Index + 21)%26; FT if(s[i]!= ’ ’) s[i++] = (’A’ ) + (s[i]-’A’ ANHANG D. LÖSUNGSVORSCHLÄGE FÜR AUSGEWÄHLTE AUFGABEN DR A FT 128 IOCCC FT Anhang E Der International Obfuscated C Code Contest fand in vielen der letzten 20 Jahre seit 1984 statt. Zur Auflockerung der Vorlesung und als (mehr oder weniger) schwierige Aufgaben, erläutere ich hier einige der Programme. Hoffentlich habe ich sie richtig verstanden. DR A E.1 1984 E.1.1 anonymous Der Autor dieses Programms int i;main(){for(;i["]<i;++i){--i;}"];read(’-’-’-’,i+++"hell\ o, world!\n",’/’/’/’));}read(j,i,p){write(j/p+p,i---j,i/i);} wollte seinen Namen nicht veröffentlicht haben: Dishonorable mention: Anonymous The author was too embarrassed that he/she could write such trash, so I promised to protect their identity. I will say that the author of this program has a well known connection with the C programming language. Das Programm lässt sich compilieren, es gibt aber eine Reihe Warnungen. In der Version 01 strukture ich das Programm zunächst nach den üblichen Regeln. Dazu ergänze ich ein include von io.h. In Unix-Systemen nehme ich syscalls.h. Den ersten Schritt zum Verständnis dieses Programms machen wir, in dem wir den Code übersichtlichler formatieren: int i; main() { for(;i["]<i;++i){--i;}"]; read(’-’-’-’,i+++"hello, world!\n",’/’/’/’)); } read(j,i,p) { write(j/p+p,i---j,i/i); } In Version 03 ergänze ich Protoypen für die (System-)Funktione read und write. Außerdem kann ich bereits einige einfache Dinge klarer schreiben: 129 ANHANG E. IOCCC 130 ’-’-’-’ = ’-’ - ’-’ FT Das ist also einfach die Subtraktion von ’-’ von ’-’. Unabhängig davon, was ’-’ numerisch ist (es ist übrigens (2d)16 = (45)10 ), 0. Ebenso ist ’/’ / ’/’ = 1; Nun „sehen“ wir Folgendes: Das Programm besteht 1. aus einer main-Funktion mit einer einigermaßen kryptischen for-Schleife und 2. einer Funktion read, die drei Parameter erhält. Analysieren wir nun bottom up, so sollten wir zunächst versuchen, die Funktion read zu verstehen. Da die Typen der Parameter nicht angegeben sind, haben sie den default Typ int. Was die Funktion write macht, müssen wir nachschlagen, z. B. in [KR88], S. 170. Dort finden wir die Signatur von write: int write(int fd, char* buf, int n) Wir brauchen uns keine großen Gedanken darüber zu machen, was ein „file descriptor“ ist (der erste Parameter). Interessant ist es zu wissen, dass der zweite Parameter ein Buffer ist, in den geschrieben wird und dass der dritte Parameter die Anzahl Bytes ist, die geschrieben werden sollen. Der Rückgabewert ist die Anzahl der geschriebenen Zeichen. Damit ist der letzte Parameter von write klar: 1 = ii Byte soll geschrieben werden. Damit kann ich den letzten Teil der for-Schleife ersetzen durch ,i+++"hello, world!\n", 1) DR A write(1 Diese lautet also nun: for(; i["]<i;++i){--i;}"]; write(1 ,i+++"hello, world!\n", Sehen wir uns nun den Rest for-Schleife an: Der erste Parameter ist leer. Die Schleifenvariable ist global deklariert, aber nicht definiert. Die Abbruchbedingung im zweiten Parameter lautet: i["]<i;++i){--i;}"] Im dritten Parameter steht: read(’-’-’-’,i+++"hello, world!\n",’/’/’/’) Der erste Parameter der read-Funktion ist ’-’-’-’. Dies wird von links nach rechts ausgewertet, d. h. genauer steht dort ′ −′ −′ −′ =′ −(′ −(′ −′ )) =′ −(′ −0) = 0. Dabei müssen wir uns nicht einmal überlegen, wie der ASCII-Code des Zeichens „’“ ist. Also lautet der Aufruf von read etwas vereinfacht: read(0,(i++)+"hello, world!\n",1) Nun können wir die kryptische for-Schleife genauer analysieren: "hello, world!\n" ist in der Addition eine Adresse. Also wird bei einem Schleifendurchlauf das i-te Zeichen von „hello, world!“ ausgegeben. Solangsam wird jetzt auch die write-Funktion verständlich: Dort steht ja nichts anderes als write(1,(i+++"hello, world!\n")--)-1,0,1); Mit Klammern wird das wie oben vielleicht besser verständlich: write(1,((i++)+"hello, world!\n")--)-1,0,1); 1)); E.1. 1984 131 FT Der erste Parameter in read und write ist 0 bzw. 1, der file descriptor. Wenden wir uns nun dem zweiten Teil des for-Konstrukts zu: i[n] = ∗(i + n). Das liefert: for(; *(i+"]<i;++i){--i;}"); write(1 ,i+++"hello, world!\n", 1)); Im letzten Teil des for-Konstrukts erfolgt die Inkrementierung von i, s. o. Im zweiten Teil laufen wir, bis wir zum Terminierungszeichen des Strings kommen. Es kommt also nur darauf an, das wie einen String der Länge von „hello, world!“ dort haben, z. B. „abcdefghijklm“. Am einfachesten liest es sich aber wohl so: for(; *(i+"hello, world!"); write(1 E.1.2 decot ,i+++"hello, world!\n", Diese Programm von Dave Decot gewann 1984 den zweiten Platz: #define #define #define #define x = double(a,b) int char k[’a’] union static struct DR A extern int floor; double (x1, y1) b, char x {sizeof( double(%s,%D)(*)()) ,}; struct tag{int x0,*xO;} *main(i, dup, signal) { { for(signal=0;*k *x * __FILE__ *i;) do { (printf(&*"’\",x); /*\n\\", (*((double(tag,u)(*)())&floor))(i))); goto _0; _O: while (!(char <<x - dup)) { /*/*\*/ union tag u x{4}; } } while(b x 3, i); { char x b,i; _0:if(b&&k+ sin(signal) ; } / * ((main) (b)-> xO));/*} */}}} E.1.3 Laman Mike Laman gewann 1984 mit diesem Programm den dritten Platz a[900]; l)char* 0;k*k< b;c;d=1 *l;{g= g;b=k ;e=1;f; atoi(* ++>>1) g;h;O; ++l); ;for(h= main(k, for(k= 0;h*h<= 1)); ANHANG E. IOCCC 132 --h;c=( <=g){ b<<5|c] ++f)a[b =0;c<h; <k/2)a[ a[b<<5 a[b<<5|c putchar( (h+=g>h ++O;for =d++,b+= <<5|c]= ++c){ b<<5|c] |c]^=a[ ]?"%-4d" ’\n’);}} *(h+1)) (f=0;f< e;for( d++,c+= for(b=0 ^=a[(k (k-(b+1 :" " /*Mike -1)>>1; O&&d<=g f=0;f<O e;e= -e ;b<k;++ -(b+1)) ))<<5|c] ,a[b<<5 Laman*/ FT g;++h); while(d ;++f)a[ &&d<=g; ;}for(c b){if(b <<5|c]^= ;printf( |c]);} Dieses Programm ist einfach zu entschlüsseln, man bringe es nur in eine sinnvoller(?) strukturierte Form: DR A a[900]; b;c; d=1;e=1; f;g;h;O; main(k,l) char* *l; { g=atoi(*++l); for(k=0;k*k<g;b=k++>>1); for(h=0;h*h<=g;++h); --h; c=((h+=g>h*(h+1))-1)>>1; while(d<=g) { ++O; for (f=0;f<O&&d<=g;++f) a[b<<5|c]=d++, b+=e; for(f=0;f<O&&d<=g;++f) a[b<<5|c]=d++,c+=e;e=-e; } for(c=0;c<h;++c) { for(b=0;b<k;++b) { if(b<k/2)a[b<<5|c]^=a[(k-(b+1))<<5|c]^=a[b<<5|c]^=a[(k-(b+1))<<5|c]; printf(a[b<<5|c]?"%-4d":" ",a[b<<5|c]); } putchar(’\n’); } } /*Mike Laman*/ Nun schreiben wir dies noch in aktueller C-Syntax (Deklaration von main) und deklarieren/definieren jede Variable vollständig. E.1.4 mullender The Grand Prize: Sjoerd Mullender & Robbert van Renesse für das Programm short main[] = { 277, 04735, -4129, 25, 0, 477, 1019, 0xbef, 0, 12800, -113, 21119, 0x52d7, -1006, -7151, 0, 0x4bc, 020004, 14880, 10541, 2056, 04010, 4548, 3044, -6716, 0x9, E.2. 2001 133 }; FT 4407, 6, 5568, 1, -30460, 0, 0x9, 5570, 512, -30419, 0x7e82, 0760, 6, 0, 4, 02400, 15, 0, 4, 1280, 4, 0, 4, 0, 0, 0, 0x8, 0, 4, 0, ’,’, 0, 12, 0, 4, 0, ’#’, 0, 020, 0, 4, 0, 30, 0, 026, 0, 0x6176, 120, 25712, ’p’, 072163, ’r’, 29303, 29801, ’e’ Sie haben keine Chance dieses Programm zum Laufen zu bekommen, wenn sie keine Vax-11 oder pdp-11 zur Verfügung haben. Sie haben eine Chance es zu verstehen, wenn Sie die Codes der Instruktionen dieser Maschinen beherrschen. Soweit zur Portabilität, wenn derartige Maschinen im Spiel waren. DR A E.2 2001 ANHANG E. IOCCC DR A FT 134 FT Anhang F Development Environments F.1 Microsoft Visual Studio Der Microsoft Compiler hat sehr viele Optionen. Damit kann man am einfachsten umgehen, wenn man diese in der IDE ändert. Da es in der IDE aber viele Dinge gibt, die Sie noch nicht kennen, beschreibe ich hier einige nützliche Optionen und wie Sie diese in einer DOS-Box verwenden. DR A /? Vielleicht ist dies der wichtigste Parameter von cl: Er sorgt dafür, dass eine Liste der Optionen auf den Bildschirm ausgegeben wird. /Wall alle Warnungen aktivieren. Das mag zwar für Anfänger nicht unbedingt die beste Option sein. Sie ist aber sehr lehrreich und ich bin der Ansicht, ein Programm sollte auch mit dieser Option keine Warnungen bringen, wenn man es ernsthaft benutzen möchte. Arbeiten Sie in VC98 oder Visual Studio .NET, so müssen Sie ein Projekt haben, bevor Sie Dateien umwandeln (compile), linken (build) oder ausführen (execute) können. Am einfachsten ist es zunächst, einfach die .c Datei zu öffnen und dann einen default workspace und ein default Project anlegen zu lassen. Das ist zwar nicht sehr profesionell, aber kann schnelle gemacht werden. Weiteres in der Vorleseung F.2 Erläuterungen zu Fehlermeldungen C0006 Include Datei kann nicht geöffnet werden. Auslöser: #include <summe03.h> Ursache: Wird der Name der Include-Datei in spitzen Klammern (<, >) angegeben, so wird der Standard-Pfad des Compilers nach der Datei durchsucht. Korrekt wäre hier gewesen #include "summe03.h" dann wird zuerst im aktuellen Verzeichnis und erst dann im Standard-Pfad gesucht. C2015 Zu viele Zeichen in der Konstanten Auslöser: #include ’summe03.h’ Ursache: Einfache Anführungszeichen können in C nur ein Zeichen einschließen. Dies war einfach ein Tippfehler. Korrekt wäre gewesen: #include "summe03.h" C2065 : ’i’: nichtdeklarierter Bezeichner C2143 : Syntaxfehler: Es fehlt ’;’ vor ’[’ Auslöser: int [2][2] = 1,2,3,4; 135 ANHANG F. DEVELOPMENT ENVIRONMENTS 136 Ursache: int [2][2] ist kein gültiger Ausdruck. Es fehlt der Name der Variablen. Korrekt wäre etwa: int a[2][2] = 1,2,3,4; FT oder Syntaxfehler: Es fehlt ’;’ vor ’eingeben’ DR A Auslöser: Deklaration einer Variablen nicht am Anfang eines Blocks. FT Literaturverzeichnis Douglas Adams. Per Anhalter durch die Galaxis. Roger & Bernhard, München, 1981. Ein Kultbuch der 80er Jahre. Der erste Band einer Trilogie, die inzwischen aus 5 Bänden besteht. [Blo71] Ernst Bloch. Subjekt - Objekt. Erläuterungen zu Hegel. Suhrkamp, Frankfurt a. M., 1971. [Cop93] James O. Coplien. Advanced C++: Programming Styles and Idioms. Addison-Wesley, Reading, MA, 1993. Ein sehr gutes Buch! Über die dort präsentierten Ansichten, was guter Programmierstil sei, kann man natürlich streiten. Aber sie zu kennen, dürfte keinem Systementwickler schaden, der C++ als eine Zielumgebung betrachtet. Coplien war einer der ersten Benutzer des ersten C++ (pre) Compilers und begleitet die Entwicklung von C++ seit Beginn. Seit dem Erscheinen von [GHJV95] könnte diese Buch auch „Design Patterns in C++“ heißen. DR A [Ada81] [Dij68] Edsger W. Dijkstra. Go To Statement Considered Harmful. Communications of the ACM, 11(3):147–148, März 1968. [GHJV95] Erich Gamma, Richard Helm, Ralph Johnson und Vlissides, John M. Design Patterns - Elements of Reusable Object-Oriented Software. Addison-Wesley, 1994, Fourth printing, September 1995. ISBN 0-201-63361-2. Ein Katalog von Mustern, mit denen häufig vorkommenden Designaufgaben in C++ oder anderen objektorientierten Programmiersprachen gelöst werden können. Vieles davon ist an anderer Stelle beschrieben, aber dies ist eine kompakte Zusammenfassung, die auch die Anwendung einiger dieser Patterns an einer Fallstudie zeigt. Die Lösungen sind praxiserprobt und helfen bei der guten Strukturierung auch (aber keineswegs nur) von C++ Software. Sicherlich eines der einflussreichsten Informatik Bücher der letzten Jahre. Ein großer Teil der Arbeit wurde bei der Firma Taligent durchgeführt. Es gibt eine deutsche Übersetzung [GHJV96]. [GHJV96] Erich Gamma, Richard Helm, Ralph Johnson und Vlissides, John M. Entwurfsmuster - Elemente wiederverwendbarer objektorientierter Software. Addison-Wesley, Bonn, Paris, Reading, MA, 1996. ISBN 3-89319-950-0. Deutsche Übersetzung von [GHJV95]. [IH04] Rolf Isernhagen und Harmut Helmke. Softwaretechnik in C und C++. Carl Hanser Verlag, München, Wien, 2. korrigierte Auflage Auflage, 2004, xx+937 Seiten. ISBN 3-446-21584-0. [Knu73] Donald E. Knuth. The Art of Computer Programming I: Fundamental Algorithms. AddisonWesley, Reading, MA, 2. Auflage, 1973, xix+634 Seiten. ISBN 0-201-03821-8. Ein Klassiker, den jeder Informatiker gelesen haben sollte. Der Stil bis hin zu den Aufgaben ist legendär. Dieser Band ist im Gegensatz zu den weiteren auch als Paperback lieferbar. [KPP96] Ulla Kirch-Prinz und Peter Prinz. C für PC<s. International Thomson Publ., Bonn, Albany and others, 1996, 655 Seiten. ISBN 3-8266-2697-4. [KR88] Brian Kernighan und Dennis Ritchie. The C Programming Language. Prentice-Hall, Englewood Cliffs, NJ, 2. Auflage, 1988, vii+272 Seiten. ISBN 0-13-110370-9. [Nag93] Eric Nagler. Learning C++ - A Hands-On Approach. West Publishing Company, Minneapolis/St. Paul, MN, 1993. Eine gute Einführung in C++. Es beginnt mit den elementarsten 137 LITERATURVERZEICHNIS 138 FT Komponenten, behandelten Klassen, Streams, Templates und endet mit Exceptions. Wer alle (über 500) Beispiele durchgearbeitet hat, sollte in Syntax und Semantik fit sein. Es ist aber kein Buch, um gutes Programmieren oder gar Software-Engineering zu lernen. Wer nach diesem Buch C++ lernen will, ist gut beraten zusätzlich z. B. das Buch von Coplien (s.o.) durchzuarbeiten. Hans-Jürgen Scheibl. Visual C++.NET für Einstieger und Fortgeschrittene. Carl Hanser Verlag, München, Wien, 2004, xxii+1430 Seiten. ISBN 3-446-22329-0. [Str91] Bjarne Stroustrup. The C++ Programming Language. Addison-Wesley, Reading, MA, 2. Auflage, 1991. Eine systematische Darstellung von C++ mit dem kompletten reference manual. Dieses Buch kann als Einführung in C++ dienen. Wer C++ im Eigenstudium und schnell lernen will, sollte zusätzlich ein Buch wie [Nag93] heranziehen und bald das Buch [Cop93] von Coplien lesen. Auch auf Design-Prinzipien und die Entwicklung von Klassenbibliotheken wird kurz eingegangen. Es gibt eine deutsche Übersetzung [Str94c]. Inzwischen ist die 3. Auflage erschienen [Str97]. [Str94a] Bjarne Stroustrup. The Design and Evolution of C++. Addison-Wesley, Reading, MA, 1994, x+461 Seiten. ISBN 0-201-54330-3. Das „Eichenbuch“. Eine hervorragende Darstellung, warum was in C++ wie funktioniert und nicht anders. An vielen Stellen wird der Bezug zwischen Designprinzipien und Grundprinzipien der Programmiersprache hergestellt. Viele andekdotische Details aus der Entstehungsgeschichte der Sprache. Es gibt eine deutsche Übersetzung [Str94b]. DR A [Sch04] [Str94b] Bjarne Stroustrup. Design und Entwicklung von C++. Addison-Wesley, Bonn, Paris, Reading, MA, 1994. Deutsche Übersetzung von [Str94a]. [Str94c] Bjarne Stroustrup. Die C++ Programmiersprache. Addison-Wesley, Bonn, Paris, Reading, MA, 4., korrigierter und erweiterter Nachdruck der 2. Auflage, 1994. Deutsche Übersetzung von [Str91]. [Str97] Bjarne Stroustrup. The C++ Programming Language. Addison-Wesley, Reading, MA, 3. Druck der 3. Auflage, 1997. Umfangreich gegenüber der 2. Auflage [Str91] überarbeitet. Behandelt die STL und andere Eigenschaften des kommenden C++ Standards. INzwischen ist die 4. Auflage erschienen: [Str13]. [Str13] Bjarne Stroustrup. The C++ Programming Language. Addison-Wesley, Reading, MA, 4. Auflage, 2013, 1368 Seiten. \t, 24 struct adresse, 84 struct complex, 83 struct punkt, 83 \xhh, 24 ^-Operator, 42 _USE_MATH_DEFINES, 67 { . . . }, 5 ˜-Operator, 42 A Ablauf Programm-, 51 Abstraktionvermögen, 83 Adams, Douglas, 137 Addition, 12, 36 Additionsoperator, 12 Adresse, 16 Adressoperator, 16, 79 Algorithmus, 7 arithmetisch Operator, binär, 36 arithmetischer Datentyp, 71 ASCII, 33, 54 atof, 72 atoi, 72 Attribut, 89 ausführbar Datei, 5 ausführen, 1 Ausgabeinheit Standard-, 19 1, 19 2, 19 Auswahloperator, 41 auto, 97 automatic, 98 DR A Symbole #define, 75, 77 #define, 67 #elif, 77 #else, 77 #endif, 77 #endif, 87 #if, 77 #ifdef, 77 #ifndef, 77 #include Präprozessor-Befehl, 7 #undef, 77 %, 36 &-Operator, 43 &-Operator, 16, 42, 79 *, 36 *-Operator, 16, 42, 88 +, 36, 37 +-Operator, 12, 13 -, 36, 37 –Operator, 13 .-Operator, 85, 88 .NET, 6 /, 36 /*. . . */, 5 /-Operator, 13 /Wall Compiler-Option, 48 /c, 65 <, 39 =, 39 ?:-Operator, 41 \", 24 #ifndef, 87 \’, 24 \0, 24 \?, 24 \\, 24 \a, 24 \b , 24 \f , 24 \n, 24 \ooo, 24 \r, 24 FT Index B B, 6 BCPL, 6 Befehl goto, 53 label, 53 Betriebssystem, 1, 98 139 INDEX 140 D d, 14, 25 dangling else problem, 52 Datei ausführbar, 5 Header-, 9, 63, 65 Implementierungs-, 65 map-, 113 Objekt-, 5 Datentyp, 79 arithmetischer, 71 DAU, 30 Dave Decot, 131 dd, 21 DEC PDP-11, 6 PDP-7, 6 Deeferenzierungsoperator, 42 Default-Sichtbarkeit, 89 Deklaration, 64 Dekrement-Operator, 36, 37 Postfix, 36 Präfix, 36 descriptor File, 19 didot point, 21 Dijkstra, Edsger Wybe, 137 Division, 13, 21, 36 Divisionsoperator, 13 do while, 50 do while-Schleife, 46, 47, 49 DOS, 31 DOS-Box, 4 Dou Xiao, xi doxygen, 2 Kommentar, 4 Du Hongbo, xi Dump, 80 DR A C c, 14, 25 C, 1, 6, 79 C#, 1 C# , 6 C++, 1, 6, 29, 87, 89, 99, 100 C++ , 89 C-Compiler, 2 C-Programm, 7, 63 Komponente, 7 C-Stil Kommentar, 5 C0006, 135 C2015, 135 C2065, 135 C2143, 135 C2166, 88 CA-IDEAL, 54 Cao Bin, xi Cast-Operator, 33 Chen Hao, xi Chen Yingjie, xi cicero, 21 cl, 6 class, 89 Claus, Volker, 54 clock, 72 cm, 21 Collatz-Folge, 55 Compiler, 3, 6, 9, 65 Compiler-Option /Wall, 48 const, 97 continue, 50 Coplien, James O., 137 cos, 71 CPL, 6 Ctrl-C, 46 Ctrl-D, 31 Ctrl-Z, 31 FT Bibliothek, 9 Standard-, 63 stdio, 7 big point, 21 binär Operator, 36 Operator, arithmetisch, 36 Bit-Manipulation, 35 Bit-Maske, 43 Bloch, Ernst, 137 Block, 97 bp, 21 break, 50, 53 Breite Feld, 24 E e, 26 E, 26 Eclipse, 3 Editor, 2 Eclipse, 3 Editor, 3 emacs, 3 Lieblings-, 3 Netbeans, 3 notepad++, 3 vi, 3 INDEX FT d, 14, 25 e, 26 E, 26 f, 14, 26 g, 26 G, 26 Hex-, 112 ld, 25 lf, 26 o, 25 s, 26 u, 25 x, 25 X, 25 Formattierungsstring, 12 Fu Xiangyun, xi Funktion, 63, 89 main, 63 Standard-, 63 funktionaler Zusammenhalt, 66 G g, 26 G, 26 Gamma, Erich, 137 Gao Cheng, xi Ge Di, xi Genauigkeit scanf, 32 Geng Xiaoming„ xi getchar, 17 global Variable, 97 globale Kopplung, 66 goto, 53 DR A Visual Studio, 3 wordpad, 3 Eindeutigkeit, 7 Eingabeinheit Standard-, 19 0, 19 Eingabepuffer, 31 Element, 89 emacs, 3 Endezeichen String-, 59 Endlichkeit, 7 Endlosschleife, 45 Engineering Software-, 63 Entscheidung Mehrweg-, 52 EOF, 17 Escape-Sequenz, 14, 23 \’, 24 \0, 24 \?, 24 \\, 24 \a, 24 \b , 24 \f , 24 \n, 24 \ooo, 24 \r, 24 \t, 24 \v, 24 \xhh, 24 \", 24 exp, 71 external, 97 141 F f, 14, 26 Fang Zongda, xi Fehlermeldung, 19 Feldbreite, vii, 24 Fernsehen, 6 fflush, 31 Fibonacci-Folge, 69 Fibonacci-Zahl, 62, 69 file descriptor, 19 file handle, 19 float.h, 28 Folge Collatz-, 55 for, 50 for-Schleife, 45, 48 Format c, 14, 25 H handle file, 19 Header-Datei, 9, 63, 65 Hegel, Georg Wilhelm Friedrich, 137 Helm, Richard, 137 Helmke, Harmut, 137 Hex-Format, 112 hexadezimal, 43 Hu Chenjun, xi I if, 51 Implementierungs-Datei, 65 in, 21 inch, 21 Inkremenoperator Postfix-, 50 INDEX 142 M M_PI, 66 Ma Xiating, xi main, 5 Funktion, 63 Makro, 76 malloc, 98 Manipulation Bit-, 35 map-Datei, 113 Maschinenregister, 98 Maske Bit-, 43 math.h, 66 max(A, B), 76 Mehrwegentscheidung, 52 member function, 89 Microsoft, 6 Microsoft Visual Studio, 135 Millimeter, 21 MinGW, 2, 109 mm, 21 Modul, 63 modulglobal Variable, 97 Modulodivision, 36 Multiplikation, 13, 21, 36 Multiplikationsoperator, 13 DR A J Java, 1, 6, 29, 99, 100 Jiang Chengyong, xi Jiang Yanfei, xi Johnson, Ralph, 137 Lisp, 3 Liste, 103 Liu Songlin, xi log10, 71 logische Operation, 35 lokal Variable, 97 Lu Ling, xi Luo Liren, xi FT Inkrement-Operator, 36, 37 Postfix, 36 Präfix, 36 Inkrementoperator Postfix-, 35 Präfix-, 35, 50 int, 5 Interrupt, 97 IOCCC, 49 isalnum, 71 isalpha, 71 isdigit, 71 Isernhagen, Rolf, 137 islower, 71 isupper, 71 K Kapselung, 87 Kernighan, Brian, 137 Kirch-Prinz, Ulla, 137 Klasse, 83, 87, 89, 100 Klassenelemente, 100 Kluge, Alexander, 65 Knuth, Donald Ervin, 137 Kommentar, 5 C-Stil, 5 doxygen, 4 Komponente C-Programm, 7 Konsole, 7, 19 Konstante symbolische, 76 Kopplung, 49, 65 globale, 66 L L-Wert, 88 label, 53 ld, 25 leicht daneben, 58 lf, 26 Li Feng, xi Lieblingseditor, 3 limits.1, 9 limits.h, 9 Linker, 6 Links-Shift, 43 linksbündig, 24 N Nagler, Eric, 137 Name symbolischer, 76 Namenskonvention, 85 Netbeans, 3 notepad, 3 notepad++, 3 O o, 25 Oberklasse, 89 Objekt, 100 Objektdatei, 5 objektorientiert, 100 INDEX FT PDP-11, 6 PDP-7, 6 Peng Bo, xi π, 66 pica, 21 point big, 21 didot, 21 scaled, 21 pointer, 16 pointer, 71 Pointer, 79 Postfix Dekrement-Operator, 36 Inkrement-Operator, 36 Postfix-Inkremenoperator, 50 Postfix-Inkrementoperator, 35 pow, 71 Präfix Dekrement-Operator, 36 Inkrement-Operator, 36 Präfix-Inkrementoperator, 35, 50 Präprozessor, 87 Präprozessor-Befehl #include, 7 printf, 13 printf, 99 Prinz, Peter, 137 privat, 89 problem dangling else, 52 Programm, 7, 64 alloc01.c, 98 C-, 63 copy01.c, 17 datatypes04.c, 32 formio13.c, 26 formio15.c, 27 formio16.c, 29, 32 formio17.c, 30 formio18.c, 31 formio19.c, 31 happybirthday1.c, 15 happybirthday2.c, 45 happybirthday3.c, 46 happybirthday4.c, 47 happybirthday5.c, 48, 49 moinmoin01.c, 13 moinmoin02.c, 14 moinmoin03.c, 14 op06.c, 41 op07.c, 41 palindrom01.c, 57, 59 palindrom02.c, 67 DR A Programmiersprache, 100 off by one, 58 oktal, 43 Operation, 89 ogische, 35 Rechen-, 35 Operatione, 89 Operator, 35 &, 16, 79 &-, 42, 43 *-, 16, 42 +, 12, 13 -, 13 ., 85 .*-, 88 .-, 88 /, 13 <, 39 =, 39 ?:, 41 ^, 42 ˜, 42 Additions-, 12 Adress, 16, 79 Adress-, 16 arithmetisch, binär, 36 Auswahl-, 41 binär, 36 Cast-, 33 Dekrement-, 36, 37 Dereferenzierungs-, 42 Inkrement-, 36, 37 Multiplikations, 13 Operator, 13 Punkt-, 88 Referenzierungs-, 42 Shift-, 42 sizeof-, 29, 33, 85, 121 Stern-, 88 structure member, 85 structure pointer, 88 Subtraktions-, 13 unär, 42 Unär, 36, 37 und-, 43 Verweis, 16 Vorzeichen-, 37 143 P Palindrom, 38 Pan Enyu, xi Pan Xinyuan, xi Pan Yiwen, xi pc, 21 INDEX 144 FT Scheibl, Hans-Jürgen, 138 schlechtes Programm, 1 Schleife, 45, 53 do while, 46 do while-, 49 Endlos-, 45 while, 46 schreiben, 1 Semantik Referenz-, 63 Wert-, 63 Sequenz Escape-, 14, 23 Set-Top Bix, 6 Shen Zhongyuan, xi Shi Zheng, xi Shift Links-, 43 Rechts-, 43 Shift-Operator, 42 Sichtbarkeit, 83, 89 Signatur, 130 sin, 71 sizeof-Operator, 29, 33, 85, 121 Software-Engineering, 63 sp, 21 Spaghetti-Code, 54 Speicher, 97 Speicherklasse, 97 Speicherverwaltung, 98 Sprung, 45, 53 sqrt, 71 srand, 72 Stack, 99, 103 Standard-Ausgabeinheit, 19 Standard-Bibliothek, 13 Standard-Eingabeinheit, 19 Standardbibliothek, 63 Standardfunktion, 63 static, 97, 100 stdin, 19 stdio.h, 7 stdio.h, 13 stdlib.h, 98 stdout, 19 Stern-Operator, 88 strcat, 71 strcmp, 71, 124 strcpy, 71 strftime, 72 String, 57, 59 Formattierungs-, 12 String-Endezeichen, 59 strlen, 71 DR A pointer01.c, 79 radius01.c, 66 schlechtes, 1 struct03.c, 87 summe.c, 63 summe02.c, 64 summe03.c, 63 summe03.h, 64 tunix.c, 3 vect05.c, 61 zeiger01.c, 16 Programmablauf, 35, 51 Programmblock, 5 Programmieren, 1 Programmiersprache, 1 objektorientiert, 100 Programmierstil, 1 Prototyp, 11 pt, 21 public, 89 Puffer Eingabe-, 31 Punkt-Operator, 88 punkt.c, 86 punkt.h, 86 putchar, 14 Pythagoras, 86 Q Qian Lingyun, xi Qin Chuan, xi Queue, 103 R Rückgabewert, 71 rand, 72 Rechengenauigkeit, 27 Rechenoperation, 35 Rechts-Shift, 43 rechtsbündig, 24 Referenzierungsoperator, 42 Referenzsemantik, 63 register, 97, 98 Register Maschinen-, 98 Reproduzierbarkeit, 9 Ritchie, Dennis, 6, 137 S s, 26 scaled point, 21 scanf, 15 scanf, 27, 30 Genauigkeit, 32 INDEX W Wang Bin, xi Wang Dongpeng, xi Wang Jiyuan, xi Wang Liming, xi Wang Liyan, xi Wang You, xi Weikang Qian, xi Wertsemantik, 63 while, 50 while-Schleife, 46, 48 wordpad, 3 Wu Fangqi, xi Wu Jiejie, xi X x, 25 X, 25 Xu Chao, xi Xu Fang, xi Xu Jingwei, xi Xu Renyi, xi DR A T Terminiertheit, 7 Thompsen, Ken, 6 time, 72 translation unit, 97 tunix.c, 3 Typ, 84 Visual Studio, Microsoft, 135 Vlissides, John M., 137 void, 71 volatile, 97 Vorzeichenoperator, 37 FT Stroustrup, Bjarne, 6, 138 struct, 71, 83, 84, 89 struct kreis, 84 struct strecke, 84 structure member operator, 85 structure pointer operator, 88 Struktogramm, 53 Subroutine, 66 Subtraktion, 13, 21, 36 Subtraktionsoperator, 13 Sun, 6 Sun Xiaojun, xi switch, 50 symbolische Konstante, 76 symbolischer Name, 76 145 U u, 25 umwandeln, 1 Umwandlungseinheit, 97 unär Operator, 42 Unär Operator, 36, 37 und-Operator, 43 union, 71 unit translation, 97 UNIX, 6, 31 Unterprogramm, 66 Utility, 1 V Variable, 7 global, 97 lokal, 97 modulglobal, 97 Vektor, 57 Vergleich, 35 Verwaltung Speicher-, 98 Verweisoperator, 16 Verzweigung, 45 vi, 3 Visual C++.NET, 4 Visual Studio, 3 Y Yang Yifan, xi Yao Fusheng, xi Ye Weiqiang, xi Yu Zhongwei, xi Yuan Zhaoxing, xi Z Zeiger, 16, 31, 79, 80, 98, 99 Zentimeter, 21 Zerlegung, 63 Zhang Chenglei, xi Zhang Fanjun, xi Zhang Jie, xi Zhang Lin, xi Zhang Meiyan, xi Zheng Haijiao, xi Zhou Ying, xi Zhu Da, xi zusammengesetzte Zuweisung, 35, 38, 44 Zusammenhalt, 49, 65 funktionaler, 66 Zuweisung, 38 zusammengesetzte, 35, 38, 44 Zwischenraumzeichen, 30 INDEX DR A Numbers 0, 19 1, 19 2, 19 FT 146