Informatik für Ingenieure: Grundlagen und Programmierung in C [überarbeitete Auflage] 9783486717419, 9783486705270

Für Ingenieure nimmt die Fähigkeit, Software programmieren zu können, immer entscheidendere Bedeutung an. C-Programmieru

200 88 4MB

German Pages 408 Year 2012

Report DMCA / Copyright

DOWNLOAD FILE

Polecaj historie

Informatik für Ingenieure: Grundlagen und Programmierung in C [überarbeitete Auflage]
 9783486717419, 9783486705270

Citation preview

Informatik für Ingenieure Grundlagen und Programmierung in C von

Prof. Dr. Axel Böttcher Prof. Dr. Franz Kneißl 3., überarbeitete Auflage

Oldenbourg Verlag München

Prof. Dr. Axel Böttcher lehrt Informatik an der Fakultät für Informatik und Mathematik der Hochschule München. Prof. Dr. Franz Kneißl lehrt Informatik an der Fakultät für Elektro- und Informationstechnik der Hochschule Regensburg.

Bibliografische Information der Deutschen Nationalbibliothek Die Deutsche Nationalbibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über http://dnb.d-nb.de abrufbar. © 2012 Oldenbourg Wissenschaftsverlag GmbH Rosenheimer Straße 145, D-81671 München Telefon: (089) 45051-0 www.oldenbourg-verlag.de Das Werk einschließlich aller Abbildungen ist urheberrechtlich geschützt. Jede Verwertung außerhalb der Grenzen des Urheberrechtsgesetzes ist ohne Zustimmung des Verlages unzulässig und strafbar. Das gilt insbesondere für Vervielfältigungen, Übersetzungen, Mikroverfilmungen und die Einspeicherung und Bearbeitung in elektronischen Systemen. Lektorat: Dr. Gerhard Pappert Herstellung: Constanze Müller Titelbild: thinkstockphotos.de Einbandgestaltung: hauser lacour Gesamtherstellung: Grafik & Druck GmbH, München Dieses Papier ist alterungsbeständig nach DIN/ISO 9706. ISBN 978-3-486-70527-0 eISBN 978-3-486-71741-9

Vorwort zur dritten Auflage Seit der ersten Auflage vor gut zehn Jahren hat sich vieles im Bereich „Informatik für Ingenieure“ verändert. Einiges ist aber auch gleich geblieben. Die Aussagen im Vorwort für die erste Auflage gelten zum größten Teil noch fast unverändert. Immer noch dominieren C und C++ die Szene der „Embedded Systems“, sind also weiterhin wichtig für Ingenieure. Und immer noch sind die Studenten, die eine Ingenieurausbildung an Hochschulen beginnen, sehr inhomogen hinsichtlich ihrer Vorkenntnisse in Informatik. Wir gehen deswegen davon aus, dass ein Buch wie das vorliegende immer noch gebraucht wird. Das hat uns motiviert, eine dritte Auflage heraus zu bringen, die aktualisiert, vollständig überarbeitet und fehlerbereinigt ist. Eine Fülle von Rezensionen hat uns wertvolle Hinweise geliefert, wie wir den Wünschen der Kunden noch besser entsprechen können. An dieser Stelle möchten wir unseren Rezensenten dafür danken. Gewünscht wurden vor allem Lösungen zu den Aufgaben, aber auch formale Änderungen, wie eine übersichtlichere Darstellung und ein ruhigeres Druckbild. Insbesondere haben wir bei der Überarbeitung • Lösungen für den größten Teil der Aufgaben aufgenommen • Neuerungen des Standards [ISO/IEC (1999)] berücksichtigt, vor allem – den Übergang von 32 auf 64-Bit-Systeme bei den Datentypen – die Einbettung von Zeichencodes mit mehr als 8 Bit – Erweiterungen der Quell- und Ausführungszeichensätze • das Textsatzsystem LATEX benutzt um das Druckbild zu verbessern • eine einheitliche und übersichtlichere Form der Syntaxdiagramme gewählt • die Entwicklungsumgebung und die Materialien, mit denen die Leser zur Übung selbst Beispiele programmieren können, auf einen aktuellen Stand gebracht Für die Unterstützung während der Arbeiten sind wir insbesondere unseren Familien zu besonderem Dank verpflichtet – auch das hat sich gegenüber der ersten Auflage nicht verändert. Axel Böttcher, Franz Kneißl

VI

Vorwort zur dritten Auflage

Vorwort zur ersten und zweiten Auflage Der Anteil der Software-Erstellung an der Ingenieurstätigkeit hat sich in den letzten Jahren dramatisch erhöht. Heute besteht die Wertschöpfung bei vielen technischen Produkten zum großen Teil aus der Entwicklung von Software - typischerweise in der Programmiersprache C oder objektorientierten Sprachen wie C++ und JAVA. Weniger dramatisch geändert hat sich in den meisten Studiengängen der Anteil der Informatik an der Gesamtstundenzahl. Um Stoff für aktuelle Themen und Anforderungen unterzubringen, müssen bisherige bewährte Inhalte reduziert werden. In vielen Fachbereichen bedeutet dies nicht nur die Reduktion von Grundlagen wie z.B. „Geschichte der Datenverarbeitung“ – auch Inhalte, die didaktisch besonders erprobt waren, fallen der Anpassung zum Opfer. Hierunter ist z. B. der Erstunterricht in einer für den Einstieg günstigen Programmiersprache wie PASCAL zu rechnen. Der schwierigere Einstieg etwa über die Sprache C wird in den ersten Semestern durch ein weiteres Problem verstärkt: die Inhomogenität der Vorkenntnisse, die die Studierenden aus ihrer schulischen und beruflichen Ausbildung mitbringen. Wir haben uns daher entschlossen, ein Buch auf den Markt zu bringen, das dieser Problematik begegnet. Inhaltlich geschieht dies durch die Konzentration auf aktuelle Themen, deren Kenntnis heute von jedem Ingenieur verlangt wird. Dazu gehört vor allem die Programmierung in C - nicht nur wegen der weiten Verbreitung - auch weil späterer Unterricht in Programmiersprachen wie C++ oder JAVA darauf aufbauen kann. Grundbegriffe der Computertechnik, Zahlendarstellung und Codes werden als nötiges Hintergrundwissen für die Programmierung vermittelt. Anwendungsorientierte Algorithmen und Beispiele bilden das nötige Anschauungsmaterial. Durch eine stark ausgearbeitete Aufbereitung wollen wir den Einstieg erleichtern. Vor allem ist damit die durchgängige Strukturierung in kleine, überschaubare Lernschritte gemeint, sorgfältige Auswahl der Darstellungsmittel wie z.B. durchgängige Verwendung von Syntaxdiagrammen, viele Fragen und Aufgaben und nicht zuletzt viele Tipps zum professionellen Arbeiten und zur Vermeidung typischer Fehler. Unterstützend legen wir dem Buch eine CD bei, die die Voraussetzungen bietet, um alle R Beispiele und Aufgaben aus dem Buch nachzuvollziehen. Enthalten ist der Borland C bzw. C++ Compiler 5.5 sowie ein passender Freeware-Editor. Damit steht dem Leser eine Entwicklungsumgebung, zur Verfügung, mit der sich alle Aufgaben und Beispiele aus dem Buch programmieren lassen (einschließlich der Grafik-Anwendungen) – für Anfänger ein ideales Startset. Entstanden ist das Buch auf der Basis von Vorlesungen und Praktika am Fachbereich Elektrotechnik der Fachhochschule Regensburg, die dort in den ersten zwei Semestern gehalten werden. Es vermittelt das gemeinsame Basiswissen der Ingenieurinformatik, das in allen technischen Fächern benötigt wird. Damit zielt es in gleicher Weise auf die Informatikausbildung anderer Fachbereiche bzw. überhaupt auf Anfänger, die in C und Informatik für Ingenieure einsteigen wollen. Wo die Studienordnung über die Inhalte des Buches hinaus Platz für Informatik bietet, ist es für den Einstieg vorgesehen, dem später weitere Vorlesungen und Praktika fol-

Vorwort zur dritten Auflage

VII

gen sollen. Zum Beispiel ist es als Basis gedacht, auf der Themen wie objektorientierte Programmierung in C++ oder JAVA, Einsatz von Klassenbibliotheken, Software Engineering, betriebssystemnahe Themen, Datenbanken oder Internet-Themen aufbauen können. Für die Unterstützung während der Arbeiten sind wir zu besonderem Dank verpflichtet • unseren Familien, denen wir auch dieses Buch widmen. Sie haben es während der ganzen Zeit geduldig ertragen, dass uns das Buchprojekt stark absorbiert hat, • Herrn Prof. Dr. Oechslein, der das Projekt unterstützt hat, • Herrn Prof. Dr. Geupel, der das Projekt verlagsseitig betreut hat, • dem Lektorat des Oldenbourg-Verlags, das unsere Ideen unterstützte. Zur Zweiten Auflage Gegenüber der ersten ist die zweite Auflage vollständig durchgesehen und fehlerbereinigt. Wir danken an Dieser Stelle für die vielen Rückmeldungen von unseren Lesern und Rezensenten. R - Umgebung ist die für Anfänger etwas Die CD wurde überarbeitet. Statt der Cygnus einfacher zu handhabende Entwicklungsumgebung mit dem Borland C++ Compiler 5.5 aufgenommen worden sowie ein passender Freeware- Editor für Programme, der Zeilennummern und Syntax-Highlighting von Schlüsselworten beherrscht.

Die Grafikpakete auf der CD wurden vereinheitlicht und um Funktionen erweitert, die von Lesern der 1. Auflage gewünscht wurden. Im Mai 2001 Axel Böttcher, Franz Kneißl

Inhaltsverzeichnis Vorwort

V

1

Grundbegriffe der Computertechnik

1

1.1

Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

1

1.2

Anwendungsprogramme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

3

1.3

Betriebssysteme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

5

1.4 1.4.1

Hardware . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Betrachtungsebenen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

7 8

1.5 1.5.1 1.5.2 1.5.3 1.5.4

Prozessoren, Busse und Speicher . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 Das Bussystem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 Der Prozessor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 Der Speicher . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 Peripheriegeräte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13

1.6

Die Befehlsebene . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13

1.7

Register-Transfer-Ebene . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16

1.8

Die Logikebene . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18

2

Zahlendarstellung

2.1

Zahlensysteme für ganze Zahlen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21

2.2

Zahlensysteme und B-Potenzen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22

2.3 2.3.1 2.3.2 2.3.3

Umwandlung zwischen Zahlensystemen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zielverfahren: Multiplikationsmethode . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Quellverfahren: Divisionsmethode . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Umwandlung zwischen Dual-, Oktal- und Hexadezimalsystem . . . . . . . . . .

2.4

Rechnen im Dualsystem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25

2.5 2.5.1 2.5.2

Rechnerinterne Darstellung von ganzen Zahlen . . . . . . . . . . . . . . . . . . . . . . . . . 26 Das Eins-Komplement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28 Das Zwei-Komplement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30

2.6 2.6.1 2.6.2

Darstellung und Umwandlung gebrochener Zahlen . . . . . . . . . . . . . . . . . . . . . 33 Zielverfahren: Divisionsmethode . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 Quellverfahren: Multiplikationsmethode . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34

21

23 24 24 25

X

Inhaltsverzeichnis

2.7

Rechnerinterne Darstellung gebrochener Zahlen . . . . . . . . . . . . . . . . . . . . . . . . 34

2.8

Aufgaben. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35

3

Zeichencodes

3.1

7-Bit ASCII . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38

3.2

8-Bit ISO 8859 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40

3.3 3.3.1

Unicode . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42 UTF-8 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43

3.4

Fragen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45

4

Einführung in das Programmieren in C

4.1

Zur Geschichte von C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47

4.2

Erste Schritte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49

4.3

Syntaxdiagramme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52

4.4

Praxis des Programmierens . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54

4.5

Aufgaben. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56

5

Grundelemente, Variablen, Konstanten, Datentypen

5.1

Übersicht. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57

5.2 5.2.1

Programmstruktur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58 main . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59

5.3 5.3.1 5.3.2 5.3.3 5.3.4

Lexikalische Grundvoraussetzungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zeichensätze . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Formatfreie Schreibweise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Bezeichner . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Einschränkungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

59 60 63 63 65

5.4 5.4.1 5.4.2 5.4.3 5.4.4

Variablen und Konstanten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Variablen und Konstanten zur Compilezeit, Deklarationen . . . . . . . . . . . . . . Variablen und Konstanten zur Ladezeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Variablen und Konstanten zur Laufzeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Verschiedene Konstanten-Begriffe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

66 66 67 68 69

5.5 5.5.1 5.5.2 5.5.3 5.5.4

Elementare Datentypen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ganzzahlige Datentypen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Gleitpunkttypen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Beispielprogramm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Benutzderdefinierte Datentypen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

70 71 79 82 82

5.6

Fragen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87

5.7

Aufgaben. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88

37

47

57

Inhaltsverzeichnis

XI

6

Formatierte Ein- und Ausgabe

89

6.1 6.1.1 6.1.2 6.1.3

Formatierte Ausgabe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Formatelemente für formatierte Ausgabe . . . . . . . . . . . . . . . . . . . . . . . . . . . Beispiele . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Fehlerquellen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

89 90 93 94

6.2 6.2.1 6.2.2

Formatierte Eingabe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94 Beispiel zur formatierten Eingabe: . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97 Besonderheiten und Fehlerquellen: . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97

6.3

Aufgaben. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98

7

Operatoren und Ausdrücke

7.1 7.1.1 7.1.2

Ein erstes Beispiel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99 Syntax von Ausdrücken . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100 Auswertung von Ausdrücken . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101

7.2

Arithmetische Ausdrücke . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105

7.3

Der Zuweisungsoperator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106

7.4

Zusammengesetzte Operatoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106

7.5

Unitäre arithmetische Operatoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107

7.6

Der Kommaoperator. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108

7.7

Wahrheitswerte und logische Ausdrücke . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108

7.8

Der konditionale Operator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110

7.9

Aufgaben. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111

8

Logische und bitweise Operatoren

8.1

Logische Verknüpfungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115

8.2

Bitweise Operatoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116

8.3

Fragen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123

8.4

Aufgaben. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123

9

Standardbibliothek

9.1

Ein-/Ausgabe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126

9.2

Datei-Ein-/Ausgabe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128

9.3

Grenzwerte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130

9.4

Mathematik . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130

9.5

Zufallszahlen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133

9.6

Zeichenbehandlung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134

99

115

125

XII

Inhaltsverzeichnis

9.7

Zeichenketten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135

9.8

Konvertierung Intern-/ Extern-Darstellung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136

9.9

Speicherverwaltung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137

9.10

Starten/ Beenden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137

9.11

Wahrheitswerte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137

9.12

Komplexe Zahlen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 138

9.13

Nicht-Standardfunktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139

9.14

Aufgaben. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140

10

Kontrollstrukturen

10.1

Bedingte Verzweigung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144

10.2

Auswahl (Fallunterscheidung) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148

10.3 10.3.1 10.3.2 10.3.3 10.3.4

Laufschleifen (Wiederholungsanweisungen) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die while-Anweisung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die do-while-Anweisung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Anwendung: Bestimmung von Nullstellen einer Funktion . . . . . . . . . . . . . . . Die for-Anweisung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

150 151 152 154 156

10.4 10.4.1 10.4.2 10.4.3

Sprunganweisungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die continue- und break-Anweisungen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die goto-Anweisung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . returnAnweisung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

159 159 161 161

10.5

Aufgaben. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 161

11

Präprozessor

11.1

Die #include-Direktive . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164

11.2

Symbolische Konstanten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164

11.3

Vordefinierte Symbole . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166

11.4

Makros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166

11.5

Bedingte Compilierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167

11.6

Beispielprogramm: Testversion Newton-Raphson . . . . . . . . . . . . . . . . . . . . . . . 170

12

Algorithmen: Reaktive Programme, Automaten

12.1

Endliche Automaten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175

12.2 12.2.1 12.2.2

Direkte Implementierung von Automaten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177 Direkte Implementierung mit goto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178 Direkte Implementierung mit Schleife und Zustandsvariable . . . . . . . . . . . . 179

143

163

173

Inhaltsverzeichnis

XIII

12.3

Beispielprogramm: Verkaufsautomat . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 180

12.4

Erkennende Automaten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 182

12.5

Aktionen in Automaten-Programmen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185

12.6 12.6.1 12.6.2

Weitere Anwendungsbeispiele . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 186 DFÜ-Protokolle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 186 Filter zur Behandlung von Zeichenfolgen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 189

12.7

Fragen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 192

12.8

Aufgaben. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 193

13

Vektoren

13.1

Abgeleitete Typen in C, Übersicht . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 195

13.2

Eindimensionale Vektoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 195

13.3

Deklaration von Vektoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 197

13.4

Zugriff auf Ganz- und Komponenten-Variable . . . . . . . . . . . . . . . . . . . . . . . . . . 198

13.5

Zur Deklarations-Syntax in C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 200

13.6

Mehrdimensionale Vektoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 201

13.7

Fragen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 203

13.8

Aufgaben. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 205

14

Algorithmen: Sortierverfahren, Zufallszahlen

207

14.1 14.1.1 14.1.2 14.1.3

Sortieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Bubblesort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Sortieren durch Auswahl . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Bucketsort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

207 208 208 209

14.2 14.2.1 14.2.2

Zufallszahlen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 210 Ein Simulator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 211 Zufallszahlen mit bestimmten Eigenschaften . . . . . . . . . . . . . . . . . . . . . . . . . . . 213

14.3

Fragen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 216

14.4

Aufgaben. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 218

15

Algorithmen: Lineare Gleichungssysteme

15.1 15.1.1 15.1.2

Die Gauß-Elimination . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 223 Algorithmus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 224 Programm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 224

15.2

Aufgaben. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 225

195

223

XIV

Inhaltsverzeichnis

16

Pointer

227

16.1

Übersicht. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 227

16.2 16.2.1 16.2.2 16.2.3 16.2.4

Programmierung mit Pointern . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Funktionsprinzip . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Syntax . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zugriff auf Pointer oder Bezugsvariable . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Pointer ohne Bezugsvariable, Nullpointer, void* . . . . . . . . . . . . . . . . . . . . . . .

227 227 229 230 232

16.3 16.3.1 16.3.2 16.3.3

Pointer und Vektoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Vektoren und Pointer-Arithmetik . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Vektorzugriff in Pointerschreibweise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Vektoren von Pointern, Pointer auf Vektoren . . . . . . . . . . . . . . . . . . . . . . . . . . .

233 233 234 236

16.4

Dynamische Variable mit malloc und free . . . . . . . . . . . . . . . . . . . . . . . . . . . . 238

16.5

Auswahlsort durch Zeigervertauschung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 242

16.6

Pointer und const . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 244

16.7

Fragen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 245

16.8

Aufgaben. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 246

17

Unterprogramme

17.1

Syntax . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 248

17.2

Der Parametermechanismus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 251

17.3

Referenzparameter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 253

17.4

Lokale, globale und statische Variablen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 256

17.5

Funktionsdeklarationen, Modularisierung und Headerdateien . . . . . . . . . . . 260

17.6

Fragen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 264

17.7

Aufgaben. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 264

18

Algorithmen: Grafikausgabe

18.1

Programmpaket für Grafikausgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 268

18.2

Kurven zeichnen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 272

18.3

Programmiertechniken: Funktion als Argument . . . . . . . . . . . . . . . . . . . . . . . . 274

18.4

Aufgaben. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 275

18.5 18.5.1

Koordinatentransformationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 275 Transformations-Schritte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 276

18.6

Aufgaben. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 278

18.7

Professionelle Programmiertechniken am Beispiel Koordinatentransformation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 279

247

267

Inhaltsverzeichnis

XV

18.7.1 18.7.2 18.7.3

Namensgebung und Bezeichner . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 279 Programmstruktur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 281 Beispielprogramm: Koordinatentransformation . . . . . . . . . . . . . . . . . . . . . . . . . 282

18.8

Aufgaben. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 284

19

Dateien

19.1

Der Datentyp FILE . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 288

19.2 19.2.1 19.2.2 19.2.3

Formatierte Ein-/Ausgabe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Formatierte Ausgabe mit fprintf() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Formatierte Eingabe mit fscanf() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Weitere Funktionen für das formatierte Einlesen von Datei: . . . . . . . . . . . .

19.3

Standarddateien . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 292

19.4

Binäre Ein-/Ausgabe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 292

19.5

Aufgaben. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 294

20

Structs und komplexe Datenstrukturen

20.1

Strukturen mit struct . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 295

20.2

Zeiger auf Strukturen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 298

20.3

Anwendungsbeispiel: Darstellung von Rechtecken . . . . . . . . . . . . . . . . . . . . . . 299

20.4

Listen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 300

20.5

Exkurs: Rekursive Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 306

20.6

Aufgaben. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 306

21

Algorithmen: Graphentheorie

21.1

Problemstellung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 309

21.2

Darstellung von Graphen durch Matrizen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 311

21.3

Der Algorithmus von Dijkstra . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 313

21.4

Aufgaben. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 318

22

Algorithmen: Interpretative Implementierung von Automaten 319

22.1

Programmiertechniken: Pointer auf Funktionen . . . . . . . . . . . . . . . . . . . . . . . . 319

22.2 22.2.1

Schema zur Umsetzung in Programme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 321 Beispielprogramm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 322

22.3

Fragen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 324

22.4

Aufgaben. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 324

287

289 289 290 291

295

309

XVI

Inhaltsverzeichnis

23

Fortgeschrittene Themen

327

23.1

Argumente und Rückgabewert von main . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 327

23.2 23.2.1 23.2.2 23.2.3 23.2.4

Typumwandlungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Explizite Typumwandlungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Automatische Typumwandlungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Anlässe von Typumwandlungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Arten von Typumwandlungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

23.3

Union-Typen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 335

24

Lösung ausgewählter Übungsaufgaben

329 329 330 330 332

339

Literaturverzeichnis

379

Index

381

1

Grundbegriffe der Computertechnik

1.1 Einführung In den vergangenen Jahrzehnten hat die Computertechnik eine rasante Entwicklung durchgemacht. • Die Hardware hat sich hinsichtlich Geschwindigkeit, Kapazität und Komplexität alle 1,5 Jahre nahezu verdoppelt. • Betriebssysteme unterstützen jeden PC-Besitzer mit Eigenschaften, die es früher nur bei Großrechnern oder teuren Workstations gab. • In der Anwendungssoftware entstanden Produkte, die von jedermann intuitiv zu bedienen sind und die nützliche Funktionen für jedermann bieten. • Die Verbreitung des Internet ist explosionsartig gestiegen. Computer in Firmen sind ohne Vernetzung gar nicht mehr vorstellbar. Tabelle 1.1 zeigt Meilensteine und Entwicklungen, auf die in den folgenden Kapiteln Bezug genommen wird. Auf Grund dieser Entwicklungen sind Computer auf breiter Basis in jeden Lebensbereich und in alle Winkel der Welt vorgedrungen. Der stärkste Motor dieser Entwicklung war natürlich der PC – der Computer für jeden – der die massenhafte Verbreitung getragen hat1 . Wachsende Stückzahlen ermöglichten immer komplexere Prozessoren zu immer geringeren Kosten. Damit einher ging ein starker Druck zur Standardisierung. Heute stellen nur noch wenige Firmen komplexe Prozessoren her und auch die Anzahl der Anbieter von Chipsätzen, die um die Prozessoren herum benötigt werden, ist überschaubar geworden. Der weitaus überwiegende Teil der Ingenieure, die heute mit Computern zu tun haben, entwirft daher keine neuen Computersysteme, sondern ist mit der Erstellung von Software beschäftigt. Aus diesem Grund wird in den folgenden Kapiteln der Hardware-Teil relativ kurz behandelt. 1 Deswegen und wegen des wahrscheinlich hohen Anteils der PC-Besitzer unter den Lesern sind im vorliegenden Kapitel die Entwicklungen und Meilensteine vorwiegend aus der PC-Perspektive dargestellt.

2

1 Grundbegriffe der Computertechnik Anwendungssoftware

1970 1969 Programmiersprachen bilden die Schnittstelle des Anwenders zum Computer 1980 Xerox Smalltalk 80

Betriebssysteme

Hardware

Internet

Großrechnersysteme 1969 UNIX V1

1970 niedriger Integrationsgrad 1970 VLSI Technik verfügbar 1975 Intel 8080 mit 0.5MIPS bei 2 MHz

1969 ARPA Net 1972 eMail

1980 Intel 8086 1981 IBM-PC mit 8086 und MS DOS 1985 Intel 386DX mit 11.4 MIPS bei 33 MHz

1980 TCP/ IP

1991 WWW, HTML 1992 Mosaic, Netscape, Explorer 1995 Java 1999 Ebay

1980 Xerox Mesa 1980 CP/M

1980 PC-Anwendungen - Tabellenkalk. - Textverarb. - Finanzverw. - Datenbank - Taschenrechner - Spiele etc.

1981 MS-DOS 1984 Apple Mac

1990 Integrierte Anwendungen Grafische Oberflächen Internet- Anwendungen Multimedia Componentware

1992 1. Virus auf Win3.11 1994 Linux 1.0 1994 OS/2 1994 Windows NT Verbreitung von Viren 2000 Windows 2000

1993 um 1995 1997 1999

2000 Verbreitung von Open Source SW PC als Media Center

Verbreitung von Linux Distributionen 2001 Windows XP, Mac OS X 10.0 „Cheetah“ 2006 Windows Vista Windows 7 (2009), 2011 Mac OS X 10.7 „Lion“

2003 Pentium 4 Multicore CPUs 2006 Intel Core 2 2010 Intel Core i7 ca. 3GHz

Mobile Endgeräte ECommerce Soziale Netze Web based Computing

Intel Generation Core i-3000 bis 3,9GHz

Smartphones, Tablet PCs, Intelligentes Stromnetz (smart grid)

2010 Zunehmende Verbreitung WebBrowser basierter Anwendungen, Virtualisierung

1990 Windows 3.0

Intel PentiPentium Pro Pentium II Pentium III

Tabelle 1.1: Zeittafel mit Meilensteinen und Entwicklungen

1.2 Anwendungsprogramme

3

Für den Ingenieur von besonderer Bedeutung ist die breite Durchdringung von technischen Produkten durch Computer. Unser heutiger technologischer Standard ist ohne Einsatz von Computertechnik gar nicht denkbar. Als Beispiele seien nur einige wenige Bereiche genannt: Fahrzeugtechnik: In einem Fahrzeug werden alle sicherheitsrelevanten Funktionen durch embedded controls gesteuert. Dazu gehören die Airbagsteuerung, Antiblockiersysteme oder elektronische Differentialsperren. Ferner gehören inzwischen elektronische Navigationssysteme zur Standardausstattung für Kraftfahrzeuge. Produktionstechnik „Fly-by-wire“ ist in Flugzeugen Stand der Technik. Dabei erkennen intelligente Sensoren die Aktionen des Piloten, übertragen die Informationen auf elektronischem Weg zu den gesteuerten Systemen und steuern die betroffenen Komponenten. Messdaten-Sammlung/-Auswertung: Moderne Umweltmesssysteme stützen sich auf Computertechnik zur Sammlung und Auswertung der Messdaten von Umweltsensoren. Telekommunikation: Umfangreiche Programme steuern die Mobilfunknetze und Handies und alle anderen Telekommunikationssysteme, die uns viele Annehmlichkeiten bringen. Mit dieser Entwicklung hat sich der Anteil der Software-Erstellung an der Ingenieurstätigkeit dramatisch erhöht. Heute besteht etwa die Wertschöpfung bei Telekommunikationsprodukten zum überwiegenden Teil aus der Entwicklung von Software. In der Entwicklung von technischen Anwendungen haben die Programmiersprache C und objektorientierte Sprachen wie C++ oder Java heute weite Verbreitung. Nachdem C++ und Java auf C aufbauen, ist heute die Programmierung in der Sprache Cunabdingbares Basiswissen, das sich jeder angehende Ingenieur aneignen muss. Die Programmiersprache C macht daher den größten Teil dieses Buches aus. Zuvor werden in den Kapiteln „Grundbegriffe der Computertechnik“, „Zahlendarstellung“ und „Zeichencodes“ Grundkenntnisse vermittelt, die als Hintergrundwissen für die Programmierung notwendig sind.

1.2 Anwendungsprogramme „Anwendungssoftware (application software), auch Applikationssoftware genannt, ist Software, die Aufgaben des Anwenders mit Hilfe eines Computersystems löst.“ [Balzert (1998)]. Die umwälzenden Veränderungen auf dem Gebiet der Anwendungssoftware sind vor allem auf geänderte Vorstellungen davon zurückzuführen, was ein Anwender ist.

4

1 Grundbegriffe der Computertechnik

Bis Ende der siebziger Jahre waren dies Institutionen oder Organisationen, die sich zur Wahrnehmung ihrer Aufgaben elektronischer Datenverarbeitungsanlagen bedienten. Dort wurden Spezialisten beschäftigt, die mit der DV-Anlage umgehen konnten. Die wichtigste Schnittstelle zum Computer waren damals Programmiersprachen. p Wenn damals z. B. das Problem sin(1.7) zu lösen war, schrieb der Programmierer ein Programm, übersetzte und startete es und bekam das Ergebnis ausgedruckt. Für die Bedienschritte war das Eintippen einer Reihe von kryptisch aussehenden Kommandozeilen nötig, wie man sie später auch noch während der Ära des DOS-PC kannte. Bewegung kam durch die Arbeiten am XEROX Palo Alto Research Center in die Szene. Dort wurde Ende der siebziger Jahre erforscht, wie man Computer allgemein zugänglich machen kann. Eine Fragestellung dabei war, wie man die Bedienung gestalten muss, um sie für Jedermann handhabbar zu machen. Das Ergebnis waren die Einführung von Dingen, die heute jedem selbstverständlich sind: Einsatz von Grafik- statt Text-orientierten Bildschirmen, fensterorientierte grafische Bedienoberflächen, Maus, das WYSIWYG-Prinzip („What You See Is What You Get“) usw.2 . XEROX hat aus diesen bahnbrechenden Arbeiten selbst nicht den kommerziellen Erfolg machen können, den später andere Firmen (z. B. Apple oder Microsoft) damit hatten. Ein großer Teil des Umsatzes mit Computern wird heute mit privaten Anwendern gemacht – die Utopie vom Computer für Jedermann ist damit weitgehend verwirklicht. Ein entscheidender Schritt auf diesem Weg war die Entwicklung einer bis dahin völlig neuen Art von Software, die sich um die Bedürfnisse von privaten Kunden bemüht. p Wenn heute ein PC-Besitzer wissen will, wie viel sin(1.7) ist, dann startet er einfach die Taschenrechner-Anwendung auf seinem Desktop und tippt 1.7 sin , wonach 0,9958236844203 erscheint. Die wichtigsten Kategorien von PC-Anwendungen finden sich in Tabelle 1.1. In den neunziger Jahren haben sich neue Trends abgezeichnet: Integrierte Anwendungen bedeutet, dass mehrere Anwendungen zusammenarbeiten. Man setzt heute z. B. voraus, dass Ergebnisse der Tabellenkalkulation mit der Maus in ein Textdokument gezogen werden können. Componentware bedeutet, dass nicht nur kleine Funktionen aus einer Standardbibliothek (vgl. Kapitel „Standardbibliothek“) verfügbar sind, sondern dass komplette Anwendungen in das eigene Programm eingebunden werden können. So kann man z. B. in ein kleines Programm zur Auswertung von Daten, das man selbst schreibt, eine komplette Datenbank als Bestandteil einbinden. Client/ Server-Anwendungen laufen nur zum Teil auf dem Computer des Benutzers (Client). Durch Kommunikation über das Netz tauscht Client Information mit 2 Ein gutes Bild über die damalige Aufbruchstimmung kann man sich durch die Darstellung in [Ingalls (1981)] machen.

1.3 Betriebssysteme

5

der Server-Seite aus und kann so Dienstleistungen des Servers auf dem Computer des Benutzers erbringen. Multimedia-Anwendungen beziehen Geräusche, synthetische oder echte Musik, Zeichnungen, Photos, berechnete Szenen und Videos in die Anwendung mit ein. Der Erlebnis-Charakter der PC-Welt wird damit dem Film und Fernsehen noch ähnlicher – und der erreichbare Kreis von potentiellen Benutzern noch größer. Für Benutzer von Computern bringen die obengenannten Errungenschaften eine kolossale Steigerung des Handhabungskomforts. Nicht so gut sieht die Sache für den Programmierer aus. Er benutzt zwar integrierte Entwicklungsumgebungen, wo er fast unbemerkt zwischen unzähligen Tools – vom Texteditor bis zum Debugger – grafisch fensterorientiert navigiert, aber sein wesentliches Kommunikationsmedium ist eine Programmiersprache. Insofern erinnert seine Tätigkeit an die Ära vor dem PC. Programme, die grafische Bedienoberflächen realisieren, sind wesentlich komplexer, als Programme im Textmodus. Letztere sind daher für den Einstieg in die Programmierung didaktisch zu bevorzugen. Für Einsteiger in die Programmierung kommt daher noch eine kleine Enttäuschung hinzu: die meisten Übungsprogramme arbeiten „nur“ mit TextEin/ -Ausgabe. In diesem Buch ist zwar nicht Platz für richtige Windows-Applikationen, aber wir wollen zumindest eine Brücke zur Welt der Grafik schlagen. In Kapitel „Algorithmen: Grafikausgabe“ werden wir wenigstens das Zeichnen mit Grafik-Primitiven kennen lernen und in verschiedenen Übungen und Aufgaben davon Gebrauch machen. Zu diesem Zweck gibt es als Material zum Buch ein Grafik-Paket.

1.3 Betriebssysteme Ein Benutzer greift nicht direkt auf seinen Computer zu, sondern mit Hilfe eines Anwendungsprogramms. Ähnlich greift das Anwendungsprogramm nicht direkt auf die Hardware zu, sondern mit Hilfe des Betriebssystems.

Benutzer greift zu auf Anwendungsprogramm greift zu auf Betriebssystem

Programme können die Dienste, die sie vom Betriebssystem benötigen, über sogegreift zu auf nannte „system calls“ aufrufen. Die verfügbaren system calls sind für das jeweilige Hardware Betriebssystem vorgegeben und als dessen API (Application Programming Interface) definiert. Abbildung 1.1: Schichtenmodell

6

1 Grundbegriffe der Computertechnik

Betriebssysteme leisten die folgenden Dienste • Systemhochlauf – Hardware-Test – „Bootstrap“-Laden des Betriebssystems – Registrierung der Hardware- und Ein-/Ausgabe-Geräte-Konfiguration • Kommandoausführung – Kommandozeilen-Interpretation (z. B. DOS-Box, Shell) – Ausführung der Programme, die die Kommando-Funktionen realisieren – Grafische Bedienoberflächen mit Desktop, Fenstern, Menüs, Dialogen etc. • Programmabwicklung – Start/ Beenden von Programmen – Zuteilung des Prozessors an laufende Programme • Betriebsmittelverwaltung, Zuteilung des Prozessors an laufende Programme – Arbeitsspeicher – Dateisystem – Zugang zu angeschlossenen Ein-/Ausgabe-Geräten – Zugang zum Internet/ lokalen Netz • Hardwareanpassung, Zugang zum Internet/lokalen Netz – Kommunikation mit den physikalischen Ein-/Ausgabe-Geräten – Bearbeitung von Unterbrechungsanforderungen von Geräten Es wird unterschieden, ob Betriebssysteme Umgebungen (Einstellungen, Verzeichnisse, Berechtigungen etc.) für einen oder für mehrere Benutzer verwalten können. Entsprechend wird das System als Multiuser- (z. B. LINUX) oder Singleuser- (z. B. DOS) System bezeichnet. Hinsichtlich der Programmabwicklung unterscheidet man Multitasking und SingletaskingBetriebssysteme. Die Eigenschaft gibt an, ob das Betriebssystem mehrere Programme simultan laufen lassen kann (z. B. Windows NT, LINUX) oder nur eines (z. B. DOS). Ein in Ausführung befindliches Programm wird auch als „Task“ oder „Prozess“ bezeichnet. In jedem Fall wird jedem Anwendungsprogramm eine Umgebung geboten, als ob es alleine auf dem Computer liefe3 . Bei nur einer Task entspricht die Umgebung der physikalischen Maschine. Wenn mehrere Tasks gleichzeitig aktiv sind, spricht man von einer „virtuellen Maschine“, die für jede Task bereitgestellt wird. Die Betriebsmittel der physikalischen Maschine müssen – unmerklich für die Tasks – vom Betriebssystem reihum abwechselnd allen Tasks zugeteilt werden. Mit dem Aufkommen von Mikroprozessoren (vgl. Kapitel 1.4) begann ein neuer Zweig in der Evolution der Betriebssysteme. Die neuen Computer waren anfangs nur mit sehr schwacher Hardware ausgestattet und nur für jeweils einen Benutzer gedacht. Daher entwickelte man einfache Singletasking/ Singleuser Betriebssysteme. 1980 hatte die Firma Digital Research mit CP/ M die Nase bei den MikroprozessorBetriebssystemen vorne. Trotzdem erhielt Microsoft den Zuschlag für das IBM-PC3 bis

auf eine etwaige Verlangsamung, wenn der Rechner mehrere Programme gleichzeitig abwickelt

1.4 Hardware

7

Betriebssystem und so startete die PC-Ära 1981 mit MS-DOS – einem Singletasking/ Singleuser System mit textbasierter Bedienung4 . 1981 hatte die Avantgarde bei XEROX mit ihrem Mesa-System bereits gezeigt, wie ein Betriebssystem aussehen musste, das fensterorientierte grafische Bedienung besonders unterstützte – es war natürlich mit Multitasking ausgestattet. Trotzdem dauerte es noch etwa 15 Jahre, bis sich auf dem PC-Massenmarkt mit Windows NT, OS/2 oder LINUX „echte“ Multitasking Systeme durchsetzten. Als Vorstufe schaffte 1990 Microsoft Windows 3.0 den Durchbruch. Diese Version hatte erst sehr eingeschränkte Multitasking-Fähigkeiten. Aber immerhin wurde die textbasierte Oberfläche von DOS durch fensterorientierte grafische Bedienung abgelöst. Die größte Verbreitung haben heute 3 Linien von PC-Betriebssystemen: • Windows 9x als populäre Zwischenformen auf dem Weg von Windows 3.x zu Windows NT • Windows NT/XP/Vista/7/. . . als Vertreter des PC-Zweigs der Multitasking/ Multiuser- Betriebssysteme • LINUX als später Nachfolger5 der UNIX-Großrechnersysteme – inzwischen sind dort natürlich ebenfalls fensterorientierte grafische Bedienoberflächen verfügbar.

1.4 Hardware Die bisher geschilderte Entwicklung wäre gar nicht möglich gewesen, wenn nicht die Hardware ca. alle 2 Jahre ihre Geschwindigkeit und Kapazität verdoppelt hätte. Von 0,1 MIPS (MIPS = Millionen Maschinen-Instruktionen pro Sekunde) 1980 ist man im Jahr 2000 bis zu 100 MIPS fortgeschritten. Inzwischen ist die Maßzahl MIPS nicht mehr so populär wie in früheren Jahren, weil die Prozessoren komplexer geworden sind und man die Instruktionen verschiedener Prozessor-Architekturen nicht ohne Weiteres vergleichen kann. Die Steigerung bei fallenden Preisen wurde durch den Einstieg in eine Technik ermöglicht, die um 1970 die nötige Reife erlangt hatte: VLSI („Very Large Scale Integration“), also die Technik, große Mengen von Schaltkreisen auf einen einzigen Silizium-Chip aufzubringen. Der Urvater 8080 der Intel-Familie war einer der frühen Prozessoren auf VLSI-Basis, die in großen Stückzahlen hergestellt wurden. Die Zeittafel in Tabelle 1.1 zeigt die Intel-Linie. Von Generation zu Generation „wanderten“ immer mehr Bausteine aus den Chipsätzen, die auf der Hauptplatine den Prozessor umgeben, auf das Prozessor-Chip selbst. 4 Es geht die Legende, dass ein Digital Research Repräsentant namens Gary Kildall wegen seinem Fliegerei-Hobby Gespräche mit IBM-Vertretern verpasste und so Bill Gates zu dessen Chance verhalf. Wie wir wissen, hat dieser sie sehr gut genutzt . . . 5 Neuimplementierung durch Linus Torvalds

8

1 Grundbegriffe der Computertechnik

1.4.1 Betrachtungsebenen Bei Milliarden von Transistoren für Prozessor, Chipsatz und Speicher ist es unmöglich, einen Computer komplett auf einem Schaltplan darzustellen. Um Computer zu betrachten, muss man sich daher auf verschiedenen Abstraktionsniveaus bewegen. Tabelle 1.2 zeigt fünf gebräuchliche Betrachtungsebenen. In der Tabelle werden folgende Begriffe verwendet: Gatter sind Schaltungen zum Verknüpfen von Bits. Flipflops sind eine Schaltungen zum Speichern jeweils eines Bits. Register der Breite n Bit bestehen aus n Flipflops, die jeweils ein Bit speichern können. Die Flipflops sind so mit ihrer Umgebung verschaltet, dass vorgesehene Transfers oder Verknüpfungen möglich sind. Auf jeder Ebene werden bestimmte Elemente dargestellt, und wie diese miteinander in Verbindung stehen. Dabei wird von den Details abstrahiert, die auf niedrigeren Ebenen gezeigt werden. Anders ausgedrückt: jede Ebene stellt dar, wie die Elemente der darüber liegenden „von innen“ aussehen bzw. funktionieren.

1.5 Prozessoren, Busse und Speicher In diesem Abschnitt werfen wir einen kurzen Blick auf die einzelnen Komponenten eines Computers. Wir beschränken uns dabei vornehmlich auf die Darstellung der weit verbreiteten Personal Computer. Prozessoren, Busse, Speicher

Befehle Register-Transfer

Logik

Hardware-Realisierung

Die Ebene stellt dar, welche Hauptelemente das System besitzt, und wie diese miteinander verbunden sind Die Ebene stellt den Satz von Befehlen (Maschineninstruktionen) dar, den der Prozessor beherrscht Die Ebene stellt die Register dar, wie diese mit ihrer Umgebung verbunden sind und welche Verknüpfungen vorgesehen sind, um den Befehlssatz zu realisieren. Die Ebene stellt Gatter und Flipflops dar und wie diese verbunden sind, um Register, Verknüpfungen und Transfers zu realisieren. Die Ebene stellt Bauteile (z. B. Transistoren, Kondensatoren etc.) dar und wie diese verbunden sind, um Gatter oder Flipflops zu realisieren

Tabelle 1.2: Betrachtungsebenen für Computer

1.5 Prozessoren, Busse und Speicher

9

1.5.1 Das Bussystem Zentraler Bestandteil eines Computers ist das Bussystem. Es verbindet alle Komponenten des Computers untereinander und ermöglicht den Transport von Informationen zwischen ihnen. Ferner können über das Bussystem Verbindungen zur Außenwelt hergestellt werden. Ein Bus besteht primär aus Leitungen zur Übertragung elektrischer Signale zwischen den angeschlossenen Komponenten. Die elektrischen Signale repräsentieren die zu übertragende Information. Busleitungen, die zum Datenaustausch dienen, heißen Datenleitungen. Sind mehr als zwei Komponenten angeschlossen – was die Regel ist – so verfügt ein Bus auch über einen Mechanismus, der es der sendenden Komponente erlaubt, eine andere als Empfänger anzugeben. Jede angeschlossene Komponente erhält dazu eine Adresse. Die Adresse der jeweils angesprochenen Komponente wird über die sog. Adressleitungen des Busses übertragen. Daneben verfügt jeder Bus auch über Steuerleitungen, über die Kontrollinformation ausgetauscht wird. Diese kann etwa dem Empfänger der Information mitteilen, was er damit zu tun hat. Typische Geräte, die an Busse angeschlossen werden, sind Steuergeräte (Controller) für Tastatur und Maus, Festplatten oder Grafik- und Netzwerkkarten. Heute übliche Bussysteme sind international standardisiert. Damit haben viele verschiedene Hersteller die Möglichkeit, Komponenten zu liefern, die am jeweiligen Bussystem betrieben werden können. Komponenten gleicher Funktionalität können beliebig gegeneinander ausgetauscht werden. Für jede Anwendung existiert eine breite Palette von Angeboten, die auf unterschiedliche Bedürfnisse und Erfordernisse zugeschnitten sind. So gibt es kostengünstige Angebote, die speziell den Massenmarkt der Heimanwender bedienen bis hin zu solchen Angeboten, die auf spezielle professionelle Anwendungen zugeschnitten sind und sich preislich in der Größenordnung eines Kleinwagens bewegen. Abbildung 1.2 S. 10 zeigt ein vereinfachtes Blockdiagramm eines Mainboards[Gigabyte (2010)] mit Intel-Chipsatz[Intel (2010)] auf Prozessoren-/Busse-/Speicher-Ebene. Bustypen, aus Abb. 1.2 sind: PCI („Peripheral Component Interconnect“) Der PCI-Bus dient der Verbindung von Peripheriegeräten mit dem Chipsatz. Das ist insbesondere der Bus, in den die Erweiterungskarten im PC-Gehäuse gesteckt werden. USB („Universal Serial Bus“) Der USB verbindet externe Geräte mit dem PC. Er ersetzt ältere Schnittstellen, insbesondere die „Serielle Schnittstelle“ und die „Parallelschnittstelle“. SATA („Serial Advanced Technology Attachment“) Über die SATA-Schnittstelle werden Festplatten oder auch DVD-Laufwerke angeschlossen. SPI („Serial Peripheral Interface“) Das SPI ist die Verbindung zum Festspeicher für das Urladen (ROM-BIOS zum Booten).

10

1 Grundbegriffe der Computertechnik

Speicher

USB LAN

Prozessor

PCIe

Chipsatz

PCIe SATA

SPI Abbildung 1.2: Vereinfachtes Blockschaltbild einer PC-Hauptplatine

LAN („Local Area Network“) Das LAN ist heute meist nach dem Ethernet-Standard IEEE 802.3 ausgeführt. Es dient der Vernetzung des Computers lokal und mit dem Internet über weitere Geräte.

1.5.2 Der Prozessor Kernstück jedes Rechners ist der Prozessor6 („Central Processing Unit“, kurz CPU). Er ist das zentrale Organ zur Manipulation von Daten. Aktuelle Prozessoren haben direkte Verbindungen zu den Einheiten, mit denen der Datentransfer besonders schnell gehen muss. Dazu zählen insbesondere der Arbeitsspeicher, eventuell auch die Grafikeinheit. Alle weiteren Verbindungen zwischen Prozessor und Peripherie verlaufen über den Chipsatz (vgl. Kap. 1.5) Die Anzahl der Adress- und Datenleitungen zum Speicher betragen stets ein Vielfaches von acht. Üblich sind heute 32 bis 64 Leitungen. Aus dem Arbeitsspeicher liest der Prozessor Instruktionen ein. Er interpretiert diese Instruktionen und führt sie aus. Die Ausführung von Instruktionen bedeutet meist, Daten zu manipulieren. Dazu sind die entsprechenden Daten einzulesen, zu verändern und zurück zu schreiben. Der Prozessor kann Daten sowohl aus dem Hauptspeicher holen als auch von allen am Chipsatz angeschlossenen Komponenten. Ein Prozessor verfügt ferner über spezielle Steuerleitungen, über die er zur Unterbrechung seiner Arbeit aufgefordert werden kann. Diese Unterbrechungen heißen Interrupts. Als Folge eines Interrupts unterbricht der Prozessor seine momentane Befehls6 In einem Prozessor-Chip können auch mehrere Prozessoren angelegt sein („Multi Cores“). Wir beschränken unsere Darstellung auf Einprozessorsysteme.

1.5 Prozessoren, Busse und Speicher

11

ausführung, um etwa eine wichtigere Aufgabe zu übernehmen und nach deren Abschluss zur ursprünglichen Aufgabe zurückzukehren.

1.5.3 Der Speicher Unter Speicher verstehen wir hier den mit Halbleitern realisierten Hauptspeicher eines Computers, nicht USB-Sticks oder Festplattenlaufwerke. Letztere zählen in unserer kurzen Übersicht zu den Peripheriegeräten (vgl. Abbildung 1.2). In Abbildung 1.3 ist das Prinzip des Zugriffs auf den Speicher dargestellt. Die Adresse des Speicherwortes, auf das zugegriffen werden soll, wird über den Adressbus in das Adressregister geschrieben (A). Soll ein Datum unter dieser Adresse gespeichert werden, so wird es über den Datenbus in das Datenregister geschrieben (D). Über Steuerleitungen wird dem Speicher mitgeteilt, ob Lese- oder Schreibzugriff gewünscht wird (R/W). Entsprechend wird entweder das Datum aus dem Datenregister an die durch das Adressregister bezeichnete Speicherzelle geschrieben, oder der Inhalt der Speicherzelle ausgelesen und im Datenregister zur Abholung bereit gestellt. Es werden zwei Klassen von Speichern unterschieden. • Festspeicher auch Nur-Lese-Speicher oder englisch „Read-Only Memory“, abgekürzt mit ROM. • Schreib-Lese-Speicher oder englisch „Random Access Memory“, abgekürzt RAM.

Adressbus Steuerbus Datenbus

Prozessor

A

W R

D

Adressregister

ArbeitsSpeicher

Datenregister

Abbildung 1.3: Prinzip des Speicherzugriffs

12

1 Grundbegriffe der Computertechnik

„Random Access“ bedeutet übersetzt „wahlfreier Zugriff“, was hier soviel bedeutet, als dass auf den Speicher jederzeit beliebig lesend oder schreibend zugegriffen werden kann. Bei den ROM-Speichern gibt es verschiedene Arten, darunter auch solche, die sich löschen und wieder beschreiben lassen. Bei diesen Speichern ist das (Wieder-) Beschreiben ein aufwändiger Vorgang und es kann nicht zu jeder Zeit beliebig gelesen oder geschrieben werden. Tabelle 1.3 S. 12 gibt einen Überblick über die verschiedenen Arten von Speichern.

Speicherklasse

Schreib/Lesespeicher

Speichertyp Name und Beschreibung SRAM Statisches RAM. Diese Speicher sind sehr schnell, erfordern aber den Einsatz von sechs Transistoren je gespeichertem Bit. DRAM

MROM

NurLesespeicher (Festspeicher)

PROM

EPROM

Dynamisches RAM. Dynamische RAMs lassen sich mit weniger Aufwand realisieren, als SRAMs (ein Transistor pro Bit), sind aber langsamer. Der Speicherinhalt wird in einem Kondensator gespeichert, der seine Ladung mit der Zeit verliert und daher ständig wieder aufgeladen werden muss (Refresh). Maskenprogrammierbares ROM. Es wird mit dem Herstellungsprozess programmiert. Der Speicherinhalt lässt sich nicht mehr ändern. Programmierbares ROM. Diese Speicher sind vom Anwender einmalig mit einem speziellen Programmiergerät programmierbar. Der Speicherinhalt lässt sich danach nicht mehr ändern. Eraseable PROM. Diese Speicher sind vom Anwender mit einem speziellen Programmiergerät programmierbar. Der Speicherinhalt lässt sich durch Bestrahlung mit einer UV-Lampe wieder löschen und neu beschreiben. Bausteine, deren Inhalt sich durch Anlegen von elektrischen Signalen löschen lässt, heißen Flash- EPROM. Wenn sich nicht nur der komplette Inhalt, sondern selektiv einzelne Adressen löschen lassen, spricht man von EEPROM (Electrically Eraseable ).

Tabelle 1.3: Verschiedene Arten von Speichern

1.6 Die Befehlsebene

13

1.5.4 Peripheriegeräte Um einen Computer zu benutzen, benötigt man verschiedene Peripheriegeräte. Wir haben in Abbildung 1.2 bereits verschiedene Schnittstellen kennen gelernt, über die sich Peripherie anschließen lässt. Man kann Peripheriegeräte grob in die folgenden Kategorien einteilen. • Peripherie zur Kommunikation mit dem Benutzer – Grafikkarte (falls Grafik nicht schon in Prozessor/Chipsatz integriert) – Maus – Tastatur – Writing Tablet – Soundkarte (falls Sound nicht schon in Chipsatz integriert) • Peripherie zur Daten- Ein/ Ausgabe – Drucker – Scanner – Digitalkamera – Tv-Karte • Speicherperipherie – Festplatten – CD-, DVD- Antriebe und -Brenner – externe Festplatten – USB-Speicher-Sticks • Peripherie zur Kommunikation mit anderen Computern oder Informationstechnologie- Geräten – Netzwerkkarte (falls Netzwerk nicht schon in Chipsatz integriert) – DSL-Modem

1.6 Die Befehlsebene Wir werden nun die Befehlsebene näher betrachten. Wir werden uns also damit beschäftigen, wie ein Prozessor Befehle abarbeitet. Die Befehle, die ein Prozessor abarbeitet sind immer Ergebnis eines Programmiervorgangs. Wir werden in diesem Buch nicht behandeln, wie man einen Computer direkt auf seiner Befehlsebene programmiert (Assemblersprache, Maschinensprache ). Vielmehr werden wir uns eines Anwendungsprogramms bedienen (der sog. Compiler), um die in späteren Kapiteln des Buches entwickelten CProgramme in Prozessorbefehle umzusetzen und ablaufen zu lassen. Aber auch wenn wir uns mit einer höheren Programmiersprache beschäftigen, ist ein gewisses Verständnis für die Abläufe auf Ebene der Prozessorbefehle erforderlich. Für tiefere Einblicke sei auf die entsprechende Fachliteratur verwiesen, etwa [Hennessy u. Patterson (1990)]. Die Aufgabe des Prozessors besteht darin, ständig Befehle aus dem Speicher zu holen und diese auszuführen. Dieser Vorgang wiederholt sich ohne Unterbrechung bis zum Abschalten des Computers. Was dabei im Einzelnen abläuft werden wir nun näher betrachten.

14

1 Grundbegriffe der Computertechnik Arbeitsspeicher

Datenbereich

Prozessor (CPU)

Stack

Stackpointer (SP) frei verwendbare Register

Operandenregister 1

Operandenregister 2

ALU

Programcounter (PC)

Codebereich

Move A1,A2 Add A1, 5

Steuerwerk (Leitwerk)

Abbildung 1.4: Modell eines einfachen Prozessors

Ein elementarer Befehl kann etwa darin bestehen, zwei Zahlen aus dem Speicher zu lesen, zu addieren und das Ergebnis in den Speicher zu schreiben. Ein wesentliches Merkmal eines Computers ist, dass die zu bearbeitenden Daten im selben Speicher stehen können, wie die auszuführenden Befehle. Es gibt auch kein äußerliches Unterscheidungsmerkmal für Befehle und Daten. Allein durch den Programmablauf ist festgelegt, welche Speicherinhalte in den Prozessor geholt und als Befehle interpretiert werden. In Abbildung 1.4 ist das Modell eines einfachen Prozessors und seines Arbeitsspeichers dargestellt. An diesem Modell wollen wir die einzelnen Komponenten eines Prozessors und seine Arbeitsweise verdeutlichen. Jeder Prozessor verfügt über mehrere Register, in denen er Operanden oder Ergebnisse zwischenspeichern, oder auch oft benutzte Daten oder Speicheradressen direkt vor Ort halten kann. Register bieten Speicherplatz für n Bits, die zur Weiterverarbeitung zur Verfügung stehen, oder zu anderen Teilen der CPU bzw. über den Bus nach außen transportiert werden können. Der Zugriff auf Register ist deutlich schneller, als der Zugriff auf Informationen im Hauptspeicher, da kein Transfer über den Prozessorbus erforderlich ist. Ein wesentliches Register ist der Befehlszähler (englisch: program counter, abgekürzt PC). Der Befehlszähler enthält immer die Adresse des nächsten zu bearbeitenden Befehls. Er ist in Abbildung 1.4 S. 14 mit einem Zeiger dargestellt, der in den Bereich des Speichers zeigt, in dem die Befehle stehen, der sog. Code-Bereich. Nach Abarbeitung eines Befehls wird der Befehlszähler auf den nächsten Befehl im Speicher positioniert. Ein weiterer Bereich des Speichers wird als sog. Kellerspeicher (auch Stapelspeicher, englisch: Stack) genutzt. Der Stack dient hauptsächlich dazu, bei Unterprogrammaufrufen

1.6 Die Befehlsebene

15

die Adresse für den Rücksprung aus dem Unterprogramm zu sichern und Parameter an das Unterprogramm zu übergeben. Wir werden darauf im Kapitel über Unterprogramme detaillierter eingehen. Auf den Stapelspeicher wird stets nur nach ganz bestimmten Vorschriften zugegriffen. Die auf dem Stapelspeicher gespeicherten Daten müssen in umgekehrter Reihenfolge wieder entnommen werden, in der sie dort abgelegt wurden. Das zuletzt abgelegte Datum muss also zuerst wieder entnommen werden. Ein spezielles Prozessorregister – der Stack-Pointer – zeigt auf die Stelle im Stapelspeicher, an die aktuell geschrieben werden darf. Ein großer Teil des Prozessors dient der Verarbeitung von Daten. Dieser „Datenpfad“ genannte Teil ist in der Abbildung auf eine Einheit zur Durchführung arithmetischer und logischer Operationen beschränkt: Diese Komponente heißt ArithmetischLogische Einheit (englisch: Arithmetic and Logical Unit, abgekürzt ALU). Sie ist hier so dargestellt, dass sie bis zu zwei Operanden verknüpfen und ein Ergebnis liefern kann. Sie kann die üblichen arithmetischen Operationen durchführen, aber auch Werte vergleichen.

ALU

Der ALU vorgelagert sind zwei Register, in denen die Operanden zwischengespeichert werden können, bis die ALU ihre Operation ausführt. Meist stehen noch weitere Register zur Verfügung, die zur Programmierung mehr oder weniger frei benutzt werden können. Wir wollen uns nun der Befehlsausführung zuwenden. Typische Befehlsarten sind in Tabelle 1.4 angegeben. Arithmetische Befehle enthalten über den Befehlscode hinaus Angaben darüber, wo die zu verknüpfenden Daten stehen bzw. wo das Ergebnis abzulegen ist.In Frage kommen für diese Angaben Register oder Speicherstellen. Oft wird auch indirekt adressiert, das bedeutet, dass die Angaben bei den Befehlen auf Register verweisen, welche dann ihrerseits die Speicheradressen der eigentlichen Operanden enthalten. Ein Beispiel über das Zusammenwirken von Befehlen in einem Programm werden wir im Abschnitt über Operatoren und Ausdrücke sehen. Wenn etwa ein Additionsbefehl ausgeführt wird, so führt der Prozessor mehrere elementare Operationen aus: zuerst müssen die Operanden herangeschafft werden. Kommen beide Operanden aus dem Speicher, so müssen sie nacheinander geholt werden, da über den Prozessorbus zu einem Zeitpunkt lediglich ein Datum transportiert werden kann. Nun stehen die Operanden in den Operandenregistern bereit. Danach wird die eigentliche Addition ausgeführt. Das Ergebnis steht dann in einem speziellen Register7 und muss von diesem ggf. noch an das eigentliche Ziel transportiert werden. 7 Dieses Register hieß früher, als Prozessoren noch über ganz wenige Register verfügten, Akkumulator-Register, kurz Akku.

16

1 Grundbegriffe der Computertechnik Befehlsart Befehle zum Transfer von Daten Arithmetische Befehle Vergleichsbefehle

Befehle zur Beeinflussung des Programmablaufs (Sprungbefehle)

Spezialbefehle

Beschreibung Zum übertragen eines Datums zwischen zwei Stellen im Speicher (ohne es zu verändern), oder zwischen Speicher und Register bzw. zwischen zwei Registern. Diese Befehle führen arithmetische Operationen, wie Addition, Subtraktion oder Multiplikation aus. Vergleichen zwei Werte miteinander. Das Ergebnis wird in einem speziellen, sog. Flag-Register gespeichert, so dass es danach weiter verwendet werden kann, etwa für bedingte Sprungbefehle. Bewirken Fortsetzung des Programms nicht mit dem unmittelbar nächsten Befehl im Speicher, sondern an einer im Befehl angegebenen Speicheradresse. Diese Befehle bewirken damit ein Überschreiben des Programmzählers mit einem neuen Wert. Der Sprung kann dabei unbedingt erfolgen, oder von einer Bedingung abhängen (etwa Ergebnis der vorangegangenen Operation, z. B. einer logischen Vergleichsoperation). Hierzu zählen Stackoperationen, Ein-/Ausgabebefehle oder Befehle zum Auslösen von Programmunterbrechungen bzw. zum Anhalten des Prozessors.

Tabelle 1.4: Befehlsarten

1.7 Register-Transfer-Ebene Zur Ausführung eines Befehls sind also innerhalb des Prozessors viele elementare Operationen zu koordinieren. Dafür sorgt ein Teil des Prozessors, der Steuerwerk heißt. Das Steuerwerk ist über spezielle Leitungen, die Steuerleitungen, mit allen anderen Komponenten der CPU verbunden und steuert diese entsprechend an, wie in Abbildung 1.5 angedeutet.

Steuerwerk

Ausführungseinheit

Befehlsregister

Teileinheiten der Ausführungseinheit, z.B.:

+ -* / ALU



Befehlsdecoder

Program Counter Abbildung 1.5: Das Steuerwerk beeinflusst alle Komponenten über Steuerleitungen

1.7 Register-Transfer-Ebene

17

Das Steuerwerk ist verantwortlich für die Durchführung des Befehlszyklus, jene Schleife in der permanent Befehle aus dem Speicher geholt und ausgeführt werden. Der Befehlszyklus umfasst folgende Phasen: 1. Holphase In dieser Phase wird der nächste auszuführende Befehl aus dem Speicher in das Befehlsregister des Steuerwerks gebracht. 2. Dekodierphase Diese Phase dient dem Entschlüsseln und interpretieren des Befehls. Der Teil des Befehls, der über die Art der Operation Auskunft gibt, wird von dem Teil getrennt, der die Quelle der Operanden beschreibt. 3. Ausführungsphase Mit Hilfe logischer Schaltungen werden Operanden geholt, Verknüpfungen durchgeführt und die Ergebnisse gespeichert. Auch wird, falls erforderlich, der Befehlszähler beeinflusst. Er wird im Normalfall erhöht, so dass er auf den nächsten Befehl zeigt. Bei einem Sprungbefehl wird er ggf. mit der im Sprungbefehl angegebenen Zieladresse überschrieben. Diese Sicht auf die Befehlsebene eines Prozessors aus der Perspektive der einzelnen Prozessorregister heißt auch Register-Transfer-Ebene. In Abbildung 1.6 ist ein 8086Prozessor auf Register-Transfer-Ebene gezeigt. Der 8086 ist zwar ein recht alter Prozessor, aber auch in modernen Pentium-Prozessoren sind die gezeigten Elemente immer noch vorhanden. Sie wurden mittlerweile durch ausgeklügelte Komponenten ergänzt, die den Prozessor schneller und effizienter machen. Eine Diskussion aller Einheiten eines Pentium-Prozessors würde den Rahmen dieses einleitenden Kapitels sprengen. Dieses Bild ist ein original-Blockdiagramm zum 8086-Prozessor, es enthält keinerlei didaktische Vereinfachungen. Es sind auch innerhalb des Prozessors mehrere Busstrukturen zu sehen, die teilweise als Prozessorbus herausgeführt sind. Im unteren Teil des Bildes sind die ALU und die allgemein verwendbaren Register zu sehen, die in besprochen wurden. Der obere Teil des Bildes zeigt den Teil des Prozessors, der Instruktionen und Adressen behandelt. Hier ist eine weitere ALU zu sehen, die ausschließlich zur Berechnung von Adressen dient. Auch sind dort Register angebracht, die exklusiv zur Speicherung von Adressen verwendet werden, z. B. der Befehlszähler. Die Ausführung eines Befehls läuft etwa folgendermaßen ab: Befehle werden aus dem Speicher über den mit C-Bus bezeichneten internen Bus in einen speziellen Befehlsspeicher geladen. Es handelt sich dabei um eine Warteschlange, durch welche die Befehle durch geschoben werden (nach dem FIFO-Prinzip: First-In First-Out) um den Befehlszyklus zu beschleunigen. Es können bereits neue Befehle geholt werden, während vorangegangene Befehle dekodiert oder ausgeführt werden. Das Kontrollsystem enthält den Befehlsdecoder. Über den A-Bus werden die Operanden aus dem Speicher geholt und in die Register der Ausführungseinheit (Execution Unit, abgekürzt EU) geschrieben.

18

1 Grundbegriffe der Computertechnik

Speicher C- Bus

Warteschlange 6 5 4 3 2 1

BIU B- Bus

ES CS SS DS IP

Befehle (FIFO)

Kontroll System A- Bus

AH AL BH BL CH CL DH DL SP BP SI DI

ALU EU Operanden Flags

Abbildung 1.6: Struktur des Intel 8086-Prozessors (Register-Transfer-Ebene)

1.8 Die Logikebene Wir wollen uns nun noch den Bausteinen zuwenden, aus denen die im vorigen Abschnitt besprochenen Komponenten wie ALU, Register oder Befehlsdecoder aufgebaut sind. Anwender von Computern und auch Programmierer finden kaum je Berührung mit dieser Ebene ihres Computers. Beschäftigung mit dieser Ebene fällt in das Fachgebiet der Digitaltechnik. Die praktische Anwendung ist fast ausschließlich in der Hand von Entwicklern von Prozessoren und anderen integrierten Schaltungen. Die Bausteine dieser Ebene sind Logikschaltungen, die zwei unterscheidbare Zustände erkennen und manipulieren können (binäre Darstellung). Diese beiden Zustände werden üblicherweise durch die logischen Werte falsch/wahr oder Null/Eins beschrieben bzw. in elektrischen

1.8 Die Logikebene

19

Schaltungen auch durch zwei unterschiedliche Spannungswerte z. B. 0Volt/5Volt repräsentiert. Ein Grundelement ist das Flipflop, das einen solchen logischen Wert speichern kann. Ein logischer Wert kann eingeschrieben und später bei Bedarf wieder ausgelesen werden. Ein solcher Wert ist die kleinste speicherbare Informationseinheit. Diese wird Bit genannt (Abkürzung für Binary Digit). Schaltungen, die logische Werte verknüpfen und manipulieren können, heißen Gatter. Das einfachste Gatter ist der Inverter. Er wandelt eine Null in Eins und eine Eins in Null um. Zwei kompliziertere Beispiele solcher Gatter, die zwei logische Werte miteinander verknüpfen, sind das UND- bzw. das ODER-Gatter. Vereinfachend kann man sich vorstellen, dass die zu manipulierenden Werte A und B je einen von zwei verbundenen Schaltern steuern. Das Verhalten des Gatters ist dann beschrieben durch die elektrische Leitfähigkeit der entstehenden Verbindung. Eine elektrische leitende Verbindung bedeutet, dass das Ergebnis der Verknüpfung „wahr“ (Eins) ist: UND-Verknüpfung

ODER-Verknüpfung

Beim UND-Gatter besteht eine elektrisch leitende Verbindung nur dann, wenn beide Schalter geschlossen sind. Beim ODER-Gatter besteht bereits dann eine elektrisch leitende Verbindung, wenn einer der beiden Schalter geschlossen ist. All diese Logikschaltungen sind aus einzelnen Transistoren aufgebaut. Der Transistor ist ein elektrisches Bauteil mit drei Anschlüssen Basis, Kollektor und Emitter. Durch die an der Basis anliegende elektrische Spannung wird sein elektrischer Widerstand zwischen Kollektor und Emitter beeinflusst. Das Material, aus dem ein Transistor aufgebaut ist (üblich sind Silizium und Gallium-Arsenid), lässt sich in seiner elektrischen Leitfähigkeit weitgehend verändern. Der Bereich geht von guter Leitfähigkeit bis zur Nicht-Leitfähigkeit (Isolation). Deshalb spricht man von Halbleitermaterial. Um dies zu erreichen, werden in den Halbleiterkristall durch physikalische und chemische Prozesse Fremdatome eingepflanzt. Je nach verwendetem Material (beispielsweise Bor oder Arsen), entstehen Zonen mit Elektronenüberschuss oder Elektronendefizit. Dieser Prozess heißt Dotierung und die entstehenden Zonen heißen n-dotiert oder pdotiert. Der Transistoreffekt entsteht an den Grenzflächen zwischen zwei unterschiedlich dotierten Zonen, vgl. Abbildung 1.7. Die Transistoren kann man sich als Schalter vorstellen. Die Schalterstellungen repräsentieren wiederum einen binären Wert wie in Abbildung 1.8.

20

1 Grundbegriffe der Computertechnik Physikalische Realisierung

Symbol im Schaltplan

Kollektor

Kollektor

n

Basis

Basis

p

Grenzfläche ("Raumladungszone")

n

Emitter Emitter

Abbildung 1.7: Transistor

Hoher Widerstand zwischen Kollektor und Emitter

Niedriger Widerstand zwischen Kollektor und Emitter

Geöffneter Schalter

Geschlossener Schalter

bzw.

Abbildung 1.8: Transistor als Schalter

Die Transistoren heutiger höchstintegrierter Schaltungen weisen eine Größe von 0,35 Mikrometer (10-6m) auf (ein menschliches Haar hat einen Durchmesser von 10 Mikrometer). Dadurch ist die Integration extrem vieler Transistoren auf kleinstem Raum möglich (der Fachbegriff dafür lautet VLSI als Abkürzung für „Very Large Scale Integration“). Die Realisierung solcher Strukturen erfordert spezielle Technologien und Spezialisten, die damit umgehen können. Das eigenständige Fachgebiet der Schaltungsintegration beschäftigt sich mit den erforderlichen Prozessen. Wir sind nun bei der kleinsten Einheit angelangt, aus der alle Prozessoren und andere Bausteine eines Computers aufgebaut sind. Von hier aus können wir den Blick zurück werfen, auf die oberste Ebene und ansehen, wie viele Transistoren nötig sind, um einen Prozessor zu bauen siehe Tabelle 1.5. Flipflop Gatter Addierer Datenpfad Pentium II Pentium 4 Intel Core I7

Speichert ein Bit Verknüpft zwei binäre Werte Addiert 32-Bit-breite Worte Komplettes Rechenwerk mit Puffern Ganzer Prozessor Ganzer Prozessor Ganzer Prozessor

6 Transistoren 4 Transistoren Über 200 Transistoren Über 100.000 Transistoren 4,5 Millionen Transistoren 42 Millionen Transistoren nahe 109 Transistoren

Tabelle 1.5: Anzahl von Transistoren

Es wird hier nochmals deutlich, dass es nicht möglich ist, alle Transistoren eines Prozessors komplett auf einem Schaltplan darzustellen.

2

Zahlendarstellung

In diesem Kapitel behandeln wir die Darstellung von Zahlen im Computer. Zuerst beschäftigen wir uns mit verschiedenen Zahlensystemen und danach werden wir das erarbeitete Wissen anwenden auf die geeignete Kodierung von Zahlen für die computerinterne Darstellung.

2.1 Zahlensysteme für ganze Zahlen Wir sind es gewohnt, Zahlen als eine Folge von Ziffern zu schreiben. Dabei verwenden wir stillschweigend immer die Darstellung zur Basis zehn, d. h. wir schreiben an den einzelnen Positionen die Einer, Zehner, Hunderter usw. Beispiel für die Zahl 275: 275 = 5 · 1 + 7 · 10 + 2 · 100. Das Dezimalsystem ist aber nur ein Beispiel eines Zahlensystems. Wir können zu jeder Zahl B ≥ 2 – die Basis genannt wird – ein Stellen- oder Positionssystem (auch polyadisches Zahlensystem) angeben. Die Ziffernwerte in einem solchen System sind 0, . . . , B − 1. Ein Zahlwort schreiben wir als Folge von Ziffern zn zn−1 . . . z1 z0 . Die Wertigkeit der Ziffer zi ist B i , also ist der Wert einer Zahl gegeben durch n

zn B + zn−1 B

n−1

0

+ . . . + z0 B =

n X

zi B i .

(2.1)

i=0

An jeder Position wird das Produkt aus dem Ziffernwert und dem Stellenwert gebildet. Diese Produkte werden aufsummiert. Zur Darstellung von Zahlen zu einer Basis größer als zehn, reicht unser gewöhnlicher Ziffernvorrat 0,...,9 nicht mehr aus und wir benutzen zusätzlich die Buchstaben: ’A’ für die Wertigkeit 10, ’B’ für 11, ’C’ für 12 usw. Da Informationen in Computern binär dargestellt werden, ist das Dualsystem, also das zur Basis B = 2 mit den beiden einzigen Ziffern 0 und 1, von besonderer Bedeutung. Häufig gebraucht werden darüber hinaus oft das Oktalsystem (B = 8, Ziffern 0,...,7) und das Hexadezimalsystem (B = 16, Ziffern 0,...,9,A,...,F). Jede Zahl lässt sich in jedem Zahlensystem darstellen. Wenn nicht eindeutig erkennbar ist, welches Zahlensystem gerade gemeint ist, so wird die Basis (in dezimaler Darstellung) als Index an das Zahlwort angefügt: • • • •

4210 ist eine Dezimalzahl 2A16 ist die gleiche Zahl, aber hexadezimal dargestellt 528 ist wieder die gleiche Zahl, nur in oktaler Darstellung 1010102 ist nochmal die gleiche Zahl, diesmal dual geschrieben

22

2 Zahlendarstellung

Bevor wir uns ansehen, wie Zahlen zwischen den Zahlensystemen umgewandelt werden können, befassen wir uns als Grundlage zuerst mit dem Rechnen mit Potenzen.

2.2 Zahlensysteme und B-Potenzen Mit einer festen Stellenzahl von n Stellen lassen sich im Zahlensystem zur Basis B insgesamt B n verschiedene Zahlen darstellen. Der Zahlenbereich erstreckt sich von 0 bis B n − 1. Es gilt: Regel 1:

Bn Bn − 1

= 10...0B } eine Eins mit n Nullen (2.2) = zz...zB } n Mal die größte Ziffer z = B − 1 zur Basis B.

Im Speicher eines Computers werden alle Informationen binär dargestellt. Eine binäre Stelle heißt Bit (vom englischen Binary digit). Werden 23 = 8 binäre Stellen zu einer Einheit zusammen gefasst so entsteht ein Byte. Die Speicherung von Information im Speicher eines Computers wird heute stets in Worten organisiert, die Vielfache von Bytes sind. Jedes der gespeicherten Worte erhält eine eigene Adresse. Adressen werden wiederum binär kodiert. Somit lassen sich mit n Bit breiten Adressen insgesamt 2n Speicherworte adressieren. Für n = 16 ergibt sich folgender Adressraum:

0000 0000 0000 00002 = 0000 0000 0000 00012 = ... 1111 1111 1111 11102 = 1111 1111 1111 11112 =

010 110

      

216 = 65536  Adressen  6553410     6553510

(2.3)

Regel 2:

x a x b = x a+b

(2.4)

Aus dieser Regel können wir den wichtigen Sachverhalt ableiten, dass die Multiplikation einer Zahl mit einer Potenz B n der Basis einem Verschieben der Ziffern um n Stellen nach links und Anfügen von n Nullen entspricht: ! a a a X X X n i xB = xi B B n = xi B i B n = xi B i+n i=0

i=0

i=0

2.3 Umwandlung zwischen Zahlensystemen

23

Beispiel: 4

101111001 | {z } Zif.folge

4 2∗2

=

z}| { |101111001 {z } 0000 2 Zif.folge

Folgende Einheiten für die Angabe der Größe von Speichern werden verwendet: Kilobyte (Einheit KB): 210 Byte = 1.024 Byte Megabyte (Einheit MB): 220 Byte = 1.048.576 Byte Gigabyte (Einheit GB): 230 Byte = 1.073.741.824 Byte Terabyte (Einheit TB): 240 Byte = 1.099.511.627.776 Byte Diese Einheiten entsprechen nicht den aus der Physik bekannten Einheiten, die auf dem Dezimalsystem basieren. Dort entspricht die Einheit „kilo“ 103 und die Einheit wird als kleines „k“ geschrieben. Es gilt lediglich 103 ≈ 210 . Die Regel 2 können wir nun auch anwenden, um auszurechnen, wie viele Speicherworte in den genannten Einheiten sich mit einer bestimmten Adressbreite adressieren lassen: 16 Bit Adressraum: 216 = 210 · 26 = 210 · 64 = 64 Kilobyte 32 Bit Adressraum: 232 = 220 · 210 · 22 = 230 · 4 = 4 Gigabyte Ferner gilt noch: Regel 3: b

(x a ) = x ab

(2.5)

Die obige Berechnung des Adressraumes könnten wir auch so zerlegen: 32 Bit Adressraum: 232 = 210·3+2 = (210 )3 · 22 = 4 Gigabyte.

2.3 Umwandlung zwischen Zahlensystemen Eine Zahl, die in verschiedenen Zahlensystemen durch unterschiedliche Zahlwörter dargestellt wird, bleibt natürlich unverändert immer die gleiche Zahl. Wir Menschen sind von frühester Kindheit an das Dezimalsystem gewöhnt und können auch nur in diesem sinnvoll rechnen. Wir lernen nun zwei Möglichkeiten kennen, zwischen verschiedenen Zahlensystemen umzuwandeln. Eine davon ist besonders gut geeignet, vom Dezimalsystem aus in ein anderes umzurechnen. Das andere Verfahren ist dann besonders geeignet, in das Dezimalsystem umzuwandeln.

24

2 Zahlendarstellung

Bei der Wandlung zwischen beliebigen nicht-dezimalen Systemen geht man am besten den Umweg über das Dezimalsystem. Die Darstellung aus 2.1 kann durch wiederholtes Ausklammern leicht umgestellt werden, wodurch das so genannte Hornerschema entsteht: N X

zi B i = (((zn B + zn−1 ) B + . . . + z2 ) B + z1 ) B + z0

(2.6)

i=0

2.3.1 Zielverfahren: Multiplikationsmethode Daraus lässt sich zuerst das Zielverfahren, auch Multiplikationsverfahren genannt, ableiten, bei dem alle Berechnungen im Zielsystem durchgeführt werden. Daher eignet sich dieses Verfahren besonders zur Umwandlung in das Dezimalsystem. Es werden die Ziffern (mit ihren Wertigkeiten im Zielsystem) in das Hornerschema 2.6 eingesetzt und die Multiplikationen schrittweise durchgeführt. Beispiel: Umwandlung der Zahl AFFE16 in das Dezimalsystem: ((A16 · 1610 + F16 ) · 1610 + F16 ) · 1610 + E16 = = ((1010 · 1610 + 1510 ) · 1610 + 1510 ) · 1610 + 1410 = = (17510 · 1610 + 1510 ) · 1610 + 1410 = 281510 · 1610 + 1410 = 4505410 In diesem Fall könnten wir alternativ auch die Wertigkeiten der Stellen mit den Ziffernwerten der jeweiligen Stelle multiplizieren und die so erhaltenen Werte addieren: i Wertigkeit B i Ziffern zi Produkt zi B i

3 409610 A16 =1010 4096010

2 25610 F16 =1510 384010

1 1610 F16 =1510 24010

0 110 E16 =1410 1410

Summe 4505410

2.3.2 Quellverfahren: Divisionsmethode Als zweites Verfahren können wir aus dem Hornerschema das Quell- oder Divisionsverfahren ableiten, bei dem im Quellsystem gerechnet wird. Damit ist dieses Verfahren besonders gut geeignet für Umrechnungen aus dem Dezimalsystem in ein anderes System. Gegeben ist der Wert einer Zahl q im Quellsystem. Im Zielsystem zur Basis B besitzt diese Zahl die Darstellung mit noch unbekannten Ziffern zi . Wenn wir q durch B dividieren, so ist der Divisionsrest in jedem Fall z0 , also der Wert der ersten gesuchten Ziffer im Zielsystem. Sukzessive Division mit Rest liefert damit alle gesuchten Ziffern: ((zn B + zn−1 )B + . . . + z1 )B + z0 = ((zn B + zn−1 )B + . . . + z2 )B + z1 Rest z0 B ((zn B + zn−1 )B + . . . + z2 )B + z1 = (zn B + zn−1 )B + . . . + z2 Rest z1 B

2.4 Rechnen im Dualsystem

25

usw.

zn =0 B Beispiel: Umwandlung der Zahl 4165110 in das Hexadezimalsystem: 41651 2603 162 10

: : : :

16 16 16 16

= = = =

2603 162 10 0

Rest Rest Rest Rest

3 11 2 10

} 1110 = B16 } 1010 = A16

Dabei ist zu beachten, dass die Ziffern in umgekehrter Reihenfolge anfallen. Die Zahl im Hexadezimalsystem lautet also A2B3.

2.3.3 Umwandlung zwischen Dual-, Oktal- und Hexadezimalsystem Für die Arbeit mit Computern sind das Oktal- und das Hexadezimalsystem von besonderer Bedeutung, da Zahlen in diesen Systemen deutlich kürzere, also handlichere Darstellungen besitzen, als im Dualsystem. Darüber hinaus ist die Umwandlung zwischen diesen Systemen und dem Dualsystem besonders einfach: 1. Einer Ziffer im Oktalsystem entsprechen genau drei Dualziffern 7148

=

111 |{z} 001 |{z} 100 |{z} 7

1

2

4

der Ziffernwert der acht Ziffern im Oktalsystem ist natürlich jeweils der selbe wie im Dezimalsystem. 2. Einer Ziffer im Hexadezimalsystem entsprechen genau vier Dualziffern AFFE16

=

1010 | {z} 1111 |{z } 1111 |{z } 1110 | {z} A F F E 1010 1510 1510 1410

2.4 Rechnen im Dualsystem Mit Dualzahlen wird prinzipiell genauso gerechnet, wie mit dezimalen. Um für die Behandlung der rechnerinternen Darstellung ganzer Zahlen gerüstet zu sein, reicht es aus, wenn wir uns mit der Addition zweier Dualzahlen mit den Ziffern xn . . . x0 bzw. yn . . . y0 beschäftigen:

+

xn yn zn

xn−1 yn−1 zn−1

... ... ...

xi yi zi

... ... ...

x0 y0 z0

26

2 Zahlendarstellung

Zwei Dualzahlen werden ziffernweise addiert, wobei mit der kleinsten Ziffer begonnen wird. Dabei können Überträge auftreten, die bei der folgenden Ziffer zu berücksichtigen Stelle i+1 Stelle i Stelle i-1

sind:

x i+1

xi

x i-1

y i+1

yi

y i-1

üi+1

üi

üi-1

zi

z i+1

z i-1

Für jede Stelle muss die Stellenbilanz erfüllt sein:1 xi + yi + üi = zi + 2üi+1 Bei gegebenen Stellen xi und yi , sowie dem von der letzten Stelle herrührenden Übertrag üi berechnet sich die Ergebnisstelle zi sowie der Übertrag für die nächste Stelle wie folgt2 : xi 0 0 0 0 1 1 1 1

yi 0 0 1 1 0 0 1 1

üi 0 1 0 1 0 1 0 1

üi+1 0 0 0 1 0 1 1 1

zi 0 1 1 0 1 0 0 1

Beispiel: Berechnung von 1111000102 + 110101102 xi yi üi zi

+

111100010 11010110 1110001100 1010111000

2.5 Rechnerinterne Darstellung von ganzen Zahlen Bisher haben wir Zahlen zwischen Zahlensystemen umgerechnet. Nun müssen wir uns damit beschäftigen, wie Zahlen im Rechner gespeichert werden können. Jede Zahl muss auf eine reihe von Bits abgebildet werden. Daher ist die Darstellung von Zahlen im Dualsystem die Voraussetzung für eine sinnvolle Abbildung von Zahlen auf Speicherworte. 1 Der

Übertrag wird mit 2 multipliziert, da er in die Stelle mit der nächsthöheren Wertigkeit eingeht. ist die Wahrheitstabelle eines so genannten Volladdierers mit drei Eingängen xi , yi und üi sowie zwei Ausgängen zi und üi+1 . 2 Dies

2.5 Rechnerinterne Darstellung von ganzen Zahlen

27

Zu beachten ist, dass entsprechend der zur Speicherung einer Zahl verwendeten Wortbreite lediglich eine begrenzte Menge von Stellen pro Zahl gespeichert werden kann. Zudem muss für negative Zahlen das Vorzeichen mit gespeichert werden können. Die Abbildung einer Zahl auf ein Speicherwort nennen wir auch Interndarstellung der Zahl. Der erste Versuch bestünde darin, das Vorzeichen in das erste Bit zu kodieren, wobei Null für ein positives und Eins für ein negatives Vorzeichen steht. Die duale Darstellung des Betrags der Zahl könnte dann in die verbleibenden Stellen kodiert werden. Es gibt jedoch Möglichkeiten, Zahlen so zu speichern, dass zur Subtraktion nicht eine Zahl von der anderen mit einem eigenen Subtraktionswerk abgezogen werden muss, sondern dass die Interndarstellungen der Zahlen einfach addiert werden können: die Komplementdarstellungen. Ein Addierer muss in jedem Fall im Rechenwerk vorhanden sein. Ein Invertierer – der zur Bildung des Komplements auch benötigt wird – ist einfach zu realisieren und gehört darüber hinaus auch zur Grundausstattung eines Rechenwerks. Betrachten wir die Subtraktion einer Zahl y von B n − 1, so stellen wir fest, dass diese sehr einfach ist, weil keine Borger auftreten: Beispiel: B = 10, n = 3, y = 937

999 -937 062

noch einfacher ist der Fall im Dualsystem: Beispiel: B = 2, n = 4, y = 0110

1111 -0110 1001

Hierbei findet lediglich eine Inversion des Subtrahenden statt, das heißt bezogen auf die n Stellen wird jede Null durch eine Eins ersetzt und umgekehrt. Jede Subtraktion x − y können wir durch eine einfache Erweiterung um ein paar Terme ganz einfach so umschreiben: x − y = x + (K − y) − K

(2.7)

Dabei wird K auch als Komplement-Minuend bezeichnet. Wir werden zunächst mit zwei Werten für K arbeiten: 1. Mit K = B n − 1 heißt y = K − y auch B − 1-Komplement von y 2. Mit K = B n heißt y = K − y das B-Komplement von y Da wir die Komplemente nur für binäre Darstellungen bestimmen, sprechen wir nur vom Eins- bzw. Zwei-Komplement. Bei allen folgenden Überlegungen betrachten wir jeweils eine feste Zahl von n Stellen, die zur Darstellung der Zahlen zur Verfügung steht.

28

2 Zahlendarstellung

2.5.1 Das Eins-Komplement Rechnen wir mit dem Eins-Komplement einer Dualzahl, so wird (2.7) zu x − y = x + (2n − 1 − y) − (2n − 1)

(2.8)

Die rechte Seite von (2.8) lässt sich ohne eigentliche Subtraktion in mehreren Schritten ausrechnen. Die Rechenschritte zur Berechnung von x − y werden an zwei Beispielen gezeigt. Beispiel 1 Stellenzahl n = 4 x = 710 = 01112 y = 510 = 01012

Beispiel 2 Stellenzahl n = 4 x = 310 = 00112 y = 510 = 01012

Rechenschritte: 1. Berechne das Eins-Komplement y = (2n − 1) − y, wozu die Bits von y lediglich zu invertieren sind. Beispiel 1 y = 1010

Beispiel 2 y = 1010

2. Berechne nun z = x + y, also eine Addition. Beispiel 1 x 0111 + 1010 z = 10001

Beispiel 2 x 0011 + 1010 z = 1101

3. Berechne x − y = z − (2n − 1). Hier unterscheiden sich die beiden Beispiele. Fall 1: z ≥ 2n Es ist z −(2n −1) = z −2n +1. Um 2n zu subtrahieren, muss man das Bit mit Stellenwert 2n (Überlauf) auf Null setzen. Danach ist noch 1 zu addieren. Da vorne eine Eins verschwindet, hinten eine Eins addiert wird, bezeichnet man diesen Vorgang auch als „Einerrücklauf“ . Beispiel 1 1|0001 0001

+

0001 1 0010

erstes Bit löschen

Eins addieren Ergebnis

Fall 2: z < 2n Wenn z < 2n ist, dann ist das Ergebnis x − y = z − (2n − 1) negativ. Wegen z − (2n − 1) = −((2n − 1) − z) ist die Berechnung durch Inversion und Voranstellung eines negativen Vorzeichens zu erledigen.

2.5 Rechnerinterne Darstellung von ganzen Zahlen Beispiel 2 1101 0010 ⇓ −0010

29

invertieren - Zeichen voranstellen Ergebnis

Im Fall z < 2n (dritter Schritt, Fall 2) handelt es sich bei z um eine negative Zahl, die zurückkomplementiert werden muss, um auf die eigentliche Zahl schließen zu können. Soll später mit z weiter gerechnet werden, so ist die Rückkomplementierung nicht erforderlich. Dass es sich um eine negative Zahl handelt ist daran zu erkennen, dass das erste Bit eins ist. Durch die Komplementdarstellung kann eine Subtraktion durch ausschließliche Anwendung der Operationen Inversion und Addition ausgeführt werden. Wir können nun all die positiven Zahlen und ihre Eins-Komplemente angeben, die sich mit einer gegebenen Wortbreite n darstellen lassen. Als Beispiel betrachten wir den Fall n=4: Dez.zahl 0 1 2 ... 6 7 8 ...

Binär mit n = 4 0000 0001 0010 ... 0110 0111 1000 ...

Komplementbildung

Eins-Komplement 1111 1110 1101 ... 1001 1000 0111 ...

Dez.zahl 0 –1 –2 ... –6 –7 ...

Mit n = 4 lassen sich insgesamt 16 verschiedene Werte eindeutig darstellen. Bei Darstellung im Eins-Komplement können wir den symmetrischen Zahlenbereich von −(2n−1 − 1) . . . + (2n−1 − 1) abbilden. Wir können die 2n binären Zahlworte im Kreis anordnen, dem so genannten Zahlenkreis, wie er in Abbildung 2.1 für n = 4 gezeigt ist. Wir sehen dort außen alle binären Werte mit vier Bit, im schraffierten Bereich die positiven Zahlen, die damit dargestellt werden können, wenn kein Eins-Komplement verwendet wird. Innen stehen die negativen Zahlen, die dargestellt werden können, wenn das EinsKomplement verwendet wird. Alle negativen Zahlen sind daran erkennbar, dass ihr erstes Bit eine Eins ist. Jede Addition (und damit auch jede Subtraktion) von Zahlen auf dem Zahlenkreis liefert wieder eine Zahl auf dem Zahlenkreis. Das liegt daran, dass die Rechnung bei Komplementdarstellung auf n Stellen beschränkt ist und ein eventueller Überlauf abgeschnitten wird. Dies müssen wir insbesondere dann beachten, wenn wir mit solchen Zahlen auf dem Computer arbeiten. Die Addition 7+1 liefert bei Eins-Komplement-Darstellung als Ergebnis den Wert -7.

30

2 Zahlendarstellung

1000 8 1001 9 -7 -6 1010 10 -5 negative Zahlen

0111 7

6

0110

1011 11 -4 1100

4

12 -3

1101

13

0101

5

-2

14 1110

0100

positive Zahlen

3 0011 2

-1 15 0 0000 1111

1 0001

0010

Abbildung 2.1: Der Zahlenkreis des Eins-Komplements für n = 4 Stellen

Ein wesentlicher Nachteil dieser Darstellung ist, dass die Null zwei mal auftritt; einmal als „positive Null“ und einmal als deren Komplement, als „negative Null“. Beim Programmieren ist später bei allen Abfragen ob ein Wert Null ist, diese Mehrdeutigkeit zu berücksichtigen, also zwei Abfragen durchzuführen, eine für die positive und eine für die negative Null. Daher hat sich die Eins-Komplement-Darstellung nicht durchgesetzt. Vielmehr wird von allen modernen Computern intern die Zwei-Komplement-Darstellung verwendet.

2.5.2 Das Zwei-Komplement Rechnen wir mit dem Zwei-Komplement einer Dualzahl, so wird (2.8) zu x − y = (2n − y) − 2n

(2.9)

Wir haben in 2.5.1 gesehen, dass zur Bildung des Eins-Komplements einer Zahl deren Stellen invertiert werden müssen. Zur Bildung des Zwei-Komplements ist zu diesem Wert lediglich noch eins zu addieren. Die rechte Seite von (2.9) lässt sich wieder ohne eigentliche Subtraktion ausrechnen: Beispiel 1 Stellenzahl n = 4 x = 710 = 01112 y = 510 = 01012

Beispiel 2 Stellenzahl n = 4 x = 310 = 00112 y = 510 = 01012

Rechenschritte: 1. Berechne das Zwei-Komplement y = y + 1, wozu die Bits von y zu invertieren sind und 1 zu addieren ist.

2.5 Rechnerinterne Darstellung von ganzen Zahlen Beispiel 1 y 1010 + 1 y = 1011

31

Beispiel 2 y 1010 + 1 y = 1011

2. Berechne nun z = x + y, also eine Addition. Beispiel 1 x 0111 + 1011 z = 10010

Beispiel 2 x 0011 + 1011 z = 1110

3. Berechne x − y = z − 2n . Hier unterscheiden sich die beiden Beispiele. Fall 1: z ≥ 2n Um 2n zu subtrahieren, muss man das Bit mit Stellenwert 2n (Überlauf) auf Null setzen. Beispiel 1 1|0010 0010

erstes Bit löschen Ergebnis

Fall 2: z < 2n Wenn z < 2n ist, dann ist das Ergebnis x − y = z − 2n negativ. Wegen z − 2n = −(2n − z) ist die Berechnung durch Zweikomplement-Bildung und Voranstellung eines negativen Vorzeichens zu erledigen. Beispiel 2 1110 0001 0010 ⇓ −0010

invertieren 1 addieren - Zeichen voranstellen Ergebnis

Im Fall z < 2n (dritter Schritt, Fall 2) handelt es sich bei z um eine negative Zahl, die zurückkomplementiert werden muss, um auf die eigentliche Zahl schließen zu können. Soll später mit z weiter gerechnet werden, so ist die Rückkomplementierung nicht erforderlich. Wir können nun all die positiven Zahlen und ihre Zwei-Komplemente angeben, die sich mit einer gegebenen Wortbreite n darstellen lassen. Als Beispiel betrachten wir den Fall n = 4:

32

2 Zahlendarstellung

1000 8 1001 9 -8 -7 1010 10 -6 negative Zahlen

0111 7

6

0110 0101

5

1011 11 -5

0100

4

-4 1100 12 -3 13 1101 -2 14 -1 1110 15 0 1111 0000

positive Zahlen

3 0011 2

0010 1 0001

Abbildung 2.2: Der Zahlenkreis des Zwei-Komplements für n = 4 Stellen Stellen

Dez.zahl 0 1 2 ... 6 7 ...

Binär mit n = 4 0000 0001 0010 ... 0110 0111 1000 ...

Komplementbildung

Zwei-Komplement 0000 1111 1110 ... 1010 1001 1000 ...

Dez.zahl 0 −1 −2 ... −6 −7 −8 ...

Die negativen Zahlen sind also genau die, die eine Eins an erster Stelle aufweisen. Das Zwei-Komplement der Null ist wieder die Null, also ist die Darstellung der Null eindeutig. Das Zwei-Komplement der binären Darstellung 1000 ist auch wieder 1000, und wird wegen der Eins an erster Stelle als -8 gewertet. Der Zahlenbereich ist somit unsymmetrisch und deckt den Bereich −2n−1 . . . + 2n−1 − 1 ab. Abbildung 2.2 zeigt den Zahlenkreis für das Zwei-Komplement bei einer Wortbreite von n = 4. Auffallend ist, dass die −1 durch ein Wort mit lauter Einsen repräsentiert wird. Der kleinste darstellbare negative Wert besteht aus einer Eins gefolgt von lauter Nullen. Dies gilt für jede Wortbreite.

2.6 Darstellung und Umwandlung gebrochener Zahlen

33

2.6 Darstellung und Umwandlung gebrochener Zahlen Die Zahlendarstellung aus 2.1 kann erweitert werden, so dass auch rationale Zahlen dargestellt werden können. Es werden dazu Stellenwerte mit negativen Potenzen der Basis angefügt und zur Trennung zwischen die Stellen zu B 0 und B −1 ein Komma gesetzt. Ein Zahlwort besitzt dann die Form zn zn−1 . . . z1 z0 , z−1 z−2 . . . z−r | {z } | {z } ganzzahliger gebrochener Anteil Anteil

(2.10)

und der Wert der Zahl ist gegeben durch zn B n +zn−1 B n−1 +. . .+z0 B 0 +z−1 B −1 +z−2 B −2 +. . .+z−r B −1 =

n X

zi B i

i=−r

(2.11) Analog zur Umwandlung ganzer Zahlen gibt es auch zur Umrechnung gebrochener Zahlen zwischen verschiedenen Zahlensystemen ein Quell- und ein Zielverfahren. Beide Verfahren basieren auf dem Hornerschema, das sich aus (2.11) ergibt, hier auf den Nachkommateil z−1 z−2 . . . z−r eingeschränkt: B −1

z−1 B −1 + z−2 B −2 + . . . + z−r B −r =   z−1 + B −1 z−2 + B −1 . . . + B −1 z−r . . .

(2.12)

Wir beschränken uns hier auf die Umrechnung zwischen dem Dezimal- und dem Dualsystem.

2.6.1 Zielverfahren: Divisionsmethode Das Zielverfahren ist wieder besonders gut geeignet, um Zahlen in das Dezimalsystem umzuwandeln. Bei diesem Verfahren wird nach dem Hornerschema (2.12) durch sukzessives Dividieren durch die Quellbasis der Wert im Zielsystem bestimmt. Beispiel: Umrechnung von 0, 10112 in das Dezimalsystem: Wir stellen das Hornerschema  1 1 1 1 auf und rechnen schrittweise von innen nach außen: 2 z−1 + 2 z−2 + 2 z−3 + 2 z−4 1,0 0,5 1,5 0,75 0,75 0,375 7 1,375

÷ + ÷ + ÷ + ÷

z−4 2 z−3 2 z−2 2 z−1 2

= = = = = = = =

1,0 0,5 1,5 0,75 0,75 0,375 1,375 0,6875

=⇒0,687510 = 0,10112

34

2 Zahlendarstellung

Bei bekannten Wertigkeiten der Stellen kann die Umwandlung auch durch Multiplikation der Ziffern mit den Wertigkeiten mit anschließendem Aufsummieren durchgeführt werden: i Wertigkeit B −i Ziffern z−i Produkt z−i B −i

-1 0,510 12 =110 0,510

-2 0,2510 02 =010 010

-3 0,12510 12 =110 0,12510

-4 0,062510 12 =110 0,062510

Summe 0,687510

2.6.2 Quellverfahren: Multiplikationsmethode Das Quellverfahren ist wieder besonders gut geeignet zur Umwandlung vom Dezimalin ein anderes System. Gegeben ist eine Zahl ohne ganzzahligen Anteil im Quellsystem. Deren Ziffern im Zielsystem sind zunächst unbekannt. Wir gehen wieder vom Hornerschema aus. Multiplikation der Zahl mit der Basis B des Zielsystems liefert vor dem Komma das B-fache der ersten Nachkommastelle, insbesondere einen Wert zwischen 0 und B − 1. Daher können wir die Darstellung im Zielsystem ermitteln, indem wir die Zahl wiederholt mit der Zielbasis multiplizieren und jeweils die Vorkommastellen abspalten und als Ziffer im Zielsystem anschreiben. Beispiel Umrechnung von 0, 562510 in das Dualsystem: 0,5625 0,125 0,250 0,5

* * * *

2 2 2 2

= = = =

1,125 0,250 0,500 1,000

→ z−1 → z−2 → z−3 → z−4

=1 =0 =0 =1

Damit ist das Ergebnis z−1 z−2 z−3 z−4 = 0, 10012

2.7 Rechnerinterne Darstellung gebrochener Zahlen Zur internen Darstellung von gebrochenen Zahlen gibt es zwei Alternativen: 1. Festpunktdarstellung3 : Die Ziffern der binären Darstellung einer gebrochenen Zahl werden auf ein Speicherwort abgebildet. Das Komma steht an einer fest vereinbarten Stelle, und muss daher nicht explizit mit abgespeichert werden. Diese Darstellung wird vor allem von Spezialprozessoren, etwa Signalprozessoren verwendet. 2. Gleitpunktdarstellung: Zur Gleitpunktdarstellung wird jede Zahl x in die Form x = ±m · B e mit einer 3 Der Begriff „Festpunkt“ ist direkt aus dem Englischen übersetzt. Im angelsächsischen Bereich wird bei gebrochenen Zahlen der Nachkomma-Bereich durch einen Punkt abgetrennt und nicht wie bei uns durch ein Komma. Wir könnten also auch von „Festkomma-“ oder „Gleitkommazahlen“ sprechen.

2.8 Aufgaben

35

Mantisse m und einem Exponenten e gebracht. Vorzeichen, Mantisse und Exponent werden dann in ein Speicherwort kodiert. Diese Darstellung wird heute bei allen PCs verwendet. Wir beschränken uns hier auf die Behandlung des Gleitpunktformats nach dem üblichen IEEE4 -Standard [IEEE (1985)]. Um eine eindeutige Darstellung x = ±m · B e zu erhalten wird zusätzlich gefordert, dass 1 ≤ m < B gelten muss. Im Dualsystem bedeutet diese Forderung, dass m von der Art 1 , z−1 z−2 . . . z−r ist, also genau eine Eins vor dem Komma hat (ausgenommen für die Darstellung der Null). Diese Eins muss dann natürlich nicht gespeichert werden. Beispiel: 2, 812510 = 10, 11012 = +1, 01101 · 21 . Abhängig von der Länge des zum speichern einer Gleitpunktzahl zur Verfügung stehenden Speicherwortes werden die Nachkommastellen der so normierten Mantisse sowie der Exponent auf bestimmte Bitpositionen abgebildet. Die genaue Darstellung werden wir in Kapitel 5.5.2, Seite 79 über Datentypen kennen lernen. Es ist zu beachten, dass sich die Interndarstellung einer ganzen Zahl im Zwei-Komplement deutlich von der Interndarstellung der selben Zahl als Gleitpunktzahl nach obigem Muster unterscheidet.

2.8 Aufgaben Aufgabe 2.1 Sie lesen, dass ein Mikroprozessor über einen 24-Bit-breiten Adressbus verfügt. Wie viele Speicherworte können damit adressiert werden? Aufgabe 2.2 Stellen Sie fest, ob Ihr Taschenrechner mit Dualzahlen rechnen kann. Falls ja, dann prüfen Sie, ob er zur Interndarstellung das Zwei-Komplement benutzt. Stellen Sie ferner fest, mit welcher Wortbreite er arbeitet. Sie können zusätzlich den „Rechner“ prüfen, den Sie im Zubehör der Windows-Betriebssysteme finden. Aufgabe 2.3 Welche negativen Zahlen repräsentieren die folgenden Interndarstellungen im Zweikomplement mit 16 Bit Wortbreite? (Dabei steht das niederwertigste Bit rechts). 1000 0000 0000 0001 1000 0000 0000 0000 4 IEEE

steht für „Institute of Electrical and Electronics Engineers“

36

2 Zahlendarstellung

Aufgabe 2.4 Geben Sie die Zwei-Komplement-Darstellung mit 16 Bit Wortbreite folgender Zahlen an. Dabei soll das niederwertigste Bit (least significant bit) rechts stehen: -1 -4096 (das ist -212 )

Aufgabe 2.5 Wieviel Speicher kann man mit n-Bit breiten Adressen adressieren? Angaben in KB, MB oder GB: n=10 n=12 n=20 n=32

adressierbar: adressierbar: adressierbar: adressierbar:

Aufgabe 2.6 Wandeln Sie die angegebene Zahl jeweils in die anderen Zahlensysteme Dual Oktal Hexadezimal

Zahl 1 101010102

Zahl 2

Zahl 3

3278 CAD16

Aufgabe 2.7 Addieren Sie schriftlich. Tragen Sie auch die Überträge der jeweiligen Stelle ein! Summand 1 Summand 2 Überträge Summe

1 1 1 . . . . . .

1 1 . .

1 0 . .

0 1 . .

0 0 . .

0 1 . .

1 1 . .

02 02 .2 .2

3

Zeichencodes

In den vorangegangenen Abschnitten haben wir uns mit der Interndarstellung von Zahlen in Computern beschäftigt. Zum Beispiel ist die interne Darstellung der Zahl 1710 in einem 16 Bit-Speicherwort die Dualzahl 0000 0000 0001 00012 . Die Interndarstellung von Zahlen kann man als eine Codierung betrachten. Ein Code ordnet den zu codierenden Elementen einer Urbildmenge die Elemente der Bildmenge, d.h. der Menge von Codes zu. Ein Code heißt umkehrbar eindeutig, wenn aus der Verschiedenheit zweier Urbilder auch die Verschiedenheit der Codes folgt. Für das Beispiel der Interndarstellung ist die Urbildmenge die Menge der Zahlen 010 − 6553510 . Die Codemenge ist durch die Menge der Dualzahlen 0000 0000 0000 00002 − 1111 1111 1111 11112 gegeben. Durch die Codierung wird jeder Zahl die Dualzahl mit dem gleichen Zahlenwert zugeordnet. Ein Zeichencode ordnet einer Menge von Schriftzeichen (Zeichensatz) umkehrbar eindeutig eine Menge von Dualzahlen zu. Je nach der Stelligkeit der Dualzahlen spricht man von 7 Bit, 8 Bit oder 16 Bit Codes. Mit Schriftzeichen ist hier ein Element aus einem Alphabet – dem Zeichensatz – gemeint, nicht ein bestimmtes grafisches Abbild aus einer Schriftfamilie (character font). Die Abbildung 3.1 zeigt die Beziehungen am Buchstaben A. Zeichencodes dienen auch der Externdarstellung, d.h. diese Codes werden bei der Ein-/ Ausgabe von Schriftzeichen verwendet. Wenn also etwa ein Programm einen Buchstaben

Gegenstand des Kapitels Element des Zeichensatzes: der Buchstabe

Element der Codemenge

0100 0001

Code: umkehrbar eindeutige Zuordnung

A

Abbildung 3.1: Codemenge, Zeichensatz, Schriftfamilie

Schriftfamilie Times Schriftfamilie Courier

A A

38

3 Zeichencodes

A am Bildschirm erscheinen lassen möchte, dann gibt es den Code für A, also z. B. 100 00012 aus. Umgekehrt wird bei einer Eingabe von A über die Tastatur der Code für A in den Speicherbereich des Programms übertragen. Wenn eine Zahl auszugeben ist – z. B. 1710 als Ergebnis einer Berechnung – dann ist nicht die Interndarstellung der Zahl auszugeben (hier also 0000 0000 0001 00012 ), sondern die Codes der Darstellung der Zahl als Schriftzeichen. Für 17 wären das die Codes für 1 und für 7. Unten werden wir sehen, dass die ASCII-Codes für diese Zeichen 011 00012 und 011 01112 sind. Glücklicherweise merkt der Programmierer von der Umsetzung zwischen Codes und Schriftzeichen meist nicht viel. Die Eingabe des Programms vor der Übersetzung und die Ein-/ Ausgabe während der Benutzung des Programms geschehen auf der Basis von Schriftzeichen. Die Umsetzung zwischen Codes im Rechner und Schriftzeichen extern geschieht hierfür automatisch. Im Kap. „Formatierte Ein-/ Ausgabe“ werden wir sehen, wie man die Umwandlung in der Programmiersprache C anstößt. Für die Codierung von Schriftzeichen gibt es natürlich potentiell sehr viele verschiedene Möglichkeiten. Daher wurden Standards eingeführt. Mitte der sechziger Jahre wurden die Codes • EBCDI • ASCII festgelegt. EBCDI1 hat vor allem in der IBM-Welt große Verbreitung gefunden. Der Code geht von einer Codierung von Ziffern durch die Dualzahlen aus, die ihren Ziffernwert repräsentieren, also 1 codiert als 00012 , 2 als 00102 . . . , 9 als 10012 . Um neben Ziffern auch andere Zeichen zu erfassen, wurden die vier Dualstellen auf acht erweitert, so dass 256 Schriftzeichen darstellbar sind. Heute ist auf den meisten Plattformen ASCII2 vorherrschend. Das ist auch der Code, mit dem wir in der Sprache C arbeiten. ASCII begann als 5 Bit-Code, wurde aber bald auf 7 Bit erweitert. Im nächsten Abschnitt werden wir die Tabelle für 7 Bit ASCII kennen lernen. Erweiterungen auf 8 Bit werden im Kap. 3.2 behandelt.

3.1 7-Bit ASCII Mit 7 Bit kann man 128 Schriftzeichen darstellen, denen die Codes 000 00002 − 111 11112 bzw. 010 − 12710 bzw. 0016 − FF16 zugeordnet sind. Für die Darstellung läge es nahe, eine Tabelle mit zwei Spalten „Code“ und „zugeordnetes Schriftzeichen“ zu verwenden. Eine solche Tabelle hätte aber ein ungünstiges Format (1/8 Seite breit, 1 Extended 2 American

Binary Coded Decimals Interchange Code Standard Code for Information Interchange

3.1 7-Bit ASCII 0

39 1

2

0

NUL

0

DLE

16

1

SOH

1

DC1

17

2

STX

2

DC2

18

3

ETX

3

DC3

19

4

EOT

4

DC4

20

5

ENQ

5

NAK

21

6

ACK

6

SYN

22

7

BEL

7

ETB

23

8

BS

8

CAN

24

9

HT

9

EM

25

A

LF

10

SUB

26

B

VT

11

ESC

27

C

FF

12

FS

28

D

CR

13

GS

29

E

SO

14

RS

30

F

SI

15

US

31

3 32

! " # $ % & ’ ( ) * + , . /

33 34 35 36 37 38 39 40 41 42 43 44 45 46 47

0 1 2 3 4 5 6 7 8 9 : ; < = > ?

4 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63

@ A B C D E F G H I J K L M N O

5 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79

P Q R S T U V W X Y Z [ \ ] ^ _

6 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95

` a b c d e f g h i j k l m n o

7 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111

p q r s t u v w x y z { | } ~ DEL

112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127

Tabelle 3.1: Die 7-Bit ASCII-Codes

mehrere Seiten hoch). Deshalb „schneidet“ man die Codetabelle in Streifen zu je 16 Einträgen und stellt diese acht Streifen nebeneinander. Das Ergebnis zeigt Tabelle 3.1. Wenn man die Codes als Hexadezimalzahlen betrachtet, dann wechselt gerade alle 16 Einträge die vordere Ziffer. Daher kann man die oben genannte „Streifencollage“ so beschriften, dass die Spalten jeweils die gemeinsame führende Hexadezimalziffer als Überschrift erhalten. Die Zeilen sind von 016 − F16 durchnummeriert. Sie enthalten in allen Spalten das Zeichen mit der betreffenden Nummer in seinem 16er-Streifen. Diese Nummer ist die niederwertige Hexadezimalziffer des Codes. Die Felder der Tabelle enthalten ein einzelnes Zeichen bzw. eine Abkürzung aus mehreren kursiv gesetzten Großbuchstaben und eine kursiv gesetzte Zahl. Einzelzeichen bedeuten Schriftzeichen aus dem Zeichensatz, eine Abkürzung bezeichnet ein Steuerzeichen und die Zahl ist der Code des Zeichens in dezimaler Angabe. Den Hexadezimalwert erhält man, indem man die Ziffern der Zeilen- und Spalten-Überschriften zum betreffenden Feld nebeneinander stellt. Wenn wir also z. B. „7-Bit ASCII“ codieren, erhalten wir gemäß Tabelle 3.1: Zeichen Code dezimal Code hexadezimal

7 55 37

45 2D

B 66 42

i 105 69

t 116 74

32 20

A 65 41

Offensichtlich enthält Tabelle 3.1 verschiedenartige Einträge: • Buchstaben a-z, A-Z

S 83 53

C 67 43

I 73 49

I 73 49

40

3 Zeichencodes • Ziffern 0-9 • Sonderzeichen, z. B. -, Leerzeichen (Code 3210 ), # etc.

Steuerzeichen sind in der Tabelle durch Abkürzungen in Großbuchstaben dargestellt. Den Steuerzeichen sieht man noch an, dass in den sechziger Jahren mit Fernschreibern gearbeitet wurde. Einige Steuerzeichen werden aber noch immer benutzt, wenn sie heute auch die Ausgabe auf einen Bildschirm oder Laserdrucker steuern. Einige der häufiger benutzten, die auch im C-Teil des Buches vorkommen, zeigt Tabelle 3.2. Eine komplette Liste der Abkürzungen findet man z.B. auf den Code-Tabellen „Latin-1 Supplement“ der Unicode-Seiten [Unicode]. BEL BS HT LF

„Bell“, Piepton aus Lautsprecher „Backspace“, Korrekturtaste „Horizontal Tab“, Tabulator „Line feed“, Zeilenvorschub

VT FF CR ESC

„Vertical Tab“, vertikaler Tabulator „Form feed“, Seitenvorschub „Carriage Return“, Wagenrücklauf „Escape“, Taste links oben

Tabelle 3.2: Häufig benutzte Steuerzeichen

3.2 8-Bit ISO 8859 Die 128 Zeichen des 7 Bit ASCII waren natürlich auf die Dauer nicht ausreichend. Nachdem alle wesentlichen Hardware-/ Software-Plattformen ohnehin mit 8 Bit Bytes arbeiteten, lag es nahe, den Standard zu erweitern. Mit 8 Bit lassen sich 256 Zeichen codieren. Die Erweiterungen des 7 Bit ASCII-Codes wurden aufwärtskompatibel 3 gestaltet. Das bedeutet, dass die bereits bestehenden Codes auch im erweiterten Standard ihre Zuordnung behalten und nur die neu hinzugekommenen Zeichen neue Codes bekommen. Für die Zeichen aus Tabelle 3.1 erhält man also den 8 Bit ASCII-Code, indem man eine führende 0 an die betreffende Dualzahl hinzufügt. Für A hat man dann z. B. 0100 00012 , also einen 8 Bit Code, der natürlich dem gleichen Zahlenwert entspricht, wie sein 7 Bit Pendant 100 00012 , nämlich 6510 . Mit der globalen Verbreitung von Computern und ihrem Vordringen in alle Lebensbereiche sind aber selbst 256 verfügbare Schriftzeichen nur eine geringe Teilmenge des eigentlich benötigten Umfangs. Für die Codes 12810 − 25510 ist die Situation deshalb leider nicht so einheitlich wie für den Bereich 010 − 12710 , denn es wurden mehrere unterschiedliche Standards für die Erweiterung festgelegt. Allein für den westlichen Kulturkreis gibt es für die Anwendung in verschiedenen geografischen Bereichen mehrere Zuordnungen der Codes 12810 − 25510 zu Schriftzeichen. Die Mitglieder aus der Standard-Familie ISO 8859 zeigt Tabelle 3.3. 3 „abwärtskompatibel“ würde bedeuten, dass die neu hinzugekommenen Codes und Zeichen auch im alten Standard verwendet werden könnten. Das ist natürlich hier nicht möglich.

3.2 8-Bit ISO 8859

41

Bezeichnungen ISO 8859-1 latin1 ISO 8859-2 latin2 ISO 8859-3 latin3 ISO 8859-4 latin4 ISO 8859-5 cyrillic ISO 8859-6 arabic ISO 8859-7 greek ISO 8859-8 hebrew ISO 8859-9 latin5

Einsatz Westeuropa Osteuropa galizisch, türkisch, Esperanto estnisch, lettisch, litauisch kyrillisch arabisch neugriechisch hebräisch wie latin1, aber türkische statt isländische Zeichen

Tabelle 3.3: Die Standard-Familie ISO 8859

Für uns ist natürlich ISO 8859-1 alias latin1 am wichtigsten, dessen über 7 Bit ASCII hinausgehende Zeichenmenge in Tabelle 3.4 dargestellt ist4 . Auch hier ist eine Teilmenge von Steuerzeichen enthalten. Eine komplette Liste der Abkürzungen findet man z.B. auf den Code-Tabellen „Latin-1 Supplement“ der Unicode-Seiten [Unicode]. 8

9

A

B

0

PAD

128

DCS

144

1

HOP

129

PU1

145

2

BPH

130

PU2

146

3

NBH

131

STS

147

4

IND

132

CCH

148

5

NEL

133

MW

149

6

SSA

134

SPA

150

7

ESA

135

EPA

151

8

HTS

136

SOS

152

9

HTJ

137

SGCI

153

A

VTS

138

SCI

154

B

PLD

139

CSI

155

C

PLU

140

ST

156

« ¬

D

RI

141

OSC

157

SHY

173

R

174

E

SS2

142

PM

158

F

SS3

143

APC

159

NBSP

¡ ¢ £ ¤ ¥ ¦ § ¨ c

a

¯

160 161



±

C 176 177

162

2

178

163

3

179

164 165 166 167 168

´ µ ¶ · ¸

180 181 182 183 184

169

1

185

170

o

186

171 172

175

» 1 4 1 2 3 4

¿

187 188 189 190 191

À Á Â Ã Ä Å Æ Ç È É Ê Ë Ì Í Î Ï

D 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207

Ð Ñ Ò Ó Ô Õ Ö × Ø Ù Ú Û Ü Ý Þ ß

E 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223

à á â ã ä å æ ç è é ê ë ì í î ï

F 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239

ð ñ ò ó ô õ ö ÷ ø ù ú û ü ý þ ÿ

240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255

Tabelle 3.4: Zuordnung zu den Codes 128-255 in ISO 8859-1

4 Vergeblich sucht man hier das e -Zeichen. e ist jünger als ISO 8859-1. Eine spätere Festlegung ISO 8859-15 sieht den Code A416 = 16410 vor. In Windows-Umgebungen wird 8016 = 12810 benutzt. Unicode (s. nächsten Abschnitt) codiert e mit 20AC16 = 836410 .

42

3 Zeichencodes

3.3 Unicode Die explosive Ausbreitung des Internet hat für weitere Dynamik in der Szene der Zeichencodes gesorgt. Internationalisierung, die jeden Winkel der Welt erreicht, ist jetzt gefragt. Dazu gehört insbesondere auch, dass Dokumente, die Zeichen aus verschiedenen Kulturkreisen enthalten, genau so einfach zu handhaben sind, wie Dokumente in einem der ISO 8859 Standards. Die diversen Standard-Ansätze zu vereinigen und alle wesentlichen Sprachen der Welt in einem einzigen Zeichensatz zu erfassen, hat sich das Unicode5 -Projekt zum Ziel gesetzt. Eine maßgebliche Einrichtung, die Informationen über Unicode verbreitet, ist das Unicode Konsortium [ISO/IEC (2003)]. Mitte der neunziger Jahre standardisierte man einen 16 Bit Code, in dem Platz für 65536 Zeichen ist. Heute ist man bei Versionen, die mehr als 65536 Zeichen enthalten, die also 24 bzw. 32 Bit für eine Code-Nummer brauchen. Unicode ist eine aufwärts kompatible Erweiterung von ISO 8859-1. Wie ISO 8859 den 7 Bit ASCII-Code unter Beibehaltung der Codes 010 − 12710 erweitert, bleiben im Unicode die Zuordnungen von ISO 8859-1 gültig. Der Dualdarstellung wurden acht führende Nullen vorangestellt. Hinzugefügt wurden die Codes 25610 − 6553510 . Enthalten sind Abschnitte für alle wichtigen Sprachen sowie Sonderzeichen für verschiedenste Zwecke. Tabelle 3.5 stellt die Codes 7 Bit ASCII, ISO 8859-1, ISO 8859-7 und Unicode für die Zeichen a, b, c, ö und ϕ gegenüber. Wenn ein Feld leer ist bedeute dies, dass das Zeichen in dem betreffenden Code nicht darstellbar ist. Zeichen a b c ö ϕ

7-Bit ASCII 110 00012 110 00102 110 00112

ISO 8859-1 0110 00012 0110 00102 0110 00112 1111 11002

ISO 8859-7 0110 00012 0110 00102 0110 00112 1111 11002

16-Bit Unicode 0000 0000 0110 00012 0000 0000 0110 00102 0000 0000 0110 00112 0000 0000 1111 11002 0000 0011 1100 01102

Tabelle 3.5: 7-Bit ASCII, 8-Bit ISO8859-1 und -7 und 16-Bit Unicode gegenüber gestellt

Die Buchstaben a, b und c sind im ASCII-Basis-Zeichensatz enthalten. Mit ISO 8859-1 kann man auch das ö darstellen. An der gleichen Code-Position enthält ISO 8859-7 (Neugriechisch) statt ö ein anderes Zeichen, nämlich ϕ. Mit den Standards ISO 8859 muss man sich also entscheiden, ob man entweder ö oder ϕ darstellen können will. Es ist auch zu erkennen, dass Unicode zwar mit ISO 8859-1 aufwärts kompatibel ist – nicht aber mit ISO 8859-7. Mit Unicode als Obermenge dieser Standards sind all diese Zeichen in einem einzigen Code darstellbar. Damit können diese Zeichen z.B. auf einer Web-Seite nebeneinander stehen und gleichzeitig korrekt dargestellt werden. 5 nähere

Informationen und Codetabellen findet man unter [Unicode]

3.3 Unicode

43

Das Beispiel demonstriert, wie Unicode die Forderung nach Internationalisierung, die jeden Winkel der Welt erreicht, bestens unterstützt. Neuere Programmiersprachen wie Java definieren Zeichenketten von vornherein auf Unicode-Basis. Für die älteren Programmiersprachen ist Anpassungsaufwand zu treiben. Vorhut bei der Anwendung von Unicode sind hier die APIs6 von Betriebssystemen (z.B. von Windows), die stark auf Internationalität angewiesen sind. Auf lange Sicht ist eine vollständige Einarbeitung von Unicode in die Programmiersprachen und Standardbibliotheken zu erwarten. Heute gibt es in den höheren Sprachen zumindest die Möglichkeit, Zeichenketten als Anordnungen von Elementen zu realisieren, die jeweils ein Unicode-Zeichen aufnehmen können7 . Bisher haben wir nur von „Codierung“ gesprochen, also welche Elemente des Zeichensatzes zu welchen Zahlen zugeordnet werden. Für den praktischen Einsatz braucht man jetzt noch Festlegungen, wie die Codes als Folge von Bytes gespeichert und übertragen werden sollen. Man kann z.B. einfach einen Code aus Unicode als Dualzahl interpretieren und die Bytes hintereinander legen. Mit der Festlegung, das Byte mit dem höheren Stellenwert als erstes zu nehmen, hat man das Format UCS8 -2 für 16-Bit-Worte, erweitert auf 32-Bit-Worte kommt man zu UCS-4. Für den Einbau von Unicode-Zeichen in Web-Seiten wird eine Möglichkeit genutzt, Unicode-Zeichen als lesbare Darstellung durch Ersatztext zu codieren. In HTML, ab Version 4 wird auf folgende Weise verfahren9 : Der normale Text wird in HTML nach ISO 8859-1 codiert. Wenn ein Zeichen vorkommt, das nicht in diesen Code-Bereich fällt, dann kann der Code durch eine Folge von Ziffern zwischen &# und ; dargestellt werden. Wenn die Ziffernfolge mit einem x eingeleitet wird, ist die Zahl hexadezimal zu interpretieren. Die Zeichen ö und ϕ können in HTMLDokumenten z. B. als ö und φ übermittelt werden. Neben der Umsetzung von Unicode in eine Zifferndarstellung gibt es in HTML auch Namen für Zeichen, die zwischen & und ; angegeben werden können. Für ö wäre das z.B. ö, für ϕ φ.

3.3.1 UTF-8 Die Darstellung von Unicode als Ersatztext wie in HTML ist nicht geeignet für Programmiersprachen oder Betriebssystem-Aufrufe, wo z.B. Dateinamen übergeben werden. Auch Darstellungen gemäß UCS-2 oder UCS-4 sind für manche Anwendungen problematisch. 6 API heißt „Application Programming Interface“, d.h. die Schnittstelle die definiert, welche Betriebssystem- Aufrufe einem Anwendungsprogramm zur Verfügung stehen. 7 in C ist das der Datentyp wchar_t, vgl. Kap. „Standard- Bibliothek“ 8 „Universal Coding System“ 9 Vgl. hierzu insbesondere auch die Aufgabe im Anschluss an Kap. „Algorithmen: Reaktive Programme, Automaten“

44

3 Zeichencodes

Für viele Zwecke, insbesondere auch für C eignet sich besser UTF-8 [ISO/IEC (2003)], ein Transfer Format, das auf die Namen Thompson und Pike zurück geht [Thompson u. Pike (1993)]: • Elemente von 7-Bit ASCII, also Codes bis 0111 11112 = 12710 werden in UTF-8 als ein Byte ASCII-kompatibel dargestellt. • Codes ab 1000 00002 = 12810 werden als Folge mehrerer Bytes dargestellt, die alle das höchstwertige Bit gesetzt haben. Bytes aus Bytefolgen können also nicht mit 7-Bit ASCII-Zeichen verwechselt werden. • Das erste Byte einer Folge beginnt mit so vielen Eins-Bits, wie der Länge der Folge entspricht, gefolgt von einem Null-Bit. Beispiel: 110x xxxx2 für eine Folge aus 2 Bytes. • Folgebytes beginnen mit einem Eins-Bit und einem Null-Bit, also 10xx xxxx2 . • Auf die Bits aus einer Bytefolge, die nicht zum Kopf (bis einschließlich erstem Null-Bit) der Bytes gehören, werden die Bits des Unicode Codes vom höchstbis zum niederwertigsten aufgeteilt. Dabei werden nur so viele führende Nullen verwendet, wie zum Auffüllen der freien Bits nötig sind. Die Abbildung 3.2 zeigt den Zusammenhang am Beispiel „ß“, das als Folge von zwei Bytes codiert wird. Das erste Byte hat zwei führende Einsen (Bits 6-7), gefolgt von einer Null (Bit 5), denn es handelt sich um eine Folge von zwei Bytes. Das zweite Byte beginnt als Folgebyte mit Eins (Bit 7) und Null (Bit 6). Im ersten Byte sind damit 5 Bits (0-4) und im zweite 6 Bits (0-5) frei – insgesamt 11 Bits. „ß“ benötigt 8 Bits, die mit 3 führenden Nullen (Bits 8-10) auf 11 ergänzt werden. Diese 11 Bits werden vom höchstwertigen zum niederwertigsten auf die freien Bits in den UTF-8 Bytes aufgeteilt. In Tabelle 3.6 S. 45 ist gezeigt, wie sich die verschiedenen Zahlenbereiche von Unicode auf eine Bytefolge in UTF-8 abbilden. 7-Bit ASCII wird gemäß Zeile 1 der Tabelle, ISO 8859-1 gemäß Zeile 2 umgesetzt. Insgesamt lassen sich alle Codes von 0 bis (232 − 1) = FF FF FF FF16 = 429496729510 in UTF-8 darstellen. Unicode Bitnr

15

14

13

12

11

10

09

08

07

06

05

04

03

02

01

00

Unicode Bit:

0

0

0

0

0

0

0

0

1

1

0

1

1

1

1

1

Utf-8 Bit:

1

1

0

0

0

0

1

1

Utf-8 Bit:

1

0

0

1

1

1

1

1

Utf-8 Bitnr

07

06

05

04

03

02

01

00

Utf-8 Bitnr

07

06

05

04

03

02

01

00

Abbildung 3.2: Unicodes 8016 − 7FF16 : Doppelbytes in UTF-8

3.4 Fragen Codebereich (hexadezimal) 00000000-0000007F 00000080-000007FF 00000800-0000FFFF 00010000-001FFFFF 00200000-03FFFFFF 04000000-7FFFFFFF

45 UTF-8-Bytefolgen (Dualzahlen) 0xxxxxxx 110xxxxx 10xxxxxx 1110xxxx 10xxxxxx 10xxxxxx 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx

Tabelle 3.6: UTF-8 Bytefolgen

3.4 Fragen Benutzen Sie die ASCII-Tabelle auf Seite 39! Aufgabe 3.1 Gegeben sei der Inhalt von fünf Bytes im Speicher eines Computers: 1001000 1100001 1101100 1101100 1101111 Welchen Text repräsentieren diese fünf Bytes, wenn sie nach der ASCII-Codetabelle in Zeichen umgesetzt werden sollen? Aufgabe 3.2 Als Ergebnis einer Rechnung sind ganzzahlige Werte angefallen, die als 16 Bit-Worte im Speicher stehen. Diese Werte sollen ausgegeben werden. Dabei wird von der Intern- in eine Extern-Darstellung gewandelt, die auf dem ASCII-Code basiert. Geben Sie die Interndarstellungen sowie die zugehörigen Externdarstellungen an! Zahlen dual oder hexadezimal angeben! Wert Interndarstellung Externdarstellung höherwertiges Byte

niederwertiges Byte

1. Ausgabebyte

2. Ausgabebyte

1010 1016 −510 Aufgabe 3.3 Codieren Sie das Wort „müßig“ in ISO-8859-1 und in UTF-8! Geben Sie die Codes hexadezimal an! ISO 8859-1 UTF-8

4

Einführung in das Programmieren in C

Nachdem die grundlegenden Konzepte des Rechneraufbaus, der Informationsverarbeitung und der Darstellung von Information bekannt sind, wenden wir uns nun der Praxis der Programmierung zu. Der Rest des Buches dient dazu, die Grundkenntnisse für das Programmieren in der Programmiersprache C zu vermitteln. Außerdem werden wir beispielhaft einige Problemstellungen mit geeigneten Lösungsmöglichkeiten besprechen, die vor allem für Ingenieure von großer praktischer Bedeutung sind.

4.1 Zur Geschichte von C In Abbildung 4.1 ist die Entwicklung der höheren Programmiersprachen dargestellt. Die Programmiersprache C ist im Verhältnis zu den schnellen Innovationszyklen, denen die gesamte Branche der Datenverarbeitung unterliegt, heute schon eine relativ alte Sprache. C wurde bereits 1972 an den Forschungslaboratorien der amerikanischen Firma Bell entwickelt, eng verknüpft mit der Entwicklung des Betriebssystems Unix. Kernighan und Ritchie [Kernighan u. Ritchie (1978)] sind die Namen der maßgeblichen Entwickler von C. Der Abbildung 4.1 können wir auch entnehmen, dass zu den Vorgängern von C Sprachen wie Algol60 und PL/1 gehören, wovon für C insbesondere das Konzept der strukturierten Programmierung übernommen wurde. Allerdings finden sich in C auch einige Konzepte, die einer Assemblersprache nicht ganz unähnlich sind (etwa die Möglichkeit, Speicherwerte zu inkrementieren), was auf den Einsatz von C zur Programmierung von Betriebssystemen zurückzuführen ist. Die Sprache C wurde in mehreren Stufen standardisiert und fortgeschrieben: K&R ist der ursprüngliche Stand von [Kernighan u. Ritchie (1978)] C89, C90, ISO/IEC 9899, ANSI X3.159 schreiben C erstmals als Standard fest. Für diese Variante von C ist die Bezeichnung „ANSI-C“ [ANSI (1989)] weit verbreitet. Wichtige Erweiterungen waren Prototypen zu Funktionen (s. Kap. Unterprogramme), Erweiterungen des Präprozessors und die Einführung von „wide characters“, also Zeichen, die durch mehr als ein Byte codiert werden. C95 führt Verbesserungen und Fehlerbereinigungen des bisherigen Standes ein, insbesondere bezüglich der Behandlung von „wide characters“

4 Einführung in das Programmieren in C

48

Assembler Sprachen

Ursprünge

1940

EA, Ausdrücke, Funktionen

1950

strukturierte Programmierung

1960

Betriebssystemtauglichkeit, Modularisierung

1970

Objektorientierte Programmierung

1980

Internet Klassenbibl. Graph. Oberfl.

1990

VBasic

Application Frameworks

2000

VB.Net

2010

Fortran

Basic

Lambda Kalkül

Cobol

Algol60

PL/1

Pascal

Lisp

Simula

C

Smalltalk80

Perl

Python

PHP

Ruby

C++

Java Script

Java

C#

Go

Scala

Groovy

Abbildung 4.1: Stammbaum einer Auswahl wichtiger Programmiersprachen

Clojure

4.2 Erste Schritte

49

C99 [ISO/IEC (1999)]führt eine ganze Reihe von Erweiterungen und Verbesserungen ein – wir werden an verschiedenen Stellen in diesem Buch darauf hinweisen. Unter anderem wird erstmals mit C99 ein ganzzahliger Typ long long mit 64 Bit eingeführt. Aufbauend auf C wurden die Programmiersprachen C++ sowie JAVA entwickelt. Diese drei Sprachen zusammen haben heute die größte praktische Bedeutung. Dieses Buch behandelt ausschließlich die Sprache C und stellt damit unverzichtbare Grundlagen für das Erlernen objektorientierter Sprachen wie C++ und JAVA dar. Auch für den Einstieg in die Programmierung im Umfeld grafischer Bedienoberflächen (WindowsProgrammierung) – die in diesem Buch nicht behandelt wird – sind solide Kenntnisse der Sprache C unabdingbar. C ist grundsätzlich eine sog. höhere Programmiersprache. Höhere Programmiersprachen unterscheiden sich von den Hardware-orientierten Assemblersprachen in mehrfacher Hinsicht. Einmal müssen hochsprachliche Programme nicht als Folge einzelner Maschinenbefehle für eine spezielle Hardware eingegeben werden, sondern in einer von der Hardware unabhängigen fest vorgegebenen Form. Ferner stehen Möglichkeiten zur Verfügung, den Programmablauf auf einem hohen Abstraktionsniveau zu beschreiben. Schließlich ist die Verwaltung von Daten im Speicher einfacher. All diese Konzepte werden wir im Folgenden ausführlich besprechen.

4.2 Erste Schritte Programmieren zu lernen heißt, selbst Anwendungsprogramme zu schreiben. Der Umgang mit Standardsoftware ist uns heute bestens vertraut. Wir nutzen allenthalben Internetbrowser um uns im World-Wide Web umzusehen, elektronische Post zu versenden und zu empfangen, und um Informationen zu beschaffen. Ebenso nutzen wir Textverarbeitungsprogramme, um Briefe oder Berichte zu verfassen. All diese Programme wurden von Anwendungsprogrammierern entwickelt und so gestaltet, dass möglichst viele Anwender ihre unterschiedlichen Aufgaben damit erledigen können. Wenn wir selbst programmieren, dann wechseln wir gewissermaßen die Perspektive und schlüpfen selbst in die Rolle des Anwendungsprogrammierers. Um eigene WindowsAnwendungen schreiben zu können, ist erheblich mehr Wissen erforderlich, als in diesem Buch zur Verfügung gestellt werden kann. Dieses Buch ist als Material für eine einsemestrige Einführungsvorlesung konzipiert. Daher sind auch fast alle Beispiele für Ein- und Ausgabe im Textmodus gedacht. An einigen Stellen werden wir die Ausgabe einfacher Grafiken in Fenster benötigen. Dafür werden wir dann Rahmenprogramme bereit halten, die uns die eigentliche WindowsProgrammierung abnehmen und die Details verbergen, so dass wir uns auf die Grundlagen der Programmiersprache C beschränken können. Bevor wir in die Programmierung mit C einsteigen, wollen wir uns klar machen, was Programmieren bedeutet. Programmieren bedeutet zunächst, Programme zu formulieren, die von einem Computer ausgeführt werden können.

4 Einführung in das Programmieren in C

50

Jedes Programm besteht aus einer Folge von Anweisungen. Diese Anweisungen müssen so formuliert sein, dass sie vom Computer auch verstanden werden können. Dazu sind genaue Regeln einzuhalten, welche die Syntax1 der Sprache beschreiben. In den folgenden Kapiteln dieses Buches werden wir die Regeln für alle vorkommenden Anweisungen ausführlich behandeln. Die Syntaxdiagramme, die wir dazu benutzen, werden im Abschnitt 4.3 vorgestellt. Die Anweisungen eines Programms werden in einer genau definierten, vom Programmierer festzulegenden Reihenfolge ausgeführt. Ebenso wichtig, wie die Kenntnis der syntaktischen Regeln ist es daher, zu wissen wie die formulierten Anweisungen den Ablauf eines Programms beeinflussen. Man spricht von Semantik, der Lehre von der Bedeutung sprachlicher Zeichen. Betrachten wir als Beispiel ein Programm, das eine Näherung der Eulerschen Zahl e bestimmt, gemäß e

'

n P i=0

1 i!

Wir benötigen dazu zunächst Platz im Speicher des Computers, an dem wir den aktuellen Wert von i sowie den Wert der jeweils zu bestimmenden Fakultät speichern können, und einen Platz, an dem wir der Reihe nach den Kehrwert der Fakultät aufsummieren können. In einer höheren Programmiersprache wie C müssen wir uns nicht darum kümmern, wo dieser Platz im Speicher bereit gestellt wird. Vielmehr werden dazu Variablen verwendet. Variablen kann man sich vorstellen, wie Container, die Werte enthalten, welche sowohl ausgelesen und weiter verwendet werden, als auch geändert, d. h. mit neuen Werten überschrieben werden können. Diese Container müssen vor dem ersten Gebrauch durch sog. „Deklaration“ bestellt werden. Für unser Beispiel könnten die Variablendeklarationen folgendermaßen aussehen: 1 2

i n t i; d o u b l e e =1.0 , nFak = 1.0;

Dabei ist ein Name für die Variable anzugeben, sowie ein Typ, der darüber Auskunft gibt, welche Art von Daten gespeichert werden sollen. Insbesondere wird damit festgelegt, wie viele Bytes für die interne Darstellung verwendet und wie die zu speichernden Daten binär codiert werden (siehe dazu Kapitel über Informationsdarstellung). Wo die in den Variablen hinterlegten Werte im Speicher abgelegt werden, muss den Programmierer nicht interessieren: Die Werte werden im Arbeitsspeicher in der Interndarstellung gespeichert (s. Abb. 4.2), also binär codiert. Der Vorgang der Umwandlung zwischen binärer Codierung und Textform bleibt dem Programmierer verborgen. Genaueres über Variablen und deren Datentypen werden wir im Kapitel „Variablen und Konstanten“ lernen. 1 Vom

griechischen Wort syntaxis: Zusammenordnung, Lehre vom Satzbau.

4.2 Erste Schritte

Arbeitsspeicher

0xa004 6 0xa012 2.7180556 0xa01c 7.2e+2 ...

51 Variable - Name - Typ - Wert - Adresse

a int 6 0xa004

Variable - Name - Typ - Wert - Adresse

e double 2.7180556 0xa012

Variable - Name - Typ - Wert - Adresse

nFak double 7.2000E+2 0xa01c

Abbildung 4.2: Variablen im Arbeitsspeicher

In den Anweisungen des Programms können dann die Werte der Variablen benutzt oder verändert werden. In der folgenden Anweisung wird etwa zu dem bisherigen Wert von e der Kehrwert der aktuellen Wertes von nFak addiert: e = e + 1.0/nFak; Neben dem Manipulieren der Daten stellt die Sprache C Konstrukte zur Verfügung, die den Ablauf des Programms beeinflussen. Damit lassen sich bedingte Verzweigungen oder Laufschleifen formulieren. So muss zur Berechnung der Näherung für die Eulersche Zahl N mal hintereinander i! bestimmt und der Kehrwert zur aktuellen Näherung hinzu addiert werden (die Eins vom Term für i = 0 haben wir als Startwert für die Variable e eingesetzt): 1 2 3 4

f o r ( i =1; i

∗/

3 4

c o n s t i n t MwSt = 19;

// M e h r w e r t s t e u e r

5 6 7 8 9 10 11 12 13 14 15 16

i n t main () { int ReNr ; // Rechnungsnummer char Datum [16]; // R e c h n u n g s d a t u m d o u b l e MwStSumme = 0.0; // MwSt i n s g e s a m t d o u b l e ReSumme = 0.0; // Rechnungssumme char Kommando ; // p=n e u e r P o s t e n f= f e r t i g char ArtBez [16]; // A r t i k e l b e z e i c h n u n g int Anz ; // S t ü c k z a h l d o u b l e EzPreis ; // E i n z e l p r e i s d o u b l e GesPreis ; // G e s a m t p r e i s d o u b l e EzMwst ; // MwSt E i n z e l p o s t e n

17 18 19 20 21

/∗ U e b e r s c h r i f t u e b e r d i e R e c h n u n g ∗/ printf ( " ReNr . und Datum eingeben \ n " ); scanf ( " % d % s " , & ReNr , Datum ); printf ( " Rechnung % d vom % s \ n \ n " , ReNr , Datum );

22 23 24 25 26 27 28 29 30 31 32

do // S c h l e i f e b i s w h i l e ( . . . ) ; { printf ( " Stück Artikel Einzelpreis Kodo . eingeben \ n " ); scanf ( " % d % s % lf % c " , & Anz , ArtBez , & EzPreis , & Kommando ); /∗ B e r e c h n u n g e n f u e r a l l e P o s t e n ∗/ GesPreis = EzPreis * Anz ; EzMwst = GesPreis * MwSt / 100.0; GesPreis = GesPreis + EzMwst ; MwStSumme = MwStSumme + EzMwst ; ReSumme = ReSumme + GesPreis ;

33 34

/∗

35 36 37

e i n e Z e i l e f u e r j e d e n P o s t e n ∗/ printf ( " %4 d % -16 s %10.2 f " , Anz , ArtBez , EzPreis ); printf ( " %10.2 f \ n " , GesPreis ); } w h i l e ( Kommando != ’f ’ );

38 39 40 41 42

/∗ A b s c h l i e s s e n d e A u s g a b e n am Ende d e r R e c h n u n g ∗/ printf ( " = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = \ n " ); printf ( " %42.2 f \ n \ n " , ReSumme ); printf ( " Inklusive % d %% MwSt EUR %.2 f \ n " , MwSt , MwStSumme );

43

r e t u r n 0;

44 45

}

Abbildung 5.10: Beispielprogramm: Rechnung drucken

84

5 Grundelemente, Variablen, Konstanten, Datentypen

float . Und nicht nur das: das komplette Programm müsste durchgesehen werden, ob es nicht noch eine Variable für die Mehrwertsteuer gibt, die vom Typ int nach float umzustellen ist. Mit systematischen Änderungen von Datentypen ist oft zu rechnen, wenn man ein Programm auf eine andere Hardware-Plattform portieren will, wo die CPU mit einer anderen Wortbreite arbeitet. In solchen Fällen hat man es leicht, wenn der umzustellende Typ an einer einzigen Stelle im Programm geändert werden kann. Dies kann man erreichen, indem man eine Typdefinitions-Anweisung benutzt, z. B. t y p e d e f i n t MwstTyp ; Alle Variablen im Programm, die die Mehrwertsteuer aufnehmen sollen, werden dann mit diesem speziellen Typ für die Mehrwertsteuer deklariert, z. B. c o n s t MwstTyp MwSt = 19;

// M e h r w e r t s t e u e r

Wenn jetzt der Typ aller Mehrwertsteuer-Variablen auf float umgestellt werden soll, dann braucht man nur die Zeile mit der Typdeklaration zu ändern: typedef

f l o a t MwstTyp ;

Allgemein gilt für die Syntax einer Typdefinition: Typdefinition typedef

Typ

Bezeichner

;

,

+ riablen mit typedef wird ein neuer Bezeichner für einen Datentyp eingeführt. Vawerden erst in folgenden Deklarationen unter Benutzung des neuen Typ-Bezeichners deklariert. Beispiele: falsch typedef double DblTyp; DblTyp = 1.7;

richtig typedef double DblTyp; DblTyp dblVar; dblVar = 1.7;

Oft werden in C Deklarationen sehr komplex. Eine häufige Anwendung von Typdefinitionen ist, solche Fälle durch die Verwendung von Typnamen zu vereinfachen. Enum Stellen Sie sich vor, Ihr Chef gibt Ihnen ein Programm, aus dem der folgende Abschnitt stammt, mit den Worten „ändern Sie das Programm so, dass auch bei Schwarzweißbildschirm 80x25 zwei Zeilen ausgegeben werden! Außerdem sollte die Rückfrage in rot kommen!“

5.5 Elementare Datentypen

1 2 3 4 5 6 7 8 9

85

... i n t Vmod ; ... Vmod = GetVideoModus (); i f ( Vmod !=3) textcolor (2); cprintf ( " % s " , " Biosdaten schreiben ? " ); i f ( Vmod ==1) printf ( " \ n " ); cprintf ( " % s " , " j oder n eingeben " ); ...

Das Problem ist, dass es hier Codierungen des Videomodus und von Farben gibt, die durch die DOS-Bios-Umgebung vorgegeben sind. Aber was ist z. B. die Farbe mit Nummer 2? Ist 1 der Videomodus Schwarzweiß, 80x25? Das sieht nach einem längeren Ausflug in die Handbücher aus! Besser wäre ein Programm, das wenigstens einen Kommentar enthielte, z. B. 1 2 3

/∗ V i d e o m o d u s : SW80x25 =3 , F 4 0 x 2 5 =1 , F 8 0 x 2 5 =2 , K e i n =0∗/ /∗ C o l o r : b l a c k =0 , b l u e =1 , g r e e n =2 , c y a n =3 , r e d =4 , m a g e n t a =5 , brown =6 , l i g h t G r a y =7∗/

Jetzt könnte man sich helfen. Noch einfacher geht die Sache, wenn man die Bezeichner und Codierungen dem Compiler überlässt und im Programm dann nur noch mit Bezeichnern arbeitet. Das erreicht man durch Aufzählungstypen (enumerations): 1 2 3 4

enum Videomodus { SW80x25 =3 , F40x25 =1 , F80x25 =2 , Kein =0 }; enum Color { black =0 , blue =1 , green =2 , cyan =3 , red = 4 , magenta =5 , brown =6 , lightGray =7 };

enum Videomodus und enum Color sind damit als Typen deklariert, • die kompatibel zu int sind, deren Wertemenge aber nur die aufgezählten ganzzahligen Werte enthält • deren Literalkonstanten durch ihre Bezeichner angegeben werden dürfen Damit lässt sich das obige Programm so schreiben: 1 2 3 4 5 6 7 8

enum Videomodus Vmod ; ... Vmod = GetVideoModus (); i f ( Vmod != SW80x25 ) textcolor ( green ); cprintf ( " % s " , " Biosdaten schreiben ? " ); i f ( Vmod == F40x25 ) printf ( " \ n " ); cprintf ( " % s " , " j oder n eingeben " ); ...

86

5 Grundelemente, Variablen, Konstanten, Datentypen

Jetzt werden Sie mühelos erraten, wie man das Programm den Wünschen des Chefs anpassen kann. Die generelle Syntax von Aufzählungstypen ist in Abb. 5.11 dargestellt. Mit einer Aufzählungsdeklaration kann man zwei Kategorien von Dingen gleichzeitig deklarieren: • Datentypen mit der Bezeichnung enum EnumBez (Zweig 1 in Abb. 5.11) • Variablen zum Typ enum EnumBez mit der Bezeichnung VariablenBez (Zweig 2 in Abb. 5.11) Für EnumBez und VariablenBez ist natürlich der jeweilige Bezeichner einzusetzen. In unserem Beispiel sind nur Datentypen, nämlich enum Videomodus und enum Color deklariert worden. Eine Variable wird im obigen Beispiel weiter unten mit der Deklaration enum Videomodus Vmod; eingeführt. Beides könnte man in einer einzigen Deklaration erledigen: Aufzaehlungsdeklaration enum 1

EnumBez

EnumWert

{

} KonstAusdr

= ,

; 2

VariablenBez =

KonstAusdr

, Abbildung 5.11: Syntaxdiagramm Aufzählungsdeklaration

5.6 Fragen

87

enum Videomodus { SW80x25 =3 , F40x25 =1 , F80x25 =2 , Kein =0} Vmod ;

Falls kein Initialisierer (= KonstAusdruck) angegeben ist, erhält der EnumWert den Wert seines linken Nachbarn plus eins. Dem ersten EnumWert wird bei Fehlen eines Initialisierers die Zahl 0 zugeordnet. Damit hätten wir einfacher schreiben können:

1 2 3

enum Videomodus { Kein , F40x25 , F80x25 , SW80x25 }; enum Color { black , blue , green , cyan , red , magenta , brown , lightGray };

Enum-Typen findet man sehr häufig in professionellen Programmen. Das liegt vor allem daran, dass ein Profi weiß, dass ihm früher oder später sein Programm wieder begegnet (oder einem anderen Programmierer, was noch schlimmer ist!). Daher versucht er, Ergebnisse aus Suchvorgängen in Handbüchern möglichst im Programmcode zu konservieren. Ein weiteres Beispiel für die Verwendung von enum-Typen finden Sie im Abschnitt 12.2.2 über die Implementierung von endlichen Automaten auf Seite 179.

5.6 Fragen Markieren Sie alle Fehler in den folgenden Programmfragmenten und beschreiben Sie rechts in Stichworten den Fehlergrund. R int rotg uen, union, spd; int mo, di, mi, do, fr, sa, so; int C220TDI, 530i; int Klassen, 1a, 1b, 3c; double _e, pi, 2Pi, -1, Null;

Richtig oder falsch? Tragen Sie 3 oder 7 in die rechte Spalte ein! 0xa 941 0xFaul -1x0 +037777777777UL -0LL

88

5 Grundelemente, Variablen, Konstanten, Datentypen

Geben Sie an, welcher Datentyp zum Wert auf der linken Seite der Tabelle passt. Wert passender Datentyp Familienname Wie viele Zahlen einen Lottotip ergeben Körpertemperatur Wie viele verschiedene Lottotips es gibt sin(x) Anfangsbuchstabe Ihres Vornamens extrem genaue astronomische Entfernung Zahl der Stockwerke eines Einfamilienhauses

5.7 Aufgaben Aufgabe 5.1 Gegeben ist unten das Fragment eines Programms, das die Exponentialreihe gemäß nebenstehender Formel bis zum Glied n berechnet.

en =

n P i=0

1 i!

a) Ergänzen Sie die Zeilen 5-6 um die Deklaration der ganzzahligen Variablen b) Ergänzen Sie die Zeilen 8-9 um die Deklaration der Gleitpunktvariablen mit hoher Genauigkeit, Initialwerte jeweils 1.0 1

#i n c l u d e < stdio .h >

2 3 4

i n t main () {

5 6 7 8 9 10

printf ( " Berechnen bis n =?\ n " ); scanf ( " % d " , & n );

11 12 13

f o r ( i =1; i i n t main () { int i = -1; l o n g l o n g ll = 0 x180000000LL ; f l o a t f = 1.0; printf ( " % d % d \ n " , i ); printf ( " % d \ n " , f ); printf ( " % u \ n " , i ); printf ( " % d \ n " , ll ); r e t u r n 0; }

Die Variable ll hat den Wert 18000000016 , also einen höheren, als der höchste mit int darstellbare Wert2 ; daher wird Typ long long verwendet. Zeile 7 Fehler: nur ein Argument Zeile 8 Fehler: Argument vom falschen Typ Zeile 9 Fehler: Vorzeichenlose Zahl erwartet, gibt 4294967295 aus statt korrekt -1 Zeile 10 Fehler: %d statt %lld: falsche longStufe; Ausgabe -2147483648

Weitere Ausgabefunktionen finden sich im Kapitel über die Standardbibliothek.

6.2 Formatierte Eingabe Für die formatierte Eingabe steht die Funktion scanf zur Verfügung, die wir im vorigen Kapitel schon verwendet haben: 2 für

eine ILP32LL-Plattform

6.2 Formatierte Eingabe

95 1

z }| { scanf(" %d %s ", &ReNr ,Datum); |{z} |{z} | {z } | {z } 2

3

2

3

Der Formatstring (1 ) gibt wiederum an, welche Werte eingelesen werden sollen. Das Syntaxdiagramm zur Bildung der Formatelemente (2 ) ist in Abbildung 6.3 dargestellt. Die wichtigsten Formatelemente stimmen mit denen für die Ausgabe überein (zumindest %c, %s, %d und %f). Als Argumente (3 ) sind Adressen anzugeben, d. h. bei Variablen ist der Operator „&“ voran zu stellen. Eine Ausnahme bilden Zeichenketten (im Beispiel: Datum). Der Name der Zeichenkette wird automatisch in die Anfangsadresse gewandelt (siehe Kapitel Pointer). Anzahl und Typ der Argumente muss wie bei printf zu den Formatelementen passen.

FormatelementEingabe % Zuweisungsunterdrueckung

*

Breite

gDezz

Longstufe hh h l ll L

Umwandlung

Uganzzahlig Ugleitpunkt Uzeichen

Abbildung 6.3: Syntaxdiagramm für ein Formatelement zur formatierten Eingabe

96

6 Formatierte Ein- und Ausgabe

Beim Lesen der Eingabe werden Leerzeichen, Tabulatoren oder Zeilenvorschübe (return) verwendet um mehrere Eingaben zu trennen. Auch Zeichenketten werden so getrennt, d. h. dass mit dem Format %s nur bis zu einem auftretenden Leerschritt oder Tabulator aber längstens bis zum Zeilenende gelesen wird. Eine Ausnahme bildet das Format %c zum Einlesen eines Zeichens. Mit diesem Formatelement werden auch „white space“-Zeichen gelesen und der betreffenden char- oder wchar_t-Variablen zugewiesen. Text, der im Formatstring vorkommt wird exakt so in der Eingabe erwartet3 . Zuweisungsunterdrückung Wenn ein * angegeben ist, wird die Zuweisung des entsprechenden Eingabefeldes an ein Argument unterdrückt. Es wird also „überlesen“. Breite Die Breite wird als Dezimalzahl angegeben. Diese Zahl gibt an, wie viele Zeichen maximal für das Formatelement eingelesen werden. Longstufe Hier gibt es hh, h, l, ll und L wie bei der formatierten Ausgabe, Abschnitt 6.1.1 auf Seite 92. Die Bedeutungen für scanf sind hh h l

ll L

Für die Umwandlungszeichen d, i, o, u, x, oder X. hh bedeutet, dass das Argument vom Typ char oder unsigned char ist. Für die Umwandlungszeichen d, i, o, u, x, oder X. h bedeutet, dass das Argument vom Typ short oder unsigned short ist. Für die Umwandlungszeichen d, i, o, u, x, oder X bedeutet l, dass das Argument vom Typ long oder unsigned long ist. Für die Umwandlungszeichen e, E, f, F, g, oder G bedeutet es, dass es sich um ein double-Argument handelt Für das Umwandlungszeichen c bedeutet es, dass es sich um einen wchar_t bzw. wint_t Typ handelt. Für das Umwandlungszeichen s bedeutet es, dass es sich um eine Zeichenkette von wchar_t-Zeichen handelt. Das Argument ist ein Pointer auf das erste Zeichen dieser Zeichenkette. Für die Umwandlungszeichen d, i, o, u, x, oder X bedeutet ll, dass das Argument vom Typ long long oder unsigned long long ist. Für die Umwandlungszeichen e, E, f, F, g, oder G bedeutet L, dass das Argument vom Typ long double ist.

Ohne Angabe wird angenommen, dass bei Umwandlungen mit den Elementen d, i, o, u oder x das Argument eine Variable mit dem Platzbedarf von int ist. Fehlt die Angabe bei Umwandlungen mit e, f oder g, wird eine Variable von der Stufe float angenommen. Analog wird bei c oder s vom Basistyp char ausgegangen. 3 Dies bedeutet insbesondere, dass scanf("Bitte geben Sie Ihren Namen ein %s", name); nicht die Wirkung des Input-Befehls von Basic hat, den Text auszugeben und dann den Wert einzulesen!

6.2 Formatierte Eingabe

97

+ muss Wenn die Größe der Variablen nicht mit diesen Annahmen übereinstimmt, man die Longstufe unbedingt angeben! Andernfalls bekommt man beim Einlesen mit scanf unsinnige Werte. Beispiele sind short , double, long long oder wchar_t Argumente. Nicht angeben muss man eine Longstufe z.B. für Argumente vom Typ int , float oder char. Umwandlungszeichen Für die Eingabe gibt es die Umwandlungen d, i, o, u, x, e, f, g, c oder s, die wir schon bei der formatierten Ausgabe in Abschnitt 6.1.1 auf Seite 92 beschrieben haben hier wird allerdings in Eingaberichtung, also von Externdarstellung in Interndarstellung umgewandelt. Bei der Eingabe gibt es keinen Unterschied zwischen Umwandlungszeichen als Großoder Kleinbuchstaben.

6.2.1 Beispiel zur formatierten Eingabe: scanf("%d %s",&ReNr | {z }, Datum | {z }); 1

2

Die Adresse der Variable ReNr wird mit dem &-Operator gebildet (1 ). Bei ZeichenkettenVariablen (2 ) (Strings) ist der Adress-Operator nicht erforderlich.

6.2.2 Besonderheiten und Fehlerquellen: • scanf geht bei Gleitpunktzahlen von der Eingabe eines Punktes zwischen dem ganzzahligen und dem gebrochenen Anteil aus (kein Komma). • Nicht gelesene Zeichen bleiben in einem Eingabepuffer stehen und werden beim nächsten Aufruf von scanf gelesen: Anweisung Tastatureingabe für i gelesener Wert scanf("%2d", &i); 123 12 scanf("%d", &i); 3 • Es wird insbesondere nicht überprüft, ob die erfolgte Eingabe dem erwarteten Format entspricht. Kann das eingegebene Zeichen nicht interpretiert werden, so wird keine Fehlermeldung erzeugt (wie z. B. in Pascal). Soll etwa eine ganze Zahl eingelesen werden (scanf("%d", &i);) und gibt der Benutzer des Programms einen Text ein, so wird nichts gelesen. Der Wert der Variablen i ist danach undefiniert. Anweisung Tastatureingabe gelesener Wert scanf("%d %d", &i, &j); 1x2 i: 1, j nicht gelesen, der Rückgabewert von scanf ist 1 scanf("%c", &c); c: ’x’ Am Rückgabewert der scanf-Funktion kann festgestellt werden, wie viele Formatfelder tatsächlich gelesen wurden:

98

6 Formatierte Ein- und Ausgabe

1 2

i f ( scanf ( " % d % d " , &i , & j ) != 2) printf ( " Fehler bei der Eingabe ...\ n " );

• Das Format %s ist mit Vorsicht zu genießen, da keine Prüfung stattfindet, ob die als Argument angegebene Zeichenkettenvariable genügend Platz bietet, um die Eingabe aufzunehmen. Abhilfe siehe im Kapitel über Dateien. • Eine Möglichkeit festzustellen, ob etwas gelesen wurde, ist der Rückgabewert der scanf()-Funktion. Dieser gibt nämlich an, wieviele Formatelemente korrekt gelesen wurden. • Die im folgenden Programmfragment angegebenen typischen Programmierfehler bei der Verwendung von scanf() führen fast sicher zum Absturz des Programms. Warum das so ist, wird in den Kapiteln über Funktionen und über Pointer klar werden. int i; double d; scanf("%d"); Argument vergessen scanf("%d", i); Adressoperator (&) vergessen scanf("%f", d); long-Angabe vergessen (%lf) Weitere Eingabefunktionen finden sich im Kapitel über die Standardbibliothek.

6.3 Aufgaben Aufgabe 6.1 Welche Ausgabe gehört zu welcher Programmzeile? Das Symbol ’ ’ steht dabei für ein Leerzeichen. Programm 1 2 3 4 5 6 7 8 9 10 11 12 13

#i n c l u d e < stdio .h > i n t main (){ i n t i =4711; c h a r c = ’x ’; f l o a t f =123.; printf ( " % d \ n " , i ); printf ( " %10 d \ n " , i ); printf ( " % d \ n " , c ); printf ( " % c \ n " , c ); printf ( " % f \ n " , f ); printf ( " % e \ n " , f ); printf ( " %10.3 f \ n " , f ); printf ( " %0.3 E \ n " , f ); r e t u r n 0; }

Ausgabe x 1.230000e+02 4711 123.000 123.000000 4711 1.230E+02 120

7

Operatoren und Ausdrücke

In diesem Kapitel lernen wir das wichtigste Handwerkszeug kennen, um Berechnungen durchführen und Daten manipulieren zu können: Operatoren und Ausdrücke. Ausdrücke entstehen durch die Verknüpfung von Operanden mittels Operatoren bzw. die Anwendung von Funktionen auf ihre Argumente. Die erste höhere Programmiersprache, die dem Programmierer eine an mathematische Formeln angelehnte Schreibweise hierfür erlaubte, war Fortran. Seither gibt es kaum eine höhere Programmiersprache, die diesen Komfort nicht bietet. Ausdrücke kommen an sehr vielen Stellen in einem C-Programm vor. Daher ist es wichtig, genau zu wissen, was unter einem Ausdruck zu verstehen ist. Ausdrücke sollten z.B. nicht mit Anweisungen verwechselt werden (vgl. z.B. „AusdrucksAnweisung“ in Kapitel 10 „Kontrollstrukturen“).

7.1 Ein erstes Beispiel Als Beispiel wollen wir einen Ausdruck betrachten, der eine der Nullstellen eines quadratischen Polynoms ax 2 + bx + c bestimmt, nach der Formel

x1/2 =

−b ±



b2 − 4ac . 2a

Das folgende Programm löst die Aufgabe. Es enthält einen Ausdruck, der eine der Nullstellen des Polynoms 4x 2 + 5x + 1 berechnet und ausgibt. Es wird dabei noch nicht untersucht, ob es wirklich eine reelle Lösung gibt. 1 2 3 4 5 6 7 8 9

#i n c l u d e < stdio .h > #i n c l u d e < math .h > i n t main () { d o u b l e a = 4. , b = 5. , c = 1. , x ; x = ( - b + sqrt ( b * b - 4. * a * c )) / (2. * a ); printf ( " % f \ n " , x ); r e t u r n 0; }

100

7 Operatoren und Ausdrücke

Für die folgenden Betrachtungen interessiert uns insbesondere die Zeile 6 aus dem Beispielprogramm. Dieses Beispiel enthält bereits alle Bestandteile, aus denen Ausdrücke zusammengesetzt werden können: x = (|{z} - b + sqrt(b * b - 4. * a * c)) / (|{z} 2. *|{z} a ) |{z} | {z } 1

2

4

3

5

Im Beispielausdruck sind 1 und 5 Variablennamen, 2 ist ein einstelliger Operator1 zur Umkehr des Vorzeichens von b, 3 ist die Anwendung der Funktion sqrt („Sqare Root“, Quadratwurzel) auf ihr Argument und 4 ist eine Literalkonstante.

7.1.1 Syntax von Ausdrücken Abbildung 7.1 zeigt, wie Ausdrücke gebildet werden. Der Übersichtlichkeit halber sind nur die für unser Beispiel wichtigen Zweige enthalten. Wir werden in den folgenden Abschnitten weitere Möglichkeiten kennen lernen – hier interessieren wir uns erst einmal für das Prinzip der Ausdrucks-Syntax. Ausdruck 1 2 3 4 5 6

Variablenname Literalkonstante Operator1stellig

Ausdruck

Ausdruck

Operator1stellig

Ausdruck

Operator2stellig

Funktionsname

Ausdruck

(

) Ausdruck ,

7

(

Ausdruck

)

Abbildung 7.1: Syntaxdiagramm für Ausdrücke (vereinfacht)

1 Die Stelligkeit eines Operators gibt an, wie viele Operanden er hat. Das Minuszeichen als Vorzeichen hat einen Operanden und ist daher einstellig; Minus als Subtraktionszeichen hat 2 Operanden und ist zweistellig

7.1 Ein erstes Beispiel

101

Elementare Bestandteile sind Variablennamen (1 ) und Literalkonstanten (2 ). Durch Verknüpfung mittels Operatoren (3 -5 ) oder die Anwendung von Funktionen (6 ) entstehen komplexere (Teil-) Ausdrücke. Operatoren unterscheiden sich dabei nicht grundsätzlich von Funktionen. Lediglich die Schreibweise ist anders: • Einstellige Operatoren stehen vor dem Operanden (Syntaxdiagramm, Zweig 3 ) oder dahinter (Zweig 4 ), zweistellige zwischen den Operanden (Syntaxdiagramm, Zweig 5 ). • Funktionen (Syntaxdiagramm, Zweig 6 ) stehen vor der Argumentliste, welche die Argumente zwischen Runden Klammern als Teilausdrücke durch Komma getrennt enthält. Die Operanden von Operatoren oder Argumente von Funktionen dürfen beliebig komplexe Teilausdrücke sein. Die Zuordnung von Teilausdrücken als Operanden zu den Operatoren wird eindeutig gemacht durch • Regeln, die die Priorität2 von Operatoren festlegen (z.B. „Punkt vor Strich“, also multiplikative Operatoren höher prior als additive (siehe Abb. 7.2 S. 102) • Regeln, die die Assoziativität3 festlegen, also ob bei aufeinander folgenden gleichprioren Operatoren von links oder von rechts zusammengefasst wird (Abb. 7.2). • die Möglichkeit, Klammern zu setzen (Syntaxdiagramm, Zweig 7 ). Die Abbildung 7.3 zeigt, wie in unserem Beispiel Teilausdrücke als Operanden den Operatoren zugeordnet werden. Dargestellt sind Variablennamen Literalkonstanten Zwischenergebnisse Operatoren bzw. Funktionen

als als als als

Rechtecke mit durchgezogener Linie Achtecke Rechtecke mit gestrichelter Linie Kreise oder Ellipsen

Wenn man – ausgehend von den Variablen und Literalkonstanten – den Pfeilen folgt, sieht man in welchen Operator die betreffende Größe als Operand eingeht.

7.1.2 Auswertung von Ausdrücken Die Auswertung von Ausdrücken erfolgt zur Laufzeit des Programms. Ein Ausdruck wird ausgewertet, wenn im Programm die Anweisung ausgeführt wird, in welcher der entsprechende Ausdruck vorkommt. Die Auswertung wird durch den Code ausgeführt, den der Compiler aus dem Ausdruck erzeugt. Um den Code zu erzeugen, erstellt sich der Compiler als Hilfsmittel eine Datenstruktur mit Informationen, wie sie auch in Abb. 7.3 dargestellt sind. Aus dieser Struktur erfolgt 2 Die Priorität gibt an, welche Operatoren stärker binden (kleinere Nummer bedeutet stärkere Bindung). 3 Die Assoziativität gibt an, ob aufeinander folgende gleichpriore Operatoren von links nach rechts (lr) oder von rechts nach links (rl) zusammengefasst werden.

102

7 Operatoren und Ausdrücke

Prio 1 2

Ass

3

rl

4 5 6 7 8 9 10 11 12 13 14 15

rl lr lr lr lr lr lr lr lr lr lr rl

16

rl

17

lr

lr

Operatoren ( ) [ ] -> . Fu() ++ -! ~ ++ -+ - * & sizeof (Typ) * / % + > < >= == != & ^ | && || ? : = += -= *= /= %= = &= |= ^= ,

Bezeichnung Klammern Vektor, Strukturauswahl, Funktionsaufruf, Inkrement (postfix), Dekrement (postfix) Negation, Einskomplement, Inkrement (präfix), Dekrement (präfix), Vorzeichen + -, Dereferenzierung, Adressbildung, Größe Typecast Multiplikation, Division, Modulus Addition, Subtraktion Linksshift, Rechtsshift kleiner, kleiner gleich, größer, größer gleich gleich, ungleich bitweises Und bitweises exklusives Oder bitweises Oder logisches Und logisches Oder konditionaler Operator Zuweisung, zusammengesetzte Zuweisungsoperatoren

Kommaoperator

Abbildung 7.2: Operatoren nach Priorität und Assoziativität

die Codeerzeugung dann so, dass im entstehenden Code folgende Bedingungen erfüllt sind • Für Namen von Variablen und Konstanten werden Anweisungen generiert, die den Wert der Größe aus dem Speicher holen. • Für Literalkonstanten werden Anweisungen generiert, die den durch die Literalkonstante dargestellten Wert liefern. • Der Code für einen Operator4 enthält die Anweisungen, die die Verknüpfung der Operanden-Werte berechnen, die dem Operator entspricht. • Die Reihenfolge der Anweisungen ist so, dass der Code für einen Operator erst durchlaufen wird, wenn vorher bereits die Anweisungen seiner Operanden-Teilausdrücke ausgeführt sind. In Abbildung 7.3 ist zu erkennen, dass Zwischenergebnisse (gestrichelte Rechtecke) gespeichert werden müssen, bis andere Teilergebnisse vorliegen. Ähnlich müsste vorgegangen werden, wenn der Ausdruck von Hand auszuwerten wäre. 4 Funktionsaufrufe

wie sqrt(...) betrachten wir in diesem Kontext wie einstellige Operatoren

7.1 Ein erstes Beispiel

103

4.

a ∗

b

b

4a





b2

4ac

c

− b2 − 4ac

b

√ √



b2 − 4ac

2.

−b + −b +



a ∗ 2a

b2 − 4ac / −b +



b2 −4ac 2a

= x Abbildung 7.3: Zuordnung von Teilausdrücken als Operanden zu den Operatoren für den Ausdruck x = (-b + sqrt(b * b - 4. * a * c)) / (2. * a)

104

7 Operatoren und Ausdrücke

Es wird hier sehr deutlich, wie komfortabel sich mit Ausdrücken umfangreiche Berechnungen formulieren lassen, ohne dass sich die Programmiererin um die Details der Auswertung kümmern muss. Die Instruktionen, die für den Transfer oder die Verknüpfung von Werten erzeugt werden müssen, hängen stark vom Typ der Daten ab. Für die Generierung der korrekten Anweisungen benötigt der Compiler daher vollständige Kenntnis über die Typen aller Teilausdrücke. Jeder (Teil-)Ausdruck besitzt nicht nur einen Wert, den der generierte Code erzeugen soll, sondern auch einen Typ, für den gilt: • Für eine Variable gilt der Typ, der in ihrer Deklaration angegeben ist. • Der Typ von Literalkonstanten ist durch ihre Syntax festgelegt. • Der Typ für ein (Zwischen-)Ergebnis, das ein Operator liefert, hängt vom Operator ab. In den folgenden Abschnitten wird es beim jeweiligen Operator angegeben. Die Typen der Ergebnisse von Funktionen werden im Kapitel 9 „Standardbibliothek“ aufgeführt. In Abbildung 7.4 sind – etwas vereinfacht – die vom Compiler generierten Befehle (sog. Assembler-Code) gezeigt, die vom Prozessor schließlich abgearbeitet werden, wenn der Ausdruck aus Abschnitt 7.1 auf Seite 99 bzw. Abbildung 7.3 auf Seite 103 ausgewertet wird. fld qword ptr [b] fmul qword ptr [b] fld dword ptr DATA:s@ fmul qword ptr [a] fmul qword ptr [c] fsub sub sp,8 fstp qword ptr [bp-42] fwait call far ptr _sqrt add sp,8 fld qword ptr [b] fchs fadd fld dword ptr DATA:s@+8 fmul qword ptr [a] fdiv fstp qword ptr [x]

b2 berechnen: b laden und mit b multiplizieren Konstante 4 laden und mit a und c multiplizieren Differenz b2 − 4ac bilden Argument für die Wurzelfunktion auf den Stack legen und Funktion sqrt aufrufen. b laden Vorzeichen umkehren -b addieren Konstante 2 laden, mit a multiplizieren und dividieren Ergebnis als x speichern

Abbildung 7.4: Vom Compiler generierter Assembler-Code für den Ausdruck x = (-b + sqrt(b * b - 4. * a * c)) / (2. * a)

7.2 Arithmetische Ausdrücke

105

7.2 Arithmetische Ausdrücke In C gibt es die üblichen arithmetischen Operatoren für die vier Grundrechenarten. Der Typ eines arithmetischen Ausdrucks (d.h. der Typ des Ergebnisses der Auswertung) hängt von den Typen der beteiligten Operanden ab. Folgende Tabelle gibt eine Übersicht über die Ergebnistypen arithmetischer Ausdrücke mit zwei Operanden für die Grundrechenarten +, -, * und /:

1. Operand

ganzzahliger Typ GleitpunktTyp

2. Operand ganzzahliger GleitpunktTyp Typ ganzzahliges GleitpunktErgebnis Ergebnis GleitpunktGleitpunktErgebnis Ergebnis

Sobald einer der beteiligten Operanden eine Gleitpunktzahl ist, so gilt dies auch für das Ergebnis. Zu beachten ist allerdings, dass das Ergebnis von Operationen mit ganzen Zahlen wieder eine ganze Zahl ist. Dies ist insbesondere bei der Division von Bedeutung, welche den ganzzahligen Anteil des Quotienten liefert: 1 2

f l o a t f = 9/4; printf ( " % f \ n " , f );

druckt 2.00000 aus. Dabei wird die Operation ganzzahlig durchgeführt und erst danach in eine Gleitpunktzahl gewandelt. Die zunächst intuitiv erwartete Zahl 2.2500 erhält man durch die Angabe eines Operators als Gleitpunktzahl, also im Fall der Angabe einer Konstante durch Anfügen des Dezimalpunkts 1 2

f l o a t f = 9./4; printf ( " % f \ n " , f );

oder durch explizite Typumwandlung mit dem cast-Operator ’(Typ)’, die wir später im Kapitel 23.2.1 S. 329 behandeln werden: 1 2

f l o a t f = ( f l o a t )9/4; printf ( " % f \ n " , f );

Darüber hinaus gibt es den Operator ’%’ zur Bildung des Divisionsrestes (Modulus) ganzer Zahlen: 1 2 3

i n t dividend = 9 , divisor = 4; printf ( " % d = % d mal % d Rest % d \ n " , dividend , divisor , dividend / divisor , dividend % divisor );

Durch die printf-Anweisung in den Zeilen 2 und 3 wird ausgegeben: 9 = 2 * 4 Rest 1. Kommen mehrere Operatoren in einem Ausdruck vor, so muss festgelegt sein, welche Operation zuerst, d. h. mit höchster Priorität ausgeführt wird. Insgesamt gibt es in

106

7 Operatoren und Ausdrücke

C 15 Prioritätsstufen. Zum Nachschlagen befindet sich eine Vorrangtabelle im hinteren Einbanddeckel dieses Buches.

+ Espow(...) gibt in C keinen Operator zum Potenzieren. Stattdessen kann die Funktion benutzt werden, die im Kapitel 9.4 „Standardbibliothek“ S. 132 beschrieben ist.

7.3 Der Zuweisungsoperator Der Zuweisungsoperator ist der erste Operator, den wir kennen lernen, der einer Variablen einen neuen Wert zuweisen kann. Das Syntaxdiagramm einer Zuweisung ist einfach: Wertzuweisung Variable

=

Ausdruck

Dabei müssen die Datentypen auf beiden Seiten verträglich sein. Idealerweise stimmt der Typ des Ausdrucks auf der rechten Seite mit dem Typ der Variablen überein. Wenn dies nicht der Fall ist, so kann es immer noch sein, dass der Typ automatisch angepasst wird. Es ist zum Beispiel kein Problem einen ganzzahligen Wert an eine GleitpunktVariable zuzuweisen. Der umgekehrte Fall ist jedoch nicht mehr in jedem Fall problemlos möglich. Im Gegensatz zu den meisten anderen Programmiersprachen ist in C die Zuweisung ein Operator. Damit wird jede Zuweisung ein Ausdruck und kann überall dort stehen, wo Ausdrücke stehen dürfen, auch wiederum auf der rechten Seite einer Zuweisung. Der Wert eines solchen Zuweisungsausdrucks ist die Variable, die den neuen Wert erhalten hat. Damit sind verkettete Zuweisungen möglich, die teilweise allerdings zu unübersichtlichen Programmen führen können: 1 2

i = j = k = 0; /∗ d a s i s t n o c h g a n z ü b e r s i c h t l i c h ∗/ i = j / ( k = 3); /∗ d i e s i s t n i c h t mehr ü b e r s i c h t l i c h ∗/

7.4 Zusammengesetzte Operatoren Für alle arithmetischen Operatoren gibt es eine weitere Variante von Operatoren, die neben der arithmetischen Operation noch eine Zuweisung ausführen. Diese können oft verwendet werden, wenn die Variable, an welche die Zuweisung erfolgt, auch auf der rechten Seite der Zuweisung steht. Die allgemeine Schreibweise für diese zusammengesetzten Operatoren lautet:

7.5 Unitäre arithmetische Operatoren

107

ZusammengesetzteOperatoren Variable

Operator

=

Ausdruck

damit können folgende Schreibweisen abgekürzt werden: Variable Variable Variable Variable

= = = =

Variable Variable Variable Variable

+ * /

Ausdruck Ausdruck Ausdruck Ausdruck

Variable Variable Variable Variable

+= -= *= /=

Ausdruck Ausdruck Ausdruck Ausdruck

Der Ausdruck wird dabei implizit geklammert, also x *= y - 4 ist äquivalent zu x = x * (y-4) etc.

7.5 Unitäre arithmetische Operatoren Bisher haben wir Operatoren für zwei Operanden besprochen. Es gibt auch Operatoren für nur einen Operanden. Dabei sind zunächst die Operatoren zur Angabe von Vorzeichen: ’+’ und ’-’ zu nennen. Ferner gibt es die wichtigen Operatoren ’++’ und ’--’ um den Wert einer Variablen um eins zu erhöhen (inkrement), oder zu erniedrigen (dekrement). Wird der jeweilige Operator vor die Variable gestellt, so wird die entsprechende Operation ausgeführt, bevor der Wert der Variablen für die weitere Auswertung des Ausdrucks verwendet wird (prä-inkrement bzw. prä-dekrement). Wird der Operator hinter die Variable gestellt, so wird die Operation danach ausgeführt (post-inkrement bzw. post-dekrement). Neben dem Zuweisungsoperator sind dies die einzigen Operatoren, die den Wert einer Variablen verändern können. Beispiele: 1 2 3 4 5 6 7 8

i n t i = 3; printf ( " % d \ n " , i = 3; printf ( " % d \ n " , i = 3; printf ( " % d \ n " , i = 3; printf ( " % d \ n " ,

++ i ); /∗ d r u c k t 4 , i h a t d a n a c h d e n Wert 4 ∗/ i ++); /∗ d r u c k t 3 , i h a t d a n a c h d e n Wert 4 ∗/ --i ); /∗ d r u c k t 2 , i h a t d a n a c h d e n Wert 2 ∗/ i - -); /∗ d r u c k t 3 , i h a t d a n a c h d e n Wert 2 ∗/

Anwendung insbesondere bei Laufschleifen, siehe Kapitel 10 „Kontrollstrukturen“. Es ist nur garantiert, dass diese „Seiteneffekte“ nach Auswertung des kompletten Ausdrucks berücksichtigt sind. Wird ein Objekt mehrfach verändert, oder verändert benutzt, so ist das Verhalten der Seiteneffekte undefiniert, wie etwa bei: 1 2

i = i ++; printf ( " % d \ n " , i ++ * i ++);

108

7 Operatoren und Ausdrücke

Undefiniert heißt, dass zwar ein Wert geliefert wird, dieser aber vom jeweiligen Compiler abhängt.

7.6 Der Kommaoperator Ausdrücke können durch Kommas voneinander abgetrennt werden. Dadurch entsteht ein neuer Ausdruck. Auswertung erfolgt von links nach rechts. Typ und Wert dieses Ausdrucks ist durch den letzten durch Komma getrennten Ausdruck gegeben. 1 2

a = 3 , b = 4 , c = 5; x = ( a = 3 , b = 5 , a * b );

Die Hauptanwendung des Kommaoperators, nämlich die Initialisierung mehrerer Variablen in einer for-Anweisung, werden wir im Kapitel 10 „Kontrollstrukturen“ behandeln.

7.7 Wahrheitswerte und logische Ausdrücke Oft ist es wichtig, Bedingungen zu überprüfen, und den weiteren Programmablauf vom Ergebnis solcher Prüfungen abhängig zu machen. Soll etwa die Steuerung für eine Heizungsanlage mit einem Mikrocontroller realisiert werden, so ist das Ein- und Ausschalten des Brenners von mehreren Bedingungen abhängig, die ständig überprüft werden müssen. Diese Bedingungen sind beispielsweise die Außentemperatur, die aktuelle Wassertemperatur im Kessel, oder der Sollwert der Vorlauftemperatur im Heizkreislauf. Wie solche bedingten Verzweigungen programmiert werden, ist Gegenstand des Kapitels 10 „Kontrollstrukturen“. Hier soll zunächst dargestellt werden, wie sich Bedingungen formulieren lassen. Wahrheitswerte werden benötigt, um anzugeben oder zu prüfen, ob gewisse Bedingungen erfüllt sind oder nicht, und um davon abhängig entsprechend zu reagieren. Zur Darstellung von Wahrheitswerten wird in C der Typ int verwendet. Dabei gelten folgende Regeln: • Jeder in einem logischen Ausdruck angegebene von Null verschiedene Wert wird als wahr interpretiert. Die Null wird als falsch interpretiert. • Jeder Vergleich in C ist ein Ausdruck vom Typ int , der den Wert 1 (wahr) oder 0 (falsch) liefert. Wenn ein logischer Wert von einem eingebauten Operator erzeugt wird, so ist dieser also 0 oder zuverlässig 1. An Stelle eines in die Sprache integrierten Typs boolean mit den Werten true und false, wie es ihn in vielen anderen Programmiersprachen gibt, stellt C seit Standard [ISO/IEC (1999)] Makros zur Verfügung, um die Hantierung von ganzzahligen Wahrheitswerten etwas lesbarer zu gestalten.

7.7 Wahrheitswerte und logische Ausdrücke

109

Wenn man stdbool.h inkludiert (vgl. Kap. 9.11 S. 137), kann man • bool statt z.B. int als Typangabe für die Deklaration von Wahrheitswert-Variablen benutzen • in Ausdrücken true statt 1 schreiben • in Ausdrücken false statt 0 schreiben Zur Formulierung von Aussagen stehen folgende Vergleichsoperatoren zur Verfügung: Operator
= == !=

Operation kleiner als kleiner oder gleich als größer als größer oder gleich als gleich ( nicht „=“) ungleich

Mit diesen Operatoren können Aussagen formuliert werden, indem die Werte von numerischen Ausdrücken verglichen werden. Zum Beispiel soll angenommen werden, dass die gemessene Außentemperatur in einer Variablen ATemp gespeichert ist und die Aussage „die Außentemperatur ist unter 5 Grad abgefallen“ zu formulieren ist: 1

ATemp < 5

Der Ausdruck ATemp wird hier verglichen mit dem konstanten Ausdruck 5. Aussagen können immer nur wahr oder falsch sein. Zur Darstellung des Wahrheitsgehalts einer Aussage werden in C ganze Zahlen verwendet. Der Wert Null (0) gibt an, dass eine Aussage falsch ist und ein von Null verschiedener Wert zeigt an, dass eine Aussage wahr ist. Somit stellt jede ganze Zahl implizit auch immer einen Wahrheitswert dar. Bei der Bewertung von Aussagen durch ein Programm wird als Wert für wahr immer die Eins generiert. Eine logische Aussage stellt also wieder einen Ausdruck dar, einen sogenannten „logischen Ausdruck“, mit ganzzahligem Typ: 1.Ausdruck 2.Ausdruck

z }| { z}|{ ATemp < 5 | {z }

logischerAusdruck, Wert 0 oder 1

Beispiel: Ausgabe der Wahrheitswerte verschiedener Aussagen über die Variable ATemp: 1 2 3 4

#i n c l u d e < stdio .h > i n t main () { i n t ATemp = 7;

110 printf ( " Wahrheitswert printf ( " Wahrheitswert printf ( " Wahrheitswert printf ( " Wahrheitswert printf ( " Wahrheitswert r e t u r n 0;

5 6 7 8 9 10 11

7 Operatoren und Ausdrücke von von von von von

ATemp < 5: ATemp > 7: ATemp >=7: ATemp !=0: ATemp ==5:

%d\n", %d\n", %d\n", %d\n", %d\n",

ATemp < 5); ATemp > 7); ATemp >=7); ATemp !=0); ATemp ==5);

}

druckt: Wahrheitswert Wahrheitswert Wahrheitswert Wahrheitswert Wahrheitswert

von von von von von

ATemp< 5: ATemp> 7: ATemp>=7: ATemp!=0: ATemp==5:

0 0 1 1 0

+ geschrieben Zur Prüfung auf Gleichheit (Zeile oben) müssen zwei Gleichheitszeichen werden, da der = -Operator für Zuweisungen verwendet wird. 9

Wir müssen noch die Priorität der Vergleichsoperatoren betrachten. In Frage steht etwa, was bei folgendem Ausdruck zuerst ausgewertet wird, wenn ATemp wieder den Wert 7 hat? 1

ATemp + 5 < 12

Wird zuerst 5 3 9 < 5 5 != 9 i j i > j j == i j = i

Wahrheitswert Wahr (1)

8

Logische und bitweise Operatoren

Wir haben bisher numerische Operatoren und Ausdrücke sowie Wahrheitswerte und logische Operatoren kennen gelernt. In diesem Kapitel werden wir uns zuerst mit Ausdrücken beschäftigen, die Wahrheitswerte manipulieren und mit solchen Operatoren, die direkt die Interndarstellung von ganzen Zahlen beeinflussen.

8.1 Logische Verknüpfungen Mit den im letzten Kapitel gezeigten Vergleichsoperatoren können wir einfache Bedingungen formulieren. Oft sind jedoch komplexe Bedingungen zu formulieren, für die Heizungssteuerung etwa, ob die Temperatur nicht nur einen gewissen Wert über- oder unterschreitet, sondern ob sie in einem bestimmten Bereich liegt. Um solche komplexen Bedingungen formulieren zu können, gibt es in C drei logische Operatoren (auch Boolesche Operatoren genannt): • Negation, Operatorsymbol: ! • UND-Verknüpfung, auch Disjunktion genannt; Operatorsymbol: && • ODER-Verknüpfung, auch Konjunktion genannt; Operatorsymbol: || Da logische Ausdrücke nur die Werte „wahr“ und „falsch“ annehmen können, lassen sich die so genannten Wahrheitstafeln aller Operandenwerte und Ergebnisse leicht angeben. Dabei ist wieder zu beachten, dass jeder von Null verschiedene Wert (in der Wahrheitstabelle durch „6=0“ symbolisiert) als wahr interpretiert wird; als Ergebnis einer Verknüpfung kann jedoch nur 0 oder 1 entstehen. Für die UND und die ODERVerknüpfung gilt folgende Wahrheitstabelle: Operanden a b 6=0 6=0 6=0 0 0 6=0 0 0

Ergebnis a||b a&&b 1 1 1 0 1 0 0 0

Für die Negation gilt: a 0 6=0

!a 1 0

116

8 Logische und bitweise Operatoren

Beispiel: Das folgende Programm gibt die obige Wahrheitstabelle aus: 1 2 3 4 5 6 7 8 9 10

#i n c l u d e < stdio .h > i n t main () { printf ( " | a | b | a || b | a && b |\ n " ); printf ( " | 1 | 1 | % d | % d |\ n " , printf ( " | 1 | 0 | % d | % d |\ n " , printf ( " | 0 | 1 | % d | % d |\ n " , printf ( " | 0 | 0 | % d | % d |\ n " , r e t u r n 0; }

1||1 , 1||0 , 0||1 , 0||0 ,

1&&1); 1&&0); 0&&1); 0&&0);

druckt: | | | | |

a 1 1 0 0

| | | | |

b 1 0 1 0

|a||b|a&&b| | 1 | 1 | | 1 | 0 | | 1 | 0 | | 0 | 0 |

Bei der Verknüpfung mehrerer logischer Ausdrücke ist folgendes zu beachten: Die Auswertung erfolgt von links nach rechts. Sie wird beendet, sobald das Ergebnis fest steht. Bei einer UND-Verknüpfung wird also nach dem ersten „falsch“ beendet, da das Ergebnis dann in jedem Fall „falsch“ ist, auch wenn alle folgenden Terme „wahr“ sind. Bei einer ODER-Verknüpfung wird nach dem ersten „wahr“ beendet, da das Ergebnis dann in jedem Fall „wahr“ ist. Dies ist insbesondere deshalb von Bedeutung, da in jedem der zu verknüpfenden Ausdrücke Zuweisungen oder Seiteneffekte (Operatoren ++ und --) erfolgen können.

8.2 Bitweise Operatoren Das zweite Thema dieses Kapitels sind die bitweisen Operatoren, also Operatoren, die direkt die Interndarstellung von ganzen Zahlen beeinflussen. Diese spielen in der praktischen technischen Anwendung eine sehr große Rolle. Oft werden Daten so kodiert, dass jedes einzelne Bit oder Bitgruppen eines Wertes spezielle Bedeutungen haben. Wir werden das in diesem Abschnitt an zwei Beispielen darstellen, die jeweils Bitgruppen innerhalb eines 32 Bit Wertes benutzen. Beispiel 1: Farben im Windows-API 1 Im Windows API werden Farben als 32-Bit Werte ohne Vorzeichen gespeichert. Jeder Farbwert enthält drei Farbkomponenten, nämlich den Rot- Grün- und BlauAnteil. Die Farbanteile sind jeweils als 8-Bit Werte zwischen 0 und 255 dargestellt. 1 Windows-API ist das Application Interface, des Windows-Betriebssystems. Es enthält insbesondere die Aufrufe, die einem Anwendungsprogramm zur Verfügung stehen, um eine System-Dienstleistung aufzurufen

8.2 Bitweise Operatoren

117

BitNr. 31 | 30 29 28{z27 26 25 24} 23 | 22 21 20{z19 18 17 16} 15 | 14 13 12 {z 11 10 9 }8 7| 6 5 4{z3 2 1 }0 Nullen

blau

grün

rot

Beispiel 2: IPv4 Adressen Jeder Rechner im Internet hat eine IP-Adresse. Momentan sind IPv4 Adressen noch weit verbreitet. Der kommende Standard ist IPv6. Meist sieht man als Externdarstellung von IPv4 Adressen die Punkt-Schreibweise: vier Zahlen zwischen 0 und 255, durch Punkte getrennt. Jede Zahl entspricht einem der vier Bytes, die die Adresse ausmachen. BitNr. 31 | 30 29 28{z27 26 25 24} 23 | 22 21 20{z19 18 17 16} 15 | 14 13 12 {z 11 10 9 }8 7| 6 5 4{z3 2 1 }0 Byte 3

Byte 2

Byte 1

Byte 0

Der Teil der Adresse, der das Netzwerk identifiziert, wird zentral von InternetBehörden vergeben. Damit der Betreiber eines Netzes seine Rechner selbst verwalten kann, wird ein anderer Teil der Adresse für die Identifizierung der individuellen Rechner (Hosts) im lokalen Netz benutzt. Für ein Klasse-C Netzwerk sieht das so aus: BitNr. 31 30 29 | {z } 28 | 27 26 25 24 23 22 21 20 19{z18 17 16 15 14 13 12 11 10 9 }8 7| 6 5 4{z3 2 1 }0 110

Netzwerk-ID

Host-ID

Die ersten drei Bits sind 110, was bedeutet, dass es sich um ein ein Klasse-C Netz handelt. In Klasse-C Netzen sind die Host Nummern, die der Netzwerkadministrator vergeben kann, acht Bit lang. Wenn wir uns vorstellen, dass wir etwa aus einem Farbwert col den Grünanteil ausgeben wollen, dann können wir nicht einfach printf("%d", col); schreiben. Vielmehr müssen wir zwei Probleme lösen: • Außer dem Grünanteil können noch weitere Bits im Farbwert gesetzt sein (rot, blau), die die Ausgabe des Grünwerts verfälschen würden • Der Grünwert ist verschoben - z.B. das Bit mit Stellenwert 20 , also die Einer des Grünwerts stehen auf Bitposition 8 des Farbwerts, würden also in der Ausgabe mit Stellenwert 256 statt 1 bewertet Ähnlich sieht es aus, wenn man aus der IPv4 Adresse die Netzwerkidentifikation extrahieren wollte. Die Operatoren, mit denen derartige Probleme behandelt werden können, lernen wir in diesem Abschnitt kennen. Analog den Verknüpfungen für Wahrheitswerte gibt es Operationen, die für Operanden mit int -Typ bitweise die Negation, bzw. UND- und ODER-Verknüpfungen durchführen. Die bitweise Negation heißt auch Einskomplement; der Operator dafür ist das Zeichen „~“. Dieser Operator benötigt lediglich einen Operanden. Die Schreibweise der anderen Operatoren ist „&“ für die bitweise UND-Verknüpfung sowie „|“ für die bitweise ODERVerknüpfung. Es wird also nur ein Symbol & bzw. | geschrieben, anstatt zweier Symbole für die Verknüpfung von Wahrheitswerten. Darüber hinaus gibt es noch den Operator „^“ für die bitweise EXOR-Verknüpfung (Exklusiv-ODER). Die Operatoren „&“, „|“ und „^“ benötigen zwei Operanden. Die Wahrheitstabelle für diese drei Operatoren sieht für jede Bitposition wie folgt aus:

118

8 Logische und bitweise Operatoren Operanden a b 1 1 1 0 0 1 0 0

Ergebnis a|b a&b a^b 1 1 0 1 0 1 1 0 1 0 0 0

Auch hier gibt es verkürzte Schreibweisen: statt a = a | b kann a |= b geschrieben werden, statt a = a & b kann a &= b und statt a = a ^ b; kürzer a ^= b geschrieben werden. Beispiel: 1

#i n c l u d e < stdio .h >

2 3 4

i n t main ( i n t argc , c h a r * argv []){ u n s i g n e d s h o r t a, b, c;

5

/∗ b i t w e i s e s Und ∗/ a = 0 x1634 ; /∗ 0 0 0 1 b = 0 xacaf ; /∗ 1 0 1 0 c = a & b; /∗ 0 0 0 0 printf ( " % x \ n " , c ); /∗ 424

6 7 8 9 10 11

/∗ b i t w e i s e s Oder ∗/ a = 0 x1634 ; /∗ 0 0 0 1 b = 0 xacaf ; /∗ 1 0 1 0 c = a | b; /∗ 1 0 1 1 printf ( " % x \ n " , c ); /∗ b e b f

12 13 14 15 16 17

/∗ b i t w e i s e s Xor ∗/ a = 0 x1634 ; /∗ 0 0 0 1 b = 0 xacaf ; /∗ 1 0 1 0 c = a ^ b; /∗ 1 0 1 1 printf ( " % x \ n " , c ); /∗ b a 9 b

18 19 20 21 22 23

0 1 1 0 0 0 1 1 0 1 0 0 ∗/ 1 1 0 0 1 0 1 0 1 1 1 1 ∗/ 1 1 1 0 1 0 1 1 1 1 1 1 ∗/ h e x a ∗/ 0 1 1 0 0 0 1 1 0 1 0 0 ∗/ 1 1 0 0 1 0 1 0 1 1 1 1 ∗/ 1 0 1 0 1 0 0 1 1 0 1 1 ∗/ h e x a ∗/

r e t u r n 0;

24 25

0 1 1 0 0 0 1 1 0 1 0 0 ∗/ 1 1 0 0 1 0 1 0 1 1 1 1 ∗/ 0 1 0 0 0 0 1 0 0 1 0 0 ∗/ h e x a ∗/

}

Darüber hinaus gibt es noch zwei Operatoren, die verwendet werden können, um das Bitmuster nach links oder rechts zu schieben. Diese Operatoren benötigen zwei Operanden: Ausdruck1 bzw.

«

Ausdruck2

8.2 Bitweise Operatoren

Ausdruck1

119

Ausdruck2

»

Alle Bits von Ausdruck 1 werden dabei um die durch Ausdruck 2 angegebene Anzahl Stellen nach links () verschoben. Beim Schieben nach links werden dabei Nullen nachgezogen und beim Schieben nach rechts wird ein Wert entsprechend dem des Vorzeichenbits nachgezogen. In verkürzter Schreibweise kann hier a > b geschrieben werden. Als Beispiel schieben wir die Größe unsigned short usx = 0xCDAB; um vier Bits nach rechts: Bitnr

15

14

13

12

11

10

09

08

07

06

05

04

03

02

01

00

Bit:

1

1

0

0

1

1

0

1

1

0

1

0

1

0

1

1

0 0 0 0

0

0

0

0

1

1

0

0

1

1

0

1

1

0

1

0

15

14

13

12

11

10

09

08

07

06

05

04

03

02

01

00

1 0 1 1

Wie man sieht, werden für eine unsigned-Größe von links Nullen nachgezogen. Numerisch betrachtet, bedeutet jeder Schiebeschritt eine ganzzahlige Division durch 2. Die Bits auf der rechten Seite werden einfach heraus geschoben, sie gehen verloren. Das Schieben nach links funktioniert analog, nur in der anderen Richtung. Wenn man eine signed-Größe nach rechts schiebt, wird statt Nullen das Vorzeichenbit nachgezogen. Der Zweck ist, dass - wenn man die Zahl als Komplement-Darstellung betrachtet - das Vorzeichen erhalten bleibt, während der Betrag pro Schiebeschritt ganzzahlig durch zwei dividiert wird. Analog zum Beispiel oben schieben wir eine Größe signed short usx = 0xCDAB; um vier Bits nach rechts:

1 1 1 1

Bitnr

15

14

13

12

11

10

09

08

07

06

05

04

03

02

01

00

Bit:

1

1

0

0

1

1

0

1

1

0

1

0

1

0

1

1

1

1

1

1

1

1

0

0

1

1

0

1

1

0

1

0

15

14

13

12

11

10

09

08

07

06

05

04

03

02

01

00

1 0 1 1

120

8 Logische und bitweise Operatoren

Anwendungsbeispiel 1: Ausgabe der Farbanteile eines 32-Bit Farbwerts Stellen wir uns vor, wir hätten einen grünlichen Farbton mit den Anteilen rot = 17110 = AB16 , grün = 20510 = CD16 , blau = 16710 = A716 . Ein Programm, das die Farbanteile isoliert und korrekt ausgibt, könnte so aussehen: 1 2 3 4

#i n c l u d e < stdio .h > i n t main ( i n t argc , c h a r * argv []){ u n s i g n e d colval = 0 x00A7CDAB ; /∗ b =167 g =205 r =171 ∗/ u n s i g n e d red , green , blue ;

5

printf ( " colval =% d \ n " , colval ); /∗ n u t z l o s ∗/

6 7

red

8

= colval & 0 x000000ff ;

9

green = colval & 0 x0000ff00 ; green = green >> 8;

10 11 12

blue

13

= colval >> 16;

14

printf ( " r =% d g =% d b =% d \ n " , red , green , blue );

15 16

r e t u r n 0;

17 18

}

In Zeile 3 belegen wir die 32 Bit unsigned-Größe colval mit dem Wert 0xA7CDAB, der den oben genannten Farbanteilen entspricht. In Zeile 4 definieren wir die Größen, die bei der Berechnung die einzelnen Farbanteile aufnehmen sollten. Zeile 6 gibt colval=10997163 aus. Wie man sieht, ist diese Information für jemand, der Farbanteile braucht, sinnlos. Um den Rotwert zu erhalten, müssen müssen die ersten 24 Bits ausgeblendet, das heißt: auf Null gesetzt werden. Man sagt auch, sie müssen ausmaskiert werden. Eine bitweise UND-Verknüpfung mit einem Wert, dessen Interndarstellung an den Stellen der unerwünschten Bits Nullen aufweist und an den Stellen der gewünschten Bits Einsen, liefert genau die interessierenden Bits. Der Wert, mit dem diese Verknüpfung durchgeführt wird, heißt Maske. colval Maske

0x00A7CDAB 0x000000ff &

000000001010011111001101 000000000000000000000000 000000000000000000000000

10101011 11111111 10101011

Die Tatsache, dass die Maske in hexadezimaler Darstellung angegeben wurde, hat ausschließlich den Grund, dass sich ein gegebenes Bitmuster leichter in eine hexadezimale Zahl, als in eine dezimale wandeln lässt: je vier Bits liefern eine hex-Ziffer. printf mit der Formatierung „%d“ in Zeile 15 wandelt den Wert in die entsprechende dezimale Externdarstellung, für Rot 171.

8.2 Bitweise Operatoren

121

Zur Ausgabe des Grünanteils müssen die entsprechenden Bits maskiert werden. Es sind dies nun die Bits 8-15. Die Maske dafür ist 0x0000ff00 (Zeile 10). Allerdings stehen nach der UND-Verknüpfung von colval mit dieser Maske die Bits an der falschen Position. Also müssen die Bits noch acht Stellen nach rechts an die richtige Position geschoben werden (Zeile 11). colval Maske

0x00A7CDAB 0x0000ff00 & » 8

0000000010100111 0000000000000000 0000000000000000 0000000000000000

11001101 11111111 11001101 00000000

10101011 00000000 00000000 11001101

Um die Bits des Blauanteils richtig zu erhalten, reicht ein Schieben um 16 Stellen nach rechts aus, da für die Bits an den Positionen 31 bis 24 automatisch Nullen nachgezogen werden. Dieser Shift wird in Zeile 13 durchgeführt. colval

0x00A7CDAB » 16

00000000101001111100110110101011 00000000000000000000000010100111

Bisher haben wir uns mit der Analyse von bitweise kodierten Informationen befasst. In C lassen sich natürlich auch bitweise kodierte Werte zusammenbauen. Dies ist insbesondere für hardwarenahe Anwendungen wichtig. Dort ist es oft erforderlich, in bestimmten Speicherstellen lediglich einzelne Bits zu verändern. Anwendungsbeispiel 2: Aufbau eines Farbwertes (ähnlich dem Makro RGB unter Windows) Mit dem folgenden Programmstück könnten wir den Farbwert wie oben aufbauen, ohne per Hexadezimaldarstellung Handarbeit zu betreiben: 1 2

i n t b =167 , g =205 , r =171; colval = r | ( g = 500) { printf ( " D " ); z =z -500; } w h i l e (z >=100) { printf ( " C " ); z =z -100; } }

18 19 20 21 22 23 24 25

/∗ z < 1 0 0 : Z e h n e r −B e h a n d l u n g ∗/ s w i t c h ( z / 10 * 10) { c a s e 90: printf ( " XC " ); z =z -90; b r e a k ; c a s e 40: printf ( " XL " ); z =z -40; b r e a k ; default: i f (z >=50) { printf ( " L " ); z =z -50; } w h i l e (z >=10) { printf ( " X " ); z =z -10; } }

26 27 28 29 30 31 32 33 34 35 36

/∗ z < 10 ∗/ s w i t c h (z) { c a s e 9: printf ( " IX " ); z =z -9; b r e a k ; c a s e 4: printf ( " IV " ); z =z -4; b r e a k ; d e f a u l t : i f (z >=5) { printf ( " V " ); z =z -5; } w h i l e (z >=1) { printf ( " I " ); z =z -1; } } printf ( " \ n " ); r e t u r n 0; }

Abbildung 10.5: Beispielprogramm: Römische Zahlen

153

154

10 Kontrollstrukturen

Kodierung in C: doWhileAnweisung do

Anweisung

while

(

ganzzAusdruck

)

10.3.3 Anwendung: Bestimmung von Nullstellen einer Funktion Nullstellenbestimmung von Funktionen ist eine häufig benötigte Anwendung. Als Beispiel dient uns hier die Keplersche Gleichung, die für zwei gegebene Werte 0 ≤ E ≤ 1 und 0 ≤ M ≤ 2π nach x aufzulösen ist: M = x − E sin(x)

(10.1)

Diese Gleichung ist u. a. wichtig, um die Position eines Satelliten zu einer gegebenen Zeit zu bestimmen, um etwa eine Antenne nachführen zu können. Es gibt keine analytische Lösung für diese Gleichung. Eine Nullstelle kann daher nur numerisch, durch wiederholte Annäherung bestimmt werden. Das Verfahren von Newton und Raphson ist ein Algorithmus, zur numerischen Bestimmung einer Nullstelle. Statt die obige Gleichung nach x aufzulösen, können wir auch eine Nullstelle der Funktion bestimmen. f (x) = x − E sin(x) − M

(10.2)

Wir verwenden hier das Verfahren von Newton und Raphson 1. Ordnung. Dazu wird ein Startwert x0 gewählt und die Funktion durch ihre Tangente im Punkt (x0 , f (x0 ))) genähert und die Nullstelle der Tangente bestimmt. Die Nullstelle x1 dieser Geraden wird als erste Näherung für die Nullstelle der Funktion genommen und das Verfahren wiederholt. Es ergibt sich eine Folge (xi ) von Näherungswerten für die Nullstelle von f (x), wie in Abbildung 10.6 auf Seite 155 dargestellt. Unter bestimmten Voraussetzungen, die hier nicht genau angegeben werden, konvergiert die Newtonfolge (xi ) gegen eine Nullstelle von f (x). Die Steigung der Tangente im Punkt xi entspricht der Steigung der Funktion f (x) in xi . Die Geradengleichung der Tangente ist folglich y = f 0 (xi ) x + f (xi ) − xi f 0 (xi ) | {z } | {z } Steigung

y−Achsabschnitt

(10.3)

10.3 Laufschleifen (Wiederholungsanweisungen)

155

y

f(x)

x3

x2

x1

x0

x

Abbildung 10.6: Konvergenz der Folge (x) gegen eine Nullstelle von f (x)

Somit ist folgende Gleichung nach x aufzulösen: f (xi ) + (x − xi )f 0 (xi ) = 0

(10.4)

Die Lösung xi ist in vielen Fällen eine „Verbesserung“ von xi−1 , d.h. x1 ist näher an der Nullstelle von f , als x0 usw. Bei der Umsetzung in ein Programm müssen drei Dinge beachtet werden: 1. Mathematisch gesehen umfasst die Folge (xi ) unendlich viele Variablen mit Zuordnung zu jeweils einer Stützstelle. Bei der programmtechnischen Umsetzung werden die Werte von xi sukzessive berechnet, müssen aber nicht alle abgespeichert werden, da nur die letztlich gefundene Nullstelle von Bedeutung ist. Im folgenden Programm nimmt eine einzige Variable - xi - nacheinander die Werte der Stützstellen der Folge auf. Anders als in der Mathematik, ist bei der Programmierung eine Variable ein Container für einen änderbaren Wert. 2. Die Nullstelle kann in der Regel nicht exakt bestimmt werden, sondern nur mit einer gewissen Genauigkeit, etwa 10−6 bei Verwendung von float oder 10−12 bei Verwendung von double. Die Iteration kann daher abgebrochen werden, wenn |f (xi )| <  mit einer vorgegebenen Genauigkeit . 3. Das Verfahren muss nicht immer konvergieren. Wenn die Funktion f (x) gar keine Nullstelle hat, oder nicht die Voraussetzungen für die Konvergenz erfüllt. Dann ist |f (xi )| <  nie erfüllt.

156

10 Kontrollstrukturen Es ist also zusätzlich dafür zu sorgen, dass die Iteration nach einer maximalen Anzahl von Schritten abgebrochen wird, um das Verweilen des Programms in einer Endlosschleife zu vermeiden

In Abbildung 10.7 auf Seite 157 ist das Listing eines Beispielprogramms dargestellt, das die Keplersche Gleichung 10.1 von Seite 154 näherungsweise löst. Die Ausgabe ist Newton - Raphson 1. Ordnung Nullstelle bei 0.486143 mit 3 Iterationen Genauigkeit : 2.878423 e -013 Zeilen 10

21-25 22 24-25

Erklärungen zu Listing „Newton-Raphson“ Abb. 10.7, S. 157 Die Bildung des absoluten Betrags wird in diesem Programm mit dem Präprozessormakro ABS durchgeführt. Makros sind Gegenstand des nächsten Kapitels, werden hier aber schon verwendet. Die do-while-Schleife Hier findet der eigentliche Iterationsschritt statt. xi wird unter Verwendung des vorigen Wertes neu bestimmt. Abbruch der Schleife. Zeile 24 testet, ob die Genauigkeit schon hinreichend ist. Zeile 25 führt zum Abbruch, wenn die maximale Iterationszahl erreicht ist.

10.3.4 Die for-Anweisung Die for-Schleife ist wiederum eine Schleife mit Prüfung der Bedingung vor Durchlauf durch die Schleife. Das Struktogramm für die for-Schleife sieht folgendermaßen aus: Ausdruck 1 Solange Ausdruck 2 wahr ist Anweisung Ausdruck 3 Kodierung in C: forAnweisung for

(

Ausdruck

;

Ausdruck

;

Ausdruck

)

Anweisung Der Ausdruck1 dient meist dazu, Initialisierungen durchzuführen. Ausdruck2 repräsentiert die Abbruchbedingung für die Laufschleife und Ausdruck3 wird am Ende der Schleife ausgewertet und wird häufig dazu benutzt, einen Schleifenzähler zu inkrementieren.

10.3 Laufschleifen (Wiederholungsanweisungen)

1 2

157

#i n c l u d e < stdio .h > #i n c l u d e < math .h >

3 4 5 6 7 8

#d e f i n e #d e f i n e #d e f i n e #d e f i n e #d e f i n e

PI 3.1415926 EPS 1. E -10 E 0.2 M ( PI /8.) MAX_SCHRITTE 100

9 10

/∗ /∗ /∗ /∗

G e n a u i g k e i t s s c h r a n k e ∗/ V o r g e g e b e n e r Wert f ü r E ∗/ V o r g e g e b e n e r Wert f ü r M ∗/ wenn EPS n i e e r r e i c h t w i r d ∗/

#d e f i n e ABS ( x ) ((( x ) >0) ? ( x ) : -( x ))

11 12 13 14

i n t main () { d o u b l e xi ; i n t it = 0;

15

printf ( " Newton - Raphson 1. Ordnung \ n " );

16 17

it = 0; xi = PI /4;

18 19 20

do {

21

xi = xi - ( xi - E * sin ( xi ) - M )/(1. - E * cos ( xi )); ++ it ; } w h i l e (( ABS ( xi - E * sin ( xi ) - M ) > EPS ) && ( it < MAX_SCHRITTE ));

22 23 24 25 26

i f ( it >= MAX_SCHRITTE ) printf ( " Mangelnde Konvergenz !\ n " ); else{ printf ( " Nullstelle bei % lf mit % d Iterationen \ n " , xi , it ); printf ( " Genauigkeit : % le \ n " , xi - E * sin ( xi ) - M ); } r e t u r n 0;

27 28 29 30 31 32 33 34 35

}

Abbildung 10.7: Beispielprogramm: Nullstellenbestimmung nach Newton-Raphson

Ab C, Standard C99 [ISO/IEC (1999)] kann der Ausdruck 1 auch mit einer Deklaration verbunden werden, wie z.B. in for ( int i=0; i

2 3 4 5 6 7 8 9 10 11

i n t main () { i n t i , sum , n =10; sum = 0; f o r ( i =1; i 0) ? (|{z} x ) : -(|{z} x )) #define ABS (|{z} x )(((|{z} 1

2

3

4

So eine Definition heißt Makrodefinition. ABS ist der Name des im Beispiel definierten Makros. Bei 1 wird definiert, dass es einen Parameter hat, der x heißt. Wenn in den folgenden Zeilen des Programms der Makroname gefolgt von Parameterangaben in (...) auftritt, dann spricht man von einem Makro-Aufruf. Dieser wird durch den Ersatztext ersetzt, ähnlich wie bei symbolischen Konstanten.

11.5 Bedingte Compilierung

167

Zuvor aber wird bei Makros der Ersatztext expandiert, d.h. alle Vorkommen von Parameterbezeichnern im Ersatztext (hier bei 2 , 3 und 4 ) werden durch den an der Aufrufstelle angegebenen Parametertext ersetzt. Beispiele: Wenn nach unserer Definition von ABS eine Zeile der Programmquelle so aussieht /∗ Test ∗/ printf("%d", ABS(-1));

dann erkennt der Präprozessor einen Aufruf des Makros ABS. Der für x angegebene Parameterwert ist für diesen Aufruf -1. Die Expansion des Ersatztextes führt zu (((-1)>0) ? (-1) : -(-1)) Nach der Behandlung durch den Präprozessor sieht also die Zeile so aus: /∗ Test ∗/ printf("%d", (((-1)>0) ? (-1) : -(-1))); Ähnlich wird in Newton.c der Aufruf ABS(xi - E*sin(xi) - M) durch den Präprozessor ersetzt durch:2 (((xi - E*sin(xi) - M)>0) ? (xi - E*sin(xi) - M) : -(xi - E*sin(xi) - M)) Generell gilt für die Schreibweise von Makrodefinitionen:3 DefineDirektive #

define

Name (

) Parameter ,

Ersatztext

11.5 Bedingte Compilierung Bedingte Compilierung heißt die Technik, durch die Definition von symbolischen Konstanten (oder deren Unterlassung) zu steuern, ob bestimmte Zeilen in die Programm2 Die Aufteilung in Zeilen kommt nicht vom Präprozessor, sondern ist hier nur zwecks Übersichtlichkeit von Hand eingeführt worden. 3 Zwischen Name und ( darf kein Leerzeichen stehen, sonst würde der Text von einschließlich ( bis Zeilenende als Ersatztext interpretiert.

168

11 Präprozessor

quelle aufgenommen werden sollen oder nicht. Meist wird dabei einer von zwei Zwecken verfolgt: • für das Programm soll zwischen Varianten umgeschaltet werden können oder • es handelt sich um ein Headerdatei und es sollen Fehler vermieden werden, wenn dieses per #include mehrfach in die Quelle aufgenommen wird. Beispiel: Wir erzeugen eine Testvariante unseres Beispielprogramms Newton.c, indem wir die Zeilen printf("%s %d ", __FILE__, __LINE__); printf("xi=%le f(xi)=%le\n", xi, xi-E*sin(xi)-M); an zwei Stellen einfügen. Um zwischen der Testversion und der Produktversion hin und her schalten zu können, klammern wir diese Zeilen jeweils in geeignete PräprozessorDirektiven: # i f d e f TEST printf ( " % s % d " , __FILE__ , __LINE__ ); printf ( " xi =% le f ( xi )=% le \ n " , xi , xi - E * sin ( xi ) - M ); #e n d i f Der Präprozessor prüft bei #ifdef, ob die angegebene symbolische Konstante vorher definiert wurde. Falls ja, werden die Folgezeilen bis #endif in die Programmquelle aufgenommen. Falls nicht, wird der Zeilenbereich zwischen #ifdef und #endif übersprungen und die betreffenden Zeilen werden nicht mit übersetzt. Jetzt haben wir die Möglichkeit, durch Angabe von #define TEST im Programmkopf die Testversion zu erzeugen. Für die Produktversion lassen wir dieses #define weg und die Testausgaben entfallen. Wir haben jetzt den Vorteil einer Testinstrumentierung unseres Programms, die die Produktversion weder durch höheren Speicherbedarf noch durch höhere Laufzeit oder überflüssige Ausgaben belastet. Das Syntaxdiagramm in Abb. 11.1 S. 169 zeigt die wichtigsten Möglichkeiten. Neben dem Test, ob das Symbol definiert ist, gibt es auch den gegenteiligen Test mit #ifndef, der dann zutrifft, wenn das Symbol nicht definiert wurde. Ähnlich wie bei den Kontrollstrukturen der Sprache C selbst, kann man auch bei der bedingten Compilierung zwei alternative Zweige angeben. Der Zweig hinter #else wird in die Programmquelle aufgenommen, wenn die Bedingung bei #ifdef bzw. #ifndef nicht zutrifft. Im Beispiel haben wir gesehen, wie man Varianten eines Programms in einer einzigen Quelle pflegen kann. Jetzt wollen wir noch den Schutz gegen Fehler durch MehrfachInklusion kennen lernen.

11.5 Bedingte Compilierung

169

BedingteCompilierungsDirektive #

ifdef

Name

ifndef

Programmzeile

#

else Programmzeile

#

endif

Abbildung 11.1: Syntaxdiagramm für Direktiven zur bedingten Compilierung

Mehrfache Inklusion einer Headerdatei kann auftreten, wenn in einem Header wieder #include-Direktiven verwendet werden – in der Praxis ein häufiger Fall. Fehler durch mehrfache Definition von Variablen oder Funktionen kann man in solchen Fällen durch bedingte Compilierung vermeiden: Man nimmt die Headerzeilen nur in die Quelle auf, wenn eine bestimmte symbolische Konstante noch nicht definiert ist. Im Header definiert man dieses Symbol. Wird der Header ein zweites Mal inkludiert, dann ist das Symbol bereits definiert und die Headerzeilen werden nicht ein weiteres Mal in die Quelle aufgenommen.

170

11 Präprozessor

Beispiel: # i f n d e f __HEADX #d e f i n e __HEADX ... // Z e i l e n d e s e i g e n t l i c h e n #e n d i f

Headefiles

Natürlich ist darauf zu achten, dass das Symbol für den Mehrfach-Inklusionstest nicht anderweitig verwendet wird. Deshalb nimmt man meist den Namen der Headerdatei mit einem ansonsten nicht verwendeten Zusatz wie z. B. ein doppel-Underscore.

11.6 Beispielprogramm: Testversion Newton-Raphson Jetzt sind alle Voraussetzungen für unser Beispielprogramm eingeführt: • die #include Direktive • #define von symbolischen Konstanten • vordefinierte Symbole __FILE__ und __LINE__ • #define von Makros • bedingte Compilierung zwischen #ifdef und #endif Das Listing der kompletten, für den Test instrumentierten Version von Newton.c ist in Abbildung 11.2 auf Seite 171 dargestellt. Für die gezeigte Version ist der Test mit #define TEST eingeschaltet. Ausgabe des Programms bei eingeschaltetem Test: Newton - Raphson 1. Ordnung Newton . c 20 xi =7.853982 e -001 f ( xi )=2.512777 e -001 Newton . c 27 xi =4.927311 e -001 f ( xi )=5.425211 e -003 Newton . c 27 xi =4.861454 e -001 f ( xi )=2.043197 e -006 Newton . c 27 xi =4.861429 e -001 f ( xi )=2.878423 e -013 Nullstelle bei 0.486143 mit 3 Iterationen . Genauigkeit : 2.878423 e -013

11.6 Beispielprogramm: Testversion Newton-Raphson

1 2

171

#i n c l u d e < stdio .h > #i n c l u d e < math .h >

3 4 5 6 7 8 9

#d e f i n e #d e f i n e #d e f i n e #d e f i n e #d e f i n e #d e f i n e

PI 3.1415926 EPS 1. E -10 /∗ G e n a u i g k e i t s s c h r a n k e ∗/ E 0.2 /∗ V o r g e g e b e n e r Wert f ü r E ∗/ M ( PI /8.) /∗ V o r g e g e b e n e r Wert f ü r M ∗/ MAX_SCHRITTE 100 /∗ N o t h a l t : K o n v e r g e n z m a n g e l ∗/ ABS ( x ) ((( x ) >0) ? ( x ) : -( x ))

10 11

#d e f i n e TEST

12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40

i n t main () { d o u b l e xi ; i n t it = 0; printf ( " Newton - Raphson 1. Ordnung \ n " ); it = 0; xi = PI /4; # i f d e f TEST printf ( " % s % d " , __FILE__ , __LINE__ ); printf ( " xi =% le f ( xi )=% le \ n " , xi , xi - E * sin ( xi ) - M ); #e n d i f do { xi = xi - ( xi - E * sin ( xi ) - M )/(1. - E * cos ( xi )); ++ it ; # i f d e f TEST printf ( " % s % d " , __FILE__ , __LINE__ ); printf ( " xi =% le f ( xi )=% le \ n " , xi , xi - E * sin ( xi ) - M ); #e n d i f } w h i l e (( ABS ( xi - E * sin ( xi ) - M ) > EPS ) && ( it < MAX_SCHRITTE )); i f ( it >= MAX_SCHRITTE ) printf ( " Mangelnde Konvergenz !\ n " ); else{ printf ( " Nullstelle bei % lf mit % d Iterationen .\ n " , xi , it ); printf ( " Genauigkeit : % le " , xi - E * sin ( xi ) - M ); } r e t u r n 0; }

Abbildung 11.2: Newton.c für den Test instrumentiert - das komplette Programm

12

Algorithmen: Reaktive Programme, Automaten

Ein großer Anteil von CPUs arbeitet nicht in Personal Computern und ist nicht mit Tastatur, Maus und Bildschirm ausgestattet. Sogenannte „embedded controls“ sind vollständig in ihre technische Umgebung integriert und steuern diese. Typischerweise handelt es sich bei diesen CPU-Typen um Mikrocontroller, die weniger komplex aufgebaut sind, als die CPU eines PCs. Dafür sind viele der Anschluss-Pins eines Mikrocontrollers für die Ein-und Ausgabe aus der bzw. in die technische Umgebung vorgesehen. Dazu gehören insbesondere Unterbrechungseingänge, digitale Ein-/ Ausgabe-Ports, über die der Controller logische Signale mit der Umgebung austauscht, Analog-Ein-/ Ausgänge und nicht zu vergessen USB- oder serielle Schnittstellen. Eine solche Steuerung reagiert auf Signale an den Eingängen und erzeugt dabei Signale an den Ausgängen, wobei ein interner Zustand berücksichtigt wird. Steuerung mit CPU

Eingänge

E i n g a b e n

Automat (Programm)

Zustand

A u s g a b e n

Ausgänge

Bei Änderungen an den Eingängen der Steuerung werden Eingaben an das Programm erzeugt. Intern verwaltet das Programm einen Zustand. Je nach Eingabe und Zustand wird eine Ausgabe und ein Folgezustand berechnet. Die Ausgaben aus dem Programm werden in Signale an den Ausgängen der Steuerung umgesetzt. Der Folgezustand wird für die Bearbeitung der nächsten Eingabe als Ausgangszustand benutzt usw. Solch ein Programm wird häufig nach einem Modell aus der Mathematik konzipiert, das mit „endlicher Automat“ bezeichnet wird. Wir betrachten im Folgenden als Beispiel die Verkaufsmaschine, die in Abbildung 12.1 auf Seite 174 dargestellt ist. Zu dieser Maschine werden wir einen endlichen Automaten entwerfen. Auf den Entwurf wird dann ein Schema angewendet, das von einem gegebenen Automaten zu einem lauffähigen Programm führt.

174

12 Algorithmen: Reaktive Programme, Automaten

Rückgabeknopf

Warenschacht

Münzschacht

5

Steuerung Sa = Riegel Schub auf M = Münzeinwurf Sz = Riegel Schub zu A = Schublade auf Ka = Riegel Kasse auf Z = Schublade zu Kz = Riegel Kasse zu L = Rückknopf loslassen D = Rückknopf drücken Aa = Riegel Auswurf auf Az = Riegel Auswurf zu Abbildung 12.1: Verkaufs-Maschine

Für die Eingaben des endlichen Automaten werden wir die Buchstaben M, L, D, A und Z verwenden (Bedeutung siehe Abb. 12.1).

Als Ausgaben des Automaten werden wir Kombinationen benötigen, die verschiedene Ausgänge aktivieren. Z. B. wird mit der Ausgabe SzAa der Riegel für die Schublade zugemacht sowie der Riegel für den Auswurf geöffnet. SzAa benötigt man, wenn Geldrückgabe gefordert wird.

Zusätzlich werden wir noch eine Ausgabe NoOp benötigen, die bedeutet, dass in dem betreffenden Schritt nichts ausgegeben wird („No Operation“).

12.1 Endliche Automaten

175

12.1 Endliche Automaten Wir gehen von der folgenden Definition1 nach Mealy2 aus. Ein Mealy-Automat ist durch (Z , z0 , E, A, T ) gegeben, wobei gilt Z z0 E A T

ist eine endliche nicht leere Menge von Zuständen z0 ∈ Z ist der Anfangszustand ist die endliche Menge der möglichen Eingaben ist die endliche Menge der möglichen Ausgaben ist die Menge der Transitionen (Zustandsübergänge). Jede Transition t ∈ T ordnet einem Ausgangszustand za ∈ Z und einer Eingabe e ∈ E einen Folgezustand zf ∈ Z und eine Ausgabe a ∈ A zu: (za , e) → (zf , a)

Wir entwerfen einen Mealy-Automaten, der unsere Verkaufsmaschine steuern kann. Als Menge der Eingaben nehmen wir E = {M , L, D, A, Z } Etwas weniger naheliegend ist die Festlegung der Zustände des Automaten. Bei diesem kreativen Akt muss man bereits die Menge der Transitionen im Auge haben, die die Abläufe im Automaten bestimmen. Für das Beispiel könnte man auf folgende Mengen Z und T kommen: Z = {Ausgangszustand, MuenzeEingeworfen, RueckgabeGefordert, SchubladeGezogen} z0 = Ausgangszustand T wird durch Tabelle 12.1 dargestellt. Eine Tabelle, mit der man die Transitionen eines Automaten festlegt, heißt „Automatentafel“. Weiter unten wird gezeigt, wie man die gleiche Menge T durch einen Graphen beschreibt. Als Menge möglicher Ausgaben wird in Tabelle 12.1 benutzt: A = {Az, Ka, NoOp, Sa, SzAa, SzKz} Wir werden jetzt am Beispiel eines Verkaufs-Ablaufs die Funktion des Automaten überprüfen. • Beginn im Ausgangszustand • Es erfolgt die Eingabe M , d.h. der Kunde wirft eine Münze ein. Der Automat reagiert mit der Transition in Zeile 1 der Tabelle: Ausgabe Sa, der Riegel für den 1 Gelegentlich werden statt Transitionen in der Definition von Automaten auch zwei Funktionen δ und η angegeben, die die Zuordnungen (za , e) → zf bzw. (za , e) → a separat festlegen (vgl. [Grosche u. a. (2003)]). 2 Im Unterschied hierzu ist die Ausgabe bei Moore-Automaten nur vom Zustand z abhängig. Statt f η verwendet man dann eine Funktion µ, die jedem Zustand seine Ausgabe zuordnet: zf → a. Da man aber zeigen kann, dass es zu jedem Moore-Automaten einen Mealy-Automaten gibt, der das gleiche Verhalten hat, und umgekehrt, beschränken wir uns hier auf den letzteren Typ.

176

12 Algorithmen: Reaktive Programme, Automaten T Zeile 1 2 3 4 5 6 7 8 9 10

za Ausgangszustand Ausgangszustand Ausgangszustand MuenzeEingeworfen MuenzeEingeworfen MuenzeEingeworfen RueckgabeGefordert SchubladeGezogen SchubladeGezogen SchubladeGezogen

e M L D D A L L Z L D

zf MuenzeEingeworfen Ausgangszustand Ausgangszustand RueckgabeGefordert SchubladeGezogen MuenzeEingeworfen Ausgangszustand Ausgangszustand SchubladeGezogen SchubladeGezogen

a Sa NoOp NoOp SzAa Ka NoOp Az SzKz NoOp NoOp

Tabelle 12.1: Automatentafel für die Verkaufsmaschine

Schub wird aufgemacht. Die Transition schaltet den Automaten in den Folgezustand MuenzeEingeworfen. • Der Kunde zieht die Schublade, woraufhin der Automat die Eingabe A erhält. Die Transition in Zeile 5 wird aktiviert, was zur Ausgabe von Ka führt. Der Kunde hört seine Münze in die Kasse fallen. Der Folgezustand des Automaten ist SchubladeGezogen • Der Kunde schließt die Schublade. Die zugehörige Eingabe ist Z . Die Transition in Zeile 8 wird aktiviert, was zur Ausgabe von SzKz führt. Daraufhin schließt die Maschine die Riegel für Schublade und Kasse. • Der Vorgang endet wieder im Ausgangszustand, den die Transition in Zeile 8 als Folgezustand hinterlassen hat. Jetzt kann der nächste Verkauf beginnen. Als Übung wird empfohlen, einen Vorgang „Verkaufsabbruch mit Geldrückgabe“ zu verfolgen. Natürlich müsste man alle Pfade durch die Zustandsmenge überprüfen, um sicher zu sein, dass der Automat in jeder Situation korrekt arbeitet, worauf wir aber hier verzichten. Die oftmalige Anwendung von Transitionen aus einer Automatentafel ist recht mühsam und fehlerträchtig. Deshalb stellt man oft die Mengen Z und T als Graphen3 dar, so dass man Abläufe durch „entlang fahren mit dem Finger“ verfolgen kann. Nach folgendem Schema erhält man zu einem endlichen Automaten den zugehörigen Graphen. • Jedem Zustand z ∈ Z wird ein Knoten (gezeichnet als Kreis, Ellipse oder Rechteck mit abgerundeten Ecken) zugeordnet. 3 Graphen

werden ausführlich im Kapitel 21 „Graphentheorie“ behandelt.

12.2 Direkte Implementierung von Automaten

177

• Jeder Knoten wird mit dem Namen des zugehörigen Zustandes beschriftet. • Jeder Transition t ∈ T mit t = (za , e, zf , a) wird eine gerichtete Kante (gezeichnet als Pfeil) vom Zustand za nach Zustand zf zugeordnet. e/a

e

a • Jede Transition wird mit ihrer Eingabe e und Ausgabe a beschriftet: → oder →

Wenn wir den Automaten aus unserem Beispiel nach diesem Schema zeichnen, bekommen wir ein Bild wie in Abbildung 12.2. Jeder Zeile aus Tab. 12.1 entspricht ein Pfeil und jedem Zustand z aus Z ein Kreis. Zur Übung wird empfohlen, den Verkaufsvorgang in diesem Graphen zu verfolgen. MuenzeEingeworfen RueckgabeGefordert

L/NoOp

D/SzAa A/Ka M/Sa

L/Az L/NoOp

Z/SzKz AusgangsZustand

Schubladegezogen

D/NoOp

L/NoOp

D/NoOp Abbildung 12.2: Automatengraph für die Verkaufs-Maschine

12.2 Direkte Implementierung von Automaten Nach der Anweisung im Kasten oben kann man aus einem Automaten rein schematisch einen Graphen erzeugen. Ähnlich lässt sich ein Schema angeben, nach dem man ein Programm erhält, das den Automaten implementiert. Wir interessieren uns an dieser Stelle für die direkte Implementierung - im Gegensatz zur interpretativen. „Direkt“ bedeutet hier, dass unter Verwendung des Schemas unten die Zustände des Automaten direkt in die Programmstruktur umgesetzt werden. Im Gegensatz dazu arbeitet eine interpretative Implementierung so, dass der Automat in Form von Daten beschrieben ist. Naheliegend ist ein Vektor, der die Automatentafel

178

12 Algorithmen: Reaktive Programme, Automaten

enthält. Das Programm, das den Automaten realisiert, liest dann jeweils die nächste Eingabe und sieht in den Daten nach, was zu tun ist. Es interpretiert die Daten als Anweisungen, den Automaten fort zu schalten. Wir werden eine interpretative Implementierung des Verkaufsautomaten-Beispiels später, im Kapitel 22 „Algorithmen: Interpretative Implementierung von Automaten“ kennen lernen. Für die direkte Implementierung eines Automaten (Z , z0 , E, A, T ) sind die folgenden beiden Schemata verbreitet.

12.2.1 Direkte Implementierung mit goto • jedem Zustand z ∈ Z wird eine Marke im Programm zugeordnet • die erste Marke im Programm entspricht dem Ausgangszustand z0 • hinter jeder Marke zu einem Zustand z ∈ Z wird die Nächste Eingabe e ∈ E eingelesen • nach dem Lesen der Eingabe e hinter der Marke für za erfolgt eine Verzweigung, wobei jeder Zweig einer anwendbaren Transition (za , e) → (zf , a) entspricht • in jedem Zweig wird die Ausgabe a ∈ A der Transition getätigt, anschließend per goto zur Marke des Folgezustands zf gesprungen

Hier wird nicht undiszipliniert im Programm herum gesprungen, das goto wird vielmehr zur direkten Abbildung der nicht hierarchischen Struktur des Automaten benutzt. Betrachten wir den Zustand za = MuenzeEingeworfen. Wenn wir das Schema für diesen Zustand durchgehen, dann kommen wir auf das folgende Programmstück. Dabei haben wir die Ein-/ Ausgänge der Verkaufsmaschine vernachlässigt und arbeiten zur Vereinfachung nur mit der Buchstaben-Darstellung der Mengen E und A. 1 2 3 4 5 6 7

Mue nzeEin geworf en : scanf ( " % c " ; & e ); s w i t c h (e) { c a s e ’D ’: printf ( " SzAa \ n " ); g o t o R uec kg ab eG ef or de rt ; c a s e ’A ’: printf ( " Ka \ n " ); g o t o SchubladeGezogen ; c a s e ’L ’: printf ( " NoOp \ n " ); g o t o Mue nzeEin geworf en ; }

12.2 Direkte Implementierung von Automaten Zeile 1 2 3 4 5 6

179

Erklärungen zum Programmstück „MuenzeEingeworfen“ mit goto Marke Eingabe einlesen Mehrfach-Verzweigung Transition Zeile 4 (Tab. 12.1 S. 176) Transition Zeile 5 (Tab. 12.1 S. 176) Transition Zeile 6 (Tab. 12.1 S. 176)

12.2.2 Direkte Implementierung mit Schleife und Zustandsvariable Etwas länger – dafür aber wegen des Verzichts auf goto übersichtlicher – wird das Programmstück von oben nach Umsetzung mit dem folgenden Schema. • Ein enum-Typ4 ordnet jedem Zustand einen Bezeichner für einen ganzzahligen Wert zu • Eine Variable enthält als Wert den jeweils aktuellen Zustand, zu Beginn den Wert für den Ausgangszustand • In einer Schleife wird zuerst die Nächste Eingabe e ∈ E eingelesen • Es folgt ein switch mit einer Verzweigung gemäß dem aktuellen Zustand z ∈ Z • In dem case für den aktuellen Zustand z gibt es wieder eine Verzweigung, hier gemäß der Eingabe e. Jeder Zweig entspricht einer anwendbaren Transition (z, e) → (zf , a) • in jedem Zweig wird die Ausgabe a ∈ A der Transition getätigt, anschließend wird der Zustandsvariablen der Wert für den Folgezustand zf zugewiesen Betrachten wir wieder den Zustand za = MuenzeEingeworfen. Wenn wir jetzt das Schema für diesen Zustand durchgehen, dann kommen wir auf das Programmstück in Listing 12.3 S. 180. Zeile 1 5 10 13 15 19 23 16, 20, 24 17, 21, 25

Erklärungen zum Programmstück „MuenzeEingeworfen“ mit Schleife Enumeration aller Zustände Definition der Zustandsvariablen und setzen auf Ausgangszustand Mehrfach-Verzweigung gemäß Wert der Zustandsvariable eine case Marke für jeden Zustand case für Transition Zeile 4 (Tab. 12.1 S. 176) case Transition Zeile 5 (Tab. 12.1 S. 176) case Transition Zeile 6 (Tab. 12.1 S. 176) Ausgabe der jeweiligen Transition Folgezustand der jeweiligen Transition setzen

180

1 2 3 4 5

12 Algorithmen: Reaktive Programme, Automaten

enum Zustaende { AusgangsZustand , MuenzeEingeworfen , Rueckgabegefordert , SchubladeGezogen }; ... c h a r e; i n t nrZustand = AusgangsZustand ;

6 7 8

w h i l e (1) { e = getch (); e = toupper ( e );

9

s w i t c h ( nrZustand ) { ... c a s e Mue nzeEin geworf en : switch ( e ) { c a s e ’D ’ : printf ( " SzAa \ n " ); nrZustand = R uec kg ab eg ef or de rt ; break ; c a s e ’A ’ : printf ( " Ka \ n " ); nrZustand = SchubladeGezogen ; break ; c a s e ’L ’ : printf ( " Noop \ n " ); nrZustand = Muenz eEinge worfe n ; break ; } break ; ... }

10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31

}

Abbildung 12.3: Programmstück zu „Schleife und Zustandsvariable“

12.3 Beispielprogramm: Verkaufsautomat Das komplette Programm – erzeugt nach dem obigen Schema – ist in Abb. 12.4 S. 182 dargestellt. In drei Punkten unterscheidet es sich von dem Programmfragment das gerade erklärt wurde: 1. statt scanf(...) wurde getch() verwendet, damit auf ein eingegebenes Zeichen reagiert wird, ohne dass Return eingegeben werden muss 2. mit toupper(...) arbeitet das Programm auch bei Eingabe von Kleinbuchstaben 3. das Programm wurde gegen Fehleingaben toleranter gemacht

12.3 Beispielprogramm: Verkaufsautomat

1 2 3

#i n c l u d e < stdio .h > #i n c l u d e < ctype .h > #i n c l u d e < conio .h >

4 5 6

enum Zustaende { AusgangsZustand , MuenzeEingeworfen , Rueckgabegefordert , SchubladeGezogen };

7 8 9 10

i n t main () { c h a r e; i n t nrZustand = AusgangsZustand ;

11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47

w h i l e (1) { e = getch (); e = toupper ( e ); s w i t c h ( nrZustand ) { c a s e R uec kg ab eg ef or de rt : switch ( e ) { c a s e ’L ’ : printf ( " Noop \ n " ); nrZustand = AusgangsZustand ; break ; } break ; c a s e AusgangsZustand : switch ( e ) { c a s e ’D ’ : printf ( " Noop \ n " ); nrZustand = AusgangsZustand ; break ; c a s e ’L ’ : printf ( " Noop \ n " ); nrZustand = AusgangsZustand ; break ; c a s e ’M ’ : printf ( " Sa \ n " ); nrZustand = Muenz eEinge worfe n ; break ; } break ; c a s e Mue nzeEin geworf en : switch ( e ) { c a s e ’D ’ : printf ( " SzAa \ n " ); nrZustand = R uec kg ab eg ef or de rt ; break ; c a s e ’A ’ : printf ( " Ka \ n " );

181

182

12 Algorithmen: Reaktive Programme, Automaten nrZustand = SchubladeGezogen ; break ; c a s e ’L ’ : printf ( " Noop \ n " ); nrZustand = Muenz eEinge worfe n ; break ;

48 49 50 51 52 53

} break ; c a s e SchubladeGezogen : switch ( e ) { c a s e ’Z ’ : printf ( " Kz \ n " ); nrZustand = AusgangsZustand ; break ; c a s e ’L ’ : printf ( " Noop \ n " ); nrZustand = SchubladeGezogen ; break ; c a s e ’D ’ : printf ( " Noop \ n " ); nrZustand = SchubladeGezogen ; break ; }

54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70

}

71

}

72 73

}

Abbildung 12.4: Verkaufsautomat als Programm - direkte Implementierung mit Schleife und Zustandsvariable

12.4 Erkennende Automaten Häufig wird eine spezielle Art von Automaten angewendet: erkennende Automaten. Ein erkennender Automat ist durch (Z , z0 , Za , Zr , E, T ) gegeben, wobei gilt Z z0 Za Zr E T

ist eine endliche nicht leere Menge von Zuständen z0 ∈ Z ist der Anfangszustand Za ⊆ Z ist die Teilmenge der akzeptierenden Zustände aus Z Zr ⊆ Z ist die Teilmenge der rückweisenden Zustände aus Z ist das Eingabe-Alphabet bzw. der Eingabe-Code, d.h. eine endliche Menge von Eingabezeichen ist die Menge der Transitionen (Zustandsübergänge). Jede Transition t ∈ T ordnet einem Ausgangszustand za ∈ Z und einer Eingabe e ∈ E einen Folgezustand zf ∈ Z zu: (za , e) → zf

12.4 Erkennende Automaten

183

Für erkennende Automaten bestehen Eingaben aus Folgen von Zeichen aus einem Alphabet bzw. Code. Eine Teilmenge der Zustände wird als Menge der akzeptierenden Zustände interpretiert. Wenn der Automat nach Eingabe einer Zeichenkette in einem solchen Zustand landet, dann sagt man „der Automat hat die Zeichenkette erkannt“. Eine andere Teilmenge der Zustände wird als Menge der zurückweisenden Zustände interpretiert. Wenn der Automat nach Eingabe einer Zeichenkette in einem dieser Zustände landet, dann sagt man „der Automat hat die Zeichenkette zurückgewiesen“. Die Menge der Zeichenketten über dem Eingabealphabet, die der Automat erkennt, wird als die vom Automaten erkannte Sprache bezeichnet.

Beispiel:

ErkannteSprache = 0 1

Die zu erkennende Sprache sind Dualzahlen (also 0/1-Folgen – evtl. auch leere), gefolgt von =

Der erkennende Automat dazu sieht aus, wie folgt: 0

Rückgewiesen

sonstiges Zeichen

Lesen

=

Akzeptiert

1 Abbildung 12.5: Automat, der Dualzahlen, gefolgt von ’=’ erkennt

In diesem Automaten sind

Z z0 Za Zr E

= {Lesen, Akzeptiert, Rückgewiesen} = {Lesen} = {Akzeptiert} = {Rückgewiesen} = {0, 1, =} ∪ {x | x ist Zeichen aus ASCII − Code, außer 0, 1, =}

184

12 Algorithmen: Reaktive Programme, Automaten T

Zeile 1 2 3 4

za Lesen Lesen Lesen Lesen

e 0 1 = x

zf Lesen Lesen Akzeptiert Rückgewiesen

Wenn wir die Dualzahl mit dem Wert 5, gefolgt von = eingeben, also die Zeichenkette 101=, dann läuft folgendes ab: • Ausgangszustand ist Lesen • mit Eingabe von 1 ist der Folgezustand wieder Lesen (Zeile 2 ) • mit Eingabe von 0 ist der Folgezustand wieder Lesen (Zeile 1 ) • mit Eingabe von 1 ist der Folgezustand wieder Lesen (Zeile 2 ) • mit Eingabe von = ist der Folgezustand Akzeptiert (Zeile 3 ) Weil Akzeptiert ein Endzustand aus der Menge Za ist, hat der Automat die Zeichenkette 101= akzeptiert. Als Programm könnte der Automat so aussehen:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

#i n c l u d e < stdio .h > #i n c l u d e < conio .h > i n t main () { c h a r e; Lesen : e = getch (); putch ( e ); s w i t c h (e) { c a s e ’1 ’: g o t o Lesen ; c a s e ’0 ’: g o t o Lesen ; c a s e ’= ’: g o t o Akzeptiert ; d e f a u l t : g o t o Rueckgewiesen ; } Akzeptiert : printf ( " Zeichenkette akzeptiert \ n " ); r e t u r n 0; Rueckgewiesen : printf ( " Zeichenkette rückgewiesen \ n " ); r e t u r n 1; }

12.5 Aktionen in Automaten-Programmen

185

12.5 Aktionen in Automaten-Programmen Oft sind neben dem eigentlichen Ablauf des Automaten in Programmen weitere Aktionen erforderlich. Man fügt dann jeweils ein Stück Programm in den Zweig der betreffenden Transition ein. Beispiel: Der erkennende Automat aus dem vorigen Abschnitt soll Dualzahlen nicht nur erkennen, sondern gleich in einen Dezimalwert konvertieren. Bei Eingabe eines Gleichheitszeichens soll die Dezimaldarstellung ausgegeben werden. Zur Zahlenkonversion benutzen wir das multiplikative Zielverfahren (vgl. Kapitel 2 „Zahlendarstellung/ Umwandlung zwischen Zahlensystemen“). Damit könnte das Programm zur Zahlenkonversion so aussehen: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23

#i n c l u d e < conio .h > #i n c l u d e < stdio .h > i n t main () { c h a r e ; i n t ZwiErg =0; Lesen : e = getch (); putch ( e ); s w i t c h (e) { c a s e ’1 ’: ZwiErg = ZwiErg *2+1; g o t o Lesen ; c a s e ’0 ’: ZwiErg = ZwiErg *2; g o t o Lesen ; c a s e ’= ’: g o t o Akzeptiert ; d e f a u l t : g o t o Rueckgewiesen ; } Akzeptiert : printf ( " % d \ n " , ZwiErg ); ZwiErg =0; g o t o Lesen ; // Ende + N e u s t a r t Rueckgewiesen : printf ( " Zeichenkette rückgewiesen \ n " ); ZwiErg =0; g o t o Lesen ; // Ende + N e u s t a r t }

Protokoll einer Sitzung mit dem Programm: 1 2 3

101=5 1100=12 19 Zeichenkette rückgewiesen

186

12 Algorithmen: Reaktive Programme, Automaten

12.6 Weitere Anwendungsbeispiele Neben weit verbreiteten Anwendungen in der Automatisierungstechnik oder der Automobiltechnik werden endliche Automaten in vielen weiteren Gebieten eingesetzt. Zwei Beispiele dazu finden sich in diesem Abschnitt.

12.6.1 DFÜ-Protokolle Wenn Sie eine Verbindung über Internet herstellen, läuft ein Vorgang nach dem Prinzip ab, wie es in dieser Aufgabe dargestellt ist. Wenn Sie ein Produkt für die Telekommunikation benutzen, dann arbeitet für den Aufbau Ihrer Verbindung ein Programm, das einen endlichen Automaten realisiert, irgendwo auf einem Vermittlungsrechner. Diese Technik ist also sehr verbreitet. Hier wird eine etwas vereinfachte Problemstellung betrachtet, wo über ein serielles Medium5 zwei PCs miteinander verbunden werden, z. B. um Dateien auszutauschen. Aus Anwendersicht könnte das so aussehen, dass auf dem einen PC ein Programm Receive.exe gestartet wird, auf dem anderen ein Programm Send.exe. Letzteres erhält den Namen einer Datei, die es dann zur Gegenseite überträgt. Die Kommunikation der beiden Programme geschieht über sog. Datagramme, d.h. Datenblöcke, die aus folgenden Teilen bestehen: • Header (Kopf), der besagt, welche Art von Datagramm gemeint ist • Länge des Datagramms • die eigentlichen Nutzdaten • Prüfsumme (zum Überprüfen auf Korrektheit bzw. fehlerfreie Übertragung) In welcher Reihenfolge sich Receive.exe und Send.exe welche Datagramme senden müssen, um einen Dateitransfer abzuwickeln, regelt ein sog. Protokoll. Im vorliegenden Beispiel könnte es etwa aussehen wie in Abb. 12.6 S. 187. Es ist ein Ablauf dargestellt6 , in dem keine Fehlerbedingungen auftreten. Der Automat in Abbildung 12.7 S. 188 berücksichtigt auch Fehler. Dabei bedeuten Eingaben wie !=XmitAccept, dass eine beliebige andere als die betreffende Eingabe erfolgt ist – entweder vom Empfangsprogramm der Gegenseite – oder auch aus der Umgebung des Sende-Programms, z. B. durch Ablauf eines Timers (Timeout).

5 z.B.

serielle Schnittstelle, USB Schnittstelle, TCP-IP etc. wird hier als Abkürzung für „transmit“, also „übertragen“ benutzt

6 „Xmit“

12.6 Weitere Anwendungsbeispiele

187

Send.exe Dateiname eingabe

Datagramme

Receive.exe

Datei öffnen

XmitRequest

Datei öffnen

Datenblock lesen

XmitAccept

Akzeptieren

Datenblock senden

Datablock

Block in Datei schreiben

Datenblock lesen

BlockAck

Block quittieren

Datenblock senden

Datablock

Block in Datei schreiben

... usw.

BlockAck

Block quittieren

Datenblock lesen

... usw.

... usw.

Dateiende (EOF)

XmitEnd

Datei schließen

Beenden

FileAcknowledge

Übertragung quittieren

Abbildung 12.6: DFÜ-Protokoll eines einfachen Datentransfers

188

12 Algorithmen: Reaktive Programme, Automaten

OpenFileError/ ErrorExit

Initialzustand

Stop

OpenFileOk/ SendXmitRequest

Wait for Accept

!=XmitAccept/ ErrorExit

FileAck/ OkExit

!=FileAck/ ErrorExit

XmitAccept/ DatenblockLesen

Xmit Block

BlockReadEOF/ SendXmitEnd

BlockReadOk/ SendDatablock

Wait for Acknowledge

Wait for File Acknowledge

BlockAck/ DatenblockLesen

!=BlockAck/ ErrorExit

Abbildung 12.7: Automatengraph zum DFÜ-Protokoll/ senderseitig

12.6 Weitere Anwendungsbeispiele

189

12.6.2 Filter zur Behandlung von Zeichenfolgen Web-Browser interpretieren Dokumente im HTML-Format. HTML ist ein Text-Format, das mit einer beliebigen Textbearbeitungs-Anwendung bearbeitet werden kann. Die abgebildete Seite könnte mit einem Texteditor etwa mit folgendem Inhalt erstellt worden sein: 1 2 3 4 5 6 7 8 9 10

< head > < meta c o n t e n t = " text / html ; charset = US - ASCII " > < t i t l e > Gr üß e

< body >

Sch ö ne Gr üß e

Man erkennt, dass die nationalen Sonderzeichen äöüß in einer Ersatzdarstellung codiert sind. HTML benutzt Unicode (s. Kapitel 3 „Zeichencodes“), d.h. eine 16 Bit Codierung. Damit kann man ca. 65.000 Zeichen aus den verschiedensten Sprachen von Arabisch bis Zulu codieren. Alle Zeichen, deren Zahlenwert zwischen 0 und 127 liegt, wurden im gezeigten Beispiel wie in einer normalen ASCII-Datei codiert. Um unabhängig von Hardware/ SoftwarePlattformen zu sein, sind die Zeichen ab 128 (d.h. bei 16 Bit ist das Byte mit dem höheren Stellenwert 6= 0) mit sog. Escape-Sequenzen codiert: EscapeSequenz &

#

Dezimalzahl

;

lesbareAbkuerzung Die Codierung der Sonderzeichen äöüÄÖÜß sieht aus, wie in Tabelle 12.2 S. 190 dargestellt. Wenn man ein Programm schreiben will, das die Escape-Sequenzen „heraus filtert“ und durch die Klartextdarstellung der Sonderzeichen für die aktuelle Arbeitsplattform ersetzt, dann muss dieses etwa wie folgt verfahren: Kopiere Byte für Byte von der Eingabe in die Ausgabe, bis ein & entdeckt wird. Falls ein # und eine Dezimalzahl und ein ; folgen, dann das Sonderzeichen ausgeben. Falls eine der lesbaren Abkürzungen und ein ; folgen, dann ebenfalls das zugehörige Zeichen ausgeben. Wenn kein korrekter Abschluss der Escape-Sequenz erkannt wird, dann das bisher „einbehaltene“ Anfangsstück ausgeben und wieder Byte für Byte kopieren usw.

190

12 Algorithmen: Reaktive Programme, Automaten

Sonderzeichen ä ö ü Ä Ö Ü ß

Codierung als Zahl ä ö ü Ä Ö Ü ß

Codierung als lesbare Abkürzung ä ö ü Ä Ö Ü ß

Tabelle 12.2: Codierung einiger deutscher Sonderzeichen in HTML

Ein vereinfachter Automatengraph für das Programm, mit dem man nur die Dezimalcodierung der Zeichen aus Tabelle 12.2 erfasst, könnte aussehen, wie in Abbildung 12.8 auf Seite 191. Während der Automat hinter einem Fluchtsymbol & versucht, die begonnene EscapeSequenz zu komplettieren, werden keine Ausgaben gemacht. Falls ein Zeichen eingegeben wird, das nicht in eine Escape Sequenz passt, werden alle Zeichen ausgegeben, die seit dem letzten Aufenthalt im Ausgangszustand eingegeben wurden und es beginnt eine neue Suche nach dem Fluchtsymbol &. Folgende Symbole und Beschriftungen werden im Automatengraphen für Transitionen benutzt: e

e ∈ {0, . . . , 9} als Beschriftung einer Transition: e ist das Eingabezeichen. Die Ausgabe ist leer

;/a

Mit dem abschließenden ; ist  eine Escape-Sequenz komplett, das Zeichen a ∈ Ä, Ö, Ü, ä, ö, ü, ß wird ausgegeben. Bedeutet ein Bündel von Transitionen (za , e) → (z0 , Zeichenfolge) vom aktuell erreichten Zustand za zurück zum Startzustand ? = z0 . Für e ∈ E sind diejenigen Eingabezeichen einzusetzen, zu denen es keine Transition (e, za ) → (zf , a) gibt mit zf 6= z0 . Mit anderen Worten: es gibt keine Escape-Sequenz mit dem bereits gelesenen Anfangsstück und dem Zeichen e. Die auszugebende Zeichenfolge enthält alle Zeichen, die auf dem Weg vom Startzustand ? bis zum aktuell erreichten Zustand eingegeben (und „einbehalten“) wurden, einschließlich e.

12.6 Weitere Anwendungsbeispiele

&

&

#

#

1

191

1

9

9

6

;/Ä

6

2

2 ;/Ö

?

;/Ü

1

1'

4

2'

0

2

3

4

8 ;/ ß

;/ä

5

0 3 8

4'

6

;/ö

6' 5

;/ü

4

2 2''

Abbildung 12.8: Filterautomat für Escape-Sequenzen in HTML-Dateien

192

12 Algorithmen: Reaktive Programme, Automaten

12.7 Fragen Aufgabe 12.1 a) Geben Sie die Form und Beschriftung der Elemente eines Automatengraphen an, ferner deren Bedeutung für den zugehörigen Automaten. b) Das folgende Programm entspricht einem endlichen Automaten. Was macht das Programm? 1

#i n c l u d e < conio .h >

2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34

v o i d beep ( v o i d ){ putch ( ’ \007 ’ ); /∗ P i e p t o n ∗/ } i n t main () { c h a r cb , ce1 , ce2 , cp ; be : cb = getch (); s w i t c h ( cb ){ c a s e ’b ’: g o t o e1 ; default: putch ( cb ); g o t o be ; } e1 : ce1 = getch (); s w i t c h ( ce1 ){ c a s e ’e ’: g o t o e2 ; default: putch ( cb ); putch ( ce1 ); g o t o be ; } e2 : ce2 = getch (); s w i t c h ( ce2 ){ c a s e ’e ’: g o t o pe ; default: putch ( cb ); putch ( ce1 ); putch ( ce2 ); g o t o be ; } pe : cp = getch (); s w i t c h ( cp ){ c a s e ’p ’: beep (); g o t o be ; default: putch ( cb ); putch ( ce1 ); putch ( ce2 ); putch ( cp ); g o t o be ; } }

12.8 Aufgaben

193

12.8 Aufgaben Aufgabe 12.2 In Abschnitt 12.6.1 auf Seite 186 ist ein Automat beschrieben, der ein DFÜ-Protokoll realisiert. a) Schreiben Sie ein Programm für den auf S. 188 abgebildeten Automaten! Simulieren Sie die Datagramme der Sendeseite, indem Sie die Ausgaben der Transaktionen mit printf(...) ausgeben. Die Datagramme, die das Empfangsprogramm sendet, sollen durch Tastatureingaben simuliert werden, die mit scanf(...) zu lesen sind. Der Einfachheit halber können dafür Zahlen eingegeben werden (die sich direkt im switch (...) vergleichen lassen). Damit das Programm trotzdem lesbar bleibt, sollten Sie dazu einen enum-Typ mit allen Events deklarieren und im Programm durchgängig benutzen. b) Entwerfen Sie den Automatengraphen für ein Empfangsprogramm, das ein zur Sendeseite passendes Protokoll realisiert! Verifizieren Sie Ihren Entwurf mit Protokollsequenzen für normale Übertragungen und solche, bei denen Probleme auftreten! Aufgabe 12.3 In Abschnitt 12.6.2 auf Seite 189 ist ein Automat beschrieben, der Escape-Sequenzen aus HTML-Dateien ersetzt. a) Schreiben Sie ein Programm, das den in Abbildung 12.8 S. 191 abgebildeten Automaten realisiert7 ! Testen Sie das Programm an einer Zeichenfolge ähnlich der im Browserfenster auf Seite 189. b) Geben Sie eine sinngemäße Ergänzung des Automatengraphen für eine exemplarisch ausgewählte lesbare Abkürzung (z. B. ä) an! c) Ergänzen Sie Ihr Programm um die passenden Anweisungen zur vorigen Teilaufgabe. d) Finden Sie eine Möglichkeit, mehrfache Pfade für die Teilsequenzen uml; im Automaten in einem einzigen Pfad zu realisieren! Führen Sie dazu eine geeignete Aktion am Ende durch! Erweitern Sie die Lösung aus c)!

7 Häufig laufen Textmode-Programme aus historischen Gründen in einem Fenster mit der DOSCodepage 850. Für die Umlaute und das scharfe s müssen Sie dann in printf ausgeben: Ä:\x8E Ö:\x99 Ü:\x9A ä:\x84 ö:\x94 ü:\x81 ß:\xE1

13

Vektoren

13.1 Abgeleitete Typen in C, Übersicht Wir haben bisher ganzzahlige und Gleitpunkt-Basistypen in C kennen gelernt. In diesem und folgenden Kapiteln werden weitere, benutzerdefinierte Typen eingeführt, die sich aus den Basistypen ableiten lassen. Unsere bisher aktuelle Deklarationssyntax erweitert sich wie in der Abbildung 13.1 auf Seite 196 dargestellt. Es finden sich neben der bisher bekannten Möglichkeit, eine einfache Variable zu deklarieren, verschiedene weitere Pfade. Im vorliegenden Kapitel interessieren uns vor allem Vektoren. Später werden Unterprogramme und Zeiger eingeführt. Zur Bildung von komplexen Typen durch ineinanderSchachtelung der Deklarations-Varianten, siehe Abschnitt 13.3 S. 197.

13.2 Eindimensionale Vektoren Kein professionelles C-Programm kommt ohne Vektoren aus. Oft benötigt man viele Variablen gleichen Typs, die man in Laufschleifen bearbeiten will oder man hat einen Algorithmus, der erst zur Laufzeit berechnet, auf welche Variable jeweils zugegriffen werden soll. Beispiel: Ein Wort wird (noch ohne Benutzung von Vektoren) eingelesen und rückwärts ausgegeben 1 2 3 4 5

c h a r c1 , c2 , c3 , c4 ; scanf ( " % c " , & c1 ); scanf ( " % c " , & c2 ); scanf ( " % c " , & c3 ); scanf ( " % c " , & c4 ); printf ( " % c " , c4 ); printf ( " % c " , c3 ); printf ( " % c " , c2 ); printf ( " % c " , c1 );

Für lange Eingaben wird das Programm sehr lang. Was ist, wenn man vorher nicht weiß, wie lange das einzugebende Wort wird? Wir kennen schon die Lösung: Zeichenketten. Das sind Anordnungen von chars, lauter Komponenten gleichen Typs, also Vektoren. Im Englischen werden solche „Aufreihungen“ auch Arrays genannt.

196

13 Vektoren

Deklaration Typ

Deklarator

; Initialisierer

= , Deklarator Bezeichner Klammern Zeiger

)

Deklarator

*

Unterprog. Vektoren

Deklarator

(

Deklarator Deklarator

Parameterliste

( [

) ]

KonstAusdr Initialisierer KonstAusdr {

Initialisierer

}

, Abbildung 13.1: Erweiterung der Deklarationssyntax

Damit lässt sich das obige Beispiel z. B. so schreiben: 1 2 3 4 5 6

c h a r cArr [4]; i n t i; f o r ( i =0; i =0; i - -) printf ( " % c " , cArr [ i ]);

In Zeile 1 wird ein Vektor mit vier chars deklariert. In den Zeilen 4 und 6 wird auf die Komponente i dieses Vektors zugegriffen.

13.3 Deklaration von Vektoren

197

13.3 Deklaration von Vektoren Die Abbildung 13.2 zeigt den einfachsten Weg durch die Möglichkeiten des Syntaxdiagramms in Abbildung 13.1 für eine Vektor-Deklaration. VektorDekl KomponentenTyp

Vektorbezeichner

[

KonstAusdr

]

; =

{

KonstAusdr

}

, Abbildung 13.2: Vektordeklaration mit Angabe des Vektorbezeichners

Deklariert wird hier ein eindimensionaler Vektor von Komponenten, die alle den angegebenen Basistyp haben. In [...] wird die gewünschte Komponentenzahl spezifiziert. Falls die Komponenten mit Werten vorbelegt werden sollen, gibt man diese in {...} durch Kommas getrennt an. Beispiel: char acV[3] = {’a’, ’b’, ’c’}; /∗ array of 3 chars ∗/ Es gelten folgende Nebenbedingungen • Die Anzahl der Komponenten muss eine natürliche Zahl sein. Bis zum Standard C90 muss die Anzahl als konstanter Ausdruck gegeben sein. Ab dem Standard C99 ist für Vektoren, die als lokale Variablen von Unterprogrammen1 und nicht static deklariert sind auch zulässig, dass die Anzahl sich als Wert eines Ausdrucks berechnet, der Variablen enthält. • Die Komponenten werden von 0 bis Komponentenanzahl-1 nummeriert • Beschränkungen der maximalen Komponentenzahl und der maximalen Größe von Vektoren sind durch die jeweiligen Entwicklungs- und Laufzeit-Umgebung vorgegeben.

Für die maximale Anzahl der Komponenten eines Vektors spielen die Adressbildungsmöglichkeiten des Zielprozessors eine besondere Rolle, denn Vektorkomponenten werden 1 siehe

Kapitel 17 „Unterprogramme“

198

13 Vektoren

vom Prozessor über eine berechnete Adresse zugegriffen. Dazu erzeugt der Compiler Code, der aus der Basisadresse und der Nummer der Vektorkomponente die KomponentenAdresse berechnet. Der erzeugte Code muss sich natürlich nach den Möglichkeiten des Prozessors richten, damit der Zugriff nicht ineffizient wird. Das führt zu Einschränkungen z.B. bei Mikroprozessoren, die etwa mit 16 Bit Zahlenwerten die Komponentenadresse berechnen. Mit 16 Bit kommt man auf Komponentenzahlen von 32767 bzw. 65535 bei vorzeichenloser Rechnung. Für die maximal mögliche Größe des Speicherplatzes, den ein Vektor belegen kann, spielen vor allem folgende Eigenschaften der Entwicklungs- und Laufzeit-Umgebung eine Rolle • Hinsichtlich des Verfügbaren Speichers gibt es Beschränkungen insbesondere in Embedded Systems, also Mikroprozessoren, die häufig nur über sehr kleine Adressbereiche oft für verschiedenartige Speichertechnologien verfügen. • Wenn Vektoren als lokale Variablen von Unterprogrammen deklariert sind, beschränkt die Stack-Größe die Maximalgröße (s. Kap. 17.4 S. 256 „Lokale, globale und statische Variablen“). • Auch für statisch deklarierte oder globale Vektoren oder Vektoren, die auf dem Heap angelegt werden, ist der Speicherplatz nicht unendlich. Es ist nur der Speicher verfügbar, der auch physikalisch oder zumindest als virtueller Speicher vorhanden ist und davon sind noch die Bereiche abzuziehen, die das Betriebssystem und die anderen laufenden Programme in Summe belegen. In 32Bit-PCUmgebungen kann man unter günstigen Umständen Vektoren in der Größenordnung von Gigabytes verwenden. C bietet auch etwas Komfort für die Deklaration von Vektoren: 1. bei Vorbelegung muss die Zahl der Komponenten nicht angegeben werden, z. B. char acV[ ] = {’a’, ’b’, ’c’}; /∗entspricht acV [3]... ∗/ 2. Unterstützung der Vorbelegung für Zeichenketten, z. B. char szV[ ] = "abc"; Bei der Vorbelegung von Zeichenketten wird automatisch ein abschließendes ’\0’Zeichen eingefügt. Die Anzahl der Vektorkomponenten wird passend gesetzt. Die Deklaration char szV[ ] = "abc"; entspricht also genau char szV[4] = {’a’, ’b’, ’c’, ’\0’};

13.4 Zugriff auf Ganz- und Komponenten-Variable Die Ganz-Variable erhält man bei Angabe des Vektorbezeichners. Beispiel: char cArr [4]; printf("%d", sizeof( cArr | {z } )); /∗ druckt 4 ∗/ Ganz− Variable

13.4 Zugriff auf Ganz- und Komponenten-Variable

199

Auf die Komponenten-Variablen greift man zu, indem man hinter den Bezeichner der Ganz-Variablen in [...] die Nummer der gewünschten Komponente schreibt: Beispiel: printf("%c",cArr[i]); oder cArr[0]= ’a’; oder c1 =cArr[2]; | {z } | {z } | {z } 1

2

3

In diesem Beispiel sind 1 , 2 und 3 Zugriffe auf Komponenten-Variablen. Wir gehen von den Deklarationen aus dem vorigen Abschnitt aus: char acV[] = {’a’, ’b’, ’c’}; /∗ array of 3 chars ∗/ char szV[ ] = "abc"; /∗ string "abc" ∗/ Die Tabelle 13.1 zeigt die damit vereinbarten Ganz- und Komponenten-Variablen. Die Spalte „Ausdruck“ gibt an, wie man den Zugriff auf die betreffende Variable in einem Ausdruck schreibt. Ausdruck acV

Ganz/ Komp. G

acV[0] acV[1] acV[2] szV

K K K G

szV[0] szV[1] szV[2] szV[3]

K K K K

Typ Vektor mit [3] char Komponenten char char char Vektor mit [4] charKomponenten char char char char

Wert

sizeof

{’a’,’b’,’c’}

3

’a’ ’b’ ’c’ "abc"≡ {’a’,’b’,’c’,’\0’} ’a’ ’b’ ’c’ ’\0’

1 1 1 4 1 1 1 1

Tabelle 13.1: Beispiel: Ganz- und Komponentenvariable

Über die Lage der Ganz-/Komponenten-Variablen zueinander im Speicher werden wir im Abschnitt 16.3 „Pointer und Vektoren“ näheres erfahren. Das folgende Beispiel zeigt eine Einfüge/ Verschiebe-Operation auf einem Vektor, wie sie für Sortieralgorithmen typisch ist.

s

c

h

a

r

c

h

a

r

vorher

Beispiel: Programm, das aus dem Wort „schar“ das Wort „chars“ macht wie im nebenstehenden Bild. Abbildung 13.3 S. 200 zeigt das Listing des Programms.

s

nachher

Übung: Umfassen der Verschiebung der Buchstaben und des Anhängens des jeweils vorher ersten durch eine weitere Schleife, so dass beides zweimal durchgeführt wird („schar“ → “harsc“).

200

1 2

13 Vektoren

#i n c l u d e < string .h > #i n c l u d e < stdio .h >

3 4 5 6

i n t main () { c h a r wort []= " schar " , ch ; i n t n;

7

ch = wort [0];

8 9

f o r ( n =0; n

2 3 4

i n t main () { double

v [3] = { 1.0 , res [3]; d o u b l e Matrix [3][3] = { { 13.2 , { 1.0 , { 0.0 , }; i n t z, s; f o r ( z =0; z #d e f i n e N 6

// B e i s p i e l p r o g r a m m

3 4 5 6 7 8 9 10 11 12 13 14 15

i n t main () { i n t j , k , t , vek [ N ] = { 3 , 1 , 14 , 7 , 5 , 1 }; f o r ( j =0; j vek [ k ]) /∗ T a u s c h b e d i n g u n g ∗/ { t = vek [ j ]; /∗ T a u s c h e n d e r E l e m e n t e ∗/ vek [ j ] = vek [ k ]; vek [ k ] = t ; } } f o r ( j =0; j < N ; j ++) printf ( " % d \ n " , vek [ j ]); /∗ A u s g a b e ∗/ r e t u r n 0;

16 17

}

14.1.3 Bucketsort Bucket heißt Eimer. Man benutzt das Verfahren, wenn man sehr viel mehr Datensätze als vorkommende Schlüsselwerte hat. Für jeden der möglichen Schlüsselwerte bildet man einen „Eimer“. Dann geht man an den Datensätzen entlang und „wirft“ jeden Datensatz in den zugehörigen Eimer. In der dritten Phase erzeugt man die sortierte Folge von Datensätzen, indem man die Eimer in aufsteigender Reihenfolge „ausleert“. Die Eimer werden durch Komponenten eines Vektors nachgebildet. Das Einwerfen von Datensätzen entspricht (wir sortieren hier nur Zahlen) dem Hochzählen der zugehörigen Komponente des Eimer-Vektors. Zum Schlüsselwert i gehört die i-te Komponente des Eimer-Vektors. Das Ausleeren der i-ten Komponente entspricht dem Anhängen von n Werten i an den Ausgabe-Vektor der sortierten Zahlen. n ist dabei das Zählergebnis, das in der i-ten Komponente des Eimer-Vektors steht. Um Platz zu sparen, kann man als Ausgabe-Vektor auch wieder den Vektor benutzen, in dem ursprünglich die zu sortierenden Zahlen standen. Diese werden von der Komponente 0 an beginnend, zum Ende des Vektors hin überschrieben.

210

14 Algorithmen: Sortierverfahren, Zufallszahlen

Algorithmus: Für jeden Eimer j Inhalt des Eimers auf 0 setzen Für jede Komponente j des zu sortierenden Vektors vek Eimer mit der Nummer vek[j]-1 um 1 hoch zählen Für jeden Eimer j Sooft, wie der Inhalt Eimer[j] angibt j+1 an den Vektor vek anhängen Für jede Komponente j des zu sortierenden Vektors vek Komponente ausgeben Beispielprogramm 1 2 3

#i n c l u d e < stdio .h > #d e f i n e N 20 #d e f i n e E 3

4 5 6 7 8 9 10 11 12 13 14 15

i n t main () { i n t j , k , t =0 , vek [ N ] = { 1 , 2 , 2 , 1 , 1 , 3 , 3 , 3 , 2 , 1 , 1, 2, 3, 3, 3, 3, 1, 2, 3, 3 }, Eimer [ E ]; f o r ( j =0; j < E ; j ++) Eimer [ j ] = 0; /∗ E i m e r l e e r f o r ( j =0; j < N ; j ++) Eimer [ vek [ j ] -1]++; /∗ E i m e r e r h . f o r ( j =0; j < E ; j ++) /∗ E i m e r a u s l e e r e n f o r ( k =0; k < Eimer [ j ]; k ++) /∗ k Elem . an v e k h a e n g e n vek [ t ++] = j +1; f o r ( j =0; j < N ; j ++) printf ( " % d \ n " , vek [ j ]); /∗ A u s g a b e

16

∗/

r e t u r n 0;

17 18

∗/ ∗/ ∗/ ∗/

}

14.2 Zufallszahlen Mit #include sind aus der Standardbibliothek verfügbar: int rand(void); liefert eine Zufallszahl Typ int zwischen 0 und RAND_MAX RAND_MAX Konstante: Maximalwert, den rand() liefern kann void srand(unsigned); Initialisiert den Zufallszahlengenerator Wenn man z. B. 6 Zufallszahlen aus dem Bereich 1-49 haben will, dann kann man programmieren:

14.2 Zufallszahlen

1 2

211

#i n c l u d e < stdio .h > #i n c l u d e < stdlib .h >

3 4 5 6 7 8

i n t main () { i n t i; f o r ( i =0; i

3 4 5 6 7 8

i n t main () { i n t i; f o r ( i =0; i #i n c l u d e < stdlib .h > #i n c l u d e < string .h > #d e f i n e MAX 128 #d e f i n e BUFLEN 64

6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33

i n t main () { c h a r puffer [ BUFLEN ]= " " ; /∗ n u r e i n m a l v o r h a n d e n ∗/ c h a r * namen [ MAX ]; /∗ p r o E i n t r a g e i n P o i n t e r ∗/ i n t i =0 , j =0 , k =0 , fStop =0; /∗ Z a e h l g r o e s s e n , M e r k e r ∗/ c h a r * pch ; /∗ H i l f s z e i g e r z . t a u s c h e n ∗/ w h i l e ((! fStop )&&( i < MAX )) /∗ S c h l e i f e : Namen e i n l e s e n ∗/ { scanf ( " % s " , puffer ); fStop = strcmp ( puffer , " stop " )==0; i f (! fStop ) { namen [ i ] = ( c h a r *) malloc ( strlen ( puffer )+1); strcpy ( namen [ i ] , puffer ); i ++; } } /∗ D o p p e l − S c h l e i f e : S o r t i e r e n m i t A u s w a h l s o r t ∗/ f o r ( j =0; j 0) /∗ T a u s c h b e d i n g u n g ∗/ { pch = namen [ j ]; namen [ j ] = namen [ k ]; /∗ T a u s c h e n Z e i g e r ∗/ namen [ k ] = pch ; } } f o r ( j =0; j < i ; j ++) /∗ A u s g a b e s o r t i e r t ∗/ printf ( " % s \ n " , namen [ j ]); /∗ j e e i n Name ∗/ f o r ( j =0; j < i ; j ++) free ( namen [ j ]); /∗ Heap f r e i g e b e n ∗/ r e t u r n 0;

34 35

}

Abbildung 16.9: Programmbeispiel „Auswahlsort durch Zeigervertauschung“

244

16 Pointer

16.6 Pointer und const Beim Zugriff über Zeigervariablen sind jeweils zwei Orte im Speicher betroffen:

Zeigervariable

Bezugsvariable

die Zeigervariable und die Bezugsvariable. Bei der Deklaration von Zeigern gibt es entsprechend zwei Möglichkeiten, den TypQualifizierer const zu verwenden, die sich durch dessen Stellung unterscheiden. const steht vor dem Deklarations-Teilausdruck, der die als const zu qualifizierende Größe bezeichnet: 1. Zeiger-Variable durch const vor Veränderungen schützen Beispiel: 1 2

i n t i = 17 , j = 117; i n t * c o n s t cpi = & i ;

Man interpretiert den Deklarationsausdruck so2 : „cpi ist eine Größe, die const ist. Wenn man sie dereferenziert erhält man int “. Mit anderen Worten: cpi ist ein konstanter Pointer auf eine int -Größe. Damit ist *cpi = 18; erlaubt und cpi = &j; falsch. 2. Bezugs-Variable durch const vor Veränderungen schützen Beispiel: 1 2

i n t i = 17 , j = 117; c o n s t i n t * pci = & i ;

Man interpretiert den Deklarationsausdruck so: „pci ist eine Größe, die man dereferenzieren kann. Wenn man sie dereferenziert erhält man int und das ist const“. Mit anderen Worten: pci ist ein Pointer auf eine unveränderliche int -Größe. Damit ist *pci = 18; falsch und pci = &j; erlaubt. Wenn jetzt pci auf j verweist, ist trotzdem anschließend *pci = 119; verboten, denn die const-Deklaration verbietet Schreibzugriffe über den Pointer. Damit wird aber die Bezugsvariable noch nicht zu einer Konstanten, denn es ist nichts über andere Wege des Zugriffs festgelegt. Erlaubt sind also im letzteren Beispiel: i = 19; und j = 119; const-Deklarationen im Zusammenhang mit Zeigern macht man hauptsächlich, weil man sich Vorteile für die Fehlersuche verspricht: Wo der Compiler Zugriffe ausschließt braucht man nicht mit Fehlern zu rechnen, d.h. nicht nach fehlerhaften Zugriffen im Programm zu suchen. 2 zur Interpretation von Deklarationsausdrücken siehe Kapitel 13.5 „Zur Deklarations-Syntax in C“ S. 200

16.7 Fragen

245

16.7 Fragen Aufgabe 16.1 Falsch oder richtig? Gegeben sind die folgenden Deklarationen 1

c h a r buf [] = " Zeichenkette " , * pCh ; i n t i ; Geben Sie zu den folgenden Zeilen an, was sie ausgeben bzw. warum sie falsch sind

2 3 4

f o r ( i =0; i < strlen ( buf ); i ++) printf ( " % c " , buf [ i ]); f o r (; * buf != ’ \0 ’; buf ++) printf ( " % c " , * buf ); f o r ( pCh = buf ; * pCh ; pCh ++) printf ( " % c " , * pCh );

Aufgabe 16.2 Umschreiben in Pointer-Schreibweise Das angegebene Programmstück dreht Zeichenketten um. Schreiben Sie es in Pointerschreibweise um! 1 2 3 4 5 6 7 8

c h a r buf [] = " Zeichenketten " , c ; i n t i , j , len ; len = strlen ( buf ); f o r ( i =0 , j = len -1; i < j ; i ++ , j - -) { c = buf [ i ]; buf [ i ] = buf [ j ]; buf [ j ] = c ; }

Aufgabe 16.3 Umschreiben in Vektorschreibweise Das angegebene Programmstück wandelt in einer Zeichenkette enthaltene Großbuchstaben in Kleinbuchstaben um. Schreiben Sie es in Vektorschreibweise um! 1 2 3 4

c h a r buf [] = " G r O s S k L e I n B u C h S t A b E n " , * pCh ; f o r ( pCh = buf ; * pCh ; pCh ++) i f ((* pCh >= ’A ’ )&&(* pCh EPS ) && ( it < MAX_SCHRITTE )); i f ( it >= MAX_SCHRITTE ) printf ( " Mangelnde Konvergenz !\ n " ); else { printf ( " Nullst . bei % lf mit % d Iterationen .\ n " , xi , it ); printf ( " Genauigkeit : % le " , f ( xi )); } 1 Bevor

Sie weiter lesen, sollten Sie die Änderung tatsächlich durchführen.

248

17 Unterprogramme

In Zeile 2 werden die aktuellen Werte von xi an die Unterprogramme übergeben. Die geforderten Änderungen sind damit viel einfacher durchzuführen und beschränken sich auf das Ändern von zwei Programmzeilen in den Unterprogrammen f und fstr. Außerdem wird das Programm viel übersichtlicher und kann sowohl von anderen Programmierern leichter verstanden werden, als auch vom Autor selbst, wenn er das Programm nach längerer Zeit wieder ansieht. Unterprogramme sind also das Mittel zur Strukturierung von Programmen und werden von professionellen Programmierern sehr intensiv eingesetzt. Weitere Informationen und Beispiele dazu finden sich in Kap. „Grafikausgabe/ Professionelle Programmiertechniken/ Programmstruktur“. Als MinimalRegeln für den Einsatz von Unterprogrammen sollte man verinnerlichen:

1. Jede Aufgabe, die mehr als ein Mal benötigt wird, sollte von einem Unterprogramm erledigt werden. 2. Kein Stück Programm sollte länger sein als eine Seite, die sich noch als Ganzes überblicken lässt. Wird diese Grenze überschritten, so sollte das betreffende Programmstück in Unterprogramme zerlegt werden.

In der Programmiersprache C werden keine verschiedenen Typen von Unterprogrammen unterschieden, wir sprechen daher statt von Unterprogrammen meist von Funktionen2 . Eine Funktion haben wir bisher immer schon geschrieben und zwar die Funktion main(). Diese Funktion stellt das Hauptprogramm dar. Sie wird vom Betriebssystem aufgerufen. Die genaue Syntax zum Erstellen von Unterprogrammen wollen wir uns nun ansehen. Im Struktogramm wird ein Unterprogramm folgendermaßen dargestellt: Unterprogramm-Name Beschreibung

Anweisungen

17.1 Syntax Das folgende Syntaxdiagramm fasst die Regeln zusammen, nach denen schon die obigen Beispiele gebildet wurden3 : 2 Dagegen werden beispielsweise in Pascal und Basic Funktionen (function) und Prozeduren (procedure/sub) unterschieden. 3 In diesem Syntaxdiagramm kann man einfachstenfalls „Funktionsbezeichner“ statt „Funktionsdeklarator“ einsetzen. Vgl. dazu Syntaxdiagramme am Beginn des Kapitels „Vektoren“.

17.1 Syntax

249

FunktionsDefinition Deklarator

(

)

Typ

FormalParDekl

void

,

Block

{

} Deklaration Anweisung

FormalParDekl Typ

Deklarator

Jede Funktion erhält einen Namen, den so genannten Funktionsbezeichner, der den in C gültigen Regeln für Bezeichner genügen muss. Nach dem Funktionsbezeichner folgt in Klammern eine Liste von formalen Parametern, die jeweils einen eigenen Bezeichner erhalten. Die runden Klammern müssen in jedem Fall gesetzt werden, auch wenn die Liste leer sein sollte. Die Definition von Funktionen kann in C nicht innerhalb von Verbundanweisungen erfolgen. Eine Schachtelung von Unterprogrammen, wie etwa in Pascal, ist in C nicht möglich! Die formalen Parameter dienen der Kommunikation mit dem Unterprogramm. Beim Aufruf der Funktion muss für jeden formalen Parameter ein Ausdruck als aktueller Parameter angegeben werden, dessen Wert dann in der Funktion zur Verfügung steht. Die Angabe der formalen Parameter erfolgt ähnlich wie die Definition von Variablen4 : FormalParameter Typ

ParameterDeklarator

Anders als bei der Variablendefinition muss hier für jeden Parameter erneut ein Typ angegeben werden. mehrere Paare von Typ/Parameterdeklaratoren werden durch Kommata voneinander getrennt. Jede Funktion kann einen Wert zurück liefern. Der Typ 4 Vgl. Syntaxdiagramme am Beginn des Kapitel „Vektoren“. Einfachstenfalls kann man „Parameterbezeichner“ für „Parameterdeklarator“ einsetzen.

250

17 Unterprogramme

dieses Rückgabewerts wird vor dem Funktionsbezeichner angegeben. Im Beispiel oben haben wir das bereits benutzt: x) |double {z } f(double | {z } 1

2

In dem Ausdruck gibt 1 an, dass die Funktion f einen Rückgabewert vom Type double liefert, und 2 , dass die Funktion einen Parameter vom Typ double hat, der in der Funktion über den Bezeichner x angesprochen wird. Dem Syntaxdiagramm entnehmen wir aber auch noch, dass vor dem Funktionsbezeichner das Schlüsselwort void angegeben werden kann. Das englische Wort „void“ bedeutet im Deutschen „leer“, was anzeigt, dass die Funktion keinen Rückgabewert liefert. Nach dem Syntaxdiagramm ist es auch möglich, vor dem Funktionsbezeichner keinen Datentyp anzugeben. Es wird dann vom Compiler eine Warnung ausgegeben und automatisch der Typ int eingesetzt. Dies ist nicht immer wünschenswert und kann zu Verwirrungen und Fehlern führen. Deshalb sollte immer ein Typ angegeben werden. Bis jetzt haben wir den so genannten Funktionskopf besprochen. Nach diesem folgt eine Verbundanweisung, also eine in {. . .} geklammerte Folge von Anweisungen, welche bei Aufruf der Funktion ausgeführt werden. In der Verbundanweisung eines Unterprogramms können Variablen definiert werden, wie wir in der Funktion main schon oft praktiziert haben. Eine Verbundanweisung, die Deklarationen enthält, wird auch als Block bezeichnet. Die dort definierten Variablen heißen lokale Variablen des Blocks. Für ANSI-C (C89) war es erforderlich, Deklarationen an den Anfang des Blocks vor die erste Anweisung zu stellen. Erst ab C99 [ISO/IEC (1999)] dürfen Anweisungen und Deklarationen gemischt werden, wie dies im Syntaxdiagramm oben dargestellt ist. Es gilt aber auch ab C99 immer noch die Einschränkung, dass Variablen erst nach Deklaration benutzt werden können. Auf die formalen Parameter kann in der Funktion wie auf lokale Variablen zugegriffen werden. Wenn eine Funktion einen Wert zurück liefern soll, so muss innerhalb ihrer Verbundanweisung mindestens ein Mal eine Return-Anweisung stehen: returnAnweisung return

; Ausdruck

Diese Anweisung bewirkt, dass der Ausdruck ausgewertet und danach die Ausführung der Funktion beendet wird. Der Wert des Ausdrucks wird als Rückgabewert zurückgegeben. Der Typ des Ausdrucks muss daher kompatibel zum angegebenen Typ im Funktionskopf sein. Eine Funktion, die keinen Rückgabewert liefert (Typ void), kann

17.2 Der Parametermechanismus

251

einfach mit return ; beendet werden (leerer Ausdruck). Ansonsten wird mit dem Ende der Verbundanweisung die Funktion in jedem Fall verlassen5 . An der Aufrufstelle der Funktion f geschieht also im einleitenden Beispiel folgendes: 1. Der Wert von xi wird an die Funktion f auf den Parameter x übergeben. −→ 1

xi = xi - f ( xi )/ fstr ( xi );

1 2 3 4

d o u b l e f( d o u b l e x) { r e t u r n x - E * sin ( x ) - M ; }

←− 2. Der Wert des Ausdrucks x - E * sin(x) - M wird als Wert von f(xi) zurück gegeben. Beim Aufruf einer Funktion muss sich der Programmierer also keine Gedanken mehr darüber machen, wie das Unterprogramm die geforderte Funktionalität bereitstellt. Deutlich wird dies am Beispiel der bereits vorhandenen Standardfunktionen. Wir haben in dem Beispiel eine Funktion zur Berechnung der Winkelfunktion sin(x) benutzt und sind dabei davon ausgegangen, dass diese Funktion das gewünschte Ergebnis liefert. Wir mussten uns nicht darum kümmern, auf welche Weise in dieser Funktion die Berechnung des Sinus durchgeführt wird (Reihenentwicklung, Tabellen,. . .). Gleiches gilt für die Funktionen printf() und scanf(). Der Leser möge sich hierzu die 1. Aufgabe zu diesem Kapitel ansehen. Wichtig ist in jedem Fall, beim Funktionsaufruf zu beachten:

+ Die Aktualparameter müssen mit den Formalparametern in Anzahl, Typ und Reihenfolge übereinstimmen. 17.2 Der Parametermechanismus Wir wollen uns nun im Detail ansehen, wie die Übergabe von Parametern an ein Unterprogramm vor sich geht. Dazu betrachten wir das einfache Modell eines Rechners, das bereits im ersten Kapitel dieses Buches eingeführt wurde – siehe Kap. 1.6 , Abb. 1.4 S. 14. Hier werden nur die Teile wiederholt, die zum Verständnis des Parametermechanismus wesentlich sind. Vergleiche dazu auch Weitzel (1996). Der Arbeitsspeicher ist in verschiedene Bereiche aufgeteilt: der Codebereich ist der Teil des Speichers, in dem sich die Maschinenbefehle des gerade ausgeführten Programms 5 Wird eine Funktion, die einen Rückgabewert liefern soll, ohne Return-Anweisung verlassen, so gibt der Compiler lediglich eine Warnung aus. Diese Warnung sollte sehr ernst genommen werden, da der Rückgabewert in diesem Fall undefiniert ist.

252

17 Unterprogramme

befinden (Code-Bereich). Der Befehlszähler (PC-Register) der CPU enthält die Adresse des nächsten auszuführenden Maschinenbefehls. Der Stack Pointer (SP) gibt die nächste freie Adresse auf dem Stack an. Im Datenbereich werden Variablen gespeichert, deren Werte dann in ein Prozessorregister geladen werden können, wenn ein Zugriff erfolgen soll. Im Beispielprogramm zur Nullstellenbestimmung liegt vor Aufruf der Funktion f(x) die in Abbildung 17.1 S. 252 dargestellte Situation vor: im Codebereich steht der Maschinencode von main sowie von f. Die Abbildungen 17.1 bzw. 17.2 vereinfachen die Darstellung etwas: der Code wird nicht durch Maschineninstruktionen dargestellt, sondern jeweils durch ein Rechteck symbolisiert und die Maschinenregister sind weggelassen. Beim Aufruf der Funktion f wird die Situation aus Abb. 17.1 übergeführt in eine Situation, wie in Abb. 17.2 dargestellt. Im Einzelnen passiert folgendes: 1. Für alle Formalparameter, lokalen Variablen und die Rücksprungadresse des Unterprogramms wird Platz auf dem Stack reserviert. Dadurch wächst der Stack, d. h. der Stackpointer wird entsprechend erhöht, im Beispiel von 0x9ff4 auf 0xa004. 2. Die Rücksprungadresse (vom aktuellen Wert des Program Counters abgeleitet) wird auf den Stack gelegt, im Beispiel 0xc110. 3. Kopieren der als Aktualparameter übergebenen Werte (hier: Wert des Ausdrucks xi) in den Speicherbereich der zugehörigen Formalparameter auf dem Stack (hier Formalparameter x). 4. Es folgt die eigentliche Verzweigung in das Unterprogramm, indem das Program Counter Register mit der Adresse des ersten Maschinenbefehls des Unterprogramms geladen wird, im Beispiel die Adresse 0xa010. Abbildung 17.2 zeigt die Situation nach der Verzweigung in das Unterprogramm f. Man

Arbeitsspeicher: Datenbereich

0x9FF4

für main RücksprungAdresse, Parameter und lokale Variablen

Arbeitsspeicher: Codebereich

Stack

0xa010

0xc108 0xc110

Funktion f Funktion main ... Aufruf f(xi) hinter Aufruf ...

Abbildung 17.1: Programmausführung vor Sprung in das Unterprogramm

17.3 Referenzparameter

253

sieht, dass die Formalparameter als lokale Variablen des Unterprogramms angesehen werden können, die mit den Werten der Aktualparameter initialisiert worden sind.

Arbeitsspeicher: Datenbereich

0x9FF4 Rücksprungadresse Parameter double x 0xa004

für main RücksprungAdresse, Parameter und lokale Variablen für f 0xc110 7.8534e-001

Arbeitsspeicher: Codebereich 0xa010

Stack 0xc108 0xc110 Wert des Ausdrucks xi

Funktion f Funktion main ... Aufruf f(xi) hinter Aufruf ...

Abbildung 17.2: Programmausführung nach dem Sprung in das Unterprogramm

Nach Beendigung des Unterprogramms wird zunächst der Stackpointer zurückgesetzt, so dass er auf die vorher gespeicherte Rücksprungadresse zeigt. Anschließend wird die Rücksprungadresse in den Befehlszähler geladen, wodurch das Programm an der Stelle fortgesetzt wird, an der vorher der Sprung in das Unterprogramm erfolgte. Durch das Umsetzen des Stackpointers verschwinden die Formalparameter und die lokalen Variablen vom Stack6 . Der Rückgabewert wird über eines der Prozessorregister übergeben.

17.3 Referenzparameter Wir haben gelernt, dass einem Unterprogramm beim Aufruf Kopien der ParameterWerte zur Verfügung gestellt werden. Welche Konsequenzen dieser Mechanismus hat, wollen wir uns jetzt noch genauer ansehen. Bei den im vorigen Kapitel behandelten Sortierverfahren besteht eine immer wiederkehrende Aufgabe darin, zwei Werte zu vertauschen. Es liegt nahe, dafür ein Unterprogramm zu schreiben. Der erste Ansatz dafür wäre der, eine Funktion swap() für das Vertauschen zweier int -Werte zu schreiben und diese dann im Programm zum Sortieren (vgl. Kapitel 14.1.2 S. 208 „Sortieren durch Auswahl“) einzusetzen, wie im Listing Abb. 17.3 S. 254 gezeigt. Das Programm druckt: 3 1 14 7 5 1, arbeitet also nicht so wie erwünscht. Um zu verstehen warum, müssen wir anhand des oben entwickelten Modells nachvollziehen, was passiert. Diese Situation ist in Abbildung 17.4 dargestellt.

6 Wenn auch die Bitmuster der Werte noch im Speicher stehen bleiben, bis der Platz anderweitig verwendet wird.

254

1 2

17 Unterprogramme

#i n c l u d e < stdio .h > #d e f i n e N 6

3 4 5 6 7 8 9

v o i d swap ( i n t a , i n t b ) { i n t c; c = a; a = b; b = c; }

10 11 12

i n t main () { i n t j , k , vek [ N ] = { 3 , 1 , 14 , 7 , 5 , 1 };

13

f o r ( j =0; j #d e f i n e N 6

3 4 5 6 7 8 9

v o i d swap ( i n t *a , i n t * b ) { i n t c; c = *a; *a = *b; *b = c; }

10 11 12

i n t main () { i n t j , k , vek [ N ] = { 3 , 1 , 14 , 7 , 5 , 1 };

13

f o r ( j =0; j i n t z = 5; d o u b l e f;

17 Unterprogramme

Namen aus Untergeordneten Blöcken sind hier nicht bekannt, d.h. ungültig, z.B.i aus dem Unterprogramm fakultaet.

4 5 6 7 8 9 10 11

d o u b l e fakultaet ( i n t n ) { d o u b l e f = 1; i n t i; f o r ( i =2; i #d e f i n e MAXD 10 i n t dimens ; // a k t u e l l e D i m e n s i o n

4 5 6 7 8 9 10 11 12 13 14

v o i d MatMult ( i n t m1 [][ MAXD ] , i n t m2 [][ MAXD ] , i n t m3 [][ MAXD ]){ // b e r e c h n e t m3 = m1 ∗ m2 i n t i, j, k; f o r ( i =0; i < dimens ; i ++) f o r ( j =0; j < dimens ; j ++) { m3 [ i ][ j ] = 0; f o r ( k =0; k < dimens ; k ++) m3 [ i ][ j ] += m1 [ i ][ k ] * m2 [ k ][ j ]; } }

15 16 17 18 19 20 21 22 23 24

i n t main () { i n t matrix1 [ MAXD ][ MAXD ]={{1 , 2 , 3} ,{4 , 5 , 6} ,{7 , 8 , 9}} , matrix2 [][ MAXD ]= {{9 , 8 , 7} ,{6 , 5 , 4} ,{3 , 2 , 1}} , matrix3 [ MAXD ][ MAXD ]; dimens = 3; MatMult ( matrix1 , matrix2 , matrix3 ); printf ( " % d \ n " , matrix3 [0][0]); r e t u r n 0; }

Abbildung 17.7: Beispielprogramm MatMult: Globale Einstellungen über globale Variablen

17.4 Lokale, globale und statische Variablen

259

Da lokale Variablen auf dem Stack angelegt werden, kann es leicht zu einer Überschreitung des Stackbereichs im Speicher kommen, wenn große Felder als lokale Variablen in main() oder einer anderen Funktion definiert werden. Abhängig vom Betriebssystem und von der Entwicklungsumgebung, stürzt das Programm möglicherweise schon beim Laden ab und erzeugt eine Fehlermeldung. In so einem Fall sollte zuerst überprüft werden, ob ein zu großes Feld als lokale Variable vorhanden ist (vgl. Kap. 13.3 S. 198 „Deklaration von Vektoren“).

Gefährlich: 1 2 3 4 5

Besser so:

i n t main () { i n t feld [32000]; ... }

1 2 3 4 5

Oder so:

i n t feld [32000]; i n t main () { ... }

1 2 3 4 5 6

i n t main () { static int feld [32000]; ... }

Die Verwendung globaler Variablen führt sehr leicht zu unübersichtlichen Programmen, da der Überblick verloren geht, wo und wann genau auf diese Variable zugegriffen wird. Globale Variablen dürfen daher ähnlich wie das goto nur mit äußerster Disziplin verwendet werden. In diesem Kapitel haben wir drei Klassen von Variablen kennen gelernt: statische und lokale Variablen sowie Funktionsparameter. Bereits im letzten Kapitel wurden die dynamischen Variablen vorgestellt. Deren Eigenschaften lassen sich wie folgt zusammenfassen:

Definition

Beginn der Lebensdauer Ablagebereich

Ende der Lebensdauer

Statische Variablen Äußerster Block, oder mit Schlüsselwort static Ab Programmstart

Dynamische Variablen Keine

Lokale Variablen Im Unterprogrammblock

Mit Expliziter Platzanforderung Heap

Ab Unterprogrammaufruf

Funktionsparameter Keine (nur Deklaration im Funktionskopf) Ab Unterprogrammaufruf

Stack

Stack

Verlassen des Unterprogramms

Verlassen des Unterprogramms

Bereich für statische Daten Programmende Explizite Freigabe

260

17 Unterprogramme

17.5 Funktionsdeklarationen, Modularisierung und Headerdateien Es wurde bereits darauf hingewiesen, dass im Programm die Präprozessordirektive #include erforderlich ist, um die mathematischen Funktionen, insbesondere sin() sinnvoll nutzen zu können. Wir können einmal ausprobieren, was passiert, wenn wir dies nicht machen: 1 2 3 4 5 6 7

#i n c l u d e < stdio .h > /∗#i n c l u d e ∗/ i n t main () { printf ( " % f \ n " , sin (3.1415925 )); r e t u r n 0; }

Wenn wir genau hinsehen, so stellen wir vielleicht noch fest, dass der Compiler eine Warnung ausgibt in der Art „Call to function ’sin’ with no prototype“. Dies bedeutet, dass wir die Funktion sin aufrufen, ohne dem Compiler vorher gesagt zu haben, welche Argumente die Funktion erwartet und was für einen Typ ihr Rückgabewert hat. Der Compiler nimmt daher als Standard den Typ int an. Das Bitmuster eines int-Wertes wird dann als das einer Gleitpunktzahl interpretiert, was diesen völlig verkehrten Ausdruck liefert. Merke:

+ werden, Jedes Unterprogramm sollte vor der Verwendung definiert oder deklariert ansonsten trifft der Compiler implizite Annahmen über die Typen von Argumenten und Rückgabewerten Wie ein Unterprogramm definiert wird, haben wir oben gesehen. Wenn man ein Unterprogramm deklariert, dann gibt man nicht das komplette Unterprogramm an, sondern man sagt dem Compiler nur, welche Typen der Rückgabewert und die Parameter des andernorts definierten Unterprogramms haben. Eine Deklaration erfolgt durch Angabe des Funktionskopfes, optional mit dem vorangestellten Schlüsselwort extern . Man spricht dabei auch von einem Prototypen der Funktion: FunktionsPrototyp Deklarator

;

(

)

Typ

FormalParDekl

void

,

17.5 Funktionsdeklarationen, Modularisierung und Headerdateien

261

In der Datei math.h wird das so für die verfügbaren mathematischen Funktionen, also auch für sin durchgeführt. Darin kann man etwas vereinfacht folgendes finden: 1

double

sin

( d o u b l e x );

Die Dateien mit der Endung „.h“ heißen Headerdateien. Es gibt viele Situationen, in denen eigene Headerdateien geschrieben werden müssen. Sehr oft ist es erforderlich, ein Projekt auf mehrere Quelldateien aufzuteilen. Dies kann beispielsweise der Fall sein, wenn mehrere Programmierer gemeinsam ein Programm schreiben. Es kann dann jeder seinen Teil in eine eigene Quelldatei schreiben. Die verschiedenen Quelldateien werden dann getrennt übersetzt und anschließend vom Linker zusammen gebunden. Man nennt diese Vorgehensweise auch Modularisierung der Software. Die einzelnen Quellprogramme heißen Module. Auch ein einzelner Programmierer kann Unterprogramme, die er für verschiedene Projekte benötigt in eine eigene Quelldatei auslagern und zu jedem Projekt hinzufügen. Etwaige Änderungen beschränken sich dann auf eine einzige zentrale Stelle. Bei großen Projekten beschleunigt diese Vorgehensweise auch die zum Übersetzen benötigte Zeit, da nach einer Änderung lediglich die von der Änderung betroffenen Quelldateien neu übersetzt werden müssen. Angenommen, wir wollen das eingangs erwähnte Programm zur Nullstellenbestimmung einer Funktion in zwei Quelldateien aufteilen. In einem ersten Schritt würden wir folgende zwei Quelldateien erhalten: 1. Quelldatei ffstr.c 1

#i n c l u d e < math .h >

2 3 4 5

#d e f i n e PI 3.1415926 #d e f i n e E 0.2 #d e f i n e M ( PI /8.)

6 7 8 9

d o u b l e f( d o u b l e x) { r e t u r n x - E * sin ( x ) - M ; }

10 11 12 13

d o u b l e fstr ( d o u b l e x ) { r e t u r n 1. - E * cos ( x ); }

2. Quelldatei newton.c 1 2 3 4 5

#i n c l u d e < stdio .h > #d e f i n e PI 3.1415926 #d e f i n e EPS 1. E -10 #d e f i n e MAX_SCHRITTE 100 #d e f i n e ABS ( x ) ((( x ) >0)?( x ): -( x ))

6 7

i n t main () {

262

17 Unterprogramme d o u b l e xi ; i n t it = 0; xi = PI /4; do { xi = xi - f ( xi )/ fstr ( xi ); ++ it ; } w h i l e (( ABS ( f ( xi )) > EPS ) && ( it < MAX_SCHRITTE )); i f ( it >= MAX_SCHRITTE ) printf ( " Mangelnde Konvergenz \ n " ); else { printf ( " Nullst . bei % lf mit % d Iterationen \ n " , xi , it ); printf ( " Genauigkeit : % le " ,f ( xi )); } r e t u r n 0;

8 9 10 11 12 13 14 15 16 17 18 19 20 21 22

}

Die Datei newton.c enthält den allgemeinen Teil der Nullstellenbestimmung und die Datei ffstr.c die spezielle Funktion und deren Ableitung. Soll eine Nullstelle einer anderen Funktion bestimmt werden, so ist lediglich in ffstr.c zu ändern. Da in der Datei newton.c keine Prototypen für die Funktionen f() und fstr() angegeben sind, liefern diese Funktionen wiederum unerwartete Ergebnisse. Ferner sehen wir, dass die #define-Anweisungen umständlich auf beide Dateien aufgeteilt werden mussten. Etwa wird PI in beiden Modulen benötigt, andere defines in jeweils nur einem. Abhilfe schafft hier eine eigene Headerdatei, in der sowohl die Prototypen, als auch alle Direktiven für den Präprozessor zusammen gefasst werden. Diese Headerdatei muss dann in jedem der beiden Module inkludiert werden. Werden mehrere Headerdateien verwendet, so besteht die Gefahr, dass ein #include für eine bestimmte Headerdatei mehrfach vorkommt, also die Datei mehrfach vom Compiler gelesen wird. Ebenso könnte ein Zirkelbezug auftreten, in dem bestimmte Headerdateien endlos immer wieder eingelesen würden. Dem begegnet man durch die Einführung spezifischer symbolischer Konstanten (Siehe Kapitel über den Präprozessor) und lässt die Headerdatei nur dann zu Ende einlesen, wenn das entsprechende Symbol noch nicht definiert wurde. Dieser Schutz gegen Mehrfachinklusion findet auch in allen StandardHeaderdateien Verwendung. Wir schreiben also für jedes Modul eine eigene Headerdatei: 1 2 3 4 5

/∗ H e a d e r d a t e i f f s t r . h ∗/ # i f n d e f _FFSTR_ #d e f i n e _FFSTR_ #i n c l u d e < stdio .h > #i n c l u d e < math .h >

6 7 8 9

#d e f i n e PI 3.1415926 #d e f i n e E 0.2 #d e f i n e M ( PI /8.)

10 11 12

d o u b l e f ( d o u b l e x ); d o u b l e fstr ( d o u b l e x );

/∗ V o r g e g e b e n e r Wert f ü r E ∗/ /∗ V o r g e g e b e n e r Wert f ü r M ∗/

17.5 Funktionsdeklarationen, Modularisierung und Headerdateien

13

263

#e n d i f

Die Headerdatei für das Modul, welches das Newton-Verfahren enthält sieht so aus: 1 2 3 4 5

/∗ H e a d e r d a t e i n e w t o n . h ∗/ # i f n d e f _NEWTON_ #d e f i n e _NEWTON_ #i n c l u d e < stdio .h > #i n c l u d e < math .h >

6 7 8 9 10 11

#d e f i n e #d e f i n e #d e f i n e #d e f i n e #e n d i f

PI 3.1415926 EPS 1. E -10 /∗ G e n a u i g k e i t s s c h r a n k e ∗/ MAX_SCHRITTE 100 /∗ N o t h a l t m a n g e l n d e K o n v e r g e n z ∗/ ABS ( x ) ((( x ) >0) ? ( x ) : -( x ))

Die Programmteile in den Quelldateien ffstr.c und newton.c sehen dann folgendermaßen aus: 1. Quelldatei ffstr.c 1

#i n c l u d e " ffstr . h "

2 3 4 5

d o u b l e f( d o u b l e x) { r e t u r n x - E * sin ( x ) - M ; }

6 7 8 9

d o u b l e fstr ( d o u b l e x ) { r e t u r n 1. - E * cos ( x ); }

2. Quelldatei newton.c 1 2

#i n c l u d e " newton . h " #i n c l u d e " ffstr . h "

3 4 5 6 7 8 9 10

i n t main () { d o u b l e xi ; i n t it = 0; xi = PI /4; do {... } ... }

Als zusammenfassende Regel können wir aufstellen: In eine Headerdatei gehören: • Sicherung gegen Mehrfachinklusion • Funktionsprototypen

264

17 Unterprogramme • Defines, Includes und Makros für den Präprozessor • Typdefinitionen • Deklaration globaler Daten (gekennzeichnet mit extern), aber keine Datendefinitionen

17.6 Fragen Überlegen Sie genau, warum die lokale Variable c in der swap-Funktion kein Zeiger sein darf in der Form int *c;...;c = a;...

17.7 Aufgaben Aufgabe 17.1 Um sich einen Eindruck zu machen, welche Arbeit dem Programmierer von einer simpel anmutenden Funktion, wie etwa der Funktion printf() abgenommen wird, sollen Sie eine Funktion void printdez(int d){...} schreiben, welche die als Parameter übergebene Zahl d korrekt als Dezimalzahl am Bildschirm ausgibt. Dabei darf lediglich die Funktion putchar() zur Ausgabe jeweils eines einzelnen Zeichens verwendet werden.

Aufgabe 17.2 Was druckt das folgende Programm aus? 1

#i n c l u d e < stdio .h >

2 3

i n t a, b, c, d;

4 5 6 7 8 9

v o i d p( i n t *c) { i n t b, e; a =11; b =12; * c =13; d =14; e =15; printf ( " in p : abcde =%3 d %3 d %3 d %3 d %3 d \ n " ,a ,b ,* c ,d , e ); }

10 11 12 13 14 15

i n t main () { a =1; b =2; c =3; d =4; printf ( " vor p : abcde =%3 d %3 d %3 d %3 d \ n " ,a ,b ,c , d ); p (& d ); printf ( " nach p : abcde =%3 d %3 d %3 d %3 d \ n " ,a ,b ,c , d );

16

r e t u r n 0;

17 18

}

17.7 Aufgaben

265

Aufgabe 17.3 Die Funktion func1 soll für den Parameter x den Wert sin(x)+cos(x) berechnen und zurückgeben. Ergänzen Sie die Funktion an den mit „...“ gekennzeichneten Stellen: 1 2 3 4

... func1 (...) { ... }

Aufgabe 17.4 Folgendes Makro berechnet den Absolutbetrag einer Zahl: 1

ABS ( X ) (( X ) >0 ? ( X ) : -( X )) Schreiben Sie analog eine Funktion gleichen Namens, die den Absolutbetrag für double-Werte berechnet. Gefragt ist also im Gegensatz zur Vorlage kein Makro, sondern eine Funktion!

Aufgabe 17.5 Folgende Funktion findemax sucht den maximal vorkommenden Wert im durch vek übergebenen Vektor (mit anz Elementen) und gibt diesen zurück. Ergänzen Sie die Funktion entsprechend: 1 2 3 4

i n t findemax ( i n t *v , i n t anz ) { // H i e r e r g ä n z e n }

Aufgabe 17.6 Schreiben Sie eine Funktion skalp, die als Parameter zwei Zeiger v und w auf Vektoren vom Typ double erhält sowie die Länge der Vektoren. Sie soll das Skalarprodukt der Vektoren9 bilden und zurück geben.

9 Zur

Erinnerung: Das Skalarprodukt zweier n-dimensionaler Vektoren v und w ist

n P i=1

vi wi

18

Algorithmen: Grafikausgabe

Grafikausgaben laufen im Endeffekt immer auf die Ausgabe einiger Grafikprimitive hinaus: Linien, Flächen, Pixelbilder oder Grafik-Text. Aber für Anwendungen mit grafischer Bedienoberfläche (GUI, Graphical User Interface) werden solche Ausgaben komplex und umfangreich. Man setzt die Fenster solcher Anwendungen deshalb aus Standard-Bedienelementen wie Eingabefeldern, Schaltflächen, Menüs oder Auswahlfeldern zusammen, die von der Software-Umgebung zur Verfügung gestellt werden. Auch die Eingaberichtung ist für GUI-Anwendungen komplexer, als im Text-Modus. Nicht die Anwendung ruft hier das System auf, um eine Eingabe durchzuführen (wie z.B. bei scanf), sondern das System ruft die Anwendung auf (call back), um ihr mitzuteilen, dass eine Eingabe erfolgt ist. Die Anwendung muss sich dann darum kümmern, woher die Eingabe kam (Maus, Tastatur, Bedienelemente etc.) und entsprechend reagieren. Ende der siebziger Jahre des vergangenen Jahrhunderts wurde im Xerox Forschungszentrum in Palo Alto mit SMALLTALK [Ingalls (1981)], das objektorientierte Programmier-Paradigma entwickelt, das sich besonders gut für fensterorientierte GUI- Anwendungen eignet. GUI-Anwendungen werden heute meist mit Hilfe der existierenden Klassenbibliotheken programmiert (z.B. .Net-Framework oder MFC in Windows-Umgebungen), die objektorientiert funktionieren. In Abb. 18.1 entspricht das dem Weg 1 . Nachdem aber C keine direkten Möglichkeiten für Objektorientierung und Benutzung von Klassenbibliotheken bietet, bleibt für uns nur Weg 2 , also die Benutzung der Windows-API-Funktionen.

Anwendungsprogramm 1

Klassenbibliothek

2

APIAufrufe Betriebssystem benutzt

Geätetreiberfür die Grafikkarte API bedeutet Application Programming Interface; es handelt sich um die Programmierschnittstelle für Windows- Abbildung 18.1: Schichtenmodell einer Grafik-Anwendung Anwendungen.

Vielfältige Dienste werden von MS-Windows über die API-Aufrufe zur Verfügung gestellt. Über einige hundert Funktionen sind alle Möglichkeiten einer Windows-Anwendung ansprechbar (nachzulesen z. B. in [Petzold (1998)]).

268

18 Algorithmen: Grafikausgabe

18.1 Programmpaket für Grafikausgaben Damit für die Übungsaufgaben die Möglichkeit besteht, einfache Grafiken auszugeben, ohne die Windows-API im einzelnen zu kennen, ist als Material zu diesem Buch das Programmpaket WinAdapt verfügbar. WinAdapt ist ein Rahmen, der alle Teile eines GUI-Applikationsprogramms enthält. In diesem Rahmen werden alle nötigen Windows API- und call back- Aufrufe durchgeführt bzw. behandelt. Um Grafik auszugeben, passen wir nur noch die Grafikausgabe-Routine VtlPaint aus diesem Programmpaket an und übersetzten das Paket neu.

GrafikausgabeRoutine VtlPaint Eingabebehandlung: call back Routinen WinAdapt-Rahmen API-Aufrufe und call backs Betriebssystem benutzt Gerätetreiberfür die Grafikkarte

Soll auf Eingaben reagiert werden, passt Abbildung 18.2: Grafik-Anwendung mit man auch noch die betreffenden call back WinAdapt-Rahmen Routinen an. In VtlPaint steht der Code, der schließlich die Grafik-Primitive ausgibt, die unser Programm erzeugen soll. Das Paket wird hier nur so weit beschrieben, wie es für die folgenden Beispiel-Programme in diesem Kapitel erforderlich ist – Näheres kann man in der ausführlichen Beschreibung im Begleitmaterial dieses Buchs finden. Quelldateien Zum Paket gehören WinAdapt.h, WinAdapt.c und tst.c. Für Übersetzung in C++ ist tst.cpp vorgesehen. Projekt anlegen Zuerst erstellt man ein eigenes Verzeichnis und kopiert die drei vorgegebenen Dateien – je nach Programmiersprache C oder C++ – hinein.

+ Konsol-Projekt Für WinAdapt-Programme braucht man ein Win32-GUI-Projekt! Mit einem funktioniert der Programmrahmen nicht. Grafik ausgeben Das Projekt sollte sich bereits übersetzen lassen und bei Start ein Testbild ausgeben ähnlich Abb. 18.3.

18.1 Programmpaket für Grafikausgaben

269

Gestartet am Sat Feb 04 16:27:52 2012

X

Abbildung 18.3: Testbild aus dem Beispielprogramm für das Grafikpaket

Das Bild wird durch das Programmstück in VtlPaint erzeugt, das in der Quelle tst.c enthalten ist. Dieses Programmstück passen wir an, wenn wir eigene Grafikausgaben machen wollen. tst.c findet sich im Listing Abb. 18.5 auf Seite 270. Das verwendete Koordinatensystem ist geräteabhängig (insbesondere von der eingestellten Auflösung der Grafikkarte). Die Orientierung der y-Achse ist nach unten. Die Einheit ist Pixel.

(0, 0) x

y

(x, y)

Abbildung 18.4: Koordinaten

Die Funktionen zur Einstellung von Farben werden mit drei Argumenten aufgerufen. Die drei Parameter geben die Rot-, Grün- und Blau- Anteile der gewünschten Farbe an. Der Wertebereich der Farbanteile ist 0 (Farbe nicht enthalten) bis 255 (Farbkanone voll aufgedreht). Im Beispielprogramm wird mit LineCol (0, 0, 255); also ein Blauton eingestellt, weswegen das nachfolgend gezeichnete Rechteck blau erscheint. Weitere Funktionen zur Ausgabe von Grafik Weitere Funktionen des Pakets, die hier nicht dargestellt werden, erlauben die Ausgabe von Polygonen und das Färben einzelner Pixel, die Einstellung der Fenstergröße und die Ausgabe der Grafik in Metafiles. (siehe hierzu das Begleitmaterial zum Buch).

270 1 2 3 4 5 6

18 Algorithmen: Grafikausgabe

#i n c l u d e " WinAdapt . h " ... c h a r szStartString [128] = " Gestartet am " ; c h a r szLaufzeit [128]; // f ü r Zyk i n t xMaus = -1 , yMaus = -1; // f ü r Maus c h a r nKey [] = " X " ; // f ü r K e y H i t

// f ü r

init

7 8 9 10 11

i n t aPoints [] = { 40 ,50 , 30 ,70 , 30 ,60 , 10 ,90 , 10 ,80 , 0 ,80 , 30 ,60 , 20 ,60 , 40 ,50 }; i n t nPoints = s i z e o f ( aPoints )/ s i z e o f ( i n t )/2; i n t i , nx , ny ;

12 13 14 15

v o i d VtlZyk ( v o i d ) { } v o i d VtlMouse ( i n t X , i n t Y ) { xMaus = X ; yMaus = Y ; } v o i d VtlKeyHit ( i n t key ) { nKey [0] = key ; }

16 17 18 19

v o i d VtlInit ( v o i d ) { time_t tStart ; tStart = time (0); strcat ( szStartString , ctime (& tStart )); }

20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37

v o i d VtlPaint ( i n t xl , i n t yo , i n t xr , i n t yu ) { LineCol (0 , 100 , 10); /∗ S e t z t T e x t f a r b e ∗/ Text (60 , 60 , szStartString ); /∗ Z e i c h n e t T e x t ∗/ Line ( xl , yo , xr , yu ); /∗ Z e i c h n e t L i n i e ∗/ LineCol (255 , 10 , 50); /∗ S e t z t L i n i e n f a r b e ∗/ FillCol (255 , 255 , 255); /∗ S e t z t F l ä c h e n f a r b e ∗/ Elli (60 , 60 , xr -60 , yu -60); /∗ Z e i c h n e t E l l i p s e ∗/ LineCol (0 , 0 , 255); /∗ S e t z t L i n i e n f a r b e ∗/ FillCol (200 , 200 , 255); /∗ S e t z t F l ä c h e n f a r b e ∗/ Rect (100 , 100 , xr -100 , yu -100); /∗ Z e i c h n e t R e c h t e c k ∗/ LineCol (0 , 0 , 0); /∗ S e t z t L i n i e n f a r b e ∗/ FillCol ( -1 , -1 , -1); /∗ S e t z t F l ä c h e n f a r b e t r a n s p a r e n t ∗/ Elli (60 , 160 , xr -60 , yu -160); /∗ Z e i c h n e t E l l i p s e ∗/ i f ( xMaus != -1 && yMaus != -1) { /∗ M a u s k l i c k m a r k i e r e n ∗/ FillCol (100 ,50 ,50); Elli ( xMaus , yMaus , xMaus +7 , yMaus +7); }

38 39 40 41 42

Text (( xr - xl )/2 , ( yu - yo )/2 , nKey ); /∗ T a s t e a l s T e x t ∗/ /∗ Z u s a t z f u n k t i o n e n ∗/ ...

43

Abbildung 18.5: Testprogramm für das Grafikpaket

18.1 Programmpaket für Grafikausgaben

271

Weitere Call Back Funktionen Durch Aufruf weiterer call back Funktionen durch das Windows-System wird dem Programm mitgeteilt, dass bestimmte Ereignisse eingetreten sind. Es kann darauf reagieren – muss aber nicht. Wenn die betreffende Funktion leer ist, erfolgt keine Reaktion. VtlInit wird nur einmalig zu Beginn aufgerufen und eignet sich daher für Initialisierungszwecke Keine Grafikausgaben in VtlInit ! VtlMouse Wird aufgerufen, wenn die Maus über dem Fenster losgelassen wird. Die Parameter geben die Mausposition bei Loslassen an. Keine Grafikausgaben in VtlMouse ! VtlKeyHit Wird aufgerufen, wenn der Benutzer ein Zeichen über eine Taste eingegeben hat. Der Parameter gibt an, welcher char (ASCII). Keine Grafikausgaben in VtlKeyHit ! VtlZyk Wird alle 200 ms aufgerufen. Hier ist Gelegenheit, Rechnungen durchzuführen, die periodisch durchgeführt werden (z.B. bei Simulationen). Keine Grafikausgaben in VtlZyk ! 1.00

0.80

0.60

0.40

0.20

-3.14

-2.51

-1.88

-1.26

-0.63

0.00 0.00

0.63

1.26

1.88

-0.20

-0.40

-0.60

-0.80

-1.00

Abbildung 18.6: Grafikausgabe: Sinuskurve, gezeichnet mit dem Grafikpaket

2.51

3.14

272

18 Algorithmen: Grafikausgabe

18.2 Kurven zeichnen Wie man eine Sinus-Kurve mit Achsenkreuz und Beschriftung zeichnen kann, zeigt das Programmlisting Abb. 18.7. Der Punkt ist, dass die Kurve durch Strecken angenähert wird. Im Beispiel sind 500 in x-Richtung äquidistante Stützpunkte der Sinus-Kurve durch gerade Strecken verbunden. Abb. 18.6 zeigt die Ausgabe. Zeile 1 2 3 8 9 10 8 11 13 14 15 16 18 19 21 26

35 37 39 41 42

Erklärungen zur „Paint-Routine für die Sinuskurve“ auf Seite 273 zur Nutzung des WinAdapt-Pakets stdio wegen sprintf math wegen Sinus Callback-Routine für Initialisierungen (hier leer) Callback-Routine für Mauseingaben (hier leer) Callback-Routine für Tastatureingaben (hier leer) Callback-Routine für Initialisierungen (hier leer) Callback-Routine für zyklische Timer-Aufrufe (hier leer) Callback-Routine für eigentliche Grafikausgabe Zeichenkette für Achsenbeschriftung und Zählvariable Rand für sonst überstehende Achsenbeschriftung w(idth) und h(eight) des Fensters, in das gemalt wird Linie für X-Achse Achsenteilung horizontal, Schritte: 1/10 Fensterbreite Zahl für Beschriftung in Zeichenkette besch ausgeben und diesen Text dann zeichnen (Interndarstellung geht nicht!) Zeichnen der Y-Achse mit Teilung und Beschriftung analog zur XAchse. Zu beachten ist, dass die Y-Achse des Koordinatensystems im Fenster nach unten orientiert ist. Anfangspunkt als den zuletzt erreichten Punkt der Kurve merken Schleife wie bei Achsenteilung, aber 500 Stützpunkte y-Wert für Stützpunkt (Orientierung d. y-Achse n. unten!) Strecke vom letzten Punkt der Kurve zum aktuellen aktuellen Punkt als den zuletzt erreichten merken

18.2 Kurven zeichnen

1 2 3

#i n c l u d e " WinAdapt . h " #i n c l u d e < stdio .h > #i n c l u d e < math .h >

4 5

d o u b l e pi =3.14;

6 7 8 9 10 11

void void void void

VtlInit ( v o i d ){ SetWinSize (900 ,600); } VtlMouse ( i n t X , i n t Y ){} VtlKeyHit ( i n t key ){} VtlZyk ( v o i d ){}

12 13 14 15 16 17 18 19 20 21 22 23 24

v o i d VtlPaint ( i n t xl , i n t yo , i n t xr , i n t yu ) { c h a r besch [64]; i n t i ; xr -=30; yu -=20; d o u b l e w =( xr - xl ) , h =( yu - yo ) , x , y ; d o u b l e xAlt , yAlt ; Line ( xl , h /2 , xr , h /2); f o r ( x = xl , i =0; i #i n c l u d e < stdlib .h >

3 4 5 6 7 8 9 10 11 12 13 14 15 16

i n t main () { i n t anZ [] = { 3 , 1 , 14 , 7 , 65 , 1 }; FILE * pdatei ; c h a r dateiname [] = " ausgabe . bin " ; pdatei = fopen ( dateiname , " w " ); i f ( pdatei == NULL ){ printf ( " Fehler beim Oeffnen der Datei \ n " ); exit ( -1); } fwrite (( v o i d *) anZ , s i z e o f ( i n t ) , 6 , pdatei ); fclose ( pdatei ); r e t u r n 0; }

Zeile 7 13

Erklärung zum Programmstück Der Name der Ausgabedatei wird diesmal über eine Zeichenkettenvariable angegeben. Der Vektor anZ wird auf einmal komplett geschrieben: 6 Elemente der Größe sizeof ( int ).

Mit dem Dateilister betrachtet, sieht die erzeugte Datei diesmal folgendermaßen aus: 00

0300 0100 0E00 0700 4100 0100

A

Die Datei wurde auf einer 16-Bit Intel-Architektur erzeugt. Deshalb wird das niederwertige Byte zuerst im Speicher abgelegt und die Folge beginnt mit 0300. Auf einer 16-Bit-Architektur werden int -Werte mit 2 Bytes dargestellt. Die Größe der Datei ausgabe.bin gemessen in Byte ist 6 * sizeof ( int ). Fast alle Bytes der Datei stellen im ISO 8859-1 Code nicht druckende Zeichen dar. Lediglich die 41 (hexadezimal für 6510 ) entspricht zufällig dem Code für A.

294

19 Dateien

Die Funktion int fread(void *ptr, size_t size, size_t n, FILE *stream) ist die zu fwrite gehörige analoge Einlesefunktion. Ihr Rückgabewert ist die Anzahl der tatsächlich gelesenen Bytes. Diese Zahl kann kleiner sein, als die Zahl der zu lesenden Bytes, wenn das Dateiende vorzeitig erreicht wurde.

19.5 Aufgaben 1. Warum dürfen bei formatierter Ausgabe die Werte nicht ohne Trennzeichen (’ ’, ’\t’ oder ’\n’) hintereinander geschrieben werden, bei binärer Ausgabe hingegen schon? 2. Schreiben Sie mit einem Texteditor eine Datei, die eine größere Anzahl von verschiedenen Namen enthält, jeder in einer eigenen Zeile. a) Es ist ein Programm zu schreiben, das in einer Schleife je einem Namen aus der Datei in einen Puffer einliest und ihn dann mit printf am Bildschirm ausgibt. b) Deklarieren Sie in Ihrem Programm einen hinreichend großen Vektor von Zeigern auf char. Kopieren Die jeden Namen nach dem einlesen in eine dynamisch angelegte Variable und tragen Sie den Pointer in eine geeignete Komponente des Vektors ein. Testen durch Ausgabe aller Namen nach Erreichen von EOF der Datei. c) Schieben Sie zwischen dem Ende des Einlesens und der Ausgabe der Namen eine Sortierphase ein, in der per Auswahl-Sort mit Vertauschung der Zeiger gearbeitet wird.

20

Structs und komplexe Datenstrukturen

20.1 Strukturen mit struct Bisher haben wir die Basistypen char, int , float usw. kennen gelernt. Ferner haben wir gesehen, wie man Felder anlegt, also die Zusammenfassung mehrerer Elemente des gleichen Typs zu einer Einheit. In der Datenverarbeitung müssen sehr oft logisch zusammen gehörende Elemente unterschiedlichen Typs gemeinsam verwaltet werden. Um unterschiedliche Komponenten zu einer Einheit zusammenzufassen, gibt es in C die Möglichkeit, Datenstrukturen, so genannte structs zu erstellen. Als Anwendung wollen wir in diesem Kapitel ein Programm erstellen, das eine kleine chemische Fabrik simuliert. Im einfachsten Fall gibt es in dieser Fabrik Tanks, zwischen denen Stoffe hin und her gepumpt werden. Wir werden dies zuerst mit nur zwei Tanks vormachen. In Abbildung 20.1 ist ein Bild der laufenden Simulation zu sehen. Es werden die Funktionen aus Kapitel 18 „Algorithmen: Grafikausgabe“ verwendet. Solche Simulationen werden in der Praxis sehr häufig eingesetzt, da sich große technische Anlagen damit bereits während der Planungsphase beurteilen lassen, ohne dass die Anlage selbst gebaut werden muss. Es gibt große Programmpakete eigens zur Simulation großer technischer Anlagen. Zuerst bilden wir eine Datenstruktur, die alle zur Beschreibung eines Tanks erforderlichen Daten enthält, wie etwa Bezeichnung, Koordinaten und Größe für die Darstellung am Bildschirm, die Aufnahmekapazität oder den aktuellen Füllstand: 1 2 3 4 5

s t r u c t Kessel { i n t xKoord , yKoord ; i n t hoehe , breite ; d o u b l e Kapazitaet , Fuellstand ; c h a r name [20]; };

Dies ist die Deklaration eines neuen Datentyps namens struct Kessel. Nach der Typdeklaration können Variablen dieses Typs erzeugt werden. Für unsere Anwendung benötigen wir zwei Variablen des Typs struct Kessel, die wir bei ihrer Definition gleich vorbelegen können: 1 2

s t r u c t Kessel Kessel1 ={20 , 20 , 150 , 100 , 100. , 50. , " Tank1 " }; s t r u c t Kessel Kessel2 ={200 , 40 , 150 , 100 , 150. , 50. , " Tank2 " };

Jede dieser Variable besitzt nun alle in der Typdeklaration angegebenen Komponenten, die im Speicher angelegt sind, wie in Abbildung 20.2 dargestellt. Insgesamt belegt eine

296

20 Structs und komplexe Datenstrukturen

Tank1

Tank2

Abbildung 20.1: Simulation einer einfachen chemischen Fabrik.

Arbeitsspeicher

0xa004

20 20 150 100

0xa01c

100. 50. Tank1

Name xKoord yKoord hoehe breite Kapazitaet Fuellstand name

Typ int int int int double double char[20]

Abbildung 20.2: Datenstruktur struct Kessel im Hauptspeicher.

Wert 20 20 150 100 100. 50. "Tank1"

Adresse 0xa004 0xa006 0xa008 0xa00a 0xa00c 0xa014 0xa01c

20.1 Strukturen mit struct

297

Variable des Typs struct Kessel also mindestens 36 Bytes1 . Wir wollen uns nun noch das Syntaxdiagramm für die Strukturdeklaration ansehen2 : Strukturdeklaration struct

Komponentendekl

{

}

Strukturname

Variablendeklarator , Komponentendekl Typ

Komponentendeklarator

;

, Darin enthalten ist sowohl eine Typdeklaration, also der Bauplan für die Datenstruktur, als auch die Möglichkeit, Variablen zu erzeugen, die nach diesem Bauplan erstellt sind. Wie aus dem Syntaxdiagramm zu ersehen ist, muss weder Strukturname angegeben werden, noch müssen Variablen definiert werden. Es gibt vier Möglichkeiten, diese zu kombinieren: 1. Es wird wie im obigen Beispiel nur der Strukturname angegeben: struct Kessel {...}; damit ist ein neuer Datentyp eingeführt und es lassen sich anschließend Variablen dieses Typs definieren. 2. Es wird sowohl Strukturname als auch eine Variablen angegeben: struct Kessel {...} Kessel1 = {...}; 3. Es wird kein Strukturname angegeben, aber es werden eine oder mehrere Variablen definiert: struct {...} Kessel1 = {...}; In diesem Fall können anschließend keine weiteren Variablen dieses Typs definiert werden. Variablen dieses Typs können auch nicht an Funktionen übergeben werden. 1 Um Speicherplatz besser verwalten zu können, kann der Compiler in einer struct Leerbytes einfügen, sodass die tatsächliche Größe einer Struktur größer sein kann, als die Summe der Größen ihrer Komponenten. 2 Im einfachsten Fall ist der Variablendeklarator ein Bezeichner (vgl. Syntaxdiagramm in Abb. 13.1 S. 196 im Abschnitt 13.1 „Vektoren – abgeleitete Typen in C“). Ferner ist einfachstenfalls der Komponentendeklarator ein Bezeichner.

298

20 Structs und komplexe Datenstrukturen

4. Es wird weder Strukturname noch Variablen angegeben. Diese Variante ist sinnlos. Der Zugriff auf die einzelnen Komponenten einer Struktur erfolgt gemäß folgendem Syntaxdiagramm: Komponentenzugriff Variablenname

.

Komponentenname

Beispiel: Bildschirmdarstellung eines Kessels unter Verwendung der Funktion Rect: Rect(Kessel1.xKoord, Kessel1.yKoord, Kessel1.xKoord + Kessel1.breite, Kessel1.yKoord+Kessel1.hoehe); Da wir mehrere Kessel zu zeichnen haben, ist es umständlich für jeden Kessel diesen Aufruf zu schreiben. Außerdem wollen wir ja noch den Füllstand anzeigen und eine Beschriftung am Kessel anbringen. Dazu schreiben wir am besten eine Funktion. Dabei machen wir uns die Eigenschaft zu Nutze, dass Strukturen für Zuweisungen und zur Übergabe an Funktionen als ganzes behandelt werden. Zur Übergabe an eine Funktion wird die Struktur komplett auf den Stack kopiert3 . 1 2

#d e f i n e TEXTOFFSET 10

3 4 5 6 7 8 9 10 11 12 13 14 15

v o i d ZeichneKessel ( s t r u c t Kessel K ) { i n t yFuell ; LineCol (0 , 0 , 0); // Z e i c h e n f a r b e s c h w a r z Rect ( K . xKoord , K . yKoord , K . xKoord + K . breite , K . yKoord + K . hoehe ); Text ( K . xKoord + TEXTOFFSET , K . yKoord + TEXTOFFSET , K . name ); LineCol (0 , 0 , 255); // Z e i c h e n f a r b e b l a u yFuell = K . hoehe ( i n t )( K . Fuellstand * K . hoehe / K . Kapazitaet ); Rect ( K . xKoord +1 , K . yKoord + yFuell , K . xKoord + K . breite -1 , K . yKoord + K . hoehe -1); }

20.2 Zeiger auf Strukturen Oft werden auch Zeiger auf Strukturen benötigt, etwa bei der Übergabe an Funktionen als Referenzparameter. Zur Vervollständigung unserer Simulation einer chemischen Fabrik benötigen wir beispielsweise eine Funktion, die das Umfüllen einer bestimmten Menge aus einem Tank in einen anderen übernimmt. Der Prototyp der Funktion könnte so aussehen: 3 Dies

ist oft ein großer Aufwand, der durch die Übergabe von Zeigern vermieden werden kann.

20.3 Anwendungsbeispiel: Darstellung von Rechtecken

1 2

299

v o i d Umfuellen ( s t r u c t Kessel * Quelle , s t r u c t Kessel * Ziel , d o u b l e Menge );

Beim Dereferenzieren der Zeiger Quelle und Ziel innerhalb der Funktion Umfuellen, ist zu beachten, dass ’.’ eine höhere Priorität hat als ’*’. Wir müssen also schreiben: (*Quelle).Fuellstand -= Menge; Diese etwas umständliche Schreibweise kann übersichtlich abgekürzt werden durch Verwendung des „Pfeiloperators“ ’->’: Pfeiloperator Zeigervariable

->

Komponentenname

Auf das Beispiel angewendet bedeutet dies: Quelle->Fuellstand -= Menge; Insgesamt sieht die Funktion dann so aus: 1 2 3 4 5 6 7

v o i d Umfuellen ( s t r u c t Kessel * Quelle , s t r u c t Kessel * Ziel , d o u b l e Menge ) { Menge = ( Menge Fuellstand ) ? Menge : Quelle - > Fuellstand ; Quelle - > Fuellstand -= Menge ; Ziel - > Fuellstand += Menge ; } Zeile 3

Erklärung zum Programmstück Hier muss aufgepasst werden, dass nicht mehr umgefüllt wird, als im Quellbehälter enthalten ist.

20.3 Anwendungsbeispiel: Darstellung von Rechtecken Als Anwendungsbeispiel wollen wir die Darstellung von Rechtecken durch Strukturen betrachten. Ein Rechteck wird durch die vier Koordinatenwerte left, right, bottom und top beschrieben: Rechtecke hätten wir vor diesem Kapitel durch vier-elementige Vektoren darstellen können: 1 2 3 4 5 6 7

#d e f i n e LEFT 0 #d e f i n e RIGHT 1 #d e f i n e BOTTOM 2 #d e f i n e TOP 3 ... i n t rechteck [] = {2 , 4 , 3 , 5}; // T r a n s l a t i o n d e s R e c h t e c k s um 1 i n x− und 2 i n y−R i c h t u n g

300

8 9 10 11

20 Structs und komplexe Datenstrukturen

rechteck [ LEFT ] += 1; rechteck [ RIGHT ] += 1; rechteck [ BOTTOM ] += 2; rechteck [ TOP ] += 2;

Eleganter ist die Verwendung von Strukturen: 1

#i n c l u d e < stdio .h >

2 3

s t r u c t rect { i n t left , right , bottom , top ;};

4 5 6 7 8 9 10 11 12

s t r u c t rect translate ( s t r u c t rect r1 , i n t tx , i n t ty ) { s t r u c t rect r ; r . left = r1 . left + tx ; r . right = r1 . right + tx ; r . bottom = r1 . bottom + ty ; r . top = r1 . top + ty ; r e t u r n r; }

13 14 15 16 17 18 19 20

i n t main () { s t r u c t rect re1 = {2 , 4 , 3 , 5} , re2 ; re2 = translate ( re1 , 1 , 2); printf ( " % d % d % d % d \ n " , re2 . left , re2 . right , re2 . bottom , re2 . top ); r e t u r n 0; } Zeile 3 5 15

Erklärungen zum Programmstück Die Struktur besteht aus vier Elementen vom Typ int . Achtung: dies ist kein Vektor! Sowohl Parameter als auch Rückgabewert der Funktion translate sind vom Typ struct rect. Der Rückgabewert der Funktion translate wird der Variablen re2 zugewiesen.

Natürlich lassen sich auch Vektoren von solchen Strukturen bilden: 1 2

s t r u c t rect rvek [10]; f o r ( i =0; i Prio = ... Wenn die Liste aufgebaut ist, so können in einer Schleife alle Elemente durchgegangen werden, wie in folgendem Programmfragment, am Beispiel der Ausgabe der Prioritäten aller Aufträge, gezeigt wird: 1 2 3 4 5 6 7

s t r u c t ListElem * Tail = root ; ... ... w h i l e ( Tail != NULL ) { printf ( " % d \ n " , Tail - > Prio ); Tail = Tail - > next ; } Zeile 1 4

Erklärungen zum Programmstück Der Zeiger Tail (Schwanz) wird benötigt, um an der Liste entlang zu gehen. Tail kann so lange auf das jeweils nächste Element gesetzt werden, bis das Listenende erreicht ist. Den NULL-Zeiger zu dereferenzieren, würde zu einem Laufzeitfehler führen.

Als nächstes wollen wir ansehen, wie ein neuer Auftrag in die Liste eingefügt wird. Wir nehmen jetzt an, dass die Liste so aufgebaut wird, dass die Elemente der Priorität nach geordnet sind. Dann gibt immer das erste Element der Liste den nächsten zu bearbeitenden Auftrag an. Man nennt diesen Vorgang auch „Sortieren durch Einfügen“. Für einen neuen Auftrag wird zuerst eine neue Datenstruktur angelegt. Die Funktion malloc() ist uns bereits aus Kapitel 16.4 „Dynamische Variable mit malloc und free“ bekannt. Für ein neues Listenelement können wir Speicherplatz mit folgendem Ausdruck dynamisch erzeugen: ( struct ListElem *) malloc( sizeof ( struct ListElem)); | {z } | {z } 1

2

Benötigt wird dabei Typ „Zeiger auf struct ListElem“ (1 ). Der Ausdruck 2 gibt die Größe eines Elements vom Typ struct ListElem an. Die Größe des benötigten Speicherbereichs wird (in Byte) als Parameter size übergeben. Die Adresse des ersten belegten Bytes wird zurückgegeben. Da die malloc-Funktion nicht weiß, welchen Typ die Daten besitzen, die an diesem Speicherplatz abgelegt werden sollen, ist der Rückgabewert vom

20.4 Listen

303

Typ „Zeiger auf void“. Der Typ dieses Zeigers wird anschließend durch explizite Typumwandlung (casting) in den benötigten Typ gewandelt (siehe Kapitel 16.4). Hier sehen wir eine Anwendung für den Datentyp void. Wenn kein Speicherbereich ausreichender Größe zur Verfügung steht, dann ist der Rückgabewert der NULL-Zeiger. Zur Erzeugung einer Struktur für einen neuen Auftrag, verwenden wir folgende Funktion: 1 2 3 4

s t r u c t ListElem * AuftragErzeugen () { s t r u c t ListElem * AuftragNeu ; AuftragNeu = ( s t r u c t ListElem *) malloc ( s i z e o f ( s t r u c t ListElem ));

5

i f ( AuftragNeu == NULL ) { printf ( " Speicher voll \ n " ); exit (1); }

6 7 8 9 10

AuftragNeu - > next = NULL ; AuftragNeu - > Prio = 0; r e t u r n AuftragNeu ;

11 12 13 14

}

Zeile 1 3

8 11-12 13

Erklärungen zum Programmstück Der return-Wert der Funktion ist vom Typ „Zeiger auf struct ListElem“ Mit der malloc-Funktion wird Platz für ein Element vom Typ struct ListElem bereit gestellt. Der Zeiger wird in einen Zeiger auf struct ListElem gewandelt. Programmabbruch mit exit(1), wenn Speicher voll, d. h. wenn malloc den NULL-Zeiger geliefert hat. Vorbelegung des neuen Elements Die Adresse des neu erzeugten Elements wird zurückgegeben.

Diese Funktion liefert uns einen Zeiger auf einen neuen Auftrag, der mit Priorität 0 vorbelegt ist und kein nachfolgendes Element besitzt:

AuftragNeu

0

Ø

In dieses Element ist dann die Priorität des neuen Auftrags entsprechend einzutragen. Um das neue Element einzufügen, wird ein weiterer Zeiger verwendet, der so lange von einem Element zum Nächsten bewegt wird, bis die Stelle gefunden ist, an der das neue Element einzufügen ist. Der Zeiger heißt im Beispiel Tail, als Abkürzung für Rest der Liste („Schwanz“) . Ist die passende Stelle gefunden, so wird das neue Element durch ändern von zwei Zeigern eingefügt:

304

20 Structs und komplexe Datenstrukturen

Vorher:

AuftragNeu root

14

12

7

Ø

3

Ø

tail

Nachher:

AuftragNeu root

14

12

7

3

tail

Für diesen Vorgang schreiben wir eine Funktion:

1 2 3 4 5 6 7 8 9 10

v o i d InsertElem ( s t r u c t ListElem * AuftragNeu ) { s t r u c t ListElem * Tail = root ; i f ( Tail == NULL || AuftragNeu - > Prio > Tail - > Prio ) { root = AuftragNeu ; AuftragNeu - > next = Tail ; } else { w h i l e (( Tail - > next != NULL ) && ( Tail - > next - > Prio > AuftragNeu - > Prio )) Tail = Tail - > next ;

11

AuftragNeu - > next = Tail - > next ; Tail - > next = AuftragNeu ;

12 13

}

14 15

}

Ø

20.4 Listen Zeile 1 2 3

4 8

12

305 Erklärungen zum Programmstück Ein Zeiger auf das neue Element wird übergeben. Mit dem Zeiger Tail wird an der Liste entlang gegangen. Wenn AuftragNeu das Erste Element werden muss, dann ist globale Variable root zu ändern. Es ist daher wichtig, zuerst Tail==NULL abzufragen. Nur wenn Tail==NULL nicht erfüllt ist, darf Tail->Prio geprüft werden! Element suchen, nach dem AuftragNeu einzuhängen ist. Wichtig ist, dass hier zuerst die Abfrage (Tail->next != NULL) durchgeführt wird. Liefert diese Abfrage „falsch“, so wird der Wert von Tail->next nicht mehr geprüft, was auch nicht zulässig wäre, falls Tail->next der NULL-Zeiger wäre. AuftragNeu nach Tail einhängen. Die Reihenfolge ist dabei wichtig: alten Wert von Tail->next vor dem Überschreiben retten.

Besondere Beachtung verdient die Abfrage if (Tail==NULL || AuftragNeu->Prio > Tail->Prio) Wenn gleich zu Anfang, nach der Initialisierung Tail = root gilt, dass Tail==NULL, dann heißt das, dass die Liste noch leer ist. Beispiel als Diagramm dargestellt:

AuftragNeu

7

Ø

root: Ø tail: Ø Diese Abfrage wird zuerst durchgeführt. Ist das Ergebnis wahr, so ist das Ergebnis der logischen ODER-Verknüpfung wahr, unabhängig vom Wahrheitsgehalt des zweiten Teils der Aussage AuftragNeu->Prio > Tail->Prio. Nur wenn Tail 6= NULL werden Tail und root dereferenziert und deren Prioritäten verglichen. Wenn sich dabei ergibt, dass AuftragNeu->Prio > Tail->Prio, dann ist das neue Element am Anfang der Liste vor dem ersten Element einzuhängen. Beispiel als Diagramm dargestellt:

AuftragNeu root

7

Ø

3

Ø tail

.

306

20 Structs und komplexe Datenstrukturen

20.5 Exkurs: Rekursive Funktionen Rekursive Funktionen sind solche, die einen Aufruf von sich selbst aufrufen. Listen können elegant mit rekursiven Funktionen durchgegangen werden, was wir am Beispiel einer Funktion zum Ausgeben der Werte der eben aufgebauten Liste ansehen und dazu das obige Beispiel zum Ausgeben der Elemente der Liste abwandeln: 1 2 3 4 5 6 7

v o i d PrintElems ( s t r u c t ListElem * Tail ) { i f ( Tail != NULL ) { printf ( " % d \ n " , Tail - > Prio ); PrintElems ( Tail - > next ); } }

8

Zeile 2 3 5

Erklärungen zum Programmstück Für den Parameter Tail wird beim Aufruf von PrintElems der rootZeiger übergeben. Ausgabe und Weitergehen an der Liste darf nur erfolgen, wenn Tail 6= NULL. Hier wird PrintElems selbst wieder aufgerufen. Als Parameter wird ein Zeiger auf das nächste Element übergeben.

20.6 Aufgaben Aufgabe 20.1 Schreiben Sie eine Funktion ZeichneVerbindung(...), welche die Verbindung zwischen zwei Kesseln zeichnet. Dazu kann einfach eine Linie zwischen den Mittelpunkten der Kessel gezeichnet werden. Die Funktion ist aufzurufen, bevor die Kessel gezeichnet werden, damit die Anteile der Verbindungslinie, die in einen der Kessel hinein ragen von den Rechtecken überdeckt werden. Aufgabe 20.2 Ein Programm für die Mitgliederverwaltung eines Vereins benötigt eine Datenstruktur zur Aufnahme der Mitgliedsdaten. a) Deklarieren Sie eine Datenstruktur mitglied, die folgende Daten aufnehmen kann: • Vorname • Nachname • Geburtsdatum

20.6 Aufgaben • Straße • Hausnummer • Postleitzahl • Wohnort b) Definieren Sie dann folgendes: • eine Variable vom Typ mitglied • einen Vektor mit zehn Elementen vom Typ mitglied • eine Variable vom Typ Zeiger auf Datenstruktur mitglied

307

21

Algorithmen: Graphentheorie

21.1 Problemstellung Wir betrachten in diesem Kapitel die Problemstellung, einen günstigen Weg zwischen zwei Orten zu finden. Dazu muss zuerst geklärt werden, was günstig heißt. Günstig kann sowohl kurz im Sinne geringer Entfernung bedeuten, um etwa beim Ausfahren von Waren eine kurze Strecke mit möglichst geringem Benzinverbrauch zu benutzen. Andererseits kann günstig auch schnell im Sinne von kurzer Fahrzeit bedeuten. Im Internet stehen viele Dienste zur Verfügung, mit denen sich günstige Wege zwischen zwei Orten ermitteln lassen, beispielsweise http://www.openstreetmap.de/ [Openstreetmap (2012)]. Am Beispiel einer Verbindung von Kirchheim bei München nach Triftern in Niederbayern sind die Ergebnisse in Abbildung 21.1 und Abbildung 21.2 dargestellt. Es ist zu erkennen, dass der kürzeste Weg nicht der schnellste sein muss. In der Praxis tritt eine Vielzahl derartiger Probleme auf, bei denen nicht das rein numerische Rechnen im Vordergrund steht, sondern Elemente, die zueinander in Beziehung stehen. Im Beispiel sind die Elemente die Orte und die Beziehung zwischen zwei Orten A und B ist die Art ihrer Verbindung. Derartige Probleme treten im Ingenieurbereich etwa dort auf, wo Straßennetze optimal auszulegen, Materialflüsse zu optimieren, oder Bauteile in elektrischen Schaltungen zu verdrahten sind. Wir befassen uns in diesem Kapitel mit dem Finden von kürzesten Wegen, einem Spezialfall aus einer Familie von Algorithmen, die zur Lösung der oben genannten Probleme sehr gut geeignet sind. Um Wege zwischen allen Orten in Deutschland bestimmen zu können, muss eine geeignete Abstraktion gefunden werden, durch die wir die Landkarte ersetzen können. Wir können etwa die Orte als Punkte zeichnen und die Straßen dazwischen als Verbindungslinien, an die wir Entfernungen oder Fahrzeiten schreiben. Letztere hängen von der auf der jeweiligen Straße fahrbaren Geschwindigkeit ab, weshalb wir uns hier auf Entfernungen beschränken. Das könnte — etwas vereinfacht und nicht maßstäblich — aussehen, wie in Abbildung 21.3 angedeutet. Was so gezeichnet wurde, ist eine Relation, die jeweils zwei Elementen einer Menge von Orten genau dann zueinander in Beziehung setzt, wenn es eine Straße zwischen ihnen gibt. Mathematisch werden solche Relationen, die über das Verhältnis zweier Elemente zueinander etwas aussagen, auch Graphen genannt. Wir können das abstrakt so formulieren: Ein mathematisches Gebilde G = (V , A), bestehend aus zwei Mengen V und A heißt Graph, wenn 1. V eine endliche, nichtleere Menge von Knoten (englisch: Vertices) ist 2. A eine Menge von Kanten ist, die entweder gerichtet (Pfeil, englisch: Arc) oder ungerichtet (Kante, englisch: Edge) sind:

310

21 Algorithmen: Graphentheorie

Abbildung 21.1: Ein nach kürzester Fahrzeit optimierter Weg (125km, 1:36h)

Abbildung 21.2: Ein nach kürzester Strecke optimierter Weg (112km, 2:07h)

Kirchheim

Dorfen

34km

55km 35km

39km Haag

35km

Mühldorf

12km

Eggenfelden

10km

Pfarrkirchen

8km 20km

Altötting

28km

Triftern

Marktl

Abbildung 21.3: Darstellung des Straßennetzes durch einen ungerichteten Graphen (vereinfacht)

21.2 Darstellung von Graphen durch Matrizen

311

z

z 4

4

3

a

d

9

4

6

3

b

c

3

2 s

4

3

a

d

9

4

4

6 3

b

c

3

2 s

Abbildung 21.4: Beispiele eines ungerichteten und eines gerichteten Graphen

Ungerichteter Graph: A ist eine Menge von zweielementigen Mengen über V: A = {{v1 , v2 }|v1 , v2 ∈ V ∧ v1 und v2 sind verbunden} Die Reihenfolge spielt somit keine Rolle, also: {v1 , v2 } = {v2 , v1 } Gerichteter Graph: A ist eine Menge von Paaren aus V : A = {(v1 , v2 )|v1 , v2 ∈ V ∧ und es führt ein Pfeil von v1 nach v2 } Die Reihenfolge ist von Bedeutung, also (v1 , v2 ) 6= (v2 , v1 ). Gerichtete Graphen werden verwendet, wenn die Richtung von Bedeutung ist, etwa wenn es im Straßennetz Einbahnstraßen gibt, oder wenn der Strom nur in einer Richtung fließen kann. Ungerichtete Graphen zeichnen wir wie in Abbildung 21.3 mit einfachen Verbindungslinien zwischen den Knoten und gerichtete Graphen mit Pfeilen. Beispiele dafür finden sich in Abbildung 21.4.

21.2 Darstellung von Graphen durch Matrizen Wir wenden uns der Frage zu, wie man Graphen durch Datenstrukturen darstellen kann, um sie dann im Computer zu verarbeiten. Wir beschränken uns hier auf die Darstellung von Graphen durch Matrizen1 . Diese Matrizen lassen sich günstig in zweidimensionalen Feldern speichern. Dabei ist die Knotenmenge geeignet auf den Zahlenbereich 0 . . . N 1 In der Literatur lassen andere effizientere, aber auch kompliziertere Datenstrukturen zur Darstellung von Graphen finden, z.B. in Knuth (1993).

312

21 Algorithmen: Graphentheorie

abzubilden, sodass das Feld in der in C üblichen Weise indiziert werden kann. Üblicherweise werden die Knoten von 0 beginnend nummeriert, womit die Verwendung der Nummer direkt als Index möglich ist. Es lässt sich zu jedem Graphen eine Matrix erstellen, die angibt, welche Knoten verbunden sind, die so genannte Adjazenzmatrix. Die Elemente mij der Adjazenzmatrix M zu einem Graphen können nur die Werte 1 (es existiert die Verbindung von Knoten i nach Knoten j) oder 0 (die Verbindung von i nach j existiert nicht) annehmen. Dabei sind die Indizes i und j Elemente der Knotenmenge V . Die Adjazenzmatrix eines ungerichteten Graphen ist also symmetrisch, d.h. mij = mji , da keine Richtung der Verbindungen angegeben ist. Dagegen ist die Adjazenzmatrix eines gerichteten Graphen im Allgemeinen unsymmetrisch. Die Adjazenzmatrizen der Graphen aus Abbildung 21.4 sehen also folgendermaßen aus: Ungerichteter Graph   a 0100 01 b 1 0 1 1 1 0   c 0 1 0 1 1 0 M =   d 0 1 1 0 0 1  s 0 1 1 0 0 0 z 1001 00

Gerichteter Graph   a 0 10 00 1 b 0 0 1 1 1 0   c 0 0 0 0 0 0 M =   d 0 1 1 0 0 0  s 0 0 1 0 0 0 z 1 00 10 0

Sind zusätzliche Angaben für die Knoten und Kanten gefordert, so lassen sich diese auch in Vektoren oder zweidimensionalen Feldern entsprechenden Typs speichern, etwa Ortsnamen an den Knoten oder Entfernungen an den Kanten. Im speziellen Fall, dass direkte Entfernungen zwischen zwei Knoten an der sie verbindenden Kante angegeben werden, entstehen so genannte Distanzmatrizen. Besteht kein direkter Weg zwischen zwei Knoten, so wird die direkte Distanz zwischen ihnen auf unendlich gesetzt. Die Distanzmatrix D gibt gleichzeitig die Konnektivität des Graphen an, denn dij < ∞ bedeutet die Existenz einer Kante bzw. eines Pfeils im Graphen von i nach j. Ungerichteter Graph  a ∞ 9 ∞∞ b9 ∞ 3 4  c ∞ 3 ∞ 6 D=  d ∞ 4 6 ∞ s ∞ 3 2 ∞ z 4 ∞∞ 3

∞ 3 2 ∞ ∞ ∞

Gerichteter Graph  4 a ∞ 9 ∞ ∞ b ∞ ∞ 3   ∞ c ∞ ∞ ∞  D=  3 d ∞ 4 6 ∞ s ∞ ∞ 2 ∞ z 4 ∞ ∞ 

∞ 4 ∞ ∞ ∞ 3

∞ 3 ∞ ∞ ∞ ∞

4 ∞ ∞ ∞ ∞ ∞

      

Direkt umgesetzt in C-Datenstrukturen könnte die Distanzmatrix des ungerichteten Graphen aus Abbildung 21.4 wie im folgenden Programmfragment aussehen. Da der Wert unendlich nicht gespeichert werden kann, wird der größte darstellbare unsigned long Wert für unendlich verwendet. Diesen Wert muss man allerdings nicht auswendig wissen und auch nicht ausrechnen, sondern es gibt die Headerdatei limits.h, in der alle Extremwerte der Standard-Datentypen zur Verfügung gestellt werden. Der größte unsigned long Wert heißt dort ULONG_MAX. Es muss dann sicher gestellt werden, dass dieser Wert nicht als Entfernung im Graphen vorkommt. Im folgenden Programmfragment dient die symbolische Konstante MAXD dazu, den Speicherplatz für insgesamt maximal 50

21.3 Der Algorithmus von Dijkstra

313

Knoten zu dimensionieren. Der Wert der Variablen dimens gibt die tatsächliche Anzahl der Knoten des Graphen an: 1 2 3 4 5 6 7 8 9 10 11

#i n c l u d e < limits .h > // I n c l u d e −D a t e i f ü r ULONG_MAX #d e f i n e MAXD 50 /∗ max . A n z a h l Knoten d e s G r a p h e n ∗/ u n s i g n e d l o n g d_mat [ MAXD ][ MAXD ] = { { ULONG_MAX ,9 , ULONG_MAX , ULONG_MAX , ULONG_MAX ,4 }, {9 , ULONG_MAX ,3 , 4, 3, ULONG_MAX } , { ULONG_MAX ,3 , ULONG_MAX ,6 , 2, ULONG_MAX } , { ULONG_MAX ,4 , 6, ULONG_MAX , ULONG_MAX ,3 }, { ULONG_MAX ,3 , 2, ULONG_MAX , ULONG_MAX , ULONG_MAX } , {4 , ULONG_MAX , ULONG_MAX ,3 , ULONG_MAX , ULONG_MAX } }; u n s i g n e d l o n g dimens = 6; /∗ A n z a h l d e r Knoten d e s G r a p h e n ∗/

21.3 Der Algorithmus von Dijkstra Ein Algorithmus zum Finden kürzester Wege in Graphen wurde 1959 von dem Mathematiker E. W. Dijkstra angegeben. Gegeben ist ein ungerichteter Graph mit nichtnegativen Entfernungen zwischen den Knoten. Ferner sind ein Startknoten s sowie ein Zielknoten z gegeben, zwischen denen der kürzest mögliche Weg zu finden ist. Der Algorithmus untersucht alle Knoten des Graphen und markiert sie. Neben der Distanzmatrix sind noch weitere Informationen zu speichern und zwar für jeden Knoten i: • Ob der Knoten bereits markiert wurde: mark(i) • Den Vorgänger des Knotens auf dem Weg vor(i) • Kürzeste bekannte Distanz des Knotens zum Startknoten s: dist(i) Im Struktogramm unten werden diese drei Informationen z.B. als Tripel (3, s, 2) zusammengefasst für einen Knoten, der markiert ist, den Vorgänger s hat und 2 als kürzeste bisher bekannte Distanz zum Startknoten. Diese Informationen können jeweils in einem eindimensionalen Feld gespeichert werden. Für den Eintrag, ob ein Knoten bereits markiert wurde, ist ein ganzzahliger Typ geeignet, wobei wie bei den Wahrheitswerten 1 bedeutet, dass der entsprechende Knoten bereits markiert wurde, und der Wert 0, dass die Markierung noch nicht erfolgt ist. Für das Programm bedeutet dies, dass folgende Daten benötigt werden: unsigned long dist[MAXD], vor[MAXD], mark[MAXD]; Anfangs sind alle Knoten unmarkiert. Zuerst wird nur der Startknoten s markiert und es wird ihm die Distanz 0 zugeordnet (Länge des kürzesten Weges von s zu sich selbst). Den anderen Knoten wird anfangs – zunächst temporär – als Wert für dist ihre Entfernung zu s zugewiesen, falls es eine direkte Verbindung zu s gibt, ansonsten der Wert ∞. Dann wird in einer Schleife von den noch unmarkierten Knoten jeweils der Knoten u mit der kürzesten temporär zugewiesenen Distanz markiert, womit dessen Distanz als kürzest mögliche Distanz zu s festgestellt ist. Für alle Nachbarn v dieses Knotens wird überprüft, ob die Distanz von s über u nach v kürzer ist, als die bisher bei v

314

21 Algorithmen: Graphentheorie

eingetragene temporäre Distanz. Falls dies der Fall ist, so wird diese kürzere Distanz als neue temporäre Distanz gespeichert. Initialisierung: ( Für jeden Knoten (−, s, ∞) k 6= s eintragen: (−, s, dsk ) Für s eintragen: (3, s, 0)

wenn s und k nicht verbunden sind wenn s und k verbunden sind

Solange es noch unmarkierte Knoten gibt Markiere den unmarkierten Knoten u mit dem geringsten Wert dist(u) Für alle unmarkierten Knoten v, die mit u verbunden sind Berechne d 0 = dist(u) + duv Ist d 0 ja von v

Einträge (−, u, d 0 )




2 3 4 5 6 7 8 9

u n s i g n e d l o n g findemin ( u n s i g n e d l o n g * mark , u n s i g n e d l o n g * dist , u n s i g n e d l o n g dimens ) { u n s i g n e d l o n g i; u n s i g n e d l o n g min ; /∗ Zum Merken d e r m i n i m a l e n D i s t a n z ∗/ u n s i g n e d l o n g merk ; /∗ zum Merken d e r Knotennummer m i t m i n i m a l e r D i s t a n z ∗/ min = ULONG_MAX ; merk = ULONG_MAX ;

10 11 12

f o r ( i =0; i < dimens ; i ++) { i f ( mark [ i ] == 0 && dist [ i ] < min ) { min = dist [ i ]; merk = i ; } } r e t u r n merk ;

13 14 15 16 17 18 19 20

}

21.4 Aufgaben Aufgabe 21.1 Wie lassen sich elektrische Widerstands-Netzwerke auf Graphen abbilden? Was sind die Kanten, was die Knoten? Aufgabe 21.2 Zeichnen Sie den Graphen, der durch die im folgenden Programmfragment definierte Distanzmatrix d_mat repräsentiert wird. Geben Sie die Distanzen an den Kanten an. 1 2

#i n c l u d e < limits .h > #d e f i n e MAXD 50

/∗ max . A n z a h l Knoten d e s G r a p h e n ∗/

3 4 5 6 7 8 9 10 11

u n s i g n e d l o n g d_mat [ MAXD ][ MAXD ] = { { ULONG_MAX ,12 , ULONG_MAX , ULONG_MAX , ULONG_MAX ,10 }, {12 , ULONG_MAX ,1 , ULONG_MAX , ULONG_MAX , ULONG_MAX } , { ULONG_MAX ,1 , ULONG_MAX ,11 , ULONG_MAX , ULONG_MAX } , { ULONG_MAX , ULONG_MAX ,11 , ULONG_MAX ,13 , ULONG_MAX } , { ULONG_MAX , ULONG_MAX , ULONG_MAX ,13 , ULONG_MAX ,9 }, {10 , ULONG_MAX , ULONG_MAX , ULONG_MAX , 9 , ULONG_MAX } };

Aufgabe 21.3 Vervollständigen Sie das Programm, das den Dijkstra-Algorithmus realisiert.

22

Algorithmen: Interpretative Implementierung von Automaten

Interpreter sind Programme, die einen bestimmten „Anweisungsvorrat“ beherrschen. Die Anweisungen für einen Interpreter liegen als Daten vor. In unserem Fall handelt es sich um die Beschreibung der Transitionen, die bei Aktivitäten des Automaten zu durchlaufen sind. Wenn man einen Interpreter dazu bringen will, sich auf bestimmte Art zu verhalten, dann passt man nicht die Anweisungen des Programms an, sondern die Daten. Generell gilt als Daumenregel: • direkte Implementierungen sind schneller (Faktor 10-100) als interpretative • interpretative Implementierungen sind flexibler anpassbar (nur ein paar Daten sind zu ändern) • Interpreter-Programme sind komplizierter als direkte Implementierungen Im vorliegenden Abschnitt sind fast alle Elemente aus C bereits bekannt, die man für die Realisierung eines Interpreters benötigt. Insbesondere sind wir in der Lage, Transitionen als Structs darzustellen, einen Vektor davon zu deklarieren und diesen mit Initialwerten zu versehen. Was wir noch nicht kennen, ist die Möglichkeit, Eingabe- und Aktions-Unterprogramme in den struct -Variablen für die Transitionen abzuspeichern. Der folgende Abschnitt liefert die nötige Theorie dazu.

22.1 Programmiertechniken: Pointer auf Funktionen Im Kapitel 18.3 haben wir bereits kennen gelernt, wie man Unterprogramme als Parameter übergeben kann. Diese Möglichkeit lässt sich verallgemeinern. Die Bezeichner von Funktionen werden in C als „Zeiger auf Funktion“ behandelt. Intern wird die Adresse des Maschinencodes abgelegt. Dies lässt sich natürlich nicht nur mit Formalparametern ausnutzen, wie im Kapitel 18.3, sondern auch mit normalen Zeigervariablen:

320

1 2 3

22 Algorithmen: Interpretative Implementierung von Automaten

#i n c l u d e < stdio .h > v o i d f ( i n t i ) { printf ( " f (% d ) aufgerufen \ n " , i ); } v o i d g ( i n t i ) { printf ( " g (% d ) aufgerufen \ n " , i ); }

4 5 6 7 8 9 10 11

i n t main () { v o i d (* ForG )( i n t ); ForG = f ; ForG (1); ForG = g ; ForG (2); r e t u r n 0; }

Zeile 2, 3 7

/∗ A u s g a b e : f ( 1 ) a u f g e r u f e n ∗/ /∗ A u s g a b e : g ( 2 ) a u f g e r u f e n ∗/

Erklärung zum Programmstück f und g sind Funktionen, die int als Parameter haben und keinen Rückgabewert liefern ForG ist Zeiger auf eine Funktion, die int als Parameter hat und keinen Rückgabewert liefert

Die Variable ForG wird als Pointer auf eine Funktion deklariert. Die Klammerung in void (*ForG)(int); ist unbedingt erforderlich, denn sonst würde die Zeile als Prototyp einer Funktion interpretiert, die einen Zeiger auf void als Rückgabewert liefert. Die Wertzuweisungen ForG = f; bzw. ForG = g; legen die Adresse der betreffenden Funktion als Wert in ForG ab. In den Anweisungen ForG(1); bzw. ForG(2); kann man die Klammern (1) bzw. (2) als „Anwendung des Aufrufoperators auf einen Funktionszeiger“ deuten. Eine ähnliche Deutung wäre übrigens auch ohne explizite Zeigervariable, also bei f(1); oder g(2); naheliegend. Mit einer Funktion lassen sich zwei Dinge tun: • Aufruf der Funktion • Benutzung der Adresse als Aktualparameter oder für eine Wertzuweisung an eine Zeigervariable Daher ist es sinnlos, Funktionszeiger zu dereferenzieren. Die Compiler ignorieren daher den Dereferenzierungsoperator und behandeln ForG gleichwertig zu *ForG gleichwertig zu **ForG ... als Zeiger auf Funktion.

22.2 Schema zur Umsetzung in Programme

321

22.2 Schema zur Umsetzung in Programme Ebenso, wie für die direkte, kann man auch für die interpretative Implementierung von Automaten ein Schema angeben, wie man von einem Automaten (Z , z0 , E, A, T ) zu einem Programm kommt. Der folgende Kasten in Abb. 22.1 enthält dieses Schema als Übersicht. Die einzelnen Punkte werden anschließend an Hand des VerkaufsautomatenBeispiels erläutert. Das vollständige Programm findet sich in Abschnitt 22.2.1 S. 322.

a) es wird ein enum-Typ deklariert, der alle Zustände z ∈ Z enthält b) für jede Eingabemöglichkeit e ∈ E wird eine Funktion realisiert, die testet, ob die betreffende Eingabe vorliegt und die in diesem Fall 1 liefert, sonst 0 c) jede Ausgabe oder Aktion a ∈ A wird als Funktion realisiert d) es wird ein stuct-Typ AutoTabEintr deklariert, der geeignet ist für die Aufnahme einer Transition (za , e, a, zf ) e) es wird ein Vektor AutoTab deklariert, der für jede Transition t ∈ T eine Komponente des struct -Typs AutoTabEintr enthält. Die Komponenten werden mit dem Ausgangszustand, der Eingabetestfunktion, der Aktionsroutine und dem Folgezustand der zugehörigen Transition initialisiert. f) es wird eine Variable AktZust definiert, die als Wert jeweils den gerade eingenommenen aktuellen Zustand aufnehmen soll. Diese Variable wird mit dem Ausgangszustand initialisiert g) in einer Schleife werden folgende Schritte wiederholt. • es wird eine Eingabe e gelesen • Zu e wird eine passende Transition in AutoTab gesucht, d.h. eine, deren Ausgangszustand mit dem gerade eingenommenen Zustand übereinstimmt und deren Eingabetestfunktion, angewandt auf e, eine 1 liefert. • Diese Transition wird ausgeführt, d.h. die Aktionsroutine wird aufgerufen und der Folgezustand als gerade eingenommener Zustand in AktZust vermerkt. Tabelle 22.1: Schema zur Umsetzung eines Automaten in ein interpretatives Programm

322

22 Algorithmen: Interpretative Implementierung von Automaten

Für das Verkaufsautomaten-Beispiel könnte die Umsetzung aussehen wie im Beispielprogramm in Abschnitt 22.2.1 S. 322:

Schritt a) b)

Zeile 7-8 10-14

c)

16-21

d)

23-27

e)

29-40

42-43

f) g)

46 48-59 49 50-57 52 53 54

Umsetzung im Programm Abschnitt 22.2.1 S. 322 Aufzählungstyp für die Zustände Hier sind die nötigen Eingabetestfunktionen definiert. Der Automat macht die Eingaben mit ch=toupper(getch()); (vgl. Kapitel 12 „Automaten“). Möglich sind die Eingaben der Buchstaben M, L, D, A und Z. Realisierung der Aktionen – hier als Ausgabefunktionen. Die Ausgaben bzw. Aktionen des Automaten sind im Kapitel 12 S. 173 „Automaten“ beschrieben. Deklaration des Automatentabellen-Eintrags. Ausgangs- und FolgeZustand werden in Z1 bzw. Z2 gespeichert. Für die Eingabetestund Aktions-Routinen sind die Pointer auf Funktion EventProc bzw. ActionProc vorgesehen Der Vektor AutoTab enthält die Automatentafel. Jeder Initialwert ist seinerseits eine durch { ... } geklammerte Folge von Werten, die jeweils einen AutoTabEintr initialisieren. Jede Zeile stellt eine Transition dar. Die erste und die letzte Spalte enthalten den Ausgangsbzw. den Folgezustand. Die einbuchstabigen Bezeichner in Spalte 2 geben die Eingabetestfunktion an und die Bezeichner der Spalte 3 eine Aktionsroutine. Die Dimension des Vektors ergibt sich aus der Anzahl der angegebenen Initialwerte. Deklaration der Variablen für den aktuellen Zustand Hauptschleife des Automaten, mit Lesen der Eingabe Schleife zur Suche nach einer passenden Transition. Mit AutoTab[i].EventProc(ch) wird die Eingabetestfunktion aufgerufen. Ausführung der Aktion mit AutoTab[i].ActionProc(); Fortschaltung zum Folgezustand durch die Zuweisung AktZust=AutoTab[i].Z2;

22.2.1 Beispielprogramm Das Programm ist nach dem oben dargestellten Schema erzeugt, d.h. es ist interpretativ realisiert. Es entspricht im Verhalten genau dem Programm, das im Kapitel 12 „Automaten“ nach der Methode der direkten Implementierung entwickelt wurde. Ein Anwender der Programme würde keinen Unterschied merken. 1 2 3 4

#i n c l u d e #i n c l u d e #i n c l u d e #i n c l u d e

< stdio .h > < conio .h > < ctype .h > < stdlib .h >

22.2 Schema zur Umsetzung in Programme

5

323

#i n c l u d e < string .h >

6 7 8

enum Zustand { Ausgangszustand , MuenzeEingeworfen , RueckgabeGefordert , SchubladeGezogen };

9 10 11 12 13 14

int int int int int

M( c h a r L( c h a r D( c h a r A( c h a r Z( c h a r

ch ) ch ) ch ) ch ) ch )

{ { { { {

return return return return return

} /∗ E i n g a b e t e s t f u . ∗/ } } } }

ch == ’M ’; ch == ’L ’; ch == ’D ’; ch == ’A ’; ch == ’Z ’;

15 16 17 18 19 20 21

void void void void void void

NoOp () Sa () SzAa () Ka () Az () SzKz ()

{}; /∗ A k t i o n s r o u t i n e n ∗/ { printf ( " Riegel Schub Auf \ n " ); } { printf ( " Riegel Schub Zu / Auswurf Auf \ n " );} { printf ( " Riegel Kasse Auf \ n " ); } { printf ( " Riegel Auswurf Zu \ n " ); } { printf ( " Riegel Schub Zu / Kasse Zu \ n " ); }

22 23 24 25 26 27

s t r u c t AutoTabEintr { enum Zustand Z1 ; int (* EventProc ) ( c h a r ch ); void (* ActionProc )(); enum Zustand Z2 ; };

28 29 30 31 32 33 34 35 36 37 38 39 40

s t r u c t AutoTabEintr AutoTab [] = { Ausgangszustand , M , Sa , { Ausgangszustand , L , NoOp , { Ausgangszustand , D , NoOp , { MuenzeEingeworfen , D , SzAa , { MuenzeEingeworfen , A , Ka , { MuenzeEingeworfen , L , NoOp , { RueckgabeGefordert , L , Az , { SchubladeGezogen , Z , SzKz , { SchubladeGezogen , L , NoOp , { SchubladeGezogen , D , NoOp , };

{ Muen zeEing eworf en } , Ausgangszustand }, Ausgangszustand }, R ue ck ga be Ge for de rt } , SchubladeGezogen } , Muen zeEing eworf en } , Ausgangszustand }, Ausgangszustand }, SchubladeGezogen } , SchubladeGezogen }

41 42 43 44 45 46

c o n s t i n t AutoTabAnz = s i z e o f ( AutoTab )/ s i z e o f ( s t r u c t AutoTabEintr ); i n t main () { i n t i ; c h a r ch ; enum Zustand AktZust = Ausgangszustand ;

47 48 49 50 51

w h i l e (1) { ch = toupper ( getch ()); f o r ( i =0; i < AutoTabAnz ; i ++) { i f (( AutoTab [ i ]. Z1 == AktZust )

&&

324

( AutoTab [ i ]. EventProc ( ch )) { AutoTab [ i ]. ActionProc (); AktZust = AutoTab [ i ]. Z2 ; break ; }

52 53 54 55 56

)

} i f ( i == AutoTabAnz ) { printf ( " Eingabefehler % d " , ch ); }

57 58

} r e t u r n 0;

59 60 61

22 Algorithmen: Interpretative Implementierung von Automaten

}

22.3 Fragen Geben Sie das Profil für die direkte und für die interpretative Implementierung von endlichen Automaten an (markieren des zutreffenden Feldes, verbinden zu einer ZickzackProfillinie): Gesichtspunkt

direkte Implementierung niedrig hoch

interpretative Implement. niedrig hoch

Code-Umfang bei großen Automaten Daten-Umfang bei großen Automaten Flexibilität bei Anpassungen AusführungsGeschwindigkeit Kompliziertheit der Programm-Anweisungen Kompliziertheit der Daten-Deklarationen

22.4 Aufgaben Aufgabe 22.1 Erzeugung eines Paritätsbits Die Abbildung 22.1 S. 325 zeigt einen Automaten, der jeweils sieben Dualziffern liest und wieder ausgibt. Am Ende einer Siebener-Sequenz wird eine zusätzliche Prüfziffer ausgegeben. Die Prüfziffer wird so gebildet, dass die acht Ziffern zusammen eine gerade Anzahl von Einsen enthalten.

22.4 Aufgaben

325

a) Überzeugen Sie sich an Hand der Folgen 0000000, 1111111, 1010101 und 0101010 von der Funktion des Automaten! Geben Sie die Folge der eingenommenen Zustände an und die Ausgegebenen Ziffern. b) Ändern Sie das oben angegebene Beispielprogramm für den Verkaufsautomaten so ab, dass der Automat für die Erzeugung des Paritätsbits realisiert wird! z0 0/0

1/1

g1 0/0

u1 1/1

1/1

g2 0/0 0/00

u2 1/1

g3 0/0

1/1

1/1

g4 1/1

g5

g6

0/01 0/0

u4 1/1

0/0

0/0

u3 1/1

0/0

0/0

0/0 u5

1/1

1/1

0/0 u6

Abbildung 22.1: Automat zur Erzeugung eines Paritybits

Aufgabe 22.2 Escape-Sequenz-Filter interpretativ Im Abschnitt 12.6.2 „Filter zur Behandlung von Zeichenfolgen“ auf Seite 189 ist ein Filter-Automat angegeben, der aus HTML-Dateien die Ersatzdarstellungen für die deutschen nationalen Sonderzeichen äöü und ß erkennt und durch diese Zeichen selbst ersetzt. Implementieren Sie den Automaten nach obigem Schema interpretativ. Vergleichen Sie den Umfang des Programms mit der direkten Realisierung!

23

Fortgeschrittene Themen

In diesem Kapitel werden verschiedene komplexere Themen behandelt, die in den vorausgegangenen Abschnitten keinen Platz gefunden haben.

23.1 Argumente und Rückgabewert von main Einfachstenfalls startet man ein Programm durch Doppelklick oder aus der Eingabeaufforderung durch Angabe des Programmnamens. Oft will man einem Programm auch Parameter mitgeben. Wenn man z. B. in der Eingabeaufforderung eingibt type xxx.txt ist die Absicht des Verfassers dieser Kommandozeile natürlich, dass type die Datei xxx.txt ausgibt. Das bedeutet, dass das Programm type mit dem Parameter xxx.txt gestartet werden soll. Typischerweise könnte das Programm type in C implementiert sein. Woher erfährt es eigentlich, dass es mit dem Parameter xxx.txt aufgerufen wurde – d.h. welche Datei es ausgeben soll? Kommandozeilenparameter werden an C-Programme als Argumente von main übergeben. Daher gibt es zwei Möglichkeiten, main zu deklarieren • wenn das Programm keine Parameter erwartet int main(void){...} bzw. int main(){...} • wenn das Programm Kommandozeilen-Parameter erwartet int main(int argc, char *argv[]){...} Übergeben werden beim Aufruf argc Anzahl der Kommandozeilenparameter. Der Kommando-Name, der als erstes auf der Kommandozeile steht, wird mitgezählt. Der Wert 1 bedeutet also, dass nur der Kommandoname angegeben wurde, also kein Weiterer Parameter.

328

23 Fortgeschrittene Themen

argv Vektor von Zeigern auf char. Jeder Zeiger argv[0] bis argv[argc-1] zeigt auf eine Zeichenkette, die in der Kommandozeile enthalten ist. Der erste (mit dem Index 0) auf den Kommando-Namen. argv[argc] ist der Nullpointer.

Im Kapitel 16.3.3 S. 237 sind Vektoren von Pointern behandelt worden. Auf den Argumentvektor argv und das obige Beispiel type xxx.txt angepasst, sieht das Diagramm aus dem Pointer-Kapitel wie in Abb. 23.1 aus.

"type" argv[0] argv[1] argv[2]==NULL

"xxx.txt"

Abbildung 23.1: Der Argumentvektor von main für type xxx.txt

Wie man sieht, erscheint der Kommandoname type als argv[0]. Mit der Deklaration von argc und argv kann man auf die Kommandozeilenparameter zugreifen. Z. B. könnte man in type die Datei xxx.txt öffnen: 1 2 3

FILE * pdatei ; ... pdatei = fopen ( argv [1] , " r " );

Abb. 23.2 S. 329 zeigt das komplette Listing eines Programms type.c, mit dem man mehrere Textdateien ausgeben kann, deren Namen auf der Kommandozeile stehen müssen. Wie in DOS oder UNIX üblich, gibt das Programm auch einen Hilfe-Text aus, wenn als Argument /? angegeben wird. Der Rückgabewert von main wird bei Beendigung des Programms an das Betriebssystem übergeben. Mit dessen Hilfe kann z. B. eine Shell (z. B. DOS-Box) den Exit-Status eines von ihr gestarteten Programms abfragen und entsprechend reagieren. Ein Wert ungleich 0 bedeutet meist eine Fehlermeldung. 0 bedeutet, dass das Programm mit Status ok beendet wurde. Alternativ zum Verlassen des Hauptprogramms mit return status; kann man auch exit(s); benutzen, vgl. Kapitel 9.10 S. 137 „Standardbibliothek/ Starten, Beenden“.

23.2 Typumwandlungen

1 2 3 4 5 6 7

329

#i n c l u d e < stdio .h > #i n c l u d e < stdlib .h > #i n c l u d e < string .h > main ( i n t argc , c h a r * argv []) { FILE * pdatei ; i n t i; c h a r ch ;

8

i f ( argc

1 2

d o u b l e d ( i n t i ){ r e t u r n i +1; }

3 4

i n t main () { i n t i1 ; l o n g l o n g l; c h a r c = ’a ’; // 97 d e z i m a l , 0 x 6 1 h e x a d e z i m a l i1 = d (1.5); l = c +( i n t )0 x80000000UL ; printf ( " % d % llx " , i1 , l ); // 2 f f f f f f f f 8 0 0 0 0 0 6 1 r e t u r n 0; }

5 6 7 8 9 10 11 12 13

Abbildung 23.4: Testprogramm: Albtraum des PASCAL-Programmierers

führt: 3

int



double

9

double



int

9 10 10

double unsigned long char

→ → →

int int int

10

int



long long

wegen der Rückgabe eines Wertes aus dem Unterprogramm wegen Übergabe eines Parameters an das Unterprogramm wegen der Wertzuweisung wegen einer expliziten Typumwandlung wegen verschieden-typiger Operanden des Operators + wegen der Wertzuweisung

1 Wer etwas über den Unterschied zwischen C und PASCAL lernen möchte, kann einmal versuchen, dieses Testprogramm nach PASCAL zu übersetzen.

332

23 Fortgeschrittene Themen

23.2.4 Arten von Typumwandlungen Umwandlung von Zahlen Im Beispiel 5

GesPreis = ( EzPreis

* Anz );

am Anfang des Kapitels wird ein Operator (hier *) auf verschiedentypige Operanden angewendet (hier double und int ). Die Angleichung der Typen erfolgt so, dass durch die Typumwandlung möglichst wenig Information verloren geht. Dazu wird die Hierarchie der arithmetischen Typen berücksichtigt und der schwächere Typ (dessen Interndarstellung weniger oder höchstens gleich viele Bits enthält) wird auf den stärkeren Typ „aufgeweitet“. Die Hierarchie für die Angleichung ist char→ short → int →long→long long→ float →double→long double Diese Aufweitung muss natürlich so erfolgen, dass der Zahlenwert erhalten bleibt. Für die ganzzahligen Werte in Komplement-Darstellung ist die Regel daher: Für signed-Typen (ganzzahlige Typen mit Vorzeichen) erfolgt die Aufweitung mit dem Vorzeichenbit (sign extend) Für unsigned-Typen (vorzeichenlose ganzzahlige Typen) erfolgt die Aufweitung mit Nullen (zero extend) Wenn von zwei anzugleichenden Werten einer signed und einer unsigned ist, erhält das Ergebnis die gleiche Vorzeichenbehandlung wie der stärkere von beiden. Dazu folgendes Zahlenbeispiel. Will man etwa das Bitmuster 111111002 eines charWertes (das entspricht dem ISO 8859-1-Code 25210 für ’ü’) auf long aufweiten, dann muss zwischen char (mit Vorzeichen) und unsigned char unterschieden werden. Fall 1, signed 1111 11002 ist als negative Zweikomplement-Darstellung zu interpretieren. Durch Rückkomplementierung gewinnen wir −1002 oder −410 . Nach der Aufweitung auf long mit Vorzeichenbit entsteht 1111 1111 1111 1111 1111 1111 1111 11002 . Durch Rückkomplementierung gewinnen wir auch in diesem Fall −1002 oder −410 . Der Wert ist also bei der Aufweitung erhalten geblieben. Fall 2, unsigned 1111 11002 ist als positive Zahl zu interpretieren, entspricht also dem Wert 25210 . Als Wert ohne Vorzeichen wird mit 0 aufgeweitet: 0000 0000 0000 0000 0000 0000 1111 11002 Diese Zahl hat natürlich auch den Wert 25210 , d.h. der Wert ist bei der Aufweitung erhalten geblieben.

23.2 Typumwandlungen

333

Diese beiden Fälle werden in folgendem kleinen Testprogramm demonstriert: 1 2 3 4 5 6 7 8 9 10

#i n c l u d e < stdio .h > i n t main () { c h a r ch = ’ü ’; u n s i g n e d c h a r uch = ’ü ’; l o n g l ; u n s i g n e d l o n g ul ; l = ch ; ul = ch ; printf ( " % lx % lx \ n " , l , ul ); l = uch ; ul = uch ; printf ( " % lx % lx \ n " , l , ul ); printf ( " ch ==252:% d \ n " , ch ==0 xfc ); printf ( " uch ==252:% d \ n " , uch ==0 xfc );

// ’ ü ’==0 x f c ==252

// f f f f f f f c

fffffffc

// fc // c h ==252:0 // u c h ==252:1

fc

11

r e t u r n 0;

12 13

}

Für die Vergleiche mit 0xfc in den beiden letzten printf(...)-Anweisungen muss man sich vergegenwärtigen, dass 0xfc die Schreibweise für eine int -Literalkonstante ist, deren Wert in 32Bit-Umgebungen gegeben ist durch das Bitmuster 0000 0000 0000 0000 0000 0000 1111 11002 ch==0xfc vergleicht dieses Bitmuster mit der Aufweitung von ch gemäß Fall 1, also mit 1111 1111 1111 1111 1111 1111 1111 11002 was den Wert 0 (falsch) ergibt. uch==0xfc vergleicht dieses Bitmuster mit der Aufweitung von uch gemäß Fall 2, also mit 0000 0000 0000 0000 0000 0000 1111 11002 was den Wert 1 (wahr) ergibt. Wandlung Zwischen Zeigertypen In diesem Abschnitt werden Inhalte aus Kap. 16.3 S. 233 „Pointer und Vektoren“ noch einmal unter dem Aspekt „Typumwandlungen“ zusammengefasst. • Nullpointer 0 kann als Pointer jeden Typs verwendet werden (wird automatisch angepasst). Ein Beispiel dazu aus Kapitel 16.2.4 S. 232 „Pointer ohne Bezugsvariable, Nullpointer, void*“ war while (gets(Zeile)!=0) ... • Der Pointertyp void* void* ist ein Pointertyp mit der Bedeutung „Pointer auf unbekannten Bezugsvariablentyp“. Jeder Pointertyp kann als void* benutzt werden; er wird dann automatisch angepasst. In der anderen Richtung – also wo ein void* -Pointer gegeben ist und ein Pointer irgendeines anderen Typs benötigt wird – muss durch explizite Wandlung (type cast) angepasst werden.

334

23 Fortgeschrittene Themen void* wird häufig für Unterprogramme benutzt, die Speicherblöcke verwalten, die erst nachträglich zu Variablen eines bestimmten Typs zugeordnet werden (z. B. malloc(...), free(...)). Ein Beispiel für diesen Gebrauch von void* aus Kapitel 16.4 S. 238 „Dynamische Variable mit malloc und free“ war pch = (char*)malloc(strlen(puffer)+1); • Vektortypen und Pointer In C geht man davon aus, dass jeder Pointer auch auf eine Vektorkomponente zeigen könnte. Diese Vorstellung wird in der Programmiersprache besonders unterstützt2 . Zwei Motive für diese Sichtweise sind insbesondere – die Übergabe von Vektoren als Adresse an Unterprogramme – die enge Verwandtschaft zwischen Pointer-Arithmetik und Vektoren in C In C werden daher Vektortypen bei Bedarf automatisch in Pointer auf die erste Komponente gewandelt. Ein Beispiel in Kapitel 16.3.3 S. 236 „Pointer auf Vektoren“ für diese automatische Wandlung war char sX[] = "Eins", sY[] = "Zwei", sHilfs[5]; ... strcpy(sHilfs, sX); ... Dabei ist die Funktion strcpy in string.h deklariert als3 char *strcpy(char *dest, char *src);

Standardisierte Interndarstellungen Zentraleinheiten arbeiten heute in der Regel mit Registern, die mehr Bits enthalten, als für die Typen char, short oder float (Gleitpunkteinheit) benötigt werden. Oft gehen Berechnungen schneller, wenn Operanden mit genauer Registerbreite verarbeitet werden. C-Compiler arbeiten daher oft so, dass Zwischenergebnisse, für die der Programmierer keine Vorgaben gemacht hat, automatisch zu den int bzw. double-Typ aufgeweitet werden, die am besten zu den Maschinenregistern passen. Betroffen sind hier insbesondere Zwischenergebnisse während der Auswertung von Ausdrücken sowie Parameter für Unterprogramme, zu denen kein Prototyp angegeben wurde. Typisch ist, dass alle Gleitpunkttypen mindestens als double und alle ganzzahligen Typen mindestens als int verarbeitet und übergeben werden. 2 Vgl.

dazu die ausführliche Darstellung in Kap. 16.3.1 S. 233 „Vektoren und Pointer-Arithmetik“ vereinfacht

3 etwas

23.3 Union-Typen

335

23.3 Union-Typen Oft werden Daten völlig verschiedenen Arten von Zugriffen unterzogen. Wir betrachten als Beispiel die Daten zu einem Kessel aus dem Kapitel 20 S. 295 über Structs: 1 2 3 4 5 6 7 8 9 10

s t r u c t Kessel { int

xKoord , yKoord ; int hoehe , breite ; d o u b l e Kapazitaet , Fuellstand ; Char name [20]; }; s t r u c t Kessel Kessel1 ;

Etwa mit Kessel1.xKoord = 20; oder Kessel1.Kapazitaet = 100.; wird auf Komponenten des Datentyps struct Kessel zugegriffen. Anders sieht die Situation aus, wenn man zusätzlich z. B. Programmteile zur Speicherverwaltung oder für Datenübertragung auf sequentielle Medien benutzt. Solche Pakete sind i.A. unabhängig von bestimmten Datentypen der Anwendung geschrieben. Oft betrachten sie die Daten als Vektoren von Bytes oder int s, analog zu folgendem Aufruf (vgl. Kapitel 19.4 S. 292) für Binärausgabe. 1 2

f o r ( i =0; i < s i z e o f ( VektorVariable ); i ++) fwrite (& VektorVariable [ i ] , 1 , 1 , pdatei );

In C gibt es mit unions eine Möglichkeit, mehrere verschiedene Sichten auf Daten in einer Deklaration anzugeben und nach Belieben eine davon für Zugriffe zu benutzen. Wir sehen uns dies an einem Beispielprogramm an, das die Variable Kessel1 durch Zugriff auf die Kessel-Komponenten vorbelegt. Anschließend werden die Bytes von Kessel1 mit fwrite binär ausgegeben. Für die Binärausgabe wird die Variable als Vektor von unsigned chars betrachtet. Es wird ein Datentyp KesselXfer deklariert, der die beiden Sichten auf Kesselvariablen in sich vereint: 1 2 3 4

u n i o n KesselXfer { s t r u c t Kessel Kess ; u n s i g n e d c h a r Vekt [ s i z e o f ( s t r u c t Kessel )]; };

5 6

u n i o n KesselXfer Kessel1 ;

Bildlich dargestellt sehen die Zugriffswege auf die Variable Kessel1 jetzt aus wie in Abbildung 23.5. Die Syntax ist analog zur Syntax von struct -Deklarationen. Wir können das Diagramm 20.1 S. 297 aus Kapitel 20 „Structs und komplexe Datenstrukturen“ also ergänzen wie in Abb. 23.6 gezeigt.

336

23 Fortgeschrittene Themen union KesselXfer Kessel1;

struct Kessel Kess { int

xKoord, yKoord; hoehe, breite;

int

unsigned char Vekt[sizeof( struct Kessel) ];

double Kapazitaet, Fuellstand;

Char

name[20];

}; Abbildung 23.5: Kessel1 als struct oder als Vektor von unsigned char

StrukturOderUnionDekl {

struct union

Strukturname

} Variablendeklarator , Komponentendekl Typ

Komponentendeklarator ,

Abbildung 23.6: Struktur- oder Union-Deklaration

;

Komponentendekl

23.3 Union-Typen

337

Einfachstenfalls ist der Komponenten-Deklarator ein einfacher Komponenten-Bezeichner. Der Unterschied zwischen union- und struct -Typen besteht also tatsächlich nur darin, dass der Compiler bei struct s die Plätze der Komponenten im Speicher fortlaufend anlegt, bei unions hingegen jede Komponente die gleiche Adresse bekommt. Listing 23.7 zeigt das komplette Beispielprogramm, das zwei Kessel-Variablen durch Zugriff über die struct -Komponenten vorbelegt und über die Vektor-Komponente der union binär in eine Datei schreibt.

338

1 2 3

23 Fortgeschrittene Themen

#i n c l u d e < stdio .h > #i n c l u d e < stdlib .h > #i n c l u d e < string .h >

4 5 6 7 8

s t r u c t Kessel { i n t xKoord , yKoord ; i n t hoehe , breite ; d o u b l e Kapazitaet , Fuellstand ; c h a r name [20]; } ;

9 10 11 12

u n i o n KesselXfer { s t r u c t Kessel Kess ; u n s i g n e d c h a r Vekt [ s i z e o f ( s t r u c t Kessel )]; };

13 14 15 16 17 18 19

i n t main () { FILE * pdatei ; i n t i; u n i o n KesselXfer Kessel1 ; u n i o n KesselXfer Kessel2 = { {200 , 40 , 150 , 100 , 150. , 50. , " Tank2 " }

};

20 21 22 23 24 25

Kessel1 . Kess . xKoord = 20; Kessel1 . Kess . yKoord = 20; Kessel1 . Kess . hoehe = 150; Kessel1 . Kess . breite = 100; Kessel1 . Kess . Kapazitaet = 100.; Kessel1 . Kess . Fuellstand = 50.; strcpy ( Kessel1 . Kess . name , " Tank1 " );

26 27 28 29 30 31 32 33 34 35

pdatei = fopen ( " ausgabe . bin " , " w " ); i f ( pdatei == NULL ) { printf ( " Fehler beim Oeffnen der Datei \ n " ); exit ( -1); } f o r ( i =0; i < s i z e o f ( Kessel1 . Vekt ); i ++) fwrite (& Kessel1 . Vekt [ i ] , 1 , 1 , pdatei ); f o r ( i =0; i < s i z e o f ( Kessel2 . Vekt ); i ++) fwrite (& Kessel2 . Vekt [ i ] , 1 , 1 , pdatei );

36 37 38 39 40 41 42

// A n d e r e M ö g l i c h k e i t : // f w r i t e ( K e s s e l 1 . Vekt , 1 , s i z e o f ( s t r u c t K e s s e l ) , p d a t e i ) ; // f w r i t e ( K e s s e l 2 . Vekt , 1 , s i z e o f ( s t r u c t K e s s e l ) , p d a t e i ) ; fclose ( pdatei ); r e t u r n 0; }

Abbildung 23.7: Zugriff auf Kessel über Union

24

Lösung ausgewählter Übungsaufgaben

Lösung 2.1 24

2

=

(siehe Seite 35)

16777216

= ˆ 16MB

Lösung 2.3

(siehe Seite 35)

1000000000000001 1000000000000000

= ˆ = ˆ

− 32767 − 32768

Lösung 2.4

(siehe Seite 36)

−1 = ˆ 1111111111111111 −4096 = ˆ 1111000000000000 Lösung 2.5 n=10 n=12 n=20 n=32

(siehe Seite 36)

adressierbar: adressierbar: adressierbar: adressierbar:

1KB 4KB 1MB 4GB

Lösung 2.6 Dual Oktal Hexadezimal Lösung 3.1 Text: „Hallo“

(siehe Seite 36) Zahl 1 101010102 2528 AA16

Zahl 2 110101112 3278 D716

Zahl 3 1100101011012 62558 CAD16 (siehe Seite 45)

340

24 Lösung ausgewählter Übungsaufgaben

Lösung 3.2

(siehe Seite 45)

Wert 1010 1016 −510

Interndarstellung

Externdarstellung

höherwertiges Byte

niederwertiges Byte

1. Ausgabebyte

2. Ausgabebyte

0016 0016 FFFF16

0A16 1016 FB16

3116 3116 2D16

3016 3616 3516

Lösung 3.3 ISO 8859-1 UTF-8

(siehe Seite 45) 6D FC DF 69 67 6D C3 BC C3 9F 69 67

Lösung 4.1

(siehe Seite 56)

3 Dezimalziffer

Dezimalziffer

Lösung 5.1

1

#i n c l u d e < stdio .h >

2 3 4 5 6

i n t main () { int i ; int n ;

7

double Summe =1.0; double Nenner =1.0;

8 9 10

printf ( " Berechnen bis n =?\ n " ); scanf ( " % d " , & n );

11 12 13

f o r ( i =1; i

2 3 4 5

i n t main () { d o u b l e x1 , x2 , y1 , y2 , xi , yi ;

6 7 8 9

printf ( " x1 , y1 , x2 , y2 und xi eingeben \ n " ); scanf ( " % lf % lf % lf % lf % lf " , & x1 , & y1 , & x2 , & y2 , & xi ); // E r g ä n z u n g :

342

24 Lösung ausgewählter Übungsaufgaben yi = y1 + (( y2 - y1 )*( xi - x1 )/( x2 - x1 ));

10 11

printf ( " y =% lf and der Stelle xi =% lf \ n " , yi , xi ); r e t u r n 0;

12 13 14

}

Lösung 7.4 1 2

(siehe Seite 113)

// Z e i t a d d i t i o n #i n c l u d e < stdio .h >

3 4 5 6 7 8

i n t main (){ i n t izh , izm , izs ; i n t idh , idm , ids ; i n t zs , ds ; i n t eh , em , es ;

// // // //

Variablen Variablen Variablen Variablen

für für für für

Input Input Zwischenergebnisse Endergebnisse

9

// Z e i t und D e l t a e i n l e s e n printf ( " Zeit eingeben hh mm ss ->" ); scanf ( " % d % d % d " , & izh , & izm , & izs ); printf ( " Delta eingeben hh mm ss ->" ); scanf ( " % d % d % d " , & idh , & idm , & ids );

10 11 12 13 14 15

// I n S e k u n d e n u m r e c h n e n zs = izh * 3600 + izm *60 + izs ; ds = idh * 3600 + idm *60 + ids ;

16 17 18 19

// es eh es em es

20 21 22 23 24 25

Ergebniswerte berechnen = zs + ds ; = ( es /3600)%24; = es %3600; = es /60; = es %60;

26

// E r g e b n i s a u s g e b e n printf ( " \ nSummenzeit % d :% d :% d \ n " , eh , em , es ); r e t u r n 0;

27 28 29 30

}

24 Lösung ausgewählter Übungsaufgaben

343

Lösung 7.5

(siehe Seite 113)

int int int int int

i=1, i=1, i=1, i=1, i=1,

j=0; j=0; j=0; j=0; j=0;

Logischer Ausdruck 7 > 3 9 < 5 5 != 9 i j i > j j == i j = i

Wahrheitswert wahr (1) falsch (0) wahr (1) wahr (1) falsch (0) wahr (1) falsch (0) wahr (1)

Lösung 8.1 1 2 3 4

(siehe Seite 124)

ia < ib :1 , ia == ib :0 ba :1 , bb :0 , ! ba :0 ba || bb :1 , ba && ! bb :1 ba :0 , bb :1

Lösung 8.2

(siehe Seite 124)

(a = ((b >= c) && ((!c) > d))); //a=0 b=2 c=0 (a = ((b >11 ); /∗ d r u c k t S t u n d e ∗/ printf ( " % d : " , ( time > >5)&0 x3f ); /∗ d r u c k t M i n u t e ∗/ printf ( " % d \ n " , ( time &0 x1f )*2 ); /∗ d r u c k t S e k u n d e ∗/

344

24 Lösung ausgewählter Übungsaufgaben

Lösung 8.4

(siehe Seite 124)

// E i n g a b e e i n e s Datums : printf ( " Tag eingeben > " ); scanf ( " % d " , & tag ); printf ( " Monat eingeben > " ); scanf ( " % d " , & monat ); printf ( " Jahr eingeben > " ); scanf ( " % d " , & jahr );

1 2 3 4 5

// H i e r d i e C o d i e r u n g d e s Datums : dat = ( jahr -1980) < 5)& 0 x000F ); /∗ d r u c k t Monat ∗/ printf ( " % d " , ( dat >> 9) + 1980); /∗ d r u c k t J a h r ∗/

11 12 13 14 15 16

Lösung 9.1

1 2 3 4 5

#i n c l u d e < stdio .h > #i n c l u d e < math .h > i n t main () { d o u b l e Pi = atan (1.0)*4.0; i n t i ; d o u b l e x , sinx , cosx ;

(siehe Seite 140)

// L ü c k e f ü r E r g ä n z u n g

6

f o r ( i =0; i #i n c l u d e < math .h >

4 5 6 7

i n t main ( i n t argc , c h a r * argv []){ d o u b l e wurzelmitln , w u r z e l m i t x h o c h e i n s d u r c h n ; d o u b l e quadmitln , q u a d m i t x h o c h e i n s d u r c h n ;

8

wurzelmitln = exp ( log (2)/2); w u r z e l m i t x h o c h e i n s d u r c h n = pow (2.0 , 1.0/2.0);

9 10 11

quadmitln = wurzelmitln * wurzelmitln ; quadmitxhocheinsdurchn = wurzelmitxhocheinsdurchn * wurzelmitxhocheinsdurchn ;

12 13 14 15

printf ( " %25.20 f quadriert = %25.20 f \ n " , wurzelmitln , quadmitln ); printf ( " %25.20 f quadriert = %25.20 f \ n " , wurzelmitxhocheinsdurchn , q u a d m i t x h o c h e i n s d u r c h n ); r e t u r n 0;

16 17 18 19 20 21

}

Lösung 9.3 1 2

(siehe Seite 141)

/∗ DegRad w a n d e l t G r a d i n Bogenmaß ∗/ #i n c l u d e < stdio .h >

3 4 5 6 7

i n t main ( i n t argc , c h a r * argv []){ d o u b l e deg ; d o u b l e pi = 4.0* atan (1.0); scanf ( " % lf " , & deg );

8

printf ( " % g Grad entsprechen % g im Bogenmass \ n " , deg , 2* pi * deg /360); r e t u r n 0;

9 10 11 12

}

346

24 Lösung ausgewählter Übungsaufgaben

Lösung 10.2 1 2 3 4 5 6 7 8 9 10 11 12 13 14

/∗ E r w e i t e r u n g : X , I v o r M und D ∗/ /∗ v o r d i e H u n d e r t e r −B e h a n d l u n g ∗/ s w i t c h ( z ){ c a s e 990: printf ( " XM " ); z =z -990; c a s e 490: printf ( " XD " ); z =z -490; c a s e 999: printf ( " IM " ); z =z -999; c a s e 499: printf ( " ID " ); z =z -499; } /∗ E r w e i t e r u n g : I v o r C und L ∗/ /∗ v o r d i e Z e h n e r −B e h a n d l u n g ∗/ s w i t c h ( z ){ c a s e 99: printf ( " IC " ); z =z - 99; c a s e 49: printf ( " IL " ); z =z - 49; }

(siehe Seite 162)

break ; break ; break ; break ;

break ; break ;

15

Lösung 10.3 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24

(siehe Seite 162)

#i n c l u d e < stdio .h > #d e f i n e EPS 1. E -10 /∗ G e n a u i g k e i t s s c h r a n k e ∗/ #d e f i n e MAX_SCHRITTE 100 /∗ wenn EPS n i e e r r e i c h t w i r d ∗/ #d e f i n e ABS ( x ) ((( x ) >0) ? ( x ) : -( x )) i n t main () { d o u b l e xi ; i n t it = 0; printf ( " Newton - Raphson 1. Ordnung \ n " ); it = 0; xi = 5.0; /∗ S t a r t w e r t ∗/ do { xi = xi -(4* xi * xi * xi -5* xi * xi +1)/(12* xi * xi -10* xi ); ++ it ; } w h i l e (( ABS (4* xi * xi * xi -5* xi * xi +1) > EPS ) && ( it < MAX_SCHRITTE )); i f ( it >= MAX_SCHRITTE ) printf ( " Mangelnde Konvergenz !\ n " ); else { printf ( " Nullstelle bei % lf mit % d Iterationen .\ n " , xi , it ); printf ( " Genauigkeit : % le \ n " , 4* xi * xi * xi -5* xi * xi +1); } r e t u r n 0; }

24 Lösung ausgewählter Übungsaufgaben

347

Lösung 10.4 1 2 3

(siehe Seite 162)

#i n c l u d e < stdio .h > #d e f i n e ZEILEN 3 #d e f i n e SPALTEN 5

4 5 6 7 8

i n t main () { i n t n, i; scanf ( " % d " , & n ); printf ( " Die Vielfachen von % d sind \ n " , n );

9

f o r ( i =1; i

2 3 4 5 6 7 8 9 10 11 12 13 14 15

i n t main () { c h a r ch = ’ \0 ’; i n t a =0 , e =0 , i =0 , o =0 , do { scanf ( " % c " , & ch ); s w i t c h ( ch ){ c a s e ’a ’: c a s e ’A ’: c a s e ’e ’: c a s e ’E ’: c a s e ’i ’: c a s e ’I ’: c a s e ’o ’: c a s e ’O ’: c a s e ’u ’: c a s e ’U ’: } } w h i l e ( ch != ’! ’ );

u =0;

a ++; e ++; i ++; o ++; u ++;

break ; break ; break ; break ; break ;

16

printf ( " a :% d e :% d i :% d o :% d u :% d \ n " , a ,e ,i ,o , u ); r e t u r n 0;

17 18 19

}

348

24 Lösung ausgewählter Übungsaufgaben

Lösung 10.6 1

(siehe Seite 162)

#i n c l u d e < stdio .h >

2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23

i n t main ( i n t argc , c h a r * argv []){ i n t n; scanf ( " % d " , & n ); i f (n >7) printf ( " % d ist groesser als 7\ n " , n ); e l s e i f (1 #i n c l u d e < conio .h >

3 4 5 6 7 8 9 10 11

enum Zustaende { start , und , sharp , eins , neun , sechs , zwei , eins_a , vier , zwei_a , null , drei , acht , vier_a , sechs_a , fuenf , zwei_b }; i n t main (){ c h a r ch ; i n t nrZustand = start ;

12 13 14 15 16 17 18 19 20 21 22 23

w h i l e (1){ ch = getch (); s w i t c h ( nrZustand ) { ... weitere Zustände analog zwei_a ... c a s e zwei_a : s w i t c h ( ch ){ c a s e ’0 ’: nrZustand = null ; break ; c a s e ’3 ’:

350

24 Lösung ausgewählter Übungsaufgaben

24 25 26 27 28 29 30 31 32 33 34 35 36 37

}

38 39

nrZustand = drei ; break ; c a s e ’8 ’: nrZustand = acht ; break ; default: printf ( " % c " , ch ); nrZustand = start ; break ; } break ; ... weitere Zustände analog zwei_a ... } i f ( ch == ’\ r ’) printf ( " \ n " ); /∗ wenn \ r a u c h \ n ∗/

}

40

Lösung 13.1

1

#i n c l u d e < stdio .h >

2 3 4 5 6 7

i n t main () { l o n g i n t vl1 [3] , vl2 [3] , lScalp , i ; /∗ E i n l e s e n v o n v l 1 und v l 2 ∗/ for ( i =0; i

2 3 4 5 6 7

i n t main (){ i n t nVek [] = { 0 , -1 , -2 , -1 , 4 , -2 , -1 , -2 , 3 , 1 }; i n t curMin = nVek [0]; i n t curAnz = 1; i n t i;

8

f o r ( i =1; i < s i z e o f ( nVek )/ s i z e o f ( i n t ); i ++) i f ( nVek [ i ] < curMin ){ curMin = nVek [ i ]; curAnz = 1; } e l s e i f ( nVek [ i ]== curMin ){ curAnz ++; }

9 10 11 12 13 14 15 16

printf ( " Min =% d , % d mal aufgetreten \ n " , curMin , curAnz ); r e t u r n 0;

17 18 19

}

Lösung 13.4

(siehe Seite 205)

a) 1

#i n c l u d e < stdio .h >

2 3

#d e f i n e N 5

4 5 6 7

i n t main (){ i n t t [ N ]; i n t n;

8 9 10 11 12 13

printf ( " Fu ell st an ds ta be ll e eingeben > " ); f o r ( n =0; n < N ; n ++){ printf ( " Menge bei n =% d : " , n ); scanf ( " % d " , & t [ n ]); }

14 15 16 17 18 19 20

n =0; w h i l e (1){ printf ( " Ganzzahligen " ); printf ( " Fuellstand 0 " ); f o r ( i =0; i < N ; i ++){ printf ( " Menge bei h =% d .0: " , i ); scanf ( " % d " , & t [ i ]); }

10 11 12 13 14 15

h =0.0; w h i l e (1){ printf ( " Fuellstand 0.0 " ); scanf ( " % s " , key );

(siehe Seite 206)

354

24 Lösung ausgewählter Übungsaufgaben w h i l e (1){ printf ( " Text eingeben > " ); scanf ( " % s " , text );

10 11 12 13

i f ( strcmp ( text , " stop " )==0) b r e a k ;

14 15

f o r ( iText =0 , iKey =0; text [ iText ]!= ’ \0 ’; iText ++){ i f ( key [ iKey ]== ’ \0 ’) iKey =0; krypt [ iText ] = text [ iText ] ^ ( key [ iKey ] & 0 xf ); } krypt [ iText ]= ’ \0 ’;

16 17 18 19 20 21

printf ( " verkryptet :% s \ n " , krypt ); } r e t u r n 0;

22 23 24 25

}

Lösung 13.6 1

(siehe Seite 206)

#i n c l u d e < stdio .h >

2 3

#d e f i n e N 3

4 5 6 7

i n t main () { d o u b l e mat1 [ N ][ N ] , mat2 [ N ][ N ] , matres [ N ][ N ]; i n t z, s, i;

8 9 10 11 12 13 14 15

/∗ E i n l e s e n ∗/ f o r ( z =0; z < N ; z ++) { printf ( " Matrix 1 , Zeile %d > " , z +1); f o r ( s =0; s < N ; s ++) scanf ( " % lf " , & mat1 [ z ][ s ]); printf ( " Matrix 2 , Zeile %d > " , z +1); f o r ( s =0; s < N ; s ++) scanf ( " % lf " , & mat2 [ z ][ s ]); }

16 17 18 19 20 21 22 23 24 25

/∗ M u l t i p l i k a t i o n ∗/ f o r ( z =0; z < N ; z ++) { f o r ( s =0; s < N ; s ++) { matres [ z ][ s ]=0.0; f o r ( i =0; i < N ; i ++) { matres [ z ][ s ] += mat1 [ z ][ i ]* mat2 [ i ][ s ]; } } }

26 27

/∗ A u s g a b e ∗/

24 Lösung ausgewählter Übungsaufgaben

355

printf ( " Produktmatrix : \ n " ); f o r ( z =0; z < N ; z ++) { f o r ( s =0; s < N ; s ++) printf ( " % g " , matres [ z ][ s ]); printf ( " \ n " ); }

28 29 30 31 32 33

r e t u r n 0;

34 35

}

Lösung 14.2

1 2

(siehe Seite 217)

# include < stdio .h > # include < string .h >

3 4 5 6 7 8

int main (){ // D e k l a r a t i o n d e s V e k t o r s m i t d e n Namen d e r Kunden i n t AnzEingelesen ; i n t i; c h a r Namen [1000][24];

9 10 11

// D e k l a r a t i o n e i n e s g e e i g n e t e n Z ä h l −V e k t o r s int Zaehl [255]; // mögliche ACII - Codes für 1. Buchst .

12 13 14 15 16

// I n i t i a l i s i e r u n g d e s Z ä h l −V e k t o r s for ( i =0; i

3 4 5 6 7 8 9

i n t main () { i n t Zahl [10]; c h a r StrZahl [10][4]; c h a r st [4]; i n t i, j, t;

10 11 12

// E i n l e s e n Z a h l e n n u m e r i s c h f o r ( i =0; i #i n c l u d e < stdlib .h > #i n c l u d e < math .h >

4 5 6 7 8

#d e f i n e #d e f i n e #d e f i n e #d e f i n e

ZZANZ 10000 HFANZ 25 VON 0 BIS 999

9 10

#d e f i n e SEITENBREITE 80

11 12 13 14

#d e f i n e MUEEXPO 300 #d e f i n e MUENORMAL 500 #d e f i n e SIGMANORMAL 150

15 16 17 18 19

i n t main (){ d o u b l e PI = 4.0 * atan (1.0); // Z ä h l v e k t o r e n

360

20 21 22 23 24 25 26 27

24 Lösung ausgewählter Übungsaufgaben i n t gleich [ HFANZ ] , expo [ HFANZ ] , normal [ HFANZ ]; i n t i, j; // S c h l e i f e n z ä h l e r i n t breite ; // B r e i t e e i n e s I n t e r v a l l s i n t zz ; // g e z o g e n e Z u f a l l s z a h l i n t index ; // i n d e x d e s I n t e r v a l l s im Z ä h l v e k t o r i n t hfMax ; // max H ä u f i g k e i t i n e i n e m Z ä h l v e k t o r i n t anzSterne ; // Anz S t e r n e i n d e r Z e i l e d o u b l e d1 , d2 ; // H i l f s g r ö ß e n f ü r N o r m a l v e r t e i l u n g

28 29

breite = ( BIS - VON +1)/ HFANZ ;

30 31 32 33 34

// Z ä h l e r i n i t i a l i s i e r e n f o r ( i =0; i < HFANZ ; i ++){ gleich [ i ] = expo [ i ] = normal [ i ] = 0; }

35 36 37

// H ä u f i g k e i t e n e r m i t t e l n f o r ( i =0; i < ZZANZ ; i ++){

38

// G l e i c h v e r t e i l u n g zz = rand ()%1000; index = ( i n t )( zz / breite ); i f ( index >=0 && index < HFANZ ) gleich [ index ]++;

39 40 41 42 43

// E x p o n e n t i a l v e r t e i l u n g zz = -( log (1 -(( d o u b l e ) rand ()/ RAND_MAX )))* MUEEXPO ; index = ( i n t )( zz / breite ); i f ( index >=0 && index < HFANZ ) expo [ index ]++;

44 45 46 47 48

// d1 d2 zz

Normalverteilung = (( d o u b l e ) rand ())/ RAND_MAX ; = (( d o u b l e ) rand ())/ RAND_MAX ; = MUENORMAL + SIGMANORMAL * sqrt ( -2.0* log ( d1 )) * cos (2.0* PI * d2 ); index = ( i n t )( zz / breite ); i f ( index >=0 && index < HFANZ ) normal [ index ]++;

49 50 51 52 53 54 55 56

}

57 58 59 60 61 62

// A u s g a b e a l s Z a h l e n f o r ( i =0; i < HFANZ ; i ++){ printf ( " %5 d %5 d %5 d \ n " , gleich [ i ] , expo [ i ] , normal [ i ]); }

63 64 65 66

// G l e i c h v e r t e i l u n g : A u s g a b e a l s B a l k e n printf ( " \ n \ nG leich vertei lung \ n " ); hfMax = gleich [0];

24 Lösung ausgewählter Übungsaufgaben

361

f o r ( i =1; i < HFANZ ; i ++){ // Maximum e r m i t t e l n i f ( hfMax < gleich [ i ]) hfMax = gleich [ i ]; } f o r ( i =0; i < HFANZ ; i ++){ // S t e r n e a u s g e b e n anzSterne = gleich [ i ]* SEITENBREITE / hfMax ; f o r ( j =0; j < anzSterne ; j ++) printf ( " * " ); printf ( " \ n " ); }

67 68 69 70 71 72 73 74 75

// E x p o n e n t i a l v e r t e i l u n g : A u s g a b e a l s B a l k e n printf ( " \ n \ n E x p o n e n t i a l v e r t e i l u n g \ n " ); hfMax = expo [0]; f o r ( i =1; i < HFANZ ; i ++){ // Maximum e r m i t t e l n i f ( hfMax < expo [ i ]) hfMax = expo [ i ]; } f o r ( i =0; i < HFANZ ; i ++){ // S t e r n e a u s g e b e n anzSterne = expo [ i ]* SEITENBREITE / hfMax ; f o r ( j =0; j < anzSterne ; j ++) printf ( " * " ); printf ( " \ n " ); }

76 77 78 79 80 81 82 83 84 85 86 87

// N o r m a l v e r t e i l u n g : A u s g a b e a l s B a l k e n printf ( " \ n \ nN ormal vertei lung \ n " ); hfMax = normal [0]; f o r ( i =1; i < HFANZ ; i ++){ // Maximum e r m i t t e l n i f ( hfMax < normal [ i ]) hfMax = normal [ i ]; } f o r ( i =0; i < HFANZ ; i ++){ // S t e r n e a u s g e b e n anzSterne = normal [ i ]* SEITENBREITE / hfMax ; f o r ( j =0; j < anzSterne ; j ++) printf ( " * " ); printf ( " \ n " ); }

88 89 90 91 92 93 94 95 96 97 98 99

r e t u r n 0;

100 101

}

Lösung 14.9 1 2 3 4 5 6 7 8

... #d e f i n e #d e f i n e #d e f i n e #d e f i n e ... #d e f i n e #d e f i n e

(siehe Seite 222)

ZZANZ 1000 HFANZ 25 VON 0 BIS 100 MUENORMAL 50 SIGMANORMAL 15

362

9 10 11 12 13 14 15 16 17 18 19 20 21

24 Lösung ausgewählter Übungsaufgaben

... // H ä u f i g k e i t e n e r m i t t e l n f o r ( i =0; i < ZZANZ ; i ++){ // N o r m a l v e r t e i l u n g d1 = (( d o u b l e ) rand ())/ RAND_MAX ; d2 = (( d o u b l e ) rand ())/ RAND_MAX ; zz = MUENORMAL + SIGMANORMAL * sqrt ( -2.0* log ( d1 )) * cos (2.0* PI * d2 ); // B r ö t c h e n a u s s o r t i e r e n i f ( zz

4 5 6 7

(siehe Seite 225)

#d e f i n e MAX _ZEILE N_LAEN GE 256 #d e f i n e MAX_ZEILEN_ANZ 128

(siehe Seite 246)

24 Lösung ausgewählter Übungsaufgaben

8 9 10 11 12 13

363

i n t main () { i n t nGelesen , i , j ; c h a r * vekZeilen [ MAX_ZEILEN_ANZ ]; i n t vekZaehl [ MAX_ZEILEN_ANZ ]; c h a r sPuffer [ M AX_ZEI LEN_LA ENGE ];

14

f o r ( i =0; i < MAX_ZEILEN_ANZ ; i ++) vekZaehl [ i ]=1;

15 16

nGelesen =0;

17 18

f o r ( i =0; i < MAX_ZEILEN_ANZ ; i ++){ gets ( sPuffer ); i f ( strcmp ( sPuffer , " stop " )==0) b r e a k ; f o r ( j =0; j < nGelesen ; j ++){ i f ( strcmp ( vekZeilen [ j ] , sPuffer )==0){ vekZaehl [ j ]++; break ; } } i f ( j == nGelesen ){ // n i c h t g e f u n d e n i f ( nGelesen

2 3 4

#d e f i n e MAXSTELLEN 10 c h a r ziffern [ MAXSTELLEN ];

5 6 7

v o i d printdez ( i n t d ){ i n t i;

(siehe Seite 264)

364

24 Lösung ausgewählter Übungsaufgaben i f (d 0; i - -) i f ( ziffern [ i ]!= ’0 ’) b r e a k ;

17 18 19 20 21

// A u s g a b e v o n h i n t e n // z i f f e r n [ i ] b i s z i f f e r n [ 0 ] f o r (; i >=0; i - -) putchar ( ziffern [ i ]);

22 23 24 25 26

}

27 28 29 30 31 32 33 34

// t e s t i n t main (){ printdez (0); putchar ( ’\ n ’ ); printdez (12345); putchar ( ’\ n ’ ); printdez ( -1234567890); putchar ( ’\ n ’ ); r e t u r n 0; }

Lösung 17.2 vor p: abcde= 1 in p: abcde= 11 nach p: abcde= 11

(siehe Seite 264) 2 12 2

3 14 3

4 14 14

15

Lösung 17.3 1 2 3 4

d o u b l e func1 ( d o u b l e x ) { r e t u r n sin ( x ) + cos ( x ); }

(siehe Seite 265)

24 Lösung ausgewählter Übungsaufgaben Lösung 17.4 1 2 3 4

365 (siehe Seite 265)

d o u b l e abs ( d o u b l e x ) { r e t u r n x > 0 ? x : -x ; }

Lösung 17.5

(siehe Seite 265)

Die Variable max wird mit dem ersten wert des Felds initialisiert, dafür läuft die Schleife erst ab dem zweiten Element. Auf eine Überprüfung, ob das Feld überhaupt so viele Elemente hat, wurde hier verzichtet. 1 2 3 4 5 6 7 8 9

i n t findemax ( i n t *v , i n t anz ) { i n t max = v [0]; i n t i; f o r ( i = 1; i < anz ; i ++) { i f ( v [ i ] > max ) max = v [ i ]; } r e t u r n max ; }

Lösung 17.6 1 2 3 4 5 6 7 8

d o u b l e skalp ( d o u b l e *v , d o u b l e *w , i n t n ) { d o u b l e result = 0; i n t i; f o r ( i = 0; i < n ; i ++) { result += v [ i ] * w [ i ]; } r e t u r n result ; }

(siehe Seite 265)

366

24 Lösung ausgewählter Übungsaufgaben

Lösung 18.1 1 2

(siehe Seite 275)

#i n c l u d e " gps . h " #i n c l u d e " WinAdapt . h "

3 4 5

#d e f i n e X 0 #d e f i n e Y 1

6 7 8 9 10

void void void void

VtlInit ( v o i d ) VtlMouse ( i n t x , i n t y ) VtlKeyHit ( i n t key ) VtlZyk ( void )

{} {} {} {}

11 12 13 14 15 16

v o i d VtlPaint ( i n t xl , i n t yo , i n t xr , i n t yu ) { d o u b l e xMin =6 , xMax =15 , yMin =47 , yMax =55; i n t nX , nY , nXAlt , nYAlt ; d o u b l e faktorX , faktorY , faktorXY ; i n t i;

17

faktorX = ( xr - xl )/( xMax - xMin ); faktorY = ( yo - yu )/( yMax - yMin ); faktorX = copysign ( fmin ( fabs ( faktorX ) , fabs ( faktorY )) , faktorX ); faktorY = copysign ( fmin ( fabs ( faktorX ) , fabs ( faktorY )) , faktorY );

18 19 20 21 22 23 24

nXAlt = ( koord [0][ X ] - xMin )* faktorX + xl ; nYAlt = ( koord [0][ Y ] - yMin )* faktorY + yu ;

25 26 27

f o r ( i =1; i < MAX_KOORD ; i ++) { nX = ( koord [ i ][ X ] - xMin )* faktorX + xl ; nY = ( koord [ i ][ Y ] - yMin )* faktorY + yu ; Line ( nXAlt , nYAlt , nX , nY ); nXAlt = nX ; nYAlt = nY ; }

28 29 30 31 32 33 34 35

}

36

Lösung 18.2 1 2

#i n c l u d e " WinAdapt . h " #i n c l u d e < math .h >

3 4 5

#d e f i n e ANZ _STUET ZSTELL EN 500 c o n s t d o u b l e pi =3.14;

(siehe Seite 278)

24 Lösung ausgewählter Übungsaufgaben

6 7 8 9 10

void void void void

VtlInit ( v o i d ){} VtlMouse ( i n t X , i n t Y ){} VtlKeyHit ( i n t key ){} VtlZyk ( v o i d ){}

11 12 13

d o u b l e xquadrat ( d o u b l e x ) { r e t u r n x * x ; } d o u b l e sinxquadrat ( d o u b l e x ){ r e t u r n sin ( x * x ); }

14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35

v o i d PlotFunc ( d o u b l e Func ( d o u b l e ) , d o u b l e xMin , d o u b l e xMax , d o u b l e yMin , d o u b l e yMax , i n t xl , i n t yo , i n t xr , i n t yu ) { d o u b l e w =( xr - xl ) , h =( yu - yo ) , x , y ; i n t nX , nY ; d o u b l e xAlt , yAlt ; i n t i; x = xMin ; y = Func ( xMin ); xAlt =( x - xMin )*( xr - xl )/( xMax - xMin )+ xl ; yAlt =( y - yMin )*( yo - yu )/( yMax - yMin )+ yu ; f o r ( x = xMin +( xMax - xMin )/ ANZ_STUETZSTELLEN , i =1; i #i n c l u d e " wgxf . h "

4 5

#d e f i n e ANZ _STUET ZSTELL EN 500

6 7

d o u b l e pi =3.14;

8 9 10 11 12

void void void void

VtlInit ( v o i d ){ } VtlMouse ( i n t X , i n t Y ){} VtlKeyHit ( i n t key ){} VtlZyk ( v o i d ){}

13 14 15

d o u b l e xquadrat ( d o u b l e x ) { r e t u r n x * x ; } d o u b l e sinxquadrat ( d o u b l e x ){ r e t u r n sin ( x * x ); }

16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36

v o i d PlotFunc ( d o u b l e Func ( d o u b l e ) , d o u b l e dXWMin , d o u b l e dXWMax , d o u b l e dYWMin , d o u b l e dYWMax , i n t xl , i n t yo , i n t xr , i n t yu ) { d o u b l e dDeltaX = ( dXWMax - dXWMin )/ A NZ_ST UETZST ELLEN ; i n t nXG , nYG , nXGAlt , nYGAlt ; d o u b l e dXW , dYW ; /∗ T r a n s f o r m a t i o n e i n s t e l l e n ∗/ WGXfSet ( dXWMin , dYWMin , dXWMax , dYWMax , ( d o u b l e ) xl , ( d o u b l e ) yu , ( d o u b l e ) xr , ( d o u b l e ) yo ); /∗ A n f a n g s p u n k t ∗/ WGXf ( dXWMin , Func ( dXWMin ) , & nXGAlt , & nYGAlt ); /∗ S t ü t z p u n k t e ∗/ f o r ( dXW = dXWMin + dDeltaX ; dXW 0) { dDeltaXG = ( xr - xl )/( d o u b l e ) nIntervalle ; dDeltaXW = ( dXWMax - dXWMin )/ nIntervalle ; f o r ( i =0 , dXW = dXWMin ; i 0) { dDeltaYG = ( yo - yu )/( d o u b l e ) nIntervalle ; dDeltaYW = ( dYWMax - dYWMin )/ nIntervalle ; f o r ( i =0 , dYW = dYWMin ;

370

24 Lösung ausgewählter Übungsaufgaben i #i n c l u d e < stdio .h > #i n c l u d e < stdlib .h >

4 5 6

#d e f i n e DIMENS 6 #d e f i n e UMX ULONG_MAX

7 8 9 10

// findemin siehe Seite 318 u n s i g n e d l o n g findemin ( u n s i g n e d l o n g * mark , u n s i g n e d l o n g * dist , u n s i g n e d l o n g dimens );

11 12 13 14 15 16 17 18 19

u n s i g n e d l o n g C [ DIMENS ][ DIMENS ] = /∗ a==0 ∗/ { UMX , 9 , UMX , UMX , /∗ b==1 ∗/ { 9 , UMX , 3, 4, /∗ c==2 ∗/ { UMX , 3 , UMX , 6, /∗ d==3 ∗/ { UMX , 4, 6 , UMX , /∗ s==4 ∗/ { UMX , 3, 2 , UMX , /∗ z==5 ∗/ { 4 , UMX , UMX , 3, };

{ UMX , 3, 2, UMX , UMX , UMX ,

4} , UMX } , UMX } , 3} , UMX } , UMX }

20 21 22

c h a r knotenbez [ DIMENS ] = { ’a ’ , ’b ’ , ’c ’ , ’d ’ , ’s ’ , ’z ’ }; u n s i g n e d l o n g s = 4 , z = 5;

23 24 25 26

u n s i g n e d l o n g mark [ DIMENS ]; u n s i g n e d l o n g dist [ DIMENS ]; u n s i g n e d l o n g vor [ DIMENS ];

27 28 29 30 31 32 33 34 35 36 37 38 39 40 41

i n t main () { u n s i g n e d l o n g k , u , v , dstrich ; // I n i t i a l i s i e r u n g f o r ( k =0; k < DIMENS ; k ++) { i f ( k != s ) { mark [ k ] = 0; vor [ k ] = s ; dist [ k ] = C [ s ][ k ]; } else { mark [ k ] = 1; vor [ k ] = s ; dist [ k ] = 0; } }

42 43 44 45

// s o l a n g e e s n o c h u n m a r k i e r t e K n o t e n g i b t w h i l e (( u = findemin ( mark , dist , DIMENS )) < UMX ) { mark [ u ] = 1;

24 Lösung ausgewählter Übungsaufgaben

373

// f ü r a l l e K n o t e n v , d i e m i t u v e r b u n d e n s i n d f o r ( v =0; v < DIMENS ; v ++) { i f ( C [ u ][ v ] < UMX ) { dstrich = dist [ u ] + C [ u ][ v ]; i f ( dstrich < dist [ v ]) { mark [ v ] = 0; vor [ v ] = u ; dist [ v ] = dstrich ; } } }

46 47 48 49 50 51 52 53 54 55 56

}

57 58

// A u s g a b e d u r c h Z u r ü c k v e r f o l g u n g printf ( " Kürzester Weg \ nLänge : % lu \ n " , dist [ z ]); k = z; w h i l e ( k != s ) { printf ( " % c " , knotenbez [ k ]); k = vor [ k ]; printf ( "