ISBN: 3-446-15497-3

Text
                    PC
professionell
Arbeitsplatzrechner in
Ausbildung und Praxis
.Programmieren in C" ist die authentische Ober-
Obersetzung des außerordentlich erfolgreichen Stan-
Standardwerkes. Der vorliegenden 2. Ausgabe ist der
neue ANSI-Standard zugrunde gefegt Die Autoren
Kernighan und Ritchie haben die Programmier-
Programmiersprache .C" erfunden und entwickelt.
Dieses Buch führt leicht verständficb in die Anwen-
Anwendung der Programmiersprache C ein. Zahlreiche
Beispiele, die meist vollständige Programme
darstellen, und praktische Übungsaufgaben
fördern das Verständnis und die unmittelbare
Anwendung von C. Die zweite Ausgabe von
.Programmieren in C" ist unentbehrlich für alle
Umsteiger von der ersten Ausgabe auf den neuen
Standard ANSI-C.


PC ressionell Arbeitsplatzrechner in Ausbildung und Praxis Kernighan/Ritchie Programmier Mit dem C-Reference Manual in deutscher Sprache Zweite Ausgabe ANSIC
PC professionell Arbeitsplatzrechner in Herausgeber: Ausbildung und Praxis Prof. Dr.-Ing. Werner Heinzel, Fulda PC professionell zeigt dem Anwender von Personal Computern den Aufbau, die Program- Programmierung und die verschiedenen Anwendungsmöglichkeiten seines „persönlichen Rechners am Arbeitsplatz" und führt ihn am Beispiel aktueller PC-Systeme oder mit der Darstellung und Diskussion von Fallbeispielen in die betriebliche Praxis derartiger Geräte ein. Personalcomputer werden in den verschiedensten Anwendungsbereichen eingesetzt: - in Klein- und Mittelbetrieben - als persönliches Arbeitsmittel in Fachabteilungen von Großbetrieben - als Ausbildungsmittel in Schulen, Hochschulen und Fortbildungsstätten - im privaten Bereich PC professionell wendet sich an diese verschiedenen Benutzer des Personal Computers. Aufgrund des didaktischen Aufbaus und der eingearbeiteten Praxisbeispiele eignen sich die Bände zum Selbststudium. Die Fachbuchreihe weist die folgenden thematischen Schwerpunkte auf: Grundlagen der Herd- und Softwaretechnologie von Personal Computern, zum Beispiel - Systemstrukturen - Betriebssysteme - Programmiersprachen - Softwarewerkzeuge Realisierte Systeme und deren Anwendungen, zum Beispiel - Benutzeranleitung für bestimmte PC-Systeme - Programmierung typischer Systeme - Anwendungen, Fallbeispiele, Softwaresammlungen - Softwarelösungen für bestimmte Branchen bzw. Anwendungsgebiete Carl Hanser Verlag München Wien
Brian W. Kernighan Dennis N/TRitchie Programmieren in C Mit dem C-Reference Manual in deutscher Sprache Zweite Ausgabe ANSIC Die deutsche Ausgabe besorgten Prof. Dr. A.T. Schreiner und Dr. Ernst Janich Eine Coedition der Verlage Carl Hanser und Prentice-Hall International
Titel der amerikanischen Originalausgabe: "The C Programming Language, Second Edition, ANSI C" by Brian W. Kernighan and Dennis M. Ritchie © Copyright 1988, 1978 by Bell Telephone Laboratories, Incorporated. Original English language edition published by Prentice-Hall International, a Division of Simon & Schuster Englewood Cliffs, New Jersey 07632 All rights reserved. No part of this publication may be reproduced, stored in a retrieval system, or transmitted, in any form or by any means, electronic, mechanical, photocopying, recording, or otherwise, without the prior written permission of the publisher. UNIX™ ist ein eingetragenes Warenzeichen der AT&T Bell Laboratories. Alle in diesem Buch enthaltenen Programme und Verfahren wurden nach bestem Wissen erstellt und mit Sorgfalt getestet. Dennoch sind Fehler nicht ganz auszuschließen. Aus diesem Grund ist das im vorliegenden Buch enthaltene Programm-Material mit keiner Verpflichtung oder Garantie irgend- irgendeiner Art verbunden. Autor und Verlag übernehmen infolgedessen keine Verantwortung und werden keine daraus folgende oder sonstige Haftung übernehmen, die auf irgendeine Art aus der Benutzung dieses Programm-Materials oder Teilen davon entsteht. ClP-Titelaufnahme der Deutschen Bibliothek Kernighan, Brian W.: Programmieren in C : mit dem C-reference-Manual in deutscher Sprache / Brian W. Kernighan ; Dennis M. Ritchie. Die dt. Ausg. besorgten A. T. Schreiner u. Ernst Janich. - 2. Ausg., ANSI C. - München ; Wien : Hanser ; London : Prentice-Hall Internat., 1990 (PC professionell) Einheitssacht.: The C-programming-language (dt.) Erg. bildet: Tondo, Clovis L.: Das C-Lösungsbuch ISBN 3-446-15497-3 (Hanser) ISBN 0-13-11O33O-X (Prentice-Hall Internat.) NE: Ritchie, Dennis M.: Dieses Werk ist urheberrechtlich geschützt. Alle Rechte, auch die der Übersetzung, des Nachdrucks und der Vervielfältigung des Buches, oder Teilen daraus, vorbehalten. Kein Teil des Werkes darf ohne schriftliche Genehmigung des Verlages in irgendeiner Form (Fotokopie, Mikrofilm oder ein anderes Verfahren), auch nicht für Zwecke der Unterrichtsgestaltung, reproduziert oder unter Verwendung elektronischer Systeme verarbeitet, vervielfältigt oder verbreitet werden. Eine Coedition der Verlage: Carl Hanser Verlag München Wien Prentice-Hall International Inc., London © 1990 Prentice-Hall International Inc., London Umschlagkonzeption: Hans Peter Willberg Druck: Druckerei Appl, Wemding Buchbinderische Verarbeitung: Ludwig Auer, Donauwörth © am Layout: Carl Hanser Verlag München Wien Printed in Germany
Vorwort Seit der Veröffentlichung von Programmieren in C im Jahr 1978 hat in der Welt der Computer eine Revolution stattgefunden. Große Computer sind viel größer und Perso- Personal-Computer leisten ähnlich viel wie die Zentralrechner vor zehn Jahren. Auch C hat sich in der Zwischenzeit geändert, allerdings nur in bescheidenem Maße, und es hat sich weit über seine Anfänge als Sprache des UNIX-Betriebssystems hinaus verbreitet. Die zunehmende Popularität von C, die Änderungen an der Sprache im Lauf der Jahre und die Entwicklung von Übersetzern durch Gruppen, die nicht am Sprachentwurf beteiligt waren, haben gemeinsam die Notwendigkeit einer präziseren und zeitgemäßeren Sprachdefinition ergeben, als sie die Erste Ausgabe dieses Buches enthalten hat. 1983 richtete das American National Standards Institute (ANSI) eine Kommission mit dem Ziel ein, „eine eindeutige und maschinenunabhängige Definition der Sprache C" zu erarbei- erarbeiten, die aber den Geist der Sprache erhalten sollte. Das Resultat ist der ANSI-Standard fürC. Der Standard formalisiert Konstruktionen, auf die zwar in der (englischen) Ersten Ausgabe hingedeutet wurde, die aber dort nicht beschrieben wurden, insbesondere Strukturzuweisungen und Aufzählungstypen. Der Standard enthält eine neue Art von Funktionsdeklaration, die die Überprüfung von Definition und Benutzung ermöglicht. Er legt eine Standard-Bibliothek fest, mit einem ausführlichen Satz von Funktionen für Eingabe und Ausgabe, Speicherverwaltung, Operationen mit Zeichenketten und für ähn- ähnliche Aufgaben. Der Standard präzisiert das Verhalten von Konzepten, die in der ur- ursprünglichen Definition nicht detailliert wurden, und stellt gleichzeitig klar heraus, wel- welche Aspekte der Sprache maschinenabhängig bleiben. Die vorliegende zweite Ausgabe von Programmieren in C beschreibt C nach der Definition im ANSI-Standard. Wir haben zwar die Stellen gekennzeichnet, wo sich die Sprache weiterentwickelt hat, aber wir haben uns auch entschieden, ausschließlich im neuen Stil zu schreiben. Im großen und ganzen sollte das keinen wesentlichen Unter- Unterschied machen; die sichtbarste Änderung ist die neue Form der Deklaration und Definiti- Definition von Funktionen. Moderne Übersetzer unterstützen schon die meisten Konzepte des Standards. Wir haben versucht, die Kürze der Ersten Ausgabe beizubehalten. C ist keine um- umfangreiche Sprache, und ein dickes Buch wird ihr nicht sehr gerecht. Wir haben die Prä- Präsentation besonders wichtiger Konzepte verbessert, wie zum Beispiel von Zeigern, denn sie bilden den Kern der C-Programmierung. Wir haben die ursprünglichen Beispiele verfeinert und haben in mehreren Kapiteln neue hinzugefügt. Zum Beispiel ist jetzt die Abhandlung über komplizierte Vereinbarungen durch Programme erweitert, die Verein- Vereinbarungen in lesbaren (deutschen) Text verwandeln und umgekehrt. Der Text dieses Bu- Buches liegt maschinenlesbar vor; wie früher wurden alle Beispiele direkt aus dem Text ge- getestet. Anhang A, die C-Sprachbeschreibung, ist nicht der Standard, sondern unser Ver- Versuch, die wesentlichen Punkte des Standards auf kleinerem Raum zu vermitteln. Er soll für Programmierer leichtverständlich sein, aber nicht als Definition für die Konstrukteure von Übersetzern dienen - diese Rolle gebührt dem Standard selbst. Anhang B ist eine
vi Vorwort Zusammenfassung der Standard-Bibliothek. Auch dieser Anhang soll zum Nachschlagen für Programmierer sein, nicht für Implementierer. Anhang C ist eine knappe Zusam- Zusammenfassung der Änderungen des Standards gegenüber der ursprünglichen Definition von C. Wie wir im Vorwort zur Ersten Ausgabe schrieben, „bewährt sich [C] mit steigen- steigender Erfahrung". Mit einem Jahrzehnt mehr an Erfahrung denken wir immer noch so. Wir hoffen, daß Ihnen dieses Buch hilft, C zu lernen und gut zu benutzen. Wir schulden großen Dank den Freunden, die uns halfen, diese zweite Ausgabe zu produzieren. Jon Bentley, Doug Gwyn, Doug Mcllroy, Peter Nelson und Rob Pike gaben uns durchdachte Kommentare zu nahezu jeder Seite von Entwürfen. Für sorgfältiges Le- Lesen danken wir Al Aho, Dennis Allison, Joe Campbell, G. R. Emiin, Karen Fortgang, Al- Allen Holub, Andrew Hume, Dave Kristol, John Linderman, Dave Prosser, Gene Spafford und Chris Van Wyk. Hilfreiche Vorschläge machten Bill Cheswick, Mark Kernighan, An- Andy Koenig, Robin Lake, Tom London, Jim Reeds, Clovis Tondo und Peter Weinberger. Dave Prosser beantwortete viele detaillierte Fragen zum ANSI-Standard. Wir benutzten Bjarne Stroustrup's C++ Übersetzer ausgiebig für lokale Tests unserer Programme und Dave Kristol stellte uns einen ANSI-C Übersetzer zum abschließenden Test zur Verfü- Verfügung. Rieh Drechsler half sehr bei der Drucklegung. Wir danken allen sehr. Brian W. Kernighan Dennis M. Ritchie
Vorwort zur Ersten Ausgabe C ist eine Programmiersprache für allgemeine Anwendungen mit einfacher Aus- Ausdrucksweise, modernen Kontroll- und Datenstrukturen und einer reichen Auswahl an Operatoren. C ist weder hochabstrahierend, noch umfangreich und wurde auch nicht für ein spezielles Anwendungsgebiet entworfen. Aber das Fehlen von Einschränkungen und die allgemeine Verwendbarkeit machen C bequemer und effektiver für viele Aufgaben als angeblich mächtigere Programmiersprachen. C wurde ursprünglich entworfen und implementiert für das UNIX-Betriebssystem auf der DEC PDP-11 von Dennis Ritchie. Das Betriebssystem, der C-Übersetzer und praktisch alle UNIX-Anwendungsprogramme (insbesondere auch alle Programme, die zur Drucklegung dieses Buches verwendet wurden) sind in C geschrieben. Übersetzer für Produktionszwecke existieren für verschiedene andere Maschinen, darunter das IBM Sy- System/370, das Honeywell 6000 System und die Interdata 8/32. C ist jedoch nicht an eine spezielle Hardware- oder Systemkonfiguration gebunden, und es ist leicht, Programme zu schreiben, die unverändert auf jeder Maschine ablaufen können, die C unterstützt. Das vorliegende Buch möchte dem Leser helfen, in C programmieren zu lernen. Es enthält zunächst einen informellen Überblick, damit neue Benutzer so schnell als möglich anfangen können. Danach folgen einzelne Kapitel über die hauptsächlichen Sprachkonzepte und schließlich eine Sprachdefinition. Wir stellen C vor allem durch Le- Lesen, Schreiben und Überarbeiten von Beispielen vor und nicht nur durch die Aufzählung von Regehi. Die Beispiele sind überwiegend komplette, funktionierende Programme und nicht nur isolierte Programmfragmente. Der Text dieses Buches existiert in maschinen- maschinenlesbarer Form; alle Beispiele wurden direkt aus diesem Text getestet. Wir haben nicht nur gezeigt, wie man die Sprache effektiv benutzt; wir haben auch versucht, soweit dies möglich ist, nützliche Algorithmen zu illustrieren sowie die Prinzipien guten Program- Programmierstils und soliden Programmentwurfs. Das Buch ist keine Einführung in das Programmieren; wir gehen davon aus, daß dem Leser einfache Programmierkonzepte - wie Variablen, Zuweisungen, Schleifen und Funktionen - geläufig sind. Ein unerfahrener Programmierer sollte trotzdem aus die- diesem Text C erlernen können; allerdings wäre die Hilfe eines erfahrenen Kollegen nütz- nützlich. Nach unserer Erfahrung hat sich C als freundliche, ausdrucksstarke und flexible Sprache für einen breiten Anwendungsbereich erwiesen. C ist leicht zu erlernen und be- bewährt sich mit steigender Erfahrung. Wir hoffen, daß Ihnen dieses Buch hilft, C gut zu benutzen. Wohlüberlegte Kritik und Vorschläge von vielen Freunden und Kollegen haben zu diesem Buch und unserem eigenen Vergnügen daran beigetragen. Insbesondere Mike Bianchi, Jim Blue, Stu Feldman, Doug Mcllroy, Bill Roome, Bob Rosin und Larry Rosler haben alle mehrere Versionen sorgfältig gelesen. Wir sind auch AI Aho, Steve Bourne, Dan Dvorak, Chuck Haley, Debbie Haley, Marion Harris, Rick Holt, Steve Johnson,
viii Vor : zur Ersten Ausgabe John Mashey, Bob Mitze, Ralph Muha, Peter Nelson, Elliot Pinson, Bill Plauger, Jerry Spivack, Ken Thompson und Peter Weinberger dankbar für hilfreiche Kommentare an verschiedenen Stellen und Mike Lesk und Joe Ossanna für unschätzbare Hilfe bei der Drucklegung. Brian W. Kernighan Dennis M. Ritchie
IX Vorwort zur deutschen Ausgabe Dieses Buch erschien erstmals 1978 in den USA und 1983 in Deutschland. Die Er- Erste Ausgabe galt über zehn Jahre in den USA und über fünf Jahre im deutschen Sprach- Sprachraum als Standard und Definition für C, eine Programmiersprache, die sich keineswegs nur in UNIX-Kreisen sehr großer Beliebtheit erfreut. C wird gerne verwendet, und in C wird gute Software entwickelt, denn für C gibt es von den ursprünglichen Entwicklern diese ausgezeichnete Beschreibung, die nicht nur Vokabeln erklärt, sondern die anhand einer Vielzahl realistischer Beispielprogramme die Prinzipien zweckmäßiger Software- Entwicklung mit C vermittelt. Das American National Standards Institute (ANSI) verabschiedet jetzt einen Stan- Standard für C. Man darf annehmen, daß sich ISO und DIN anschließen werden. Kernighan und Ritchie halten sich in der vorliegenden Zweiten Ausgabe an den neuen ANSI- Standard. Damit gibt es nach wie vor ein Buch über C, in dem vor allem steht, wie sich die Erfinder den Umgang mit ihrem Werkzeug vorstellen. Das Buch eignet sich aber auch für Umsteiger von „K&R-C" auf ansi-C: im Anhang C sind die wichtigsten Ände- Änderungen auf knapp vier Seiten zusammengefaßt, und im Anhang A in der C-Sprachbe- schreibung kommentieren Kernighan und Ritchie leger-bissig die Entwicklungen der letzten zehn Jahre. Wir konnten im allgemeinen sehr wortgetreu übersetzen, mit einer wesentlichen Ausnahme: im Deutschen kann man Deklarationen, die Eigenschaften von Namen ver- vereinbaren, völlig von Definitionen unterscheiden, die dazu auch noch Speicherplatz bereit- bereitstellen, denn für beide Vorgänge zusammen gibt es im Deutschen noch den Oberbegriff der Vereinbarung. Diese Unterscheidung sollte vor allem die C-Sprachbeschreibung im Anhang A leichter verständlich machen. Schon in der Ersten Ausgabe haben wir uns sehr sorgfältig bemüht, in der Überset- Übersetzung einige Begriffe zu prägen: Wir sprechen von Vektoren, damit diese nicht im Kon- Konflikt zu Bit-Feldern stehen, von denen es ja gerade keine Vektoren gibt. Vektoren haben Elemente, die Komponenten überlassen wir den Strukturen. Wir unterscheiden auch sehr sorgfältig die aktuellen Argumente, deren Werte in C an die formalen Parameter von Funktionen übergeben werden. In dieser Zweiten Ausgabe kommen einige neue Begriffe hinzu, und manche Be- Begriffe wurden geändert. Bei der Wahl der Fachausdrücke ließen wir uns von gewissen Kriterien leiten: Wir folgen dem Sprachgebrauch und sprechen jetzt doch von einer Bibliothek. Wir möchten „selbst-verständliche" Worte verwenden und sagen deshalb zum Beispiel statt Literal lieber Konstante. Wir möchten nicht unnötig „neu-deutsch" formu- formulieren und bevorzugen deshalb statt Compiler lieber Übersetzer, statt Header-Datei lieber nach wie vor Definitionsdatei und statt String lieber Zeichenkette. Aus dem Verweisoperator • wurde der Inhaltsoperator, denn Verweis kann man zu leicht mit reference und damit vielleicht mit dem Adreßoperator & verwechseln. Die Ver- Vereinigung verschiedener Typen zu verschiedener Zeit in einem Objekt, kurz union, haben wir von Variante in der Ersten Ausgabe jetzt in Union umgetauft, und ihre Komponenten nennen wir Alternativen. Den Namen einer Struktur bezeichnen wir als Etikett, denn auch eine Union und eine Aufzählung können ein Etikett haben. Die neuen Eigenschaften const und volatile bezeichnen wir als Attribute eines Typs.
Vorwort z" leutschen Ausgabe Insgesamt hat uns bei der Wortwahl sehr stark Siegfried Mahkorn's DIN-Wortliste* beeinflußt. Das Sachverzeichnis am Schluß des vorliegenden Buchs enthält eine Reihe von Querverweisen von englischen Begriffen zu unseren deutschen Übersetzungen. Der Satz dieses Buchs erfolgte natürlich mit einem UNIX-System: wir verwendeten Liangs Trennalgorithmus aus Knuths %X mit deutschen Trennmustern von Norbert Schwarz, ein Programm zur Aufbereitung von C-Programmen, Kernighans Zeichenspra- Zeichensprache pic, Lesks Tabellensetzer tbl, Kernighan und Cherrys mathematischen Prozessor eqn, den für XENIX angepaßten device-independent troff der ELAN Computer Group, ein neues Dialog-Layout-Programm sowie ELANs Postprozessor für SoftFonts auf einem LaserJet+ von Hewlett-Packard. Brian Kernighan hat uns ein Magnetband mit den Originalquellen zur Verfügung gestellt, und er hat uns eine Reihe von Korrekturen über UseNet geschickt. Ulrich Breckel, Ottmar Schuhbauer und Silke Seehusen lasen die C-Sprachbeschreibung, Elke und Peter Albrecht sahen den ganzen Text durch und Mitarbeiter des Carl Hanser Ver- Verlags plagten sich mit den Buchstabier- und Trennungslisten. Wir danken all diesen Hel- Helfern - hierbei auch einem namenlosen, engagierten Lektor - sehr herzlich. Ulm und Hollage, im November 1989 Ernst Janich Axel T. Schreiner * „Zweisprachiges Register von Benennungen aus Normen und Norm-Entwürfen des din" Arbeitspapier 395 der Gesellschaft für Mathematik und Datenverarbeitung mbH, Sankt Augustin, Juni 1989.
Inhaltsverzeichnis Vorwort v Vorwort zur Ersten Ausgabe vii Vorwort zur deutschen Ausgabe ix Einführung 1 1 Eine Übersicht in Beispielen 5 1.1 Erste Schritte 5 1.2 Variablen und Arithmetik 8 1.3 Die for- Anweisung 13 1.4 Symbolische Konstanten 14 1.5 Zeicheneingabe und Ausgabe 15 1.5.1 Dateien kopieren 15 1.5.2 Zeichen zählen 17 1.5.3 Zeilenzahlen 19 1.5.4 Wörterzählen 20 1.6 Vektoren 21 1.7 Funktionen 23 1.8 Argumente - Wertübergabe 26 1.9 Zeichenvektoren 27 1.10 Externe Variablen und Gültigkeitsbereich 30 2 Datentypen, Operatoren und Ausdrucke 35 2.1 Variablennamen 35 2.2 Datentypen und Speicherbedarf 36 2.3 Konstanten 37 2.4 Vereinbarungen 39 2.5 Arithmetische Operatoren 40 2.6 Vergleiche und logische Verknüpfungen 41 2.7 lypumwandlungen 42 2.8 Inkrement- und Dekrement-Operatoren 46 2.9 Bit-Manipulationen 47 2.10 Zuweisungen und Ausdrücke 49 2.11 Bedingter Ausdruck 50 2.12 Vorrang und Reihenfolge bei Bewertungen 51 3 Kontrollstrukturen 55 3.1 Anweisungen und Blöcke 55 3.2 if-else 55 3.3 else-if 56 3.4 switch 58 3.5 Schleifen - while und for 59 3.6 Schleifen - do-while 62 3.7 break und continue 63 3.8 goto und Marken 64
xü Inhaltsverzeichnis 4 Funktionen und Programmstruktur 67 4.1 Grundbegriffe . . . 67 4.2 Funktionen ohne ganzzahliges Resultat 70 4.3 Externe Variablen 72 4.4 Regehi zum Gültigkeitsbereich 78 4.5 Definitionsdateien 80 4.6 static 81 4.7 register 81 4.8 Blockstruktur 82 4.9 Initialisierung 83 4.10 Rekursion 84 4.11 Der C-Preprozessor 86 4.11.1 Definitionsdateien einfügen 86 4.11.2 Textersatz 87 4.11.3 Bedingte Übersetzung 88 5 Zeiger und Vektoren 91 5.1 Zeiger und Adressen 91 5.2 Zeiger und Funktionsargumente 93 5.3 Zeiger und Vektoren 95 5.4 Adreß-Arithmetik 97 5.5 c/iar-Zeiger und Funktionen 101 5.6 Vektoren von Zeigern; Zeiger auf Zeiger 104 5.7 Mehrdimensionale Vektoren 107 5.8 Initialisierung von Zeigervektoren 109 5.9 Zeiger kontra mehrdimensionale Vektoren 109 5.10 Argumente aus der Kommandozeile 110 5.11 Zeiger auf Funktionen 114 5.12 Komplizierte Vereinbarungen 117 6 Strukturen 123 6.1 Die Grundbegriffe 123 6.2 Strukturen und Funktionen 125 6.3 Vektoren von Strukturen 127 6.4 Zeiger auf Strukturen 131 6.5 Rekursive Strukturen 133 6.6 Suchen in Tabellen 138 6.7 typedef 140 6.8 Unionen 141 6.9 Bit-Felder 143 7 Eingabe und Ausgabe 145 7.1 Standard-Eingabe und Standard-Ausgabe 145 7.2 Formatierte Ausgabe - printf 147 7.3 Variable Argumentlisten 149 7.4 Formatierte Eingabe - scanf 150 7.5 Dateizugriff 153
Inhaltsverzeichn: hü 7.6 Fehlerbehandlung - stderr und exit 156 7.7 Zeilen-Eingabe und -Ausgabe 158 7.8 Weitere Funktionen 159 7.8.1 Operationen mit Zeichenketten 159 7.8.2 Tests für Zeichenklassen und Umwandlung 160 7.8.3 ungetc 160 7.8.4 Kommandoausführung 160 7.8.5 Speicherverwaltung 160 7.8.6 Mathematische Funktionen 161 7.8.7 Zufallszahlengenerator 161 8 Die Schnittstelle zum UNIX-Betriebssystem 163 8.1 File-Deskriptoren 163 8.2 Elementare Ein- und Ausgabe - read und write 164 8.3 open, treat, close, unlink 165 8.4 Random-Zugriff - Iseek 168 8.5 Beispiel: Eine Implementierung von fopen und getc 168 8.6 Beispiel: Kataloge ausgeben 172 8.7 Beispiel: Funktionen zur Speicherverwaltung 177 A C-Sprachbeschreibung 183 A.l Einführung 183 A.2 Lexikalische Konventionen 183 A.2.1 Eingabesymbole 183 A.2.2 Kommentare 183 A.2.3 Namen 184 A.2.4 Reservierte Worte 184 A.2.5 Konstanten 184 A.2.6 Konstante Zeichenketten 186 A.3 Syntax-Schreibweise 187 A.4 Die Bedeutung von Namen 187 A.4.1 Speicherklasse 187 A.4.2 Elementare Datentypen 188 A.4.3 Abgeleitete Typen 189 A.4.4 Attribute für Typen 189 A.5 Objekte und L-Werte 189 A.6 Typumwandlungen 189 A.6.1 Integer-Erweiterung 189 A.6.2 Integer-Umwandlung 190 A.6.3 Integer- und Gleitpunktwerte 190 A.6.4 Gleitpunkttypen 190 A.6.5 Arithmetische Umwandlungen 190 A.6.6 Zeiger und Integer-Werte 191 A.6.7 void 192 A.6.8 Zeiger auf void 192
Inhaltsverzeichnis A.7 Ausdrücke 193 A.7.1 Erzeugung von Zeigerwerten 193 A.7.2 Primärausdrücke 194 A.7.3 Postfix-Ausdrücke 194 A.7.4 Unäre Operatoren 197 A.7.5 Typumwandlungen 198 A.7.6 Multiplikative Operatoren 199 A.7.7 Additive Operatoren 199 A.7.8 S/u/t-Operatoren 200 A.7.9 yergleiche 200 A.7.10 Äquivalenzvergleiche 201 A.7.11 UND-Verknüpfung von Bits 201 A.7.12 Exklusive ODER-Verknüpfung von Bits 201 A.7.13 ODER-Verknüpfung von Bits 201 A.7.14 Logische UND-Verknüpfung 202 A.7.15 Logische ODER-Verknüpfung 202 A.7.16 Bedingter Ausdruck 202 A.7.17 Zuweisungen 203 A.7.18 Komma als Operator 203 A.7.19 Konstante Ausdrücke 204 A.8 Vereinbarungen 204 A.8.1 Speicherklassen 205 A.8.2 Typangaben 206 A.8.3 Strukturen und Unionen 207 A.8.4 Aufzählungen 210 A.8.5 Deklaratoren 211 A.8.6 Die Bedeutung von Deklaratoren 211 A.8.7 Initialisierung 215 A.8.8 Typnamen 217 A.8.9 typedef 217 A.8.10 Äquivalenz von Typen 218 A.9 Anweisungen 218 A.9.1 Marken an Anweisungen 219 A.9.2 Ausdruck als Anweisung 219 A.9.3 Block 219 A.9.4 Auswahlanweisungen 220 A.9.5 Wiederholungsanweisungen 220 A.9.6 Sprunganweisungen 221 A.10 Externe Vereinbarungen 222 A.10.1 Funktionsdefinitionen 222 A.10.2 Externe Vereinbarungen 224 A.11 Gültigkeitsbereich und Bindung 225 A.11.1 Gültigkeitsbereich im Text 225 A.11.2 Bindung 226
Inhaltsverzeichn xv A.12 Der Preprozessor 226 A.12.1 Drei-Zeichen-Folgen 227 A.12.2 Verbinden von Zeilen 227 A.12.3 Makrodefinition und Expansion 227 A.12.4 Einfügen von Dateien 229 A.12.5 Bedingte Übersetzung 230 A.12.6 Zeilenkontrolle 231 A.12.7 Fehlermeldungen 231 A.12.8 pragma 231 A.12.9 Leere Anweisung 231 A.12.10 Vordefinierte Namen 232 A.13 Grammatik 232 B Die Standard-Bibliothek 239 B.l Ein-und Ausgabe: <stdio.h> 239 B.l.l Dateioperationen 240 B.1.2 Formatierte Ausgabe 241 B.1.3 Formatierte Eingabe 243 B.1.4 Ein-und Ausgabe von Zeichen 244 B.1.5 Direkte Ein- und Ausgabe 246 B.1.6 Positionieren in Dateien 246 B.1.7 Fehlerbehandlung 247 B.2 Tests für Zeichenklassen: <ctype.h> 247 B.3 Funktionen für Zeichenketten: <string.h> 248 B.4 Mathematische Funktionen: <math.h> 249 B.5 Hilfsfunktionen: <stdlib.h> 251 B.6 Fehlersuche: <assert.h> 253 B.7 Variable Argumentlisten: <stdarg.h> 253 B.8 Globale Sprünge: <setjmp.h> 254 B.9 Signale: <signal.h> 254 B.10 Funktionen für Datum und Uhrzeit: <time.h> 255 B.ll Grenzwerte einer Implementierung: <limits.h> und <float.h> . . . 257 C Änderungen in Kürze . 259 Sachverzeichnis 263
Einführung C ist eine Programmiersprache für allgemeine Anwendungen. C ist eng mit dem UNIX-Betriebssystem verbunden, wo die Sprache entwickelt wurde, denn sowohl das Sy- System selbst, als auch die meisten Programme, die damit eingesetzt werden, sind in C ge- geschrieben. Die Sprache selbst ist jedoch nicht von einem bestimmten Betriebssystem oder einer bestimmten Maschine abhängig. C wurde zwar als „Systemprogrammierspra- „Systemprogrammiersprache" bezeichnet, da die Sprache sich sehr gut zum Schreiben von Übersetzern und Be- Betriebssystemen eignet; wesentliche Programme in vielen anderen Bereichen wurden je- jedoch ebenfalls in C realisiert. Viele der wichtigen Ideen in C stammen von der Sprache BCPL, die Martin Ri- Richards entwickelt hat. BCPL beeinflußte C indirekt durch die Sprache B, die Ken Thomp- Thompson 1970 für das erste UNIX-System auf der DEC PDP-7 implementiert hat. BCPL und B sind „typenlose" Sprachen. Im Gegensatz dazu hat C eine Reihe von Datentypen. Die elementaren Typen sind Zeichen sowie ganze Zahlen und Gleitpunkt- Gleitpunktzahlen verschiedener Größe. Darüber hinaus gibt es eine Hierarchie von abgeleiteten Datentypen, die mit Hilfe von Zeigern, Vektoren, Strukturen und Unionen erzeugt wer- werden. Ausdrücke werden aus Operatoren und Operanden gebildet; jeder Ausdruck, insbe- insbesondere eine Zuweisung und ein Funktionsaufruf, kann eine Anweisung sein. Zeiger er- erlauben maschinenunabhängige Adreß-Arithmetik. C besitzt die fundamentalen Kontrollstrukturen, die für wohlstrukturierte Pro- Programme notwendig sind: Zusammenfassung von Anweisungen, Entscheidungen (if-else), Auswahl von einem aus einer Menge von möglichen Fällen (switch), Schleifen mit Test des Abbruchkriteriums am Anfang (while, for) oder am Ende (do) sowie vorzeitiges Ver- Verlassen einer Schleife (break). Funktionen können Werte der elementaren Typen, aber auch von Strukturen, Unionen oder Zeigern als Resultat liefern. Jede Funktion darf rekursiv aufgerufen wer- werden. Die lokalen Variablen einer Funktion sind typischerweise „automatisch", das heißt, sie werden bei jedem Aufruf der Funktion neu erzeugt. Funktionsdefinitionen können nicht verschachtelt werden, jedoch können Variablen im Stil der Algol-Blockstruktur ver- vereinbart werden. Die Funktionen eines C-Programms können sich in verschiedenen Quelldateien befinden, die getrennt voneinander übersetzt werden. Variablen können in- intern zu einer Funktion oder extern und trotzdem nur innerhalb einer Quelldatei bekannt oder auch im ganzen Programm erreichbar definiert werden. Ein Preprozessor ersetzt Makros im Programmtext, fügt andere Quelldateien ein und ermöglicht bedingte Übersetzung. C ist eine relativ „maschinennahe" Sprache. Diese Charakterisierung ist nicht ab- abwertend zu sehen; sie drückt nur aus, daß C mit den gleichen Objekten umgeht wie die meisten Computer selbst, nämlich mit Zeichen, Zahlen und Adressen. Diese können verknüpft und verschoben werden mit den arithmetischen und logischen Operatoren, die üblicherweise in echten Maschinen vorhanden sind. C verfügt über keine Operationen, mit denen man zusammengesetzte Objekte wie Zeichenketten, Mengen, Listen oder Vektoren direkt bearbeiten kann. Es gibt keine Operationen, die einen ganzen Vektor oder eine Zeichenkette manipulieren; man kann
2 Einführung jedoch eine Struktur als Einheit kopieren. Die Sprache selbst definiert keine Speicher- Speicherverwaltung außer statischer Definition und der Stack-Verwaltung, die für lokale Varia- Variablen in Funktionen besteht; es gibt kerne Halde (heap) oder Wiedergewinnung von Spei- Speicher (garbage collection). Schließlich hat C selbst keine Einrichtungen für Ein- und Aus- Ausgabe; es gibt keine READ- oder WRITE-Anweisungen und keine eingebauten Techniken für Dateizugriff. Alle diese abstrakteren Mechanismen müssen als explizit aufgerufene Funktionen zur Verfügung gestellt werden. Die meisten C-Implementierungen enthalten eine relativ standardisierte Sammlung solcher Funktionen. Ahnlich gibt es in C nur die einfachen Kontrollstrukturen mit einem Eingang und einem Ausgang: Tests, Schleifen, Zusammenfassungen und Unterprogramme, nicht aber spezielle Einrichtungen für Multiprogrammierung, parallele Operationen, Prozeß-Syn- Prozeß-Synchronisation oder Coroutinen. Manche mögen das Fehlen derartiger Sprachelemente als beachtlichen Nachteil ansehen („ich muß wirklich eine Funktion aufrufen, um zwei Zeichenketten zu verglei- vergleichen?"), aber die Beschränkungen im Sprachumfang bieten außerordentliche Vorteile. Da C eine ziemlich kleine Sprache ist, kann sie auf kleinem Raum beschrieben und schnell erlernt werden. Ein Programmierer darf durchaus erwarten, daß er die ganze Sprache kennt, versteht und wirklich regelmäßig benutzt. Viele Jahre lang war die Definition von C die „C-Sprachbeschreibung" im Anhang zur Ersten Ausgabe von Programmieren in C. 1983 richtete das American National Stan- Standards Institute (ANSI) eine Kommission ein, die eine moderne, umfassende Definition von C vorlegen sollte. Die resultierende Definition, der ANSI-Standard oder „ANSI-C", wurde Ende 1988 abgeschlossen. Die meisten Sprachkonzepte des Standards werden von modernen Übersetzern bereits unterstützt. Der Standard beruht auf der ursprünglichen „Sprachbeschreibung". Die Sprache hat sich ziemlich wenig geändert; ein Ziel des Standards war, daß die meisten existenten Programme gültig bleiben oder daß wenigstens die Übersetzer entsprechende Warnun- Warnungen erzeugen können. Für die meisten Programmierer ist die wichtigste Änderung eine neue Syntax zur Deklaration und Definition von Funktionen. Eine Funktionsdeklaration kann jetzt die Parameter der Funktion beschreiben; die Syntax der Definition wurde analog geändert. Diese zusätzliche Information macht es Übersetzern viel leichter, Fehler aufgrund von Widersprüchen zwischen Argumenten und Parametern zu entdecken; nach unserer Er- Erfahrung ist das eine sehr nützliche zusätzliche Eigenschaft der Sprache. Es gibt andere kleine Sprachänderungen. Strukturzuweisungen und Aufzähltypen (enum) waren schon weit verbreitet und sind jetzt offiziell Teil der Sprache. Gleitpunkt- Gleitpunktrechnung kann nun mit einfacher Genauigkeit erfolgen. Die arithmetischen Eigenschaf- Eigenschaften, insbesondere für vorzeichenlose Typen, wurden klargestellt. Der Preprozessor ist aufwendiger. Die meisten dieser Änderungen werden die meisten Programmierer kaum betreffen. Ein zweiter wesentlicher Beitrag des Standards ist die Definition einer Bibliothek, die zu C gehört. Sie legt Funktionen zum Zugriff auf das Betriebssystem fest (zum Bei- Beispiel, um Dateien zu lesen und zu schreiben), für formatierte Ein- und Ausgabe, Spei- Speicherverwaltung, Operationen mit Zeichenketten etc. Eine Sammlung von Standard-Defi-
Einführung 3 nitionsdateien erlaubt einheitlichen Zugriff zu Vereinbarungen von Funktionen und Da- Datentypen. Programme, die mit Hilfe dieser Bibliothek auf das umgebende System zugrei- zugreifen, können sicher sein, daß der Zugriff überall kompatibel ist. Der größte Teil der Bi- Bibliothek ist aufgebaut wie die stdio-Bibliothek im UNIX-System. Diese Standard-Biblio- Standard-Bibliothek für Ein- und Ausgabe wurde in der Ersten Ausgabe beschrieben, und sie war auch auf anderen Systemen weit verbreitet. Auch hier werden die meisten Programmierer we- wenig Änderungen feststellen. Die Datentypen und Kontrollstrukturen in C werden direkt von den meisten Com- Computern unterstützt. C-Programme benötigen daher nur ein kleines Laufzeitsystem. Die Funktionen der Standard-Bibliothek werden nur explizit aufgerufen und können deshalb vermieden werden, wenn man sie nicht braucht. Die meisten können in C geschrieben werden, und abgesehen von den Betriebssystem-Details, die sie verbergen, sind sie selbst portabel. Obgleich C den Fähigkeiten vieler Rechner angepaßt ist, ist die Sprache doch un- unabhängig von einer bestimmten Maschinenarchitektur. Mit einer gewissen Sorgfalt ist es leicht, portable Programme zu schreiben, das heißt, Programme, die ohne Änderungen auf einer Reihe von Computern ausgeführt werden können. Der Standard stellt Portabi- litätsprobleme klar heraus und schreibt einen Satz Konstanten vor, die die Maschine cha- charakterisieren, auf der das Programm ausgeführt wird. C ist nicht streng typgebunden, aber während seiner Evolution wurde die Typprü- Typprüfung verschärft. Die ursprüngliche Definition von C verpönte zwar, aber erlaubte doch den Austausch von Zeigern und Integer-Werten; das wurde schon lange verboten, und der Standard verlangt jetzt die korrekten Vereinbarungen und expliziten Umwandlungs- Umwandlungsoperationen, die schon bisher von guten Übersetzern durchgesetzt wurden. Die neuen Funktionsdeklarationen sind ein weiterer Schritt in diese Richtung. Übersetzer warnen vor den meisten Typfehlern und es gibt keine automatische Umwandlung von unverträgli- unverträglichen Datentypen. Trotzdem bleibt C bei der grundsätzlichen Auffassung, daß Program- Programmierer wissen, was sie tun; sie müssen nur ihre Absichten explizit kundtun. Schließlich hat C, wie jede andere Sprache, ihre Mängel. Einige der Operatoren haben den falschen Vorrang; Teile der Syntax könnten besser sein. Nichtsdestotrotz hat sich C als eine außerordentlich effektive und ausdrucksstarke Sprache für einen breiten Bereich von Programmieranwendungen erwiesen. Der Rest des Buches ist folgendermaßen gegliedert: Kapitel 1 ist eine Übersicht der zentralen Teile von C in Beispielen. Der Leser soll möglichst schnell anfangen kön- können, da wir fest überzeugt sind, daß man eine neue Programmiersprache nur erlernt, wenn man gleich darin Programme schreibt. Wir setzen grundsätzliche Programmier- Programmierkenntnisse voraus; es gibt hier keine Erklärung von Computern, von Übersetzungsvor- Übersetzungsvorgängen oder für die Bedeutung von Ausdrücken wie n = n+l. Wo immer dies möglich war, haben wir versucht, nützliche Programmiertechniken aufzuzeigen; dieses Buch möchte jedoch kein Nachschlagewerk für Datenstrukturen und Algorithmen sein. Wenn wir wählen mußten, haben wir uns auf die Programmiersprache konzentriert. Kapitel 2 bis 6 diskutieren verschiedene Aspekte von C ausführlicher und dabei mehr formell als dies in Kapitel 1 geschieht. Dabei liegt die Betonung noch immer auf Beispielen von abgeschlossenen, nützlichen Programmen und nicht auf isolierten Frag-
4 Einführung menten. Kapitel 2 befaßt sich mit den elementaren Datentypen, Operatoren und Aus- Ausdrücken. Kapitel 3 behandelt Kontrollstrukturen: if-else, switch, while, for etc. Kapitel 4 behandelt Funktionen und Programmstruktur - externe Variablen, Gültigkeitsberei- Gültigkeitsbereiche, mehrere Quelldateien usw. Kapitel 5 beschreibt Zeiger- und Adreß-Arithmetik. Kapitel 6 behandelt Strukturen und Unionen (struct und union). Kapitel 7 beschreibt die Standard-Bibliothek, die eine allgemeine Schnittstelle zum Betriebssystem darstellt. Diese Bibliothek wird im ANSI-Standard definiert, und sie soll auf allen Maschinen zur Verfügung stehen, wo es C gibt, damit Programme, die diese Bi- Bibliothek für Eingabe, Ausgabe und andere Systemfunktionen benutzen, unverändert von einem System auf ein anderes transferiert werden können. Kapitel 8 beschreibt eine Schnittstelle zwischen C-Programmen und dem UNIX- Betriebssystem und konzentriert sich dabei auf Eingabe, Ausgabe, das Dateisystem und Speicherverwaltung. Obgleich Teile dieses Kapitels auf UNIX-Systeme zugeschnitten sind, sollten selbst Programmierer, die andere Systeme benutzen, in diesem Kapitel noch nütz- nützliche Information finden. Insbesondere liefert dieses Kapitel einen Einblick, wie eine Version der Standard-Bibliothek implementiert ist sowie Vorschläge, um übertragbare Programme zu realisieren. Anhang A enthält eine Sprachbeschreibung. Die offizielle Beschreibung der Syn- Syntax und Semantik von C ist der ANSI-Standard selbst. Dieses Dokument wendet sich je- jedoch vor allem an die Autoren von Übersetzern. Die Sprachbeschreibung im vorliegen- vorliegenden Buch beschreibt die Sprache knapper und ohne den juristischen Stil. Anhang B ist eine Zusammenfassung der Standard-Bibliothek, wiederum eher für Benutzer als für Im- Implementierer. Anhang C ist eine kurze Zusammenfassung der Änderungen gegenüber der ursprünglichen Sprache. Im Zweifelsfall bleiben jedoch der Standard und der eigene Übersetzer die endgültigen Autoritäten für die Sprache.
1 Eine Übersicht in Beispielen Beginnen wir mit einer schnellen Einführung in C. Wir möchten die wichtigsten Sprachelemente anhand von realistischen Programmen zeigen, ohne uns dabei jedoch im Detail, in Regeln oder Ausnahmen zu verlieren. Im Augenblick versuchen wir nicht, vollständig oder auch nur präzise zu sein (abgesehen davon, daß die Beispiele natürlich korrekt sein sollen). Wir möchten, daß Sie so schnell wie möglich den Punkt erreichen, an dem Sie nützliche Programme schreiben können. Dazu müssen wir uns auf die ele- elementaren Dinge konzentrieren: Variablen und Konstanten, Arithmetik, Kontrollstruktu- Kontrollstrukturen, Funktionen und rudimentäre Möglichkeiten zur Eingabe und Ausgabe. Wir lassen absichtlich in diesem Kapitel Sprachelemente von C aus, die wichtig sind, um größere Programme zu schreiben. Dazu gehören Zeiger, Strukturen, eine große Zahl von Opera- Operatoren, mit denen C so reich ausgestattet ist, verschiedene Kontrollstruktur-Anweisungen und die Standard-Bibliothek. Unser Vorgehen hat Nachteile. Am leichtesten sieht man das daran, daß alle De- Details eines bestimmten Sprachelements nicht hier aufgefunden werden können. Durch ih- ihre Kürze kann diese Übersicht auch in die Irre führen. Da die Beispiele nicht die vollen Möglichkeiten von C ausnützen, sind sie nicht so prägnant und elegant wie sie sein könn- könnten. Wir haben versucht, diese Effekte zu minimieren, aber seien Sie gewarnt. Ein wei- weiterer Nachteil ist, daß spätere Kapitel notwendigerweise Teile des vorliegenden Kapitels wiederholen. Wir hoffen, daß die Wiederholung eher hilft als stört. Erfahrene Programmierer sollten in jedem Fall in der Lage sein, aus diesem Kapi- Kapitel für ihre eigenen Programmieraufgaben zu extrapolieren. Anfänger sollten zusätzlich eigene kleine, ähnliche Programme schreiben. Beide Gruppen können dieses Kapitel als Rahmen benutzen für die späteren, mehr detaillierten Beschreibungen in den nachfol- nachfolgenden Kapiteln. 1.1 Erste Schritte Eine neue Programmiersprache lernt man nur, wenn man in ihr Programme schreibt. Die erste Programmieraufgabe ist für alle Sprachen dieselbe: Ein Programm soll folgende Wörter ausgeben: hello, world Hier haben wir die große Hürde; um sie zu überwinden, müssen Sie in der Lage sein, Ih- Ihren Programmtext irgendwo zu erzeugen, ihn erfolgreich zu übersetzen, zu montieren, auszuführen, und Sie müssen herausfinden, wohin Ihre Ausgabe ging. Sind diese mecha- mechanischen Details gemeistert, dann ist alles andere vergleichsweise leicht. In C sieht ein Programm, das hello, world druckt, so aus: «include <stdio.h> mainO t printf("hello, world\n">;
1 Ein bersicht in Beispielen Wie Sie dieses Programm genau ausführen, hängt davon ab, welches System Sie benutzen. Beim UNIX Betriebssystem, zum Beispiel, müssen Sie den Programmtext in ei- einer Datei erzeugen, deren Name mit .c endet, etwa hello.c. Dann übersetzen Sie diese Datei mit dem Kommando cc hello.c Falls Sie nichts falsch gemacht haben, also etwa ein Zeichen ausgelassen oder etwas falsch buchstabiert haben, wird die Übersetzung ohne weitere Meldungen vor sich gehen, und es entsteht eine ausführbare Datei mit dem Namen a.out. Wenn man diese Datei mit dem Kommando a.out ausführt, dann resultiert die Ausgabe hello, world Bei anderen Betriebssystemen sind die Regeln möglicherweise anders; Sie sollten da einen lokalen Experten konsultieren. «include <stdio.h> Information zur Slandard-Ein/Ausgabe-Bibliothek einfügen. Funktion namens main definieren, mainO . ' ... die keine Argumentwerte empfangt. C Anweisungen von main stehen in geschweiften Klammem. printf ("hei lo, world\n">; main ruft die Bibliotheksfunktion printf auf, um diese Zeichenfolge zu drucken; > \n stellt den Zeilentrtnner dar. Das erste C-Programm Einige Erklärungen zum Programm selbst: Unabhängig von seiner Größe besteht ein C-Programm aus Funktionen und Variablen. Eine Funktion besteht aus Anweisungen, die definieren, welche Aktionen ausgeführt werden sollen, und die Variablen enthalten die Werte, die während der Ausführung benutzt werden. C-Funktionen sind vergleichbar den Funktionen und Unterprogrammen von Fortran oder den Prozeduren und Funktio- Funktionen von Pascal. In unserem Beispiel ist main so eine Funktion. Normalerweise können Sie Funktionen mit beliebigen Namen bezeichnen, aber main ist ein besonderer Name - Ihr Programm beginnt seine Ausführung am Anfang von main. Dies bedeutet, daß jedes Programm eine Funktion namens main irgendwo haben muß. Die Funktion main wird normalerweise andere Funktionen aufrufen, um ihre Auf- Aufgaben zu erledigen; solche Funktionen können von Ihnen geschrieben worden sein oder aus Bibliotheken stammen, die zu Ihrer Verfügung stehen. Die erste Zeile des Pro- Programms «include <stdio.h> bewirkt, daß der Übersetzer Information über die Standard-Ein/Ausgabe-Bibliothek ein- einfügt. Diese Zeile erscheint am Anfang vieler C-Programme. Die Standard-Ein/Ausga- Standard-Ein/Ausgabe-Bibliothek wird im Kapitel 7 und im Anhang B beschrieben.
1.1 Erste Schrit Eine Methode, um Daten zwischen Funktionen auszutauschen, besteht für die auf- aufrufende Funktion darin, eine Werteliste, die Argumente, der aufgerufenen Funktion als Parameter zur Verfügung zu stellen.* Die Klammern, die dem Funktionsnamen in der Definition folgen, umgeben die Parameterliste; in unserem Beispiel ist main eine Funkti- Funktion ohne Parameter, dies wird durch leere Klammern () ausgedrückt. Die geschweiften Klammern { } umgeben die Anweisungen, aus denen die Funkti-» on besteht. Die Funktion main enthält nur die eine Anweisung printf("hello, world\n">; Eine Funktion wird aufgerufen, indem man ihren Namen angibt, gefolgt von einer Liste von Argumentwerten in Klammern. Somit wird die Funktion printf mit dem Argument "hello, world\n" aufgerufen, printf ist eine Bibliotheksfunktion, die Ausgabe erzeugt; in diesem Fall die Zeichenkette zwischen den Doppelanführungszeichen. Eine Zeichenfolge in Doppelanführungszeichen, wie "hello, world\n", wird konstante Zeichenkette (string constant) genannt. Vorläufig werden wir Zeichenketten nur als Argumente für printf und andere Funktionen verwenden. Die Zeichenfolge \n in dieser Zeichenkette ist die C-Schreibweise für den Zeilentrenner, das Zeichen, das dafür sorgt, daß die Ausgabe am linken Rand und auf ei- einer neuen Zeile fortgesetzt wird. Wenn Sie die Folge \n auslassen (ein lohnendes Expe- Experiment), sehen Sie, daß in Ihrer Ausgabe kein Zeilenvorschub stattfindet. Sie müssen \n benutzen, um einen Zeilentrenner in das Argument von printf einzubringen; falls Sie et- etwa printf("hello, world "); angeben, wird der C-Übersetzer eine Fehlermeldung erzeugen. printf trennt Zeilen niemals implizit, folglich können mehrere Aufrufe dazu die- dienen, eine Ausgabezeile stückweise zusammenzufügen. Unser erstes Programm hätte auch wie folgt formuliert werden können: «include <stdio.h> mainO t printf("hello, "); printf("Horld"); printf("\n"); > Dieses Programm produziert dieselbe Ausgabe wie das vorherige. Beachten Sie, daß \n nur ein einzelnes Zeichen repräsentiert. Eine Darstellung mit Gegenschrägstrich wie \n ist ein allgemeiner und erweiterbarer Mechanismus zur Re- Repräsentierung von Steuerzeichen oder Zeichen, die auf dem Eingabegerät nicht vorhan- vorhanden oder nur schwer zu erhalten sind. C erlaubt beispielsweise auch die Darstellungen \t für ein Tabulatorzeichen, \b für einen backspace, also zum Überdrucken des vorherge- vorhergehenden Zeichens (solange das Ausgabegerät dies erlaubt), \" zur Darstellung des Dop- * Wir bezeichnen als Argument den Wert, der bei einem Funktionsaufruf übergeben wird, und als Parameter die Variable, die diesen Wert in der aufgerufenen Funktion repräsentiert A.d.Ü.
8 1 Eine ersieht in Beispielen pelanführungszeichens, und \\ für den Gegenschrägstrich selbst. Abschnitt 2.3 enthält eine vollständige Aufzählung. Aufgabe 1-1. Führen Sie das hello, world Programm auf Ihrem System aus. Untersu- Untersuchen Sie, was passiert, wenn Sie Teile des Programms auslassen; versuchen Sie, einige Fehlermeldungen hervorzurufen. O Aufgabe 1-2. Experimentieren Sie mit anderen Darstellungen mit Gegenschrägstrich, stellen Sie also fest, was passiert, wenn ein Argument für printf die Zeichenfolge \c enthält, wobei c ein Zeichen ist, das hier noch nicht erwähnt wurde. D \2 Variablen und Arithmetik E/9)(°F-32) und gibt folgende Unser nächstes Programm benutzt die Regel °C Temperaturtabelle in Fahrenheit und Celsius aus: 0 20 40 60 80 100 120 140 160 180 200 220 240 260 280 300 -17 -6 4 15 26 37 48 60 71 82 93 104 115 126 137 148 Das Programm besteht wiederum aus der Definition einer einzigen Funktion mit dem Namen main. Es ist länger als das, das hello, world ausgibt, aber es ist nicht komplizier- komplizierter. Es führt ein paar neue Dinge ein, unter anderem Kommentare, Vereinbarungen, Va- Variablen, arithmetische Ausdrücke, Schleifen und formatierte Ausgabe, «include <stdio.h> /* Umwandlung von Fahrenheit in Celsius fuer fahr « 0, 20, .... 300 */ mainO C int fahr, Celsius; int lower, upper, step; lower = 0; /* untere Grenze der Temperaturtabelle */ upper » 300; /* obere Grenze */ step - 20; /* Schrittbreite */ fahr ■ lower; while (fahr <= upper) { celsius * 5 * (fahr-32) / 9; printf("%d\tXd\n", fahr, celsius); fahr « fahr + step;
1.2 Variablen unc ithmetik Die zwei Zeilen /* Umwandlung von Fahrenheit in Celsius fuer fahr * 0, 20 300 V sind ein Kommentar, der in diesem Fall kurz erklärt, was das Programm tut. Der C- Übersetzer ignoriert alle Zeichen zwischen /• und •/. Kommentare können überall dort stehen, wo auch ein Leerzeichen oder ein Tabulatorzeichen oder ein Zeilentrenner ste- stehen kann; sie können freizügig benutzt werden, um ein Programm leichter verständlich zu machen. In C müssen alle Variablen vereinbart werden, bevor sie benutzt werden. Norma- Normalerweise geschieht dies am Anfang einer Funktion vor allen ausführbaren Anweisungen. Eine Vereinbarung beschreibt die Eigenschaften von Variablen,* sie besteht aus einem Typ und einer Liste von Variablen, die dann diesen Typ besitzen, wie zum Beispiel int fahr, celsius; int lower, upper, step; Der Typ int legt fest, daß die angegebenen Variablen ganzzahlige Werte annehmen. Im Gegensatz dazu führt float Gleitpunktvariablen ein, deren Werte mit Mantisse und Expo- Exponent repräsentiert werden. Signifikanz und Wertebereich hängen von der jeweiligen Im- Implementierung ab. 16-Bit int-Werte zwischen -32768 und +32767 sind genauso üblich wie int mit 32 Bits. Eine float-Zahl benötigt typischerweise 32 Bits, das ergibt minde- mindestens sechs signifikante Dezimalziffern mit einem Wertebereich zwischen etwa 10'38 und 10+38. Neben int und float hat C verschiedene andere elementare Datentypen: char ein einzelnes Zeichen short ein kleiner, ganzzahliger Wert long ein großer, ganzzahliger Wert double ein Gleitpunktwert mit doppelter Genauigkeit Die Größen dieser Objekte sind ebenfalls maschinenabhängig. Darüber hinaus gibt es Vektoren, Strukturen und unions aus diesen elementaren Typen, Zeiger auf solche Objekte und Funktionen, die derartige Werte als Resultat liefern. Wir werden dies alles zu seiner Zeit kennenlernen. Die Berechnungen in unserem Temperaturumwandlungs-Programm beginnen mit den Zuweisungsanweisungen lower = 0; upper * 300; step = 20; fahr * lower; die den Variablen ihre Anfangswerte zuweisen. In C wird jede Anweisung mit einem Se- Semikolon abgeschlossen. Jede Zeile in der Temperaturtabelle wird in gleicher Weise berechnet, folglich be- benutzen wir eine Schleife, die einmal pro Ausgabezeile wiederholt wird; dies ist die Aufga- Aufgabe der while-Schleife * Wir unterscheiden zwei Arten von Vereinbarungen: Definitionen, die Objekte erzeugen, und Deklarationen, die nur die Eigenschaften von Objekten festlegen. A.d.Ü.
10 1 Eini versieht in Beispielen while (fahr <« upper) { Die while-Schleife funktioniert folgendermaßen: Nach while steht eine Bedingung in Klammern. Trifft sie zu (ist also der Wert von fahr kleiner oder gleich dem Wert von upper), wird der Rumpf der Schleife ausgeführt (also die drei Anweisungen, die von den geschweiften Klammern { und } eingeschlossen sind). Dann wird die Bedingung wieder- wiederum geprüft, und falls sie zutrifft, werden die abhängigen Anweisungen wieder ausgeführt. Trifft die Bedingung nicht zu (fahr ist größer als upper), so wird die Schleife beendet und die Ausführung des Programms fortgesetzt mit der Anweisung, die der Schleife folgt. In diesem Programm gibt es keine weiteren Anweisungen, und folglich wird die Ausführung beendet. Von while können mehrere Anweisungen abhängen, wenn sie in geschweifte Klam- Klammern eingeschlossen sind wie im Temperaturumwandlungs-Programm. Alternativ kann auch eine einzelne Anweisung ohne geschweifte Klammern von while abhängen, wie das folgende Beispiel zeigt: while (i < j) i = 2 • i; Auf alle Fälle rücken wir im Quellprogramm die von while abhängigen Anweisungen um eine Tabulatorposition ein (was wir durch vier Leerzeichen angedeutet haben), damit auf Anhieb ersichtlich ist, welche Anweisungen innerhalb der Schleife sind. Einrücken be- betont die logische Struktur des Programms. Obgleich in C Anweisungen beliebig auf den Quellzeilen angeordnet werden können, so sind doch sauberes Einrücken und die Ver- Verwendung von Zwischenraum unerläßlich, damit Programme für Menschen leichter lesbar sind. Wir empfehlen, nur eine Anweisung pro Zeile zu schreiben und Operatoren mit Leerzeichen zu umgeben. Die Position von geschweiften Klammern ist weniger wichtig, obwohl manche Leute da ganz fanatisch werden. Wir haben in diesem Buch eine von mehreren populären Stilrichtungen benutzt. Gewöhnen Sie sich einen Stil an, den Sie für zweckmäSig halten, und benutzen Sie ihn dann grundsätzlich. Die wesentliche Arbeit bei der Temperaturumwandlung wird innerhalb der while- Schleife erledigt. Die Temperatur in Celsius wird berechnet und an die Variable Celsius zugewiesen durch die Anweisung celsius = 5 * (fahr-32) / 9; Wir multiplizieren mit 5 und dividieren dann durch 9, und wir vermeiden den Ausdruck S/9; in C wird, wie in vielen anderen Sprachen, bei ganzzahliger Division abgeschnitten, Bruchteile werden also ignoriert. 5 und 9 sind int-Werte und 5/9 ist deshalb null, und dadurch würden dann auch alle Temperaturen zu null. Dieses Beispiel zeigt auch ein bißchen mehr, wie printf arbeitet, printf ist eine all- allgemeine Funktion zur Umwandlung von Darstellungsformaten; wir werden diese Funkti- Funktion ausführlich in Kapitel 7 beschreiben. Das erste Argument für printf ist eine Zeichen- Zeichenkette, die gedruckt werden soll. Formatelemente, die jeweils mit % beginnen, geben an, wo die nachfolgenden Argumente einzufügen sind und in welcher Form sie dargestellt
1.2 Variablen u Arithmetik 11 werden sollen. Zum Beispiel benennt %d ein ganzzahliges Argument, so daß mit der An- Anweisung printf(HXd\tXd\nHf fahr, celsius); zwei ganzzahlige Werte fahr und celsius ausgegeben werden, mit einem Tabulatorzei- Tabulatorzeichen (\t) dazwischen. Für jedes Formatelement im ersten Argument von printf muß nachfolgend ein wei- weiteres Argument angegeben werden: Argumente und Formatelemente müssen in Anzahl und Datentyp zueinander passen, andernfalls enthält die Ausgabe falsche Ergebnisse. Übrigens ist printf nicht Teil der Programmiersprache C; in C selbst ist Eingabe oder Ausgabe nicht definiert. Bei printf handelt es sich nur um eine nützliche Funktion aus der Bibliothek, die normalerweise für C-Programme zur Verfügung steht. Die Ei- Eigenschaften von printf sind jedoch im ANSI-Standard definiert, somit sollten die Eigen- Eigenschaften bei jedem Übersetzer oder jeder Bibliothek, die den ANSI-Standard implemen- implementieren, immer gleich sein. Damit wir uns auf C selbst konzentrieren können, werden wir Eingabe und Ausga- Ausgabe bis zum Kapitel 7 kaum behandeln. Insbesondere werden wir formatierte Eingabe bis dahin zurückstellen. Falls Sie Zahlen eingeben wollen, sollten Sie die Beschreibung der Funktion scanf im Abschnitt 7.4 lesen, scanf verhält sich ähnlich wie printf, allerdings wird eine Eingabe verarbeitet und nicht eine Ausgabe erzeugt. Das Programm zur Umrechnung der Temperaturen enthält noch einige Probleme. Eines der einfachen betrifft die Ausgabe, die nicht sehr schön aussieht, weil die Zahlen nicht rechtsbündig ausgegeben werden. Das laßt sich einfach beheben; wenn wir zu je- jedem %d von printf noch eine Breitenangabe liefern, werden die Zahlen rechtsbündig in diesem Bereich ausgegeben. Zum Beispiel können wir mit printfC'XJd %6d\n", fahr, celsius); die erste Zahl jeder Zeile auf einer drei Stellen breiten Fläche ausgeben, und die zweite Zahl auf sechs Stellen Breite. 0 20 40 60 80 100 -17 -6 4 15 26 37 Ein schwierigeres Problem haben wir uns durch die Verwendung ganzzahliger Arithmetik eingehandelt: die Celsius-Temperaturwerte sind nicht sehr genau. Zum Bei- Beispiel ist 0°F in Wirklichkeit etwa - 17.8°C, und nicht -17. Um genauere Resultate zu er- erhalten, sollten wir Gleitpunktarithmetik statt ganzzahliger Arithmetik verwenden. Dies erfordert einige Änderungen in unserem Programm. Eine zweite Version sieht folgen- folgendermaßen aus:
12 1 Eine jrsicht in Beispielen »include <stdio.h> /* Umwandlung von Fahrenheit in Celsius fuer fahr * 0, 20, .... 300; Version mit Gleitpunkt */ mainO { float fahr, Celsius; int lower, upper, step; lower » 0; /* untere Grenze der Temperaturtabelle */ upper = 300; /* obere Grenze */ step = 20; /* Schrittbreite */ fahr - lower; while (fahr <= upper) C Celsius = E.0/9.0) * (fahr-32.0); printf(3.0f X6.1f\n", fahr, Celsius); fahr = fahr + Step; Dies ist ziemlich so wie vorher, nur sind jetzt fahr und Celsius als float definiert, und die Regel für die Umwandlung ist natürlicher formuliert. Wir konnten in der letzten Version S/9 nicht benutzen, weil die ganzzahlige Division diesen Ausdruck zu Null abge- abgeschnitten hätte. Ein Dezimalpunkt in einer Konstanten gibt an, daß es sich um eine Gleitpunktkonstante handelt, somit wird 5.0/9.0 nicht abgebrochen, weil es sich als Bruch zweier Gleitpunktwerte ergibt. Hat ein arithmetischer Operator ganzzahlige Operanden, wird eine ganzzahlige Operation durchgeführt. Hat ein arithmetischer Operator jedoch einen ganzzahligen und einen Gleitpunktoperanden, wird der ganzzahlige Wert in einen Gleitpunktwert umge- umgewandelt, bevor die Gleitpunktoperation durchgeführt wird. Wenn wir fahr-32 geschrie- geschrieben hätten, wäre 32 automatisch auf Gleitpunkt gewandelt worden. Nichtsdestotrotz - schreibt man Gleitpunktkonstanten mit explizitem Dezimalpunkt, auch wenn sie einen ganzzahligen Wert haben, betont das für den Leser den Gleitpunktaspekt. Im nächsten Kapitel wird im Detail erklärt, wann ganzzahlige Werte in Gleitpunkt- Gleitpunktwerte umgewandelt werden. Im Moment halten wir fest, daß in der Zuweisung fahr = lower; und bei der Bedingung while (fahr <= upper) so verfahren wird, wie wir erwarten - der int-Wert wird in float umgewandelt, bevor die Rechenoperation stattfindet. Das Formatelement %3.0f in printf verlangt, daß eine Gleitpunktzahl (im vorlie- vorliegenden Fall fahr) in einer Fläche dargestellt wird, die mindestens drei Zeichen breit ist und bei der kein Dezimalpunkt und keine Ziffern nach dem Dezimalpunkt erscheinen. %6.1f beschreibt eine weitere 7jM (celsius), die wenigstens sechs Stellen einnehmen soll mit einer Ziffer nach dem Dezimalpunkt. Die Ausgabe sieht folgendermaßen aus: 0 -17.8 20 -6.7 40 4.4
1.3 Die/or-Anw ng 13 Breite und Genauigkeit brauchen nicht angegeben zu werden: %6f bedeutet, daß der Zahlenwert wenigstens sechs Zeichen breit sein soll; %2f verlangt, daß zwei Stellen nach dem Dezimalpunkt ausgegeben werden sollen, aber eine minimale Breite der Ausgabe ist nicht vorgegeben; und %f schließlich vereinbart nur, daß die Zahl als Gleitpunktwert dar- dargestellt wird. Xd als dezimale ganze 7MI ausgeben %6d als dezimale ganze Zahl ausgeben, mindestens 6 Zeichen breit Xf als Gleitpunktzahl ausgeben X6f als Gleitpunktzahl ausgeben, mindestens 6 Zeichen breit %.2f als Gleitpunktzahl ausgeben, 2 Zeichen hinter dem Dezimaipunkt als Gleitpunktzahl ausgeben, mindestens 6 Zeichen breit und 2 hinter dem Dezimalpunkt printf erkennt unter anderem auch Formatelemente wie %o für oktale Darstellung, %x für hexadezimale Darstellung, %c für ein einzelnes Zeichen und %s für eine Zeichenket- Zeichenkette. %% schließlich stellt % selbst dar. Aufgabe 1-3. Ändern Sie das Programm zur Temperaturumwandlung so ab, daß über der Tabelle eine Titelzeile gedruckt wird. D Aufgabe 1-4. Schreiben Sie ein Programm, das eine Tabelle von Fahrenheit-Werten in Abhängigkeit von Celsius-Werten erzeugt. D 1.3 Die/or-Anweisung Es gibt viele verschiedene Möglichkeiten, ein Programm für eine bestimmte Aufga- Aufgabe zu formulieren. Betrachten wir eine Variante zur Temperaturumwandlung: #include <stdio.h> /* Umwandlung von Fahrenheit in Celsius */ mainO t int fahr; for (fahr = 0; fahr <= 300; fahr = fahr + 20) printf<»X3d X6.1f\n", fahr, E.0/9.0)*<fahr-32)>; > Dieses Programm liefert das gleiche Resultat, aber es sieht doch ganz anders aus. Vor allem haben wir auf alle Variablen verzichtet; es verbleibt nur noch fahr. Wir haben es als int vereinbart. Anfangs- und Endwert der Tabelle sowie die Schrittbreite erscheinen nur noch als Konstanten in der for-Anweisung, selbst auch eine neue Konstruktion, und der Ausdruck zur Berechnung der Temperatur in Celsius ist jetzt das dritte Argument zu printf, und nicht mehr eine separate Zuweisung. Diese letztere Modifikation ist ein Beispiel einer allgemeinen Regel in C - in je- jedem Zusammenhang, in dem der Wert einer Variablen von bestimmtem Typ benutzt werden darf, kann man auch einen komplizierteren Ausdruck mit diesem Typ verwenden. Da das dritte Argument von printf ein Gleitpunktwert sein muß, der zum Formatelement %6.1f paßt, kann als drittes Argument ein beliebiger Gleitpunktausdruck verwendet wer- werden.
14 1 Eut 'rersicht in Beispielen Die for-Anweisung ist eine Schleife, und zwar eine Verallgemeinerung der while- Schleife. Wenn man die for-Schleife mit der früheren while-Schleife vergleicht, sollte die Funktionsweise klar werden. Innerhalb der Klammern sind drei Teile, die durch Semiko- Semikolon getrennt werden. Der erste Teil, die Initialisierung fahr = 0 wird einmal ausgeführt, unmittelbar bevor die Schleife begonnen wird. Der zweite Teil ist die Bedingung, die die Ausführung der Schleife kontrolliert: fahr <= 300 Diese Bedingung wird ausgewertet; trifft sie zu, dann werden die abhängigen Anweisun- Anweisungen der Schleife (hier ein einzelner Aufruf von printf) ausgeführt. Schließlich wird die Inkrementierung fahr = fahr + 20 ausgeführt, und die Bedingung der Schleife wird erneut ausgewertet. Die Ausführung der Schleife ist zu Ende, falls die Bedingung nicht mehr zutrifft. Analog wie von while kann von for eine einzelne Anweisung abhängen oder eine Reihe von Anweisungen, die in geschweifte Klammern eingeschlossen sind. Initialisierung, Bedingung und Inkremen- Inkrementierung können beliebige einzelne Ausdrücke sein. Ob man while oder for verwendet, hängt davon ab, was klarer erscheint, for ist normalerweise sinnvoll für Schleifen, bei denen Initialisierung und Inkrementierung ein- einfache Anweisungen sind, die logisch zusammenhängen, denn dann ist for kompakter als while, und die Anweisungen zur Kontrolle der Schleife werden an einer Stelle zusam- zusammengefaßt. Aufgabe 1-5. Andern Sie das Temperaturumwandlungs-Programm so ab, daß die Ta- Tabelle in umgekehrter Reihenfolge gedruckt wird, das heißt, von 300 Grad bis 0 Grad. D 1.4 Symbolische Konstanten Eine letzte Überlegung, bevor wir die Temperaturumwandlung endgültig verlassen. Es ist ein schlechter Stil, wenn man „magische Zahlen" wie 300 und 20 tief in einem Pro- Programm versteckt; wenn jemand das Programm später lesen muß, enthalten solche Zahlen wenig Information, und es ist schwierig, sie systematisch zu ändern. Eine Möglichkeit magische Zahlen zu handhaben, besteht darin, ihnen sinnvolle Namen zu geben. Mit ei- einer #deflne-Zeile wird ein symbolischer Name oder eine symbolische Konstante als spezi- spezieller Text vereinbart: #def ine Name Ersatztext Daran anschließend wird an allen Stellen, wo ein solcher Name nicht innerhalb von An- Anführungszeichen oder als Teil eines anderen Namens verwendet wird, der Name durch den entsprechenden Ersatztext ersetzt. Der Name sieht so aus wie ein Variablenname: eine Folge von Buchstaben und Ziffern, die mit einem Buchstaben beginnt. Der Ersatztext ist eine beliebige Zeichenfolge; es brauchen keine Ziffern zu sein.
1.5 Zeicheneing und Ausgabe 15 #include <stdio.h> «define LOWER 0 /* untere Grenze der Temperaturtabelle */ «define UPPER 300 /* obere Grenze */ «define STEP 20 /* Schrittbreite */ /* Umwandlung von Fahrenheit in Celsius */ mainO t int fahr; for (fahr * LOWER; fahr <= UPPER; fahr = fahr + STEP) printf(ßd X6.1f\n", fahr, E.0/9.0)*(fahr-32)); > Die Größen LOWER, UPPER und STEP sind symbolische Konstanten, keine Variablen, und erscheinen deshalb nicht in Vereinbarungen. Namen symbolischer Konstanten wer- werden üblicherweise in Großbuchstaben geschrieben, damit sie leicht von Variablennamen, in Kleinbuchstaben, unterschieden werden können. Man beachte, daß am Ende der #define-Zeile kein Semikolon steht. 1.5 Zeicheneingabe und Ausgabe Wir werden jetzt eine Reihe von Programmen betrachten, die alle Operationen mit Zeichen durchführen. Sie werden feststellen, daß viele Programme einfach aufwendigere Versionen der Prototypen sind, die wir hier diskutieren. Das Modell, das von der Standard-Bibliothek für Ein- und Ausgabe unterstützt wird, ist sehr einfach. Ein- oder Ausgabe von Text, unabhängig von wo er kommt oder wohin er geht, wird als Strom von Zeichen behandelt. Ein Zeichenstrom besteht aus ei- einer Folge von Zeichen, die in Zeilen unterteilt sind; jede Zeile besteht aus beliebig vielen Zeichen, eventuell auch keinen, denen ein Zeilentrenner folgt. Die Bibliothek muß dafür sorgen, daß jeder Ein- und Ausgabestrom sich an dieses Modell hält; der C-Programmie- rer, der die Bibliothek benutzt, braucht sich nicht darum zu kümmern, wie Zeilen außer- außerhalb des Programms repräsentiert werden. Die Standard-Bibliothek enthält mehrere Funktionen, die einzelne Zeichen lesen oder schreiben, getchar und putchar sind die einfachsten davon, getchar liefert bei je- jedem Aufruf das nächste Eingabezeichen vom Zeichenstrom und liefert es als Funktions- Funktionswert. Das heißt, nach der Zuweisung c - getcharO enthält die Variable c das nächste Eingabezeichen. Normalerweise werden diese Zeichen von der Tastatur angefordert; Eingabe aus Dateien wird in Kapitel 7 behandelt. Die Funktion putchar gibt jedesmal, wenn sie aufgerufen wird, ein Zeichen aus: putchar(c) gibt den Inhalt der int-Variablen c als Zeichen aus, normalerweise auf dem Bildschirm. Man kann Aufrufe von putchar mit Aufrufen von printf abwechseln; die Ausgabe er- erscheint immer in der Reihenfolge, in der die Funktionen aufgerufen wurden. 1.5.1 Dateien kopieren Überraschend viele nützliche Programme benötigen keine Ein- und Ausgaberouti- Ausgaberoutinen außer getchar und putchar. Das einfachste Beispiel ist vielleicht das Programm, das seine Eingabe zeichenweise in seine Ausgabe kopiert:
16 1 Eine irsicht in Beispielen ein Zeichen eingeben while ( Zeichen bedeutet nicht Dateiende > eingegebenes Zeichen ausgeben neues Zeichen eingeben Wenn man diesen Algorithmus in C formuliert, erhält man #ine lüde <stdio.h> /* Kopierprogramm, Version 1 */ main() C int c; c = getchar(); while (c != EOF) { putchar(c); c = getcharO,' Der Vergleichsoperator ! = bedeutet „nicht gleich". Was bei der Tastatur oder dem Bildschirm als Zeichen erscheint, ist natürlich, wie alles andere, intern als Bit-Muster abgespeichert. Der Typ char wird dafür verwendet, um Zeichen abzulegen, aber auch jeder andere ganzzahlige Typ kann dafür verwendet werden. Wir haben int aus einem kleinen, aber dafür umso wichtigeren Grund verwen- verwendet. Das Problem ist, das Ende der Eingabe zu erkennen. Zur Lösung des Problems liefert getchar am Ende der Eingabe einen bestimmten Funktionswert, der nicht mit ei- einem realen Zeichen verwechselt werden kann. Dieser Wert ist EOF. Der symbolische Name EOF steht für „end of file" oder „Eingabeende". Wir müssen c unbedingt mit ei- einem Typ vereinbaren, der groß genug ist, damit jeder Funktionswert von getchar zuge- zugewiesen werden kann. Wir können dazu char nicht benutzen, weil c zusätzlich zu allen möglichen char-Werten auch noch EOF darstellen muß. Deshalb benutzen wir den Typ int. EOF ist eine ganze Zahl, die in <stdio.h> vereinbart ist. Der spezielle numerische Wert spielt keine Rolle, solange er nicht mit einem char-Wert übereinstimmt. Da wir die symbolische Konstante verwenden, können wir sicher sein, daß nichts in unserem Pro- Programm von dem speziellen Wert abhängt. Erfahrene C-Programmierer würden das Kopierprogramm noch prägnanter for- formulieren. In C kann jede Zuweisung, wie zum Beispiel c = getcharO in Ausdrücken benutzt werden; der Wert einer Zuweisung ist jeweils gerade der Wert, der an die linke Seite zugewiesen wird. Das bedeutet, daß eine Zuweisung als Teil eines größeren Ausdrucks verwendet werden kann. Wenn man also die Zuweisung eines Zei- Zeichens an c in die Bedingung der while-Schleife aufnimmt, erhält man folgendes Kopier- Kopierprogramm:
1.5 Zeicheneing? md Ausgabe 17 tfinclude <stdio.h> /* Kopierprogramm, Version 2 */ mainO { int c; while ((c « getcharO) I* EOF) putchar(c); > Die Bedingung der while-Schleife liest ein Eingabezeichen, weist es an c zu und überprüft dann, ob gerade das Ende der Eingabe vorliegt. Ist dies nicht der Fall, wird die von while abhängige Anweisung ausgeführt, wird also das eingelesene Zeichen ausgegeben. Die while-Schleife wird dann wiederholt. Ist schließlich das Ende der Eingabe erreicht, wird die while-Schleife beendet und damit auch die main-Funktion. In dieser Version erfolgt die Eingabe zentral - wir benutzen getchar nur noch ein- einmal - und damit ist das Programm kürzer. Das Programm ist dann kompakter, und - wenn das Idiom verstanden ist - leichter lesbar. Sie werden diesen Stil oft sehen. (Man kann aber auch übertreiben und undurchdringlichen Programmtext schreiben; wir wollen das möglichst vermeiden.) Die Klammern um die Zuweisung innerhalb der Bedingung sind notwendig. Der Vorrang von != ist höher als der Vorrang der Zuweisung =. Dies bedeutet, daß ohne Klammern der Vergleich ! = vor der Zuweisung = ausgeführt würde. Das heißt, die For- Formulierung c » getcharO 1= EOF ist äquivalent zu c « (getcharO I« EOF) Dies hat den unerwünschten Effekt, daß c den Wert 0 oder 1 erhält, in Abhängigkeit da- davon, ob der Aufruf von getchar das Ende der Eingabe erreicht hat. (Davon mehr in Ka- Kapitel 2.) Aufgabe 1-6. Prüfen Sie nach, daß der Ausdruck getcharO != EOF den Wert 0 oder 1 liefert. D Aufgabe 1-7. Schreiben Sie ein Programm, das den Wert von EOF ausgibt. D 1.5.2 Zeichen zählen Das nächste Programm zählt Zeichen; es ist eine einfache Erweiterung des Kopier- Kopierprogramms: «include <stdio.h> /* Eingabezeichen zaehlen, Version 1 */ main() { long nc; nc « 0; while (getcharO !« EOF) ++nc; printf("Xld\n", nc);
18 1 Eine "rsicht in Beispielen Die Anweisung ++nc; zeigt einen neuen Operator ++, dieser bedeutet um 1 vergrößern. Wir könnten dies auch als nc = nc+1 formulieren, aber ++nc ist präziser und oft effizienter. Ein analoger Ope- rator — wird dazu benutzt, um um 1 zu verringern. Die Operatoren ++ und — können sowohl vor ihren Operanden (++nc) als auch hinter ihren Operanden (nc++) stehen; wie in Kapitel 2 noch erklärt wird, liefern diese beiden Arten verschiedene Werte in Aus- Ausdrücken, aber beide inkrementieren nc. Wir werden vorläufig diese Operatoren vor ihre Operanden stellen. Wir zählen die Zeichen mit Hilfe einer long-Variablen anstelle einer int-Variablen. Ganze Zahlen vom Typ long werden mit mindestens 32 Bits repräsentiert. Obwohl auf einigen Systemen int und long gleich repräsentiert werden, wird auf anderen int mit 16 Bits repräsentiert, mit einem Maximalwert von nur 32767; man würde nur sehr kleine Eingabedateien benötigen, um unseren Zahler vom Typ int zum Überlaufen zu bringen. Ein Formatelement %Id gehört bei printf zu einem long-Argument. Möglicherweise könnten wir noch größere Zahlenwerte darstellen, indem wir einen Zähler vom Typ double (also eine doppelt genaue float-Variable) benutzen. Wir wollen auch noch eine for-Schleife anstelle von while benutzen, um eine andere Schreibweise für die Schleife zu demonstrieren, «include <stdio.h> /* Eingabezeichen zaehlen, Version 2 */ mainO { double nc; for (nc = 0; getcharO 1= EOF; ++nc) printfC'X.OAn", nc); ) printf verwendet das Formatelement %f sowohl für float als auch für double-Argumente; %.0f sorgt dafür, daß die Ausgabe keinen Dezimalpunkt und keine Dezimalstellen enthält, da diese Null sind. Eigentlich hängen von der for-Schleife keine Anweisungen ab, da alle nötigen Be- Berechnungen innerhalb der Bedingung und im Inkrementierungsteil erfolgen. Die Gram- Grammatikregeln von C verlangen allerdings, daß von for grundsätzlich eine Anweisung ab- abhängt. Das einzelne Semikolon, eine sogenannte leere Anweisung, sorgt dafür, daß diese Regel befolgt wird. Ein solches isoliertes Semikolon schreiben wir auf einer separaten Zeile, damit es sofort sichtbar ist. Wir sollten bei dem Zählprogramm noch folgendes beachten: Enthält die Eingabe keine Zeichen, dann ist die while- oder for-Bedingung bereits beim ersten Aufruf von getchar nicht erfüllt, das Programm liefert also den Wert 0, den korrekten Wert. Dies ist sehr wesentlich. An while- und for-Schleifen ist sehr praktisch, daß die Bedingung am Anfang der Schleife geprüft wird, bevor die Ausführung der abhängigen Anweisungen beginnt. Gibt es nichts zu tun, dann wird auch nichts getan, auch wenn damit die abhän- abhängigen Anweisungen gar nicht ausgeführt werden. Programme sollten auch dann vernünf-
1.5 Zeicheneing und Ausgabe 19 tig reagieren, wenn sie „keine" Eingabe erhalten. Die while- und for-Schleifen helfen, bei Grenzbedingungen vernünftig zu reagieren. 1.53 Zeilen zählen Das nächste Programm zählt die Eingabezeilen. Wie vorher erwähnt, sorgt die Standard-Bibliothek dafür, daß ein Eingabetextstrom als Folge von Zeilen erscheint, die jeweils durch einen Zeilentrenner \n beendet sind. Folglich muß man nur die Zeilen- Zeilentrenner zählen. #ine lüde <stdio.h> /* Eingabezeilen zaehlen */ mainO { int c, nl; nl = 0; while ((c - getcharO) 1 = EOF) if (c « '\n') ++nl; printf("Xd\n", nl); > Von while hängt nunmehr eine if-Anweisung ab, die ihrerseits den Zählvorgang ++nl kontrolliert. Eine if-Anweisung überprüft eine Bedingung, die in Klammern einge- eingeschlossen ist. Trifft die Bedingung zu, so wird die abhängige Anweisung (oder eine Reihe von Anweisungen, die in geschweifte Klammern { } eingeschlossen sind) ausgeführt. Wir haben wiederum eingerückt, um zu zeigen, wie die Anweisungen voneinander abhängen. Das doppelte Gleichheitszeichen == drückt in C einen Vergleich auf Gleichheit aus (analog zum einfachen = in Pascal oder .EQ. in Fortran). Das Symbol unterscheidet den Vergleich von der Zuweisung = in C. Vorsicht: C-Neulinge schreiben gelegentlich = und meinen ——. Wie wir in Kapitel 2 sehen werden, ist das Resultat normalerweise ein legaler Ausdruck, also werden Sie keine Warnung bekommen. Ein Zeichen, das in einfache Anführungszeichen eingeschlossen ist, repräsentiert einen ganzzahligen Wert, der dem numerischen Wert des Zeichens im Zeichensatz des Rechners entspricht. Wir bezeichnen dies als Zeichenkonstante, obwohl das nur eine an- andere Schreibweise für kleine ganze Zahlen ist. Beispielsweise ist 'A' eine Zeichenkon- Zeichenkonstante; im ASCH-Zeichensatz ist dies der Wert 65, eben die Repräsentierung des Buchsta- Buchstabens A. Natürlich bevorzugen wir W und nicht 65. Im ersten Fall ist die Bedeutung offen- offensichtlich, und außerdem ist der Wert von einem bestimmten Zeichensatz unabhängig. Die Darstellungen mit Gegenschrägstrich, die wir für konstante Zeichenketten ein- eingeführt haben, sind auch bei Zeichenkonstanten erlaubt. In Bedingungen und arithmeti- arithmetischen Ausdrücken steht daher '\n' für den Wert des Zeilentrenner-Zeichens. Der Wert ist 10 in ASCII. Sie sollten genau beachten, daß '\n' ein Einzelzeichen darstellt und in Ausdrücken für einen einfachen ganzzahligen Wert steht; "\n" andrerseits ist eine Zei- Zeichenkette, die zufälligerweise nur ein einzelnes Zeichen enthält, eben den Zeilentrenner. Der Unterschied zwischen Zeichenketten und einzelnen Zeichen wird in Kapitel 2 weiter ausgeführt. Aufgabe 1-8. Schreiben Sie ein Programm, das Leerzeichen, Tabulatorzeichen und Zei- Zeilentrenner zählt. D
20 1 Eine ersieht in Beispielen Aufgabe 1-9. Schreiben Sie ein Kopierprogramm, das Folgen von Leerzeichen in der Eingabe durch ein einzelnes Leerzeichen in der Ausgabe ersetzt. D Aufgabe 1 -10. Schreiben Sie ein Programm, das die Eingabe zur Ausgabe kopiert und dabei jedes Tabulatorzeichen durch \t ersetzt, jeden backspace durch \b und jeden Ge- Gegenschrägstrich durch \\. Dadurch werden Tabulatorzeichen und backspace -Zeichen ein- eindeutig sichtbar. Q 1.5.4 Wörter zählen Unser viertes nützliches Programm zählt Zeilen, Wörter und Zeichen, wobei wir die einfache Definition verwenden, daß ein Wort jede Zeichenfolge ist, die weder Leer- Leerzeichen noch Tabulatorzeichen oder Zeilentrenner enthält. (Dies ist eine sehr primitive Version des UNIX-Dienstprogramms wc.) «include <stdio.h> «define IN 1 /• in einem Wort •/ «define OUT 0 /* ausserhalb eines Wortes */ /* Zeilen, Worte und Zeichen zaehlen */ mainO C int c, nl, nw, nc, state; state = OUT; nl » nw * nc = 0; while ((c = getcharO) Is EOF) { ++nc; if (c «= '\n') ++nl; if (c « ■ ' || c « '\n' || c « '\t') state » OUT; else if (state == OUT) { state » IN; ++nw; > > printfC'Xd Xd %d\n", nl, nw, nc); > Jedesmal, wenn dieses Programm das erste Zeichen in einem Wort erkennt, wird das Wort gezählt. Die Variable State hält fest, ob das Programm im Augenblick ein Wort oder einen Zwischenraum liest; am Anfang ist das Programm „nicht in einem Wort", da- dazu gehört der Wert OUT. Wir bevorzugen die symbolischen Konstanten IN und OUT und nicht die Zahlenwerte 1 und 0, da durch die symbolischen Konstanten das Programm leichter lesbar wird. In so einem kleinen Programm macht das kaum einen Unterschied, aber in größeren Programmen lohnt sich im Interesse leichterer Verständlichkeit der ge- geringe zusätzliche Aufwand, von vornherein so zu formulieren. Sie werden auch feststel- feststellen, daß umfangreiche Änderungen in Programmen leichter vorzunehmen sind, in denen magische Zahlenwerte nur als symbolische Konstanten eingeführt werden. Die Anweisung nl = nw = nc « 0; weist allen drei Variablen den Wert 0 zu. Dies ist kein Sonderfall, sondern eine Konse- Konsequenz der Tatsache, daß jede Zuweisung ein Ausdruck ist, und somit einen Wert besitzt,
1.6 Vektoren 21 und daß Zuweisungen von rechts nach links ausgeführt werden. Wir könnten dies auch so formulieren: nl » (nw = (nc = 0)); Der Operator 11 bedeutet ODER, also bedeutet die if-Bedingung if (c « • ' || c =« '\n' || c == '\t") „wenn c ein Leerzeichen ist oder c ein Zeilentrenner ist oder c ein Tabulatorzeichen ist". (Zur Erinnerung: die Darstellung mit Gegenschrägstrich \t ist eine grafische Repräsen- Repräsentierung des Tabulatorzeichens.) Analog gibt es einen Operator && für UND; && hat Vor- Vorrang vor 11. Ausdrücke, die mit && oder 11 verknüpft sind, werden von links nach rechts bewertet; dabei wird die Bewertung abgebrochen, sobald feststeht, ob die Verknüpfung zutrifft oder nicht. Wenn also c ein Leerzeichen ist, muß nicht mehr weiter untersucht werden, ob ein Zeilentrenner oder ein Tabulatorzeichen vorliegt, folglich werden diese Vergleiche nicht ausgeführt. Dies ist hier nicht besonders wichtig, aber in komplizierte- komplizierteren Bedingungen ist es sehr wesentlich, wie wir bald sehen werden. Das Beispiel demonstriert auch eine eise-Anweisung, die genau dann ausgeführt wird, wenn die Bedingung in einer if-Anweisung nicht zutrifft. Allgemein hat if-else fol- folgende Form: if ( expression ) statement \ else statementi Genau eine der zwei Anweisungen innerhalb der if-else-Konstruktion wird ausgeführt. Trifft expression zu, so wird statement\ ausgeführt; trifft expression nicht zu, dann wird statementi ausgeführt. Jedes Statement kann eine einzelne Anweisung oder eine Gruppe von Anweisungen in geschweiften Klammern sein. Unser Programm zum Zählen von Wörtern zeigt ein eise, von dem ein if abhängt, das seinerseits zwei Anweisungen kontrol- kontrolliert. Aufgabe 1-11. Wie würden Sie das Wörterzählprogramm überprüfen? Welche Art der Eingabe wird am ehesten Fehler aufdecken, falls welche vorhanden sind? D Aufgabe 1-12. Schreiben Sie ein Programm, das seine Eingabe ausgibt, und zwar ein Wort pro Zeile. D 1.6 Vektoren Unser nächstes Programm zählt die einzelnen Ziffern, Zwischenraumzeichen (also Leerzeichen, Tabulatorzeichen und Zeilentrenner) und alle anderen Zeichen. Dies ist natürlich eine künstliche Aufgabenstellung, aber wir können eine Reihe von C-Sprachele- menten in einem Programm zeigen. Die Eingabezeichen fallen in zwölf Kategorien. Deshalb benutzen wir lieber einen Vektor, um die einzelnen Ziffern zu zählen, und nicht zehn verschiedene Variablen. Hier ist eine Version eines solchen Programms:
22 1 Eine ersieht in Beispielen #ine lüde <stdio.h> /* Ziffern, Zwischenraum und andere Zeichen zaehlen */ main() t int c, i, nwhite, nother; int ndigit[101; nwhite = nother « 0; for (i = 0; i < 10; ++i) ndigitti] = 0; while C(c = getcharO) I- EOF) if (c >= '0' && c <= '9') ♦♦ndigittc-'O']; else if (c *= ' ' || c « '\n' || c == '\t') ++ nwhite; eise ++ nother; printf("digits ="); for (i = 0; i < 10; ++i) printfC %d", ndigitti]); printfC, white space = %d, other = %d\n", nwhite, nother); > Wendet man das Programm auf sich selbst an, erhält man folgende Ausgabe:* digits =930000000 1, white space = 126, other = 359 Die Definition int ndigit [10]; vereinbart ndigit als Vektor mit zehn ganzzahligen Werten. Vektorindizes beginnen in C immer bei 0, also sind die Elemente ndigit[0], ndigit[l], ndigit[9]. Dies zeigt sich auch in den for-Schleifen, die diesen Vektor initialisieren und später ausgeben. Ein Index kann ein beliebiger ganzzahliger Ausdruck sein, also insbesondere eine int-Variable wie i oder eine ganzzahlige Konstante. Dieses Programm hängt kritisch von einigen Eigenschaften der Repräsentierung von Ziffern ab. Beispielsweise entscheidet die Bedingung if Cc >= '0' && c <= '9') ob das Zeichen in c eine Ziffer ist. Wenn ja, dann ist ihr numerischer Wert c - '0' Dies funktioniert nur, wenn '0', '1', '9' aufeinanderfolgende, aufsteigende Werte sind. Glücklicherweise gilt dies in allen Zeichensätzen. Nach Definition sind char-Werte einfach kleine ganze Zahlen, folglich kann man char-Variablen und -Konstanten exakt wie int in arithmetischen Ausdrücken verwenden. Dies ist eine naheliegende und bequeme Konvention; beispielsweise ist der Ausdruck c- '0* ganzzahlig mit einem Wert zwischen 0 und 9; dieser Wert korrespondiert zur Ziffer '0' bis *9', die in c gespeichert ist, und kann deshalb als Index für den Vektor ndigit verwen- verwendet werden. * Tabulatorzeichen sind im Buch als vier Leerzeichen dargestellt, zahlen aber als ein Zeichen. A.d.Ü.
1.7 Funktionen 23 Die Folge von Anweisungen if (c >= '0' && c <= '9') ++ndigit[c-'O']; else if (c == ' ' || c == '\n' || c == '\t') ++ nwhite; else ++ nother; entscheidet, ob ein Zeichen eine Ziffer ist, ein Zwischenraum oder ein anderes Zeichen. Das Muster if ( conditioni ) statement i else if ( conditioni ) statement 2 else statementn kommt häufig in Programmen vor, um eine Auswahl aus mehreren Fällen zu treffen. Von oben angefangen, wird eine Bedingung nach der anderen bewertet, bis eine condition zutrifft; dann wird der zugehörige Anweisungsteil Statement ausgeführt, und die Ausführung der gesamten Konstruktion ist beendet. (Jedes Statement kann natürlich wie- wieder aus mehreren Anweisungen innerhalb von geschweiften Klammern bestehen.) Ist keine Bedingung erfüllt, so wird die Anweisung nach dem abschließenden eise aus- ausgeführt, sofern eines in der Konstruktion vorhanden ist. Fehlen ein abschließendes eise und eine davon abhängige Anweisung, wie in unserem Wörterzählprogramm, so findet keine Aktion statt. Zwischen dem einführenden if und dem abschließenden eise kann ei- eine beliebige Anzahl von else if ( condition ) statement Konstruktionen stehen. Stilistisch erscheint es ratsam, diese Konstruktion so anzuord- anzuordnen, wie wir vorgeführt haben; wird jedes if gegen das vorhergehende eise eingerückt, wandert eine lange Folge von Entscheidungen nach rechts über die Seite hinaus. Im Kapitel 3 diskutieren wir die switch-Anweisung. Dies ist eine andere Möglich- Möglichkeit, eine Auswahl unter vielen Fällen zu treffen, switch ist besonders dann angebracht, wenn geprüft werden muß, ob der Wert eines int- oder char-Ausdrucks einer von einer Reihe von Konstanten entspricht. Als Kontrast werden wir im Abschnitt 3.4 das vorlie- vorliegende Programm nochmals mit switch formulieren. Aufgabe 1 -13. Schreiben Sie ein Programm, das ein Histogramm der Längen der einge- eingegebenen Wörter ausgibt. Es ist am einfachsten, dieses Histogramm horizontal anzuord- anzuordnen; eine vertikale Anordnung erfordert etwas höheren Aufwand. D Aufgabe 1 -14. Schreiben Sie ein Programm, das die Häufigkeit von verschiedenen Ein- Eingabezeichen als Histogramm darstellt. G 1.7 Funktionen In C ist eine Funktion äquivalent zu einem Unterprogramm oder zu einer Funktion in Fortran oder zu einer Prozedur oder Funktion in Pascal. Eine Funktion ist bequem, um eine Berechnung einzukapseln, die dann aufgerufen werden kann, ohne daß man sich
24 1 Ei- Jbersicht in Beispielen um ihre Implementierung kümmern muß. Mit vernünftig entworfenen Funktionen kann man ignorieren, wie eine Aufgabe gelöst wird; es genügt zu wissen, was getan wurde. In C ist der Gebrauch von Funktionen leicht, bequem und effizient; Sie werden oft sehen, daß eine Funktion definiert und nur einmal aufgerufen wird, einfach weil dadurch ein Stück Programmtext klarer wird. Bisher haben wir nur Funktionen wie printf, getchar und putchar benutzt, die uns zur Verfügung gestellt wurden; es ist jetzt an der Zeit, einige eigene Funktionen zu ent- entwickeln. Da C keinen Potenz-Operator hat wie •• in Fortran, wollen wir eine Funktions- Funktionsdefinition anhand der Funktion power(m,n) zeigen, die einen ganzzahligen Wert m mit einer positiven ganzzahligen Zahl n potenziert. Das heißt, der Wert von powerB,5) ist 32. Diese Funktion ist sicher keine praktische Implementierung von ••, da sie nur positi- positive Potenzen von kleinen Zahlen bearbeiten kann, aber sie ist zur Illustration gut genug. (Die Standard-Bibliothek enthält eine Funktion pow(x,y), die xy berechnet.) Wir zeigen hier die Funktion power und ein Hauptprogramm, um sie auszuführen, damit Sie die gesamte Struktur auf einmal sehen. #ine lüde <stdio.h> int power(int m, int n); /* power-Funktion ausprobieren */ mainO { int i; for (i = 0; i < 10; ++i) printf("%d %d %d\n", i, powerB,i), power(-3,i)); return 0; > /* power: base hoch n; n >= 0 */ int power(int base, int n) < < int i, p; P= 1; for (i = 1; i <= n; p = p * base; return p;' > Eine Funktionsdefinition hat folgende Form: Resultattyp Funktionsname( evtl. Panuneterdefinitionen ) < Vereinbarungen Anweisungen > Funktionen können in beliebiger Reihenfolge und auch in einer oder mehreren Quellda- Quelldateien definiert werden, eine einzelne Funktion kann jedoch nicht auf mehrere Dateien verteilt werden. Wenn das Quellprogramm in mehreren Dateien formuliert wird, werden Sie möglicherweise einiges tun müssen, um das Programm zu übersetzen und zu montie- montieren, aber das hängt mit dem Betriebssystem zusammen und ist keine Eigenschaft der Programmiersprache. Wir nehmen deshalb vorläufig an, daß beide Funktionen in einer
1.7 Funktioner 25 Datei stehen; damit ändert sich nichts von dem, was Sie bisher über den Umgang mit C- Programmen wissen. Die Funktion power wird von main in der Zeile printf("%d %d %d\n", i, powerB,i), power(-3,i)); zweimal aufgerufen. Jeder Aufruf übergibt zwei Argumente an power, und die Funktion liefert jedesmal einen ganzzahligen Wert, der umgewandelt und ausgegeben werden soll. In einem Ausdruck ist powerB,i) ein ganzzahliger Wert, genau wie 2 und i. (Nicht alle Funktionen liefern ein ganzzahliges Resultat; damit beschäftigen wir uns in Kapitel 4.) Die erste Zeile der Funktion power selbst int power(int base, int n) definiert die Datentypen und Namen der Parameter und den Typ des Resultats, das die Funktion liefert. Die Namen der Parameter zu power sind innerhalb von power nur lokal bekannt, sie sind für andere Funktionen nicht sichtbar: Andere Funktionen können die gleichen Namen ohne Konflikte benutzen. Dies gilt auch für die lokalen Variablen i und p: Die Variable i in power ist unabhängig von der Variablen i in main. Wir bezeichnen als Parameter eine Variable, die in der Aufzählung zwischen den runden Klammern in der Funktionsdefinition aufgeführt ist, und wir nennen Argument den Wert, der beim Aufruf der Funktion verwendet wird. Manchmal findet man dafür auch die Begriffe Formalparameter und Aktualparameter. power berechnet einen Wert und liefert ihn an main mit Hilfe der return- Anweisung. Nach return kann ein beliebiger Ausdruck stehen: return expression ; Eine Funktion muß keinen Resultatwert liefern; eine return-Anweisung ohne einen nachfolgenden Ausdruck beendet die Ausführung einer Funktion, liefert aber keinen nützlichen Wert an den Aufrufer. Gleiches gilt, wenn das Ende des Programmtexts einer Funktion, also die abschließende geschweifte rechte Klammer, erreicht wird. An der auf- aufrufenden Stelle darf der Wert, den eine Funktion liefert, ignoriert werden. Sie haben sicher bemerkt, daß am Ende von main eine return-Anweisung steht. Da main eine Funktion wie jede andere ist, kann sie einen Wert an den Aufrufer zurück- zurückliefern. Dieser Wert wird an die Umgebung geliefert, in der das Programm ausgeführt wurde. Typischerweise bedeutet der Wert Null, daß das Programm normal beendet wur- wurde; andere Werte als Null deuten auf ungewöhnlichen oder fehlerhaften Abbruch. Zur Vereinfachung haben wir in unseren main-Funktionen bisher keine return-Anweisungen verwendet. Wir werden sie im folgenden hinzufügen, um daran zu erinnern, daß Pro- Programme ihrer Umgebung ein Resultat zurückliefern sollten. Die Deklaration int power(int m, int n); direkt vor main vereinbart, daß power eine Funktion ist, die zwei int-Argumente erwartet und int als Resultat liefert. Diese Deklaration, ein sogenannter Funktionsprototyp (oder kurz Prototyp), muß mit der Definition und den Aufrufen von power übereinstimmen. Es ist ein Fehler, wenn Definition oder Benutzung einer Funktion nicht mit ihrem Prototyp übereinstimmen.
26 1 EU bersicht in Beispielen Die Parameternamen müssen in Prototyp und Funktionskopf nicht übereinstim- übereinstimmen. Tatsächlich sind die Parameternamen bei einem Funktionsprototyp optional, und wir hätten int powerCint, int); schreiben können. Sinnvoll gewählte Namen sind jedoch eine gute Dokumentation, des- deshalb werden wir sie oft benutzen. Ein Hinweis auf die Geschichte: Die größte Änderung zwischen ANSI-C und frühe- früheren Versionen betrifft Deklaration und Definition von Funktionen. In der ursprünglichen Definition von C hätte man die power-Funktion folgendermaßen geschrieben: /* power: base hoch n; n >= 0 */ /* (Version im alten Stil) */ power(base, n) int base, n; < int i, p; P= 1; for (i = 1; i <= n; ++i) p = p * base; return p; > Die Parameternamen wurden in den Klammern angegeben, und ihr Typ wurde vor der öffnenden geschweiften Klammer deklariert; für nicht deklarierte Parameter wurde der Typ int angenommen. (Der Funktionsrumpf bleibt unverändert.) Die Deklaration von power am Anfang des Programms hätte folgendermaßen aus- ausgesehen: int powerO; Eine Aufzählung der Parameter war nicht erlaubt; folglich konnte der Übersetzer nicht ohne weiteres prüfen, ob power richtig aufgerufen wurde. Tatsächlich nahm man als Voreinstellung an, daß power ein int-Resultat liefert, folglich hätte man die ganze Dekla- Deklaration häufig weggelassen. Die neue Syntax für Funktionsprototypen macht es dem Übersetzer viel einfacher, Fehler bei Anzahl oder Typ der Argumente zu entdecken. Der alte Stil der Deklaration und Definition funktioniert in ANSI-C noch, wenigstens für eine Übergangszeit. Wir empfehlen jedoch dringend, daß Sie den neuen Stil benutzen, wenn Sie einen Übersetzer haben, der ihn unterstützt. Aufgabe 1 -15. Schreiben Sie das Programm zur Temperaturumwandlung aus Abschnitt 1.2 so um, daß eine Funktion zur Umwandlung verwendet wird. D 1.8 Argumente - Wertübergabe Ein Aspekt von C-Funktionen mag Programmierer überraschen, die an manche an- andere Sprachen gewöhnt sind, insbesondere an Fortran. In C werden Argumente an Funktionen grundsätzlich als Werte übergeben. Dies bedeutet, daß die aufgerufene Funk- Funktion die Argumentwerte in temporären Variablen erhält und nicht die ursprünglichen Ar- Argumente selbst. C verhält sich dadurch etwas anders als Sprachen, bei denen Verweise übergeben werden wie in Fortran, oder als var-Parameter in Pascal. Dort hat die aufge-
1.9 Zeichenvekl ^i 27 rufene Funktion Zugriff auf das ursprüngliche Argument und nicht nur auf eine lokale Kopie. Der wesentliche Unterschied ist, daß in C die aufgerufene Funktion eine Variable in der aufrufenden Funktion nicht ändern kann; die aufgerufene Funktion kann nur den privaten, temporären Wert ändern. Wertübergabe ist jedoch ein Vorteil, kein Nachteil. Wertübergabe führt normaler- normalerweise zu kompakteren Programmen mit weniger zusätzlichen Variablen, da Parameter in der aufgerufenen Routine als zweckmäßig initialisierte, lokale Variablen benutzt werden können. Hier ist beispielsweise eine Variante von power, die diese Eigenschaft ausnützt: /* power: base hoch n; n>=0; Version 2 */ int power(int base, int n) < int p; for (p = 1; n > 0; —n) p = p * base; return p; > Der Parameter n wird als temporäre Variable benutzt. Mit n wird rückwärts gezählt (ei- (eine for-Schleife, die rückwärts läuft), bis der Wert 0 erreicht ist; wir benötigen die Varia- Variable i nicht mehr. Unabhängig davon, was wir innerhalb von power mit dem Parameter n anstellen, bleibt das Argument unverändert, mit dem power ursprünglich aufgerufen wur- wurde. Falls nötig, können wir auch dafür sorgen, daß eine Funktion eine Variable in der aufrufenden Funktion ändern kann. Der Aufrufer muß die Adresse der Variablen liefern (aus technischer Sicht einen Zeigerwert für die Variable), und die aufgerufene Funktion muß den Parameter als Zeiger deklarieren und die eigentliche Variable mit Hilfe des Zeigerwerts erreichen. Wir werden Zeiger in Kapitel 5 besprechen. Für Vektoren ist die Sache anders. Wird der Name eines Vektors als Argument übergeben, dann erhält die Funktion die Adresse des ersten Elements im Vektor - Vek- Vektorelemente werden nicht kopiert. Wird zum übergebenen Adreßwert ein Index hinzu- hinzugefügt, so kann die Funktion beliebige Vektorelemente erreichen und ändern. Dies wird im nächsten Abschnitt behandelt. 1.9 Zeichenvektoren Die häufigsten Vektoren in C sind Vektoren von Zeichen. Wir wollen Zeichenvek- Zeichenvektoren und ihre Manipulation illustrieren, indem wir ein Programm schreiben, das eine Reihe von Textzeilen einliest und die längste Zeile ausgibt. Die Programmstruktur ist einfach genug: white ( es gibt noch eine Zeile ) if ( sie ist länger als die bisher längste ) Zeile speichern Zeilenlänge speichern längste Zeile ausgeben Diese Skizze macht deutlich, daß das Programm auf natürliche Weise in Teile zerfällt. Eine Funktion liest eine neue Zeile ein, eine andere Funktion überprüft sie, eine weitere speichert sie ab, und der Rest des Programms kontrolliert dieses Vorgehen.
28 1 Eine Übt ;ht in Beispielen Da sich das Problem so leicht strukturieren läßt, sollten wir die einzelnen Funktio- Funktionen auch so formulieren. Schreiben wir also zunächst eine separate Funktion getline um die nächste Zeile zu lesen. Wir werden versuchen, getline so zu gestalten, daß die Funk- Funktion auch für andere Anwendungen nützlich ist. getline sollte mindestens anzeigen, wenn das Eingabeende erreicht ist; nützlicher wäre es, wenn getline die Länge der gelesenen Zeile liefert oder Null am Dateiende. Null ist ein akzeptabler Wert für das Dateiende, da Null nie mit einer gültigen Zeilenlänge verwechselt werden kann. Jede Zeile enthält wenigstens ein Zeichen; auch eine Zeile, die nur aus einem Zeilentrenner besteht, hat die Länge 1. Finden wir eine Zeile, die länger ist als alle vorhergehenden Zeilen, so müssen wir diese neue Zeile irgendwo abspeichern. Dies führt zu einer neuen Funktion copy, um die neue Zeile sicher aufzubewahren. Schließlich benötigen wir ein Hauptprogramm, um getline und copy zu überwa- überwachen. Hier ist das Resultat: «include <stdio.h> «define MAXLINE 1000 /* maximale Laenge einer Eingabezeile */ int getline(char lineü, int max line); void copy(char to[], char from[]); /* laengste Eingabezeile ausgeben */ mainO int len; /* Laenge der momentanen Eingabezeile */ int max; /* bisheriges Maximum */ char line[MAXLINE]; /* momentane Eingabezeile */ char longest[MAXLINE]; /* bisher laengste Zeile */ max = 0; while (den = getline(line, MAXLINE)) > 0) if den > max) < max = len; copydongest, tine); if (max > 0) /* es gab ueberhaupt eine Zeile */ printfC'Xs", longest); return 0; /* getline: Zeile an s, Laenge als Resultat */ int getline(char s[], int lim) int c, i; for <i=0; i<lim-1 && <c=getchar())l=EOF && cl='\n'; ++i) s[i] = c; if (c == '\n') < s[i] = c; sti] = '\0'; return i; >
1.9 Zeichenvektoren 29 /* copy: 'from' nach 'to' kopieren; 'to' muss gross genug sein */ void copy(char to[], char front]) { int i; i - 0; white ((toti) = frorati]) != '\0') Die Funktionen getline und copy sind am Anfang des Programms deklariert; wir nehmen an, daß für das Programm eine einzige Datei verwendet wird. main und getline liefern sich Information mit Hilfe von zwei Argumenten und ei- einem Resultatwert. In getline werden die Parameter in der Zeile int getline(char s[], int lim) deklariert, die festlegt, daß der erste Parameter s ein Vektor ist und der zweite, lim, ein ganzzahliger Wert. Wenn die Größe eines Vektors in einer Vereinbarung angegeben wird, bezweckt man damit, Speicherplatz anzulegen. Die Länge des Vektors s muß in getline nicht vereinbart werden, da sie in main festgelegt wird, getline liefert ein Funkti- Funktionsergebnis mit Hilfe einer return-Anweisung, wie das auch die power-Funktion tat. Diese Zeile legt auch fest, daß getline ein int-Resultat liefert; da int die Voreinstellung ist, hätte man den Typnamen weglassen können. Manche Funktionen liefern ein nützliches Ergebnis; andere, wie zum Beispiel copy, führen nur gewisse Aktionen aus und liefern keinen Resultatwert. Der Resultattyp von copy ist void; dadurch wird explizit angegeben, daß kein Resultatwert geliefert wird. getline stellt das Zeichen '\0' (das Nulbeichen, dessen Wert 0 ist) an das Ende des erzeugten Vektors, um so das Ende der Zeichenkette zu markieren. Diese Konvention wird auch in C befolgt: Erscheint eine konstante Zeichenkette wie "heUo\n" in einem C-Programm, dann wird dies als Vektor von Zeichen abgelegt, der die Zeichen aus der Zeichenkette enthält und am Ende ein '\0'. h | e t t 0 \n \0 Das Formatelement %s bei printf erwartet als Argument eine Zeichenkette, die in dieser Form gespeichert ist. Auch copy geht davon aus, daß sein Eingabeargument mit '\0' ab- abgeschlossen ist, und kopiert dieses Zeichen in das Ausgabe-Argument. (Aus all diesem folgt, daß '\0' in gewöhnlichem Text nicht vorkommt.) Im Vorbeigehen sollten wir erwähnen, daß selbst ein so kleines Programm wie die- dieses einige diffizile Entwurfsprobleme verursacht: Was sollte main beispielsweise tun, wenn eine Eingabezeile geliefert wird, die länger ist als angenommen? getline funktio- funktioniert korrekt, das heißt, getline sammelt keine weiteren Zeichen, wenn der Zeichenvek- Zeichenvektor gefüllt ist, und dies auch dann, wenn kein Zeilentrenner-Zeichen gefunden wird. Um festzustellen, ob die Eingabezeile zu lang ist, kann main die Zeilenlänge und das letzte Zeichen überprüfen und dann beliebig verfahren. Um unser Beispiel überschaubar zu halten, haben wir dieses Problem ignoriert.
30 1 Eint »ersieht in Beispielen Ruft man getline auf, so weiß man im voraus nicht, wie lang eine Eingabezeile sein kann; folglich schützt getline gegen Überschreiben des Argumentvektors. Andrerseits weiß der Aufrufer der copy-Funktion bereits (oder kann es jedenfalls herausfinden), wie lang die beteiligten Zeichenketten sind. Wir haben daher beschlossen, keine Fehlerbe- Fehlerbehandlung in copy vorzunehmen. Aufgabe 1 -16. Ändern Sie das Hauptprogramm so ab, daß es für beliebig lange Zeilen die Länge und so viel Text als möglich korrekt ausgibt. D Aufgabe 1-17. Schreiben Sie ein Programm, das alle Eingabezeilen ausgibt, die länger als 80 Zeichen sind. D Aufgabe 1 -18. Schreiben Sie ein Programm, das Leerzeichen und Tabulatorzeichen am Ende von Eingabezeilen entfernt und das auch völlig leere Zeilen unterdrückt. D Aufgabe 1-19. Schreiben Sie eine Funktion reverse(s), die die Zeichenkette s umkehrt. Benutzen Sie diese Funktion dazu, ein Programm zu schreiben, das seine Eingabe zeilen- zeilenweise umkehrt. D 1.10 Externe Variablen und Gültigkeitsbereich Variablen in main wie line, longest etc., sind privat oder lokal in main. Da diese Variablen innerhalb von main vereinbart sind, hat keine andere Funktion auf sie direkten Zugriff. Gleiches gilt für die Variablen in anderen Funktionen; beispielsweise ist die Va- Variable i in getline völlig verschieden von der Variablen i in copy. Jede lokale Variable in einer Funktion wird nur erzeugt, wenn die Funktion aufgerufen wird und verschwindet, wenn die Funktion verlassen wird. Aus diesem Grund werden solche Variablen norma- normalerweise als automatische Variablen bezeichnet, entsprechend der Terminologie in ande- anderen Sprachen. Auch wir werden in Zukunft die Bezeichnung „automatisch" für diese lo- lokalen Variablen benutzen. (In Kapitel 4 wird die Speicherklasse static diskutiert, in der lokale Variablen ihre Werte zwischen Funktionsaufrufen beibehalten.) Da die Existenz von automatischen Variablen von Funktionsaktivierungen abhängt, behalten automatische Variablen ihre Werte von einem Funktionsaufruf zum nächsten nicht bei. Am Beginn einer Funktion müssen automatische Variablen jeweils explizit mit Werten belegt werden. Geschieht dies nicht, ist ihr Wert Undefiniert. Alternativ zu automatischen Variablen können Variablen definiert werden, die extern zu allen Funktionen sind, das heißt, Variablen, die per Namen in jeder Funktion beliebig verfügbar sind. (Dieser Mechanismus gleicht common in Fortran oder Varia- Variablen in Pascal, die im äußersten Block deklariert sind.) Da externe Variablen global ver- verfügbar sind, können sie anstelle von Argumenten dazu benutzt werden, Daten zwischen Funktionen zu übergeben. Außerdem existieren externe Variablen permanent, sie sind in ihrer Lebensdauer nicht abhängig von Funktionsaufrufen. Auch nach Abschluß eines Funktionsaufrufs behalten sie den Wert bei, der ihnen innerhalb der Funktion zugewie- zugewiesen wurde. Eine externe Variable muß definiert werden, genau einmal, außerhalb von allen Funktionen; dadurch wird ihr Speicherplatz reserviert. Die Variable muß auch in jeder Funktion deklariert werden, in der sie benutzt werden soll; dabei wird der Typ der Varia- Variablen angegeben. Eine solche Deklaration erfolgt entweder explizit mit einer extern- Anweisung, oder implizit durch Kontext. Um dieses Konzept zu verdeutlichen, wollen
1.10 Externe Va len und Gültigkeitsbereich 31 wir das Programm zur Erkennung der längsten Zeile so verändern, daß line, longest und max externe Variablen sind. Dazu müssen wir die Aufrufe, Deklarationen und Aktions- Aktionsteile aller drei Funktionen ändern. #ine lüde <stdio.h> «define HAXLINE 1000 /* maximale Laenge einer Eingabezeile */ int max; /* bisheriges Maximum */ char line[HAXLINE]; /* momentane Eingabezeile */ char longest[HAXLINE]; /* bisher laengste Zeile */ int getline(void); void copy(void); /* laengste Eingabezeile ausgeben; spezielle Version */ mainO { int len; extern int max; extern char longest[]; max = 0; while (den = getlineO) > 0) if (len > max) { max = len; copy(); > if (max > 0) /* es gab ueberhaupt eine Zeile */ printfC'Xs", longest); return 0; > /* gettine: spezielle Version */ int getline(void) < int c, i; extern char line[]; for (i = 0; i < HAXLINE-1 && (c=getchar()) != EOF && c != '\n'; ++i) line[i] = c; if (c == '\n') < tine[i] = c; ; '\0'; return i; /* copy: spezielle Version */ void copy(void) i nt i ; extern char line[], longest []; i = 0; while ((longest[i] = line[i]) != '\0') ++i; >
32 1 Eine ' ersieht in Beispielen Die externen Variablen in main, getline und copy werden in den ersten Zeilen die- dieses Beispiels definiert, hier wird der Datentyp festgelegt und Speicherplatz reserviert. Syntaktisch gleichen externe Definitionen den Definitionen von lokalen Variablen, aber da externe Definitionen außerhalb von Funktionen erfolgen, sind die Variablen extern. Eine Funktion kann eine externe Variable erst benutzen, wenn der Name dieser Varia- Variablen innerhalb der Funktion bekannt ist. Dies kann mit Hilfe einer extern-Deklaration in der Funktion erreicht werden; diese Deklaration ist identisch zur Definition, jedoch wird noch das reservierte Wort extern hinzugefügt. Unter bestimmten Umständen kann die extern-Deklaration unterbleiben. Er- Erscheint die Definition einer externen Variablen in der Quelldatei bevor die Variable in ei- einer Funktion benutzt wird, so braucht man keine extern-Deklaration in der Funktion. Die extern-Deklarationen in main, getline und copy sind daher redundant. Üblicherwei- Üblicherweise definiert man alle externen Variablen am Anfang der Quelldatei und benutzt dann kei- keine extern-Deklarationen. Besteht ein Programm aus mehreren Quelldateien und ist eine Variable etwa in der ersten Datei definiert, wird aber in der zweiten und dritten Datei benutzt, dann müs- müssen in der zweiten und dritten Datei extern-Deklarationen stehen, um die Verweise auf die Variable über die Quelldateien hinweg zu verbinden. Üblicherweise werden extern- Deklarationen von Variablen und Funktionen in einer separaten Datei gesammelt, einer sogenannten Definitionsdatei (header file), die dann mit einer #include-Anweisung am Anfang jeder Quelldatei vom C-Preprozessor eingefügt wird. Per Konvention wird die Endung .h für Definitionsdateien verwendet. Die Funktionen der Standard-Bibliothek werden zum Beispiel in Definitionsdateien wie <stdio.h> deklariert. Dieser Komplex wird ausführlich in Kapitel 4 diskutiert, die Bibliothek selbst wird in Kapitel 7 und An- Anhang B vorgestellt. Da die speziellen Versionen von getline und copy keine Argumente haben, sollten ihre Prototypen am Anfang der Datei logischerweise getline() und copy() sein. Zwecks Kompatibilität mit älteren C-Programmen sieht der Standard jedoch eine leere Aufzäh- Aufzählung als Deklaration vom „alten" Stil an und schaltet jegliche Überprüfung der Argu- Argumentliste ab; das Wort void muß für eine explizit leere Liste verwendet werden. Dies wird in Kapitel 4 weiter diskutiert. Beachten Sie, daß wir die Wörter Definition und Deklaration sehr sorgfältig benut- benutzen, wenn wir in diesem Abschnitt die Vereinbarung von externen Variablen diskutieren. Definition bezeichnet die Vereinbarung, in der für eine Variable Speicherplatz reserviert wird; Deklaration bezeichnet die Vereinbarungen, die zwar die Variable beschreiben, aber eben keinen Speicherplatz reservieren. Übrigens besteht eine Tendenz, alle nur denkbaren Variablen extern zu vereinba- vereinbaren, denn das vereinfacht anscheinend die Zusammenarbeit zwischen Funktionen - Pa- Parameterlisten sind kurz, und Variablen sind immer verfügbar, wenn wir sie benötigen. Aber externe Variablen sind immer verfügbar, sogar dann, wenn wir sie nicht benutzen wollen. Sich zu sehr auf externe Variablen zu verlassen, ist höchst gefährlich, denn es führt zu Programmen, deren Datenverbindungen absolut nicht offensichtlich sind - Va- Variablen können an unerwarteten Stellen und sogar unabsichtlich verändert werden, und das Programm ist sehr schwer zu ändern. Die zweite Fassung des Programms zur Erken-
1.10 Externe Var 'en und Gültigkeitsbereich 33 nung der längsten Zeile ist stilistisch schlechter als die erste, zum Teil aus den gerade er- erwähnten Gründen, und zum Teil, da die allgemeine Verwendbarkeit zweier nützlicher Funktionen dadurch eingeschränkt wird, daß in ihnen explizit die Namen der Variablen erscheinen, die beeinflußt werden. Bis hierher haben wir behandelt, was man den konventionellen Kern von C nennen könnte. Mit dieser Handvoll Sprachelemente kann man nützliche Programme von be- beachtlicher Größe realisieren, und es wäre wohl eine gute Idee, wenn Sie jetzt eine Pause einlegen würden, um dies auch zu tun. Die folgenden Aufgaben schlagen Ihnen Pro- Programme vor, die etwas aufwendiger sind als die in diesem Kapitel besprochenen. Aufgabe 1-20. Schreiben Sie ein Programm detab, das Tabulatorzeichen in der Eingabe durch die korrekte Anzahl von Leerzeichen ersetzt, um zur nächsten Tabulatorposition vorzurücken. Nehmen Sie dazu eine feste Anzahl von Tabulatorpositionen an, beispiels- beispielsweise alle n Spalten. Sollte n eine Variable oder eine symbolische Konstante sein? D Aufgabe 1-21. Schreiben Sie ein Programm entab, das Folgen von Leerzeichen durch die minimale Anzahl von Tabulatorzeichen und Leerzeichen ersetzt, um die gleichen Zwischenräume zu erzielen. Benutzen Sie die gleichen Tabulatorpositionen wie für detab. Wenn entweder ein Tabulatorzeichen oder ein einzelnes Leerzeichen genügen würde, um eine Tabulatorposition zu erreichen, was sollte bevorzugt werden? D Aufgabe 1-22. Schreiben Sie ein Programm, das lange Eingabezeilen in zwei oder mehr kürzere Zeilen teilt, und zwar nach dem letzten Zeichen in einem Wort, das noch ganz vor der Spalte n in der Eingabe steht. Sorgen Sie dafür, daß Ihr Programm sich auch für sehr lange Eingabezeilen vernünftig verhält, und daß auch Zeilen verarbeitet werden können, die keine Leerzeichen oder Tabulatorzeichen vor der angegebenen Spalte ent- enthalten. D Aufgabe 1-23. Schreiben Sie ein Programm, das alle Kommentare aus C-Programmen entfernt. Dabei sollten Sie nicht vergessen, Zeichenketten (in Doppelanführungszeichen) und Zeichenkonstanten (in einfachen Anführungszeichen) korrekt zu behandeln, o Aufgabe 1-24. Schreiben Sie ein Programm, das ein C-Programm auf elementare Syn- Syntaxfehler überprüft, wie zum Beispiel falsch verschachtelte runde Klammern, geschweifte Klammern und eckige Klammern. Vergessen Sie dabei nicht, Anführungszeichen, Dop- Doppelanführungszeichen und Kommentare korrekt zu behandeln. (Es ist nicht leicht, ein völlig allgemeingültiges Programm für diesen Zweck zu formulieren.) D
35 2 Datentypen, Operatoren und Ausdrücke Variablen und Konstanten sind die grundsätzlichen Datenobjekte, die ein Pro- Programm manipuliert. Vereinbarungen führen die Variablen ein, die benutzt werden sol- sollen, und legen fest, welchen Typ diese Variablen besitzen und vielleicht auch, welchen Anfangswert. Operatoren kontrollieren, was mit Werten geschieht. In Ausdrücken wer- werden Variablen und Konstanten mit Operatoren verknüpft, um neue Werte zu produzie- produzieren. Der Datentyp eines Objekts legt seine Wertemenge und die Operatoren fest, die darauf anwendbar sind. Von diesen Bausteinen handelt das vorliegende Kapitel. Im ANSI-Standard wurden viele kleine Änderungen und Erweiterungen an den ele- elementaren Typen und Ausdrücken vorgenommen. Es gibt nun alle ganzzahligen Datenty- Datentypen signed und unsigned, also mit oder ohne Vorzeichen, und es gibt Notationen für vor- vorzeichenlose Konstanten und für hexadezimale Zeichenkonstanten. Gleitpunktarithmetik kann auch mit einfacher Genauigkeit erfolgen; außerdem ist der Datentyp long double für höhere Genauigkeit verfügbar. Konstante Zeichenketten können während der Über- Übersetzung aneinandergehängt werden. Die schon lange existierenden Aufzählungen (enumerations) wurden auch formal in die Sprache aufgenommen. Objekte können mit const vereinbart werden, um zu verhindern, daß sie verändert werden können. Die Re- Regeln für automatische Umwandlungen zwischen den arithmetischen Typen wurden erwei- erweitert, um den umfangreicheren Satz an Datentypen abzudecken. 2.1 Variablennamen Obgleich wir das nicht im ersten Kapitel gesagt haben, gibt es Einschränkungen in bezug auf die Namen von Variablen und symbolischen Konstanten. Namen bestehen aus Buchstaben und Ziffern; dabei muß das erste Zeichen ein Buchstabe sein. Der Unter- Unterstrich „ " zählt als Buchstabe; dieses Zeichen ist manchmal nützlich, um die Lesbarkeit von langen Variablennamen zu verbessern. Das erste Zeichen eines Variablennamens sollte jedoch kein Unterstrich sein, da Bibliotheksfunktionen oft solche Namen verwen- verwenden. Groß- und Kleinbuchstaben werden unterschieden; somit sind x und X zwei ver- verschiedene Namen. Traditionellerweise verwendet man in C Kleinbuchstaben für Varia- Variablennamen und Großbuchstaben für symbolische Konstanten. Mindestens die ersten 31 Zeichen eines internen Namens sind signifikant. Bei ex- externen Namen, wie den Namen von Funktionen und globalen Variablen, werden unter Umständen auch weniger als 31 Zeichen unterschieden, denn externe Namen können von verschiedenen Assemblern und Ladern benutzt werden, auf die die Sprachdefinition kei- keinen Einfluß nehmen kann. Für externe Namen garantiert der Standard nur, daß minde- mindestens sechs Zeichen, aber dabei nicht unbedingt Groß- und Kleinbuchstaben, unterschie- unterschieden werden. Worte wie if, else, int, float usw. sind reserviert und können nicht als Varia- Variablennamen benutzt werden. Reservierte Worte müssen kleingeschrieben werden. Vernünftigerweise wählt man Variablennamen so, daß sie den Zweck einer Varia- Variablen andeuten, und daß eine Verwechslung durch Tippfehler unwahrscheinlich wird. Wir benutzen gern kurze Namen für lokale Variablen, insbesondere für Schleifenindizes, und längere Namen für externe Variablen.
36 2 Datentypen, O itoren und Ausdrücke 2.2 Datentypen und Speicherbedarf In C gibt es nur wenige elementare Datentypen: char ein Byte, kann ein Zeichen aus dem Zeichensatz der Maschine aufnehmen. int ein ganzzahliger Wert, üblicherweise in der für die Maschine „natürlichen" Größe. f loat ein einfach genauer Gleitpunktwert. double ein doppelt genauer Gleitpunktwert. Zusätzlich gibt es Varianten dieser elementaren Typen, short und long beziehen sich auf ganzzahlige Werte: short int sh; long int counter; In diesen Vereinbarungen kann das Wort int ausgelassen werden; normalerweise ge- geschieht dies. Mit short und long sollen verschieden lange ganzzahlige Werte zur Verfügung ste- stehen, soweit dies praktikabel ist; int wird normalerweise die „natürliche" Größe für eine bestimmte Maschine sein, short belegt oft 16 Bits, long 32 Bits und int entweder 16 oder 32 Bits. Es steht jedem Übersetzer frei, sinnvolle Größen für seine Maschine zu wählen, nur mit den Einschränkungen, daß short und int wenigstens 16 Bits haben, long minde- mindestens 32 Bits, und daß short nicht länger als int und int nicht länger als long sein darf. signed oder unsigned können auf char oder jeden Integer-Typ angewandt werden. unsigned-Werte sind immer positiv oder Null, und ihre Werte gehorchen den arithmeti- arithmetischen Regeln modulo 2", dabei ist n die Anzahl der Bits für einen Typ. Wenn zum Bei- Beispiel char-Werte mit 8 Bits repräsentiert werden, haben Variablen vom Typ unsigned char Werte zwischen 0 und 255, signed char dagegen Werte zwischen -128 und 127 (auf einer Maschine mit 2-Komplement-Darstellung). Ob für einfache char-Variablen signed oder unsigned angenommen wird, ist maschinenabhängig; druckbare Zeichen sind je- jedoch immer positiv. long double bezeichnet Gleitpunktrechnung mit besonders hoher Genauigkeit (extendedprecision). Wie bei ganzen Zahlen, ist die Größe von Gleitpunktobjekten im- implementierungsabhängig, float, double und long double könnten für eine, zwei oder drei verschiedene Größen stehen. Die Standard-Definitionsdateien <limits.h> und <float.h> enthalten symbolische Konstanten für alle Größen. Dort sind auch andere Eigenschaften der Maschine und des Übersetzers ersichtlich. Im Anhang B werden diese erläutert. Aufgabe 2-1. Schreiben Sie ein Programm zur Bestimmung des Wertebereichs von Va- Variablen vom Typ char, short, int und long, sowohl für signed als auch unsigned, indem Sie die entsprechenden Werte aus den Standard-Definitionsdateien ausgeben und durch direkte Berechnung. Schwieriger, falls Sie sie berechnen: bestimmen Sie den Wertebe- Wertebereich der verschiedenen Gleitpunkttypen. G * Nationale Zeichen, zum Beispiel Umlaute oder Akzente, benötigen eine 8-Bit-Darstellung und können als signed char negative Werte annehmen. A.d.D.
2.3 Konstanten 37 2.3 Konstanten Eine ganzzahlige Konstante wie 1234 hat den Typ int. Eine long-Konstante wird mit dem Buchstaben I oder L am Ende geschrieben, zum Beispiel 123456789L. Ist eine normale ganzzahlige Konstante zu groß für den Typ int, so gilt auch sie als long. Vorzei- Vorzeichenlose Konstanten werden mit u oder U am Ende geschrieben; die Endung ul oder UL bezeichnet unsigned long. Gleitpunktkonstanten enthalten einen Dezimalpunkt A23.4) oder einen Exponen- Exponenten (le-2) oder beides; ohne ein besonderes Suffix sind sie vom Typ double. Die Endung f oder F bezeichnet Konstanten vom Typ float, das Suffix I oder L vereinbart long double. Ganzzahlige Konstanten können statt dezimal auch oktal (in Basis 8) und hexadezi- hexadezimal (in Basis 16) formuliert werden. Beginnt eine ganzzahlige Konstante mit 0 (Null), so wird sie in Basis 8 interpretiert; beginnt die Konstante mit Ox oder OX, so gilt Basis 16. Beispielsweise kann der Dezimalwert 31 als 037 in Basis 8 und Oxlf oder 0X1F in Basis 16 dargestellt werden. Auch nach hexadezimalen und oktalen Konstanten kann L für long und U für unsigned folgen; OXFUL ist eine Konstante vom Typ unsigned long mit dem dezimalen Wert 15. Eine Zeichenkonstante ist ganzzahlig und wird als ein Einzelzeichen innerhalb von Anführungszeichen geschrieben, wie etwa V. Der Wert einer Zeichenkonstanten ist der numerische Wert des Zeichens im Zeichensatz der Maschine. Beispielsweise hat die Zei- Zeichenkonstante '0' für die Ziffer Null im ASCII-Zeichensatz den Wert 48, was nichts mit dem numerischen Wert 0 zu tun hat. Schreibt man '0' anstelle eines numerischen Werts wie 48, der vom Zeichensatz abhängig ist, so ist das Programm unabhängig vom jeweili- jeweiligen numerischen Weit und leichter zu lesen. Zeichenkonstanten können in numerischen Operationen genau wie andere Integer-Werte verwendet werden; sie werden jedoch am häufigsten zu Vergleichen mit anderen Zeichen verwendet. Gewisse Zeichen können in Zeichenkonstanten und in konstanten Zeichenketten mit Hilfe von Ersatzdarstellungen wie etwa \n (Zeilentrenner) angegeben werden. Diese Ersatzdarstellungen sehen zwar wie zwei Zeichen aus, stellen aber nur ein Zeichen dar. Darüber hinaus kann ein beliebiges, byte-großes Bit-Muster mit '\ooo' dargestellt werden, dabei ist ooo eine Folge von ein bis drei oktalen Ziffern @...7), oder mit dabei ist hh eine Folge von einer oder mehreren hexadezimalen Ziffern @...9, a...f, A...F). Somit könnten wir folgendes schreiben: «define VTAB '\013' /* ASCII Vertikal-Tabulator */ «define BELL '\007' /• ASCII Klingelzeichen */ oder auch hexadezimal: «define VTAB '\xb' /* ASCII Vertikal-Tabulator */ «define BELL '\x7' /* ASCII Klingelzeichen */
38 2 Datentypen, Ope iren und Ausdrücke Hier sind alle Ersatzdarstellungen: \8 \b \f \n \r Klingelzeichen backspace Seitenvorschub Zeilentrenner Wagenrücklauf \\ \? \' \" \ooo Gegenschrägstrich Fragezeichen Anführungszeichen Doppelanführungszeichen oktale Zahl \t Tabulatorzeichen \xA/i hexadezimale Zahl \v Vertikal-Tabulator Die Zeichenkonstante '\0' steht für das Zeichen mit Wert 0, das sogenannte Null- Nullzeichen (NUL). '\0' wird oft statt 0 geschrieben, um zu betonen, daß sich ein bestimmter Ausdruck mit Zeichen befaßt, der numerische Wert ist aber trotzdem Null. Ein konstanter Ausdruck ist ein Ausdruck, an dem nur Konstanten beteiligt sind. Solche Ausdrücke werden vom Übersetzer bewertet und nicht zur Laufzeit, folglich kön- können diese Ausdrücke überall dort benutzt werden, wo Konstanten auftreten können, wie bei «define HAXLINE 1000 char line[MAXLlNE+1]; oder «define LEAP 1 /• bei Schaltjahren */ int days[31+28+LEAP+31+30+31+30+3H31+30+31+30+31]; Eine konstante Zeichenkette {string literal) ist eine Folge von beliebig vielen Zei- Zeichen (auch keinen), die von Doppelanführungszeichen umgeben ist, wie etwa "Ich bin eine Zeichenkette11 oder "" /* leere Zeichenkette */ Die Doppelanführungszeichen sind nicht Teil der Zeichenkette, sondern begrenzen sie nur. Die gleichen Ersatzdarstellungen gelten für Zeichenkonstanten wie für konstante Zeichenketten; \" stellt das Doppelanführungszeichen dar. Konstante Zeichenketten können bei der Übersetzung aneinandergehängt werden: "hello," " world" ist äquivalent zu "hello, world" Damit kann man lange konstante Zeichenketten gut auf mehrere Zeilen im Programm- Programmtext verteilen. Formal betrachtet ist eine konstante Zeichenkette ein Vektor von Zeichen. Die in- interne Repräsentierung einer Zeichenkette hat ein Nullzeichen '\0' am Ende, folglich wird als Speicherplatz ein Zeichen mehr benötigt, als die Anzahl der Zeichen zwischen den Doppelanführungszeichen. Durch diese Darstellung ist die Länge einer Zeichenkette nicht begrenzt, aber Programme müssen eine Zeichenkette ganz durchlaufen, um ihre Länge zu bestimmen. Die Funktion strlen(s) aus der Standard-Bibliothek liefert die Länge einer Zeichenkette s ohne das abschließende '\0'-Zeichen. Hier ist unsere Versi- Version:
2.4 Vereinbar, jn 39 /* strlen: liefert die Laenge von s */ int strlen(char int i; i = 0; while (s[i) l= '\0') ++i; return i; > strlen und andere Funktionen zur Bearbeitung von Zeichenketten sind in der Standard- Definitionsdatei <string.h> vereinbart. Sie sollten sehr sorgfältig zwischen einer Zeichenkonstante und einer Zeichenkette mit einem einzelnen Zeichen unterscheiden: V ist nicht das Gleiche wie V. Die Zei- Zeichenkonstante V ist ein ganzzahliger Wert und stellt den numerischen Wert des Buchsta- Buchstabens x im Zeichensatz der Maschine dar. Die konstante Zeichenkette "x" ist ein Zeichen- Zeichenvektor und enthält ein Zeichen (den Buchstaben x) und ein Nullzeichen '\0'. Es gibt noch eine andere Art von Konstanten, die Aufzählungskonstanten (enumeration constants). Eine Aufzählung ist eine Folge von konstanten ganzzahligen Werten, wie etwa enum boolean < NO, YES >; Der erste Name in einer enum-Liste hat den Wert 0, der nächste 1 und so weiter, wenn keine expliziten Werte angegeben sind. Wenn manche Werte nicht angegeben sind, set- setzen nicht explizit angegebene Werte die aufsteigende Reihenfolge vom letzten expliziten Wert ab fort, wie hier im zweiten Beispiel: enum escapes < BELL = '\a', BACKSPACE = '\b', TAB = '\t', NEULINE = '\n', VTAB = '\v', RETURN = '\r' >; enum months < JAN = 1, FEB. MAR, APR, HAY, JUN, JUL, AUG, SEP, OCT. NOV, DEC >; /* FEB hat den Wert 2, MAR den Wert 3, etc. */ Namen in verschiedenen Aufzählungen müssen sich unterscheiden. Die Werte in einer Aufzählung müssen nicht verschieden sein. Aufzählungen sind bequem, um konstante Werte mit Namen zu verknüpfen, als Al- Alternative zu #define mit dem Vorteil, daß die Werte implizit generiert werden können. Obwohl Variablen vom Typ enum vereinbart werden dürfen, müssen die Übersetzer nicht prüfen, daß das, was in solch einer Variablen abgelegt wird, ein zulässiger Wert für die Aufzählung ist. Trotzdem - Variablen mit enum-Typ bieten die Möglichkeit zur Prü- Prüfung und sind deshalb oft besser als #define. Eine Testhilfe könnte außerdem die Fähig- Fähigkeit besitzen, Werte von enum-Variablen symbolisch auszugeben. 2.4 Vereinbarungen Alle Variablen müssen vor Gebrauch vereinbart werden, allerdings können manche Vereinbarungen implizit aus dem Kontext folgen. Eine Vereinbarung gibt einen Typ an und enthält eine Liste von einer oder mehreren Variablen dieses Typs, wie etwa int lower, upper, step; char c, lineMOOO];
40 2 Datentypen, Open en und Ausdrücke Variablen können beliebig auf mehrere Vereinbarungen verteilt werden; die vorherge- vorhergehenden Listen könnte man auch so schreiben: int lower; int upper; int step; char c; char lineMOOO]; So ist der Programmtext zwar länger, aber man kann bequem einen Kommentar zu jeder Vereinbarung hinzufügen oder später Änderungen vornehmen. Eine Variable kann bei ihrer Vereinbarung auch initialisiert werden. Folgt dem Namen ein Gleichheitszeichen und ein Ausdruck, wird der Ausdruck als Initialisierung verwendet: char esc = '\\'; int i = 0; int limit = MAXLINE+1; float eps = 1.0e-5; Gehört die fragliche Variable nicht zur automatischen Speicherklasse, findet die In- Initialisierung nur einmal statt, sozusagen bevor das Programm ausgeführt wird; die Initiali- Initialisierung muß dann ein konstanter Ausdruck sein. Eine explizit initialisierte automatische Variable wird bei jedem Eintritt in die Funktion oder in den Block initialisiert, zu dem sie gehört; die Initialisierung kann ein beliebiger Ausdruck sein. Nach Voreinstellung wer- werden externe und statische Variablen auf Null initialisiert. Automatische Variablen ohne explizite Initialisierung haben Undefinierte (d.h., sinnlose) Werte. Mit dem Attribut const kann bei der Vereinbarung einer Variablen angegeben wer- werden, daß sich ihr Wert nicht ändert. Bei einem Vektor bedeutet const, daß die Elemente nicht verändert werden. const double e = 2.71828182845905; const char msg[] • "warning: "; const kann auch bei Vektorparametern verwendet werden, um anzugeben, daß die Funk- Funktion den Vektor nicht verändert: int strlen(const Chart]); Versucht man, eine const-Variable zu ändern, ist das Resultat implementierungsabhän- implementierungsabhängig- 2.5 Arithmetische Operatoren Die binären arithmetischen Operatoren sind +, -,*, / und der Operator %, der den Rest nach ganzzahliger Division liefert. Bei Integer-Division wird ein Bruchteil ab- abgeschnitten. Der Ausdruck x X y liefert den Rest, der entsteht, wenn x durch y dividiert wird, und ist daher Null, wenn y gerade x exakt teilt. Beispielsweise ist ein Jahr ein Schaltjahr, wenn die Jahreszahl durch 4, aber nicht durch 100 teilbar ist, nur sind Vielfache von 400 doch Schaltjahre. Deshalb if ((year H == 0 M year X 100 1=0) || year X 400 == 0) printfC'Xd ist ein Schaltjahr\n", year); else printfC'Xd ist kein Schaltjahr\n", year);
41 Der Operator % kann auf float oder double-Werte nicht angewendet werden. Für nega- negative Operanden ist maschinenabhängig, in welcher Richtung bei / abgeschnitten wird und welches Vorzeichen das Resultat von % erhält. Auch die Aktionen bei Verlassen des Wertebereichs (overflow oder underflow) hängen von der jeweiligen Maschine ab. Die binären Operatoren + und - haben den gleichen Vorrang, der geringer ist als der Vorrang von •, / und %, welcher wiederum geringer ist als der Vorrang der unären Operatoren + und -, also von positiven und negativen Vorzeichen. Arithmetische Ope- Operationen werden von links her zusammengefaßt. Tabelle 2-1 am Ende dieses Kapitels faßt Vorrang und Assoziativität aller Operato- Operatoren zusammen. 2.6 Vergleiche und logische Verknüpfungen Die Vergleichsoperatoren sind Diese Operatoren haben alle gleichen Vorrang. Die Äquivalenzoperatoren ■« i = haben geringeren Vorrang. Vergleiche haben geringeren Vorrang als arithmetische Ope- Operatoren. Daher wird ein Ausdruck wie i < lim-1 als i < (lim-1) bewertet, was man auch erwarten würde. Interessanter sind die logischen Operatoren && und 11. Ausdrücke, die mit && oder 11 verknüpft sind, werden strikt von links nach rechts bewertet, und zwar nur solan- solange, bis das Resultat der logischen Verknüpfung feststeht. Die meisten C-Programme ba- basieren auf diesen Eigenschaften. Beispielsweise ist hier eine Schleife aus der Eingabe- Eingabefunktion getline, die wir in Kapitel 1 formuliert haben: for (i=0; i<lim-1 && <c=getchar()) != '\n' && c != EOF; ++i) sti] = c; Bevor ein neues Zeichen eingelesen wird, muß man prüfen, daß im Vektor s auch Platz vorhanden ist, folglich muß der Test i < lim-1 erfolgen, bevor ein Zeichen eingelesen wird. Wenn dann kein Platz mehr vorhanden ist, können wir auch kein weiteres Zeichen einlesen. Analog sollten wir c nicht mit EOF vergleichen, bevor getchar aufgerufen wurde: Deshalb müssen Aufruf und Zuweisung erfolgen, bevor das Zeichen in c geprüft wird. Der Vorrang von && ist größer als der Vorrang von 11, und beide haben geringe- geringeren Vorrang als die Vergleichs- und Äquivalenzoperatoren, folglich benötigen Ausdrücke wie i<Um-1 && <c = getcharO) != '\n' && c != EOF keine zusätzlichen Klammern. Da jedoch der Vorrang von ! = höher ist als der Vorrang der Zuweisung, sind im Ausdruck <c = getcharO) 1= >\n' die Klammern nötig, um das beabsichtigte Resultat herbeizuführen: Zuweisung an c und erst dann Vergleich zu '\n\
42 2 Datentypen, < ratoren und Ausdrücke Nach Definition ist der numerische Wert einer Vergleichsoperation 1, falls der Ver- Vergleich zutrifft, und 0, falls er nicht zutrifft. Der unäre Negationsoperator ! liefert 0, wenn sein Operand nicht 0 ist, und 1, wenn sein Operand 0 ist. Der Operator ! wird häufig bei Konstruktionen wie if ((valid) verwendet, anstelle von if (valid == 0) Man kann schwer verallgemeinern, was besser ist. Angaben wie ivalid lesen sich leicht („wenn nicht gültig"), aber kompliziertere Formulierungen sind oft schwer verständlich. Aufgabe 2-2. Schreiben Sie eine Schleife, die zu der obigen for-Schleife äquivalent ist, ohne die Operatoren && oder 11 zu verwenden. G 2.7 Typumwandlungen Hat ein Operator Operanden verschiedenen Typs, dann werden ihre Werte nach einer kleinen Anzahl von Regeln in einen gemeinsamen Datentyp umgewandelt. Im all- allgemeinen finden implizit nur solche Typumwandlungen statt, die einen „schmäleren" Operanden in einen „breiteren" umwandeln ohne dabei Information zu verlieren, wie et- etwa die Umwandlung eines ganzzahligen Werts in einen Gleitpunktwert in einem Aus- Ausdruck wie f + i. Sinnlose Ausdrücke, wie etwa ein float-Wert als Vektorindex, sind verbo- verboten. Ausdrücke, die zu Informationsverlust führen könnten, wie die Zuweisung eines län- längeren Integer-Typs an einen kürzeren oder eines Gleitpunkttyps an einen Integer-Typ, können eine Warnung hervorrufen, aber sie sind nicht illegal. Ein char-Wert ist nur eine kleine ganze Zahl, also können char-Werte beliebig in arithmetischen Ausdrücken benutzt werden. Damit können manche Zeichentransforma- Zeichentransformationen sehr flexibel formuliert werden. Als Beispiel betrachten wir diese naive Imple- Implementierung der Funktion atoi, die eine Ziffernkette in ihr numerisches Äquivalent um- umwandelt. /* atoi: Ziffernkette s in int umwandeln */ int atoi (char s[]) < int i, n; n = 0; for (i = 0; sti] >» '0' && sti] <« '9'; ++i) n = 10 * n + (sti] - '0'); return n; > Wie wir in Kapitel 1 schon besprochen haben, liefert der Ausdruck s[i] - '0' den numerischen Wert des Zeichens, das in s[i] gespeichert ist, da die Werte von '0', '1' etc. eine zusammenhängende, aufsteigende Folge bilden. Ein weiteres Beispiel einer Umwandlung von char in int-Werte ist die Funktion lower, die ein einzelnes Zeichen in einen Kleinbuchstaben umwandelt - allerdings nur im ASCII-Zeichensatz. Ist das Argument kein Großbuchstabe, so liefert lower diesen Wert unverändert.
2.7 Typumwar ngen 43 /* lower: c in Kleinbuchstaben umwandeln; ASCII */ int lower(int c) < if (c >= 'A' && c <= 'Z1) return c + 'a' - 'A1; eise return c; > Dies funktioniert im AScii-Zeichensatz, da sich die jeweiligen Groß- und Kleinbuchsta- Kleinbuchstaben um einen konstanten Wert unterscheiden, und da jedes Alphabet zusammenhängend ist - zwischen A und Z liegen ausschließlich Buchstaben. Letzteres gilt jedoch nicht für den EBCDic-Zeichensatz, also würde diese Funktion für EBCDIC mehr als nur Buchstaben umwandeln. Die Standard-Definitionsdatei <ctype.h>, die in Anhang B beschrieben wird, defi- definiert einen Satz von Funktionen für Prüfungen und Umwandlungen, die vom Zeichensatz unabhängig sind. Zum Beispiel liefert die Funktion tolower(c) für einen Großbuchstaben c den Wert des dazugehörigen Kleinbuchstabens*, also ist tolower ein portabler Ersatz für die oben gezeigte Funktion lower. Ähnlich kann c >= '0' && c <= '91 durch isdigit(c) ersetzt werden. Wir werden von nun an die Funktionen von <ctype.h> benutzen. Bei der Umwandlung von Zeichen in numerische Werte ist eine Feinheit zu beach- beachten. C definiert nicht, ob Variablen vom Typ char numerische Werte mit oder ohne Vor- Vorzeichen sind. Kann bei Umwandlung eines char-Werts in einen int-Wert je eine negative Zahl entstehen? Die Antwort zu dieser Frage ist je nach Maschinenarchitektur verschie- verschieden. Bei manchen Maschinen wird ein char-Wert, dessen höchstes Bit 1 ist, durch Pro- Propagierung des Vorzeichens in einen negativen Integer-Wert verwandelt. Bei anderen Sy- Systemen wird ein char-Wert dadurch in einen int-Wert umgewandelt, daß links Bits mit Wert 0 hinzukommen, also ist das Resultat immer positiv. Die Definition von C garantiert jedoch, daß kein Zeichen aus dem druckbaren Standard-Zeichensatz der jeweiligen Maschine einen negativen Wert besitzt,* so daß die- diese Zeichen in Ausdrücken immer positive Größen sind. Speichert man jedoch beliebige Bit-Muster als char-Werte, so können diese auf manchen Maschinen negativ und auf an- anderen positiv erscheinen. Für Portabilität sollte man signed oder unsigned angeben, wenn andere Daten als Zeichen in char-Variablen gespeichert werden sollen. Vergleiche wie i > j und logische Verknüpfungen mit && und 11 haben nach Defi- Definition den Wert 1, falls sie zutreffen, und 0, falls nicht. Also wird in der Zuweisung d = c >= '0' && c <= '9' d auf 1 gesetzt, falls c eine Ziffer ist, und auf 0, falls nicht. Funktionen, wie isdigit kön- können jedoch jeden von Null verschiedenen Wert für „wahr" liefern. In einer Bedingung bei if, while, for usw., bedeutet „wahr" einfach „nicht Null", also ist das gleichbedeutend. * Nationale Zeichen, zum Beispiel Umlaute und Akzente, benötigen eine 8-Bit-Darstellung. Sie gelten bei Funktionen wie tolower nicht als Buchstaben und können als signed char negative Werte annehmen. A.d.Ü.
44 2 Datentypen, O itoren und Ausdrücke Implizite arithmetische Umwandlungen funktionieren etwa so, wie man erwarten müßte. Im allgemeinen wird bei binären Operatoren wie + oder •, wenn zwei Operan- Operanden mit verschiedenem Typ vorliegen, der Wert vom „niedrigeren" in den „höheren" Typ umgewandelt, bevor die Operation ausgeführt wird. Das Resultat hat dann den höheren Typ. Im Abschnitt 6 im Anhang A werden die Typumwandlungsregeln genau aufgeführt. Falls jedoch keine unsigned-Operanden beteiligt sind, genügen die folgenden vereinfach- vereinfachten Regeln: Wenn einer der Operanden long double ist, wird der andere in long double um- umgewandelt. Ist andernfalls einer der beteiligten Operanden double, so wird der andere Ope- Operand in double umgewandelt. Ist dagegen einer der beiden Operanden float, so wird der andere ebenfalls in float umgewandelt. Andernfalls werden char und short in int umgewandelt. Falls dann einer der Operanden long ist, wird der andere in long umgewandelt. Man beachte, daß float-Werte in einem Ausdruck nicht automatisch in double- Werte verwandelt werden; dies ist anders als in der früheren Definition von C. Im allge- allgemeinen arbeiten mathematische Funktionen, wie die in <math.h>, mit doppelter Ge- Genauigkeit, float wird hauptsächlich verwendet, um bei großen Vektoren Speicherplatz zu sparen, und, seltener, um Rechenzeit bei Maschinen zu sparen, bei denen doppelt genaue Arithmetik besonders teuer ist. Wenn unsigned-Operatoren beteiligt sind, sind die Typumwandlungsregeln kompli- komplizierter. Das Problem liegt darin, daß Vergleiche zwischen Werten mit und ohne Vorzei- Vorzeichen maschinenabhängig sind, da sie von den Größen der verschiedenen Integer-Typen abhängen. Nehmen wir zum Beispiel an, daß int 16 Bits und long 32 Bits hat, dann ist -1L < 1U, weil 1U vom Typ unsigned in den Typ signed long umgewandelt wird. And- Andrerseits ist - 1L > 1UL, weil in diesem Fall - 1L in unsigned long umgewandelt wird und deshalb als große positive Zahl erscheint. Bei Zuweisungen finden Typumwandlungen statt; der Wert auf der rechten Seite wird in den Typ der linken Seite umgewandelt, und das ist der Typ des Resultats. Wie oben beschrieben, wird ein Zeichen mit oder ohne Propagierung des Vorzei- Vorzeichens in einen ganzzahligen Wert umgewandelt. Längere Integer-Werte werden in kürzere oder in char-Werte umgewandelt, indem die überzähligen Bits von links her unterdrückt weiden. Also bleibt bei int i; char c; i = c; c * i; der Wert von c unverändert. Dies gilt unabhängig davon, ob das Vorzeichen propagiert wird oder nicht. Wenn jedoch die Reihenfolge der Zuweisungen vertauscht wird, kann Information verlorengehen. Ist x ein float- und i ein int-Objekt, dann verursachen sowohl x = i als auch i = x Typumwandlungen; bei Umwandlung von float in Int wird ein Bruchteil abgeschnitten.
2.7 Typumwai ngen 45 Ob bei der Umwandlung von double in float der Wert gerundet oder abgeschnitten wird, ist implementierungsabhängig. Da ein Argument eines Funktionsaufrufs ein Ausdruck ist, finden Typumwandlun- Typumwandlungen auch statt, wenn Argumente an Funktionen übergeben werden. Gibt es keinen Pro- Prototyp für die Funktion, werden char und short in int umgewandelt und float in double. Deshalb haben wir Parameter auch dann als int und double deklariert, wenn die Funkti- Funktion mit char und float aufgerufen wurde. Schließlich kann man explizite Typumwandlungen in einem beliebigen Ausdruck mit einer unären Umwandlungsoperation, einem sogenannten cast*, erzwingen. Bei ( type-name ) expression wird der Wert des Ausdrucks unter Benutzung der oben angegebenen Typumwandlungs- Typumwandlungsregeln in den angegebenen Typ umgewandelt. Exakt bedeutet eine explizite Umwand- Umwandlungsoperation dasselbe, als wenn der Ausdruck einer Variablen vom angegebenen Typ zugewiesen wird, die dann anstelle der ganzen Konstruktion verwendet wird. Beispiels- Beispielsweise erwartet die Bibliotheksfunktion sqrt ein double-Argument und liefert sinnlose Re- Resultate, wenn versehentlich etwas anderes übergeben wird, (sqrt ist in < math.h > dekla- deklariert.) Ist also n ein ganzzahliger Wert, dann kann mit sqrt(Cdouble) n) der Wert von n in double umgewandelt werden, bevor er als Argument an sqrt überge- übergeben wird. Man beachte, daß die Umwandlungsoperation den Wert von n im verlangten Typ liefert; n selbst wird dabei nicht verändert. Wie aus der Tabelle am Ende dieses Ka- Kapitels hervorgeht, hat die explizite Umwandlungsoperation den gleichen hohen Vorrang wie andere unäre Operatoren. Wenn Parameter in einem Funktionsprototyp deklariert werden, wie das normaler- normalerweise geschehen sollte, bewirkt die Deklaration eine automatische Umwandlung aller Ar- Argumente, wenn die Funktion aufgerufen wird. Gilt also zum Beispiel der folgende Proto- Prototyp für sqrt double sqrt(double); dann wird beim Aufruf root2 ■ sqrtB); die int-Konstante 2 in einen double-Wert 2.0 umgewandelt, ohne daß dazu eine explizite Umwandlungsoperation nötig ist. Die Standard-Bibliothek enthält eine portable Implementierung eines Pseudo-Zu- fallszahlengenerators und eine Funktion um den Ausgangswert zu initialisieren; in rand gibt es eine Umwandlungsoperation: unsigned long int next = 1; /* rand: liefert Pseudo-Zufallszahl im Bereich 0..32767 */ int rand(void) t next = next * 1103515245 + 12345; return (unsigned int)(next/65536) X 32768; * Zu deutsch Gipsverband, eine sehr plastische Umschreibung.
46 2 Datentypen, Oper -en und Ausdrücke /* srand: setze Ausgangswert fuer randO */ void srandCunsigned int seed) t next = seed; Aufgabe 2-3. Schreiben Sie die Funktion htoi(s), die eine Zeichenkette mit hexadezi- hexadezimalen Ziffern (einschließlich eines optionalen Ox oder OX) in den entsprechenden ganz- ganzzahligen Wert umwandelt. Dabei sind die zulässigen Ziffern 0 bis 9, a bis f und A bis F. ü 2.8 Inkrement- und Dekrement-Operatoren C verfügt über zwei ungewöhnliche Operatoren zur Inkrementierung und Dekre- mentierung von Variablen. Der Inkrement-Operator ++ addiert 1 zu seinem Operan- Operanden, der Dekrement-Operator — subtrahiert 1. Wir haben häufig ++ dazu verwendet, Variablen zu inkrementieren, zum Beispiel if (c « '\n') Ungewöhnlich ist, daß ++ und — sowohl vor als auch nach ihren Operanden an- angegeben werden können (Präfix-Notation ++n oder Postfix-Notation n++). In beiden Fäl- Fällen wird die Variable n inkrementiert. Allerdings wird n im Ausdruck ++n inkremen- inkrementiert, bevor der resultierende Wert verwendet wird, während n bei n++ inkrementiert wird, nachdem der Wert von n benutzt wurde. Wird also der Resultatwert weiterverwen- weiterverwendet, so sind die Ausdrücke ++n und n++ verschieden. Hat n zum Beispiel den Wert 5, dann erhält bei x = n++; x den Wert 5; bei x = ++n; erhält x aber den Wert 6. n ist anschließend in beiden Fällen 6. Die Inkrement- und De- Dekrement-Operatoren können nur auf Variablen angewendet werden; ein Ausdruck wie (i+j)++ ist illegal. Wird kein Resultatwert, sondern nur der Inkrement-Effekt benötigt, wie in if (c == '\n'> dann sind Präfix- und Postfix-Notation gleichbedeutend. Es gibt jedoch Situationen, wo genau einer der Resultatwerte benötigt wird. Betrachten wir beispielsweise die Funktion squeeze(s,c), die alle Kopien des Zeichens c aus der Zeichenkette s entfernt. /* squeeze: alle Zeichen c in s loeschen */ void squeezeCchar sC), int c) t int i, j; for (j = j = 0; sCi) != '\0'; i++) if (SCi) != c) sCj++) = sCi); s[j) = '\0'; }
2.9 Bit-Manipti jnen 47 Das Zeichen in Position i wird untersucht. Ist es nicht c, dann wird das Zeichen aus Posi- Position 1 in Position j kopiert, und nur daran anschließend wird j inkrementiert, um auf eine neue verfügbare Position zu zeigen. Dies ist vollkommen äquivalent zu if (s[ij != c) t s[j) = sti); Ein weiteres ähnliches Beispiel findet sich in der getline-Funktion, die wir in Kapi- Kapitel 1 geschrieben haben. Dort können wir if (c == '\n') t s[i) = c; ersetzen durch die kompaktere Formulierung if (c « '\n'> s[i++) = c; Als drittes Beispiel betrachten wir die Standard-Funktion strcat(s,t), die die Zei- Zeichenkette t an das Ende der Zeichenkette s anhängt, strcat geht davon aus, daß in s genügend Platz vorhanden ist, um beide Zeichenketten aufzunehmen. So wie wir sie ge- geschrieben haben, liefert strcat kein Funktionsresultat; die Version aus der Standard-Bi- Standard-Bibliothek liefert einen Zeiger auf die resultierende Zeichenkette. /* strcat: t an das Ende von s anhaengen; s muss dabei gross genug sein */ void strcat(char s[), char t[]) t int i, j; i » j * 0; white (s[i) 1= '\0') /* Ende von s finden •/ i++; while ((s[i++) = t[j++)) != '\0') /* t kopieren */ Nachdem jeweils ein Zeichen aus t nach s kopiert worden ist, wird der Inkrement-Opera- tor ++ in Postfix-Notation auf i und j angewendet, damit diese Indizes im nächsten Durchgang durch die Schleife wieder auf neue Zeichen zeigen. Aufgabe 2-4. Schreiben Sie eine Variante von squeeze(sl,s2), die jedes Zeichen aus sl entfernt, das in der Zeichenkette s2 vorkommt, ü Aufgabe 2-5. Schreiben Sie eine Funktion any(sl,s2), die die Position des ersten Zei- Zeichens in der Zeichenkette sl als Resultatwert liefert, das in der Zeichenkette s2 vor- vorkommt. Enthält sl kein Zeichen aus s2, so soll der Resultatwert -1 sein. (Die Biblio- Bibliotheksfunktion strpbrk macht dasselbe, liefert aber einen Zeiger auf die Stelle.) a 2.9 Bit-Manipulationen C verfügt über sechs Operatoren für Bit-Manipulationen; diese Operatoren können nur auf Integer-Operanden angewendet werden, also auf char, short, int und long, und zwar signed oder unsigned.
48 2 Datentypen, Oper zn und Ausdrücke & UND-Verknüpfung von Bits | ODER-Verknüpfung von Bits Exklusive-ODER-Verknüpfung von Bits « Bit-Verschiebung nach links » Bit-Verschiebung nach rechts Bit-Komplement (unär) Die UND-Verknüpfung von Bits, der Operator &, wird oft dazu benutzt, eine Aus- Auswahl von Bits auf 0 zu setzen. Beispielsweise löscht n = n & 0177; in n alle Bits mit Ausnahme der letzten sieben. Mit dem ODER-Operator | setzt man Bits auf 1: x = x | SET_ON; schaltet in x genau die Bits ein, die in SET_ON auf 1 gesetzt sind. Die Exklusive-ODER-Verknüpfung von Bits, der Operator *■, setzt jede Bit-Position auf 1, in der seine Operanden verschiedene Bit-Werte haben, und auf 0, wenn sie gleich sind. Man muß die Bit-Verknüpfungen & und | von den logischen Verknüpfungen && und 11 unterscheiden, denn die logischen Verknüpfungen berechnen eine Bedingung von links nach rechts. Hat beispielsweise x den Wert 1 und y den Wert 2, dann ist x & y Null, aber x && y ist Eins. Die Shift -Operatoren << und » verschieben ihren linken Operanden um so viele Bit-Positionen nach links oder rechts, wie der rechte Operand angibt, der positiv sein muß. Also bewegt x << 2 den Wert von x um zwei Positionen nach links und schiebt Null-Bits nach; dies ist äquivalent zu einer Multiplikation mit 4. Wird ein Wert mit Typ unsigned nach rechts geschoben, so wird immer Null nachgeschoben. Wird ein vorzei- vorzeichenbehafteter Wert nach rechts geschoben, so wird auf manchen Systemen das Vorzei- Vorzeichen-Bit nachgeschoben (arithmetic shift) und auf anderen Systemen Null-Bits (logical shift). Der unäre Operator " komplementiert die Bits in einem ganzzahligen Wert; das heißt, ein Bit mit Wert 1 wird in 0 verwandelt und umgekehrt. Zum Beispiel setzt x « x & -077 die letzten sechs Bits auf Null. Man beachte, daß x & 77 von der Wortlänge des Rech- Rechners unabhängig ist und damit besser als etwa x & 0177700, wo davon ausgegangen wird, daß x eine Größe mit 16 Bits ist. Die portable Formulierung verursacht zur Aus- Ausführungszeit des Programms keinen zusätzlichen Aufwand, denn 77 ist ein konstanter Ausdruck und kann bereits vom Übersetzer bewertet werden. Um einige Bit-Operatoren vorzuführen, betrachten wir die Funktion getbits(x,p,n), die aus x ein Feld mit n Bits liefert (entsprechend nach rechts geschoben), das in Position p beginnt. Wir gehen davon aus, daß die Bit-Position 0 das rechte Ende von x bezeichnet und daß n und p vernünftige positive Werte sind. Beispielsweise liefert getbits(x,4,3) die drei Bits in den Positionen 4, 3 und 2, entsprechend nach rechts geschoben.
2.10 Zuweisunge d Ausdrücke 49 /* getbits: n Bits ab Position p */ unsigned getbits(unsigned x, int p, int n) i return (x » (p*1-n>) & -(-0 « n); > Der Ausdruck x » (p+l-n) verlagert das gewünschte Feld an das rechte Ende eines Worts.  besteht nur aus Bits mit dem Wert 1; schiebt man dies mit <<n um n Bit- Positionen nach links, so entsteht eine Maske, die in den rechten n Positionen 0 enthält und sonst 1; wird dieser Wert schließlich mit " komplementiert, so entsteht eine Maske, die rechts in n Bit-Positionen 1 enthält. Aufgabe 2-6. Schreiben Sie eine Funktion setbits(x,p,n,y), die als Resultat den Wert x liefert, wobei die n Bits ab Position p durch die n am weitesten rechts stehenden Bits von y ersetzt sind. G Aufgabe 2-7. Schreiben Sie eine Funktion invert(x,p,n), die beim Wert von x genau die n Bits komplementiert (also von 1 in 0 und umgekehrt verwandelt), die in Bit-Position p beginnen. Die anderen Bits sollen unverändert bleiben, ü Aufgabe 2-8. Schreiben Sie eine Funktion rightrot(x,n), die den Integer-Wert x um n Bit-Positionen nach rechts rotiert, ü 2.10 Zuweisungen und Ausdrücke Im Ausdruck i = i + 2 wird die Variable auf der linken Seite sofort am Anfang der rechten Seite wiederholt; dies kann kompakter als i += 2 formuliert werden. Der Operator += ist ein sogenannter Zuweisungsoperator. Für die meisten binären Operatoren (also für Operatoren, die wie + einen rechten und linken Operanden besitzen) existieren entsprechende Zuweisungsoperatoren op=, wobei op aus folgender Operatorliste stammt: ♦ -*/X«»s* | Sind expr\ und expri Ausdrücke, dann ist expri op= expri äquivalent zu expr\ = ( exprx ) op ( expr2 ) abgesehen davon, daß expr\ nur einmal bewertet wird. Man beachte dabei die Klam- Klammern um expri'. x *= y + 1 ist äquivalent zu x » x * (y ♦ 1> und nicht zu x = x * y + 1
50 2 Datentypen, O .toren und Ausdrücke Beispielsweise zählt die Funktion bitcount die Anzahl Bits mit Wert 1 in ihrem ganzzahligen Argument: /* bitcount: 1-Bits zaehlen */ int bitcount(unsigned x) t int b; for (b = 0; x 1= 0; x »= 1) if (x & 01) return b; > Da der Parameter x als unsigned deklariert ist, ist sicher, daß beim Verschieben nach rechts Null-Bits und nicht Vorzeichen-Bits nachgeschoben werden, und zwar unabhängig von der Maschine, auf der das Programm ausgeführt wird. Ganz abgesehen von der prägnanten Formulierung reflektieren zusammengesetzte Zuweisungsoperatoren eher die menschliche Denkweise. Wir sagen schließlich „Addiere 2 zu i" oder „Vergrößere i um 2" und nicht „Nimm i, addiere 2 und schreibe das Resultat zurück nach i." Deshalb ist der Ausdruck i += 2 der Formulierung i = i+2 vorzuziehen. Darüber hinaus wird in einem komplizierten Ausdruck wie yyvaltyypvtp3+p4) + yypv[p1+p2)) += 2 der Text durch den zusammengesetzten Zuweisungsoperator leichter verständlich: Der Leser muß nicht mühsam feststellen, daß zwei lange Ausdrücke tatsächlich gleich sind, oder sich gar überlegen, warum sie nicht gleich sind. Schließlich kann ein zusammenge- zusammengesetzter Zuweisungsoperator dem Übersetzer helfen, effizienten Code zu generieren. Wir haben bereits gesehen, daß ein Zuweisungsoperator den zugewiesenen Wert als Resultat liefert und in Ausdrücken verwendet werden kann; das häufigste Beispiel ist: while ((c = getcharO) 1 = EOF) Die anderen Zuweisungsoperatoren (+=, - = etc.) können ganz analog in Ausdrücken verwendet werden, dies ist jedoch weniger häufig. In allen solchen Ausdrücken ist der Resultattyp einer Zuweisung immer der Typ ihres linken Operanden, und der Resultatwert ist der Wert nach der Zuweisung. Aufgabe 2-9. In einem Zahlensystem, das auf dem 2-Komplement beruht, löscht der Ausdruck x &= (x-1) das rechteste Bit mit Wert 1 in x. Erklären Sie warum. Benutzen Sie diesen Trick, um eine schnellere Version von bitcount zu realisieren, ü 2.11 Bedingter Ausdruck Die Anweisungen if (a > b) z = a; eise z = b; weisen das Maximum von a und b an z zu. Der bedingte Ausdruck, der mit dem Opera- Operatorpaar „?:", formuliert wird, bietet eine Alternative für diese und ähnliche Konstruktio- Konstruktionen. Im Ausdruck
2.12 Vorrang un ^ihcnfolgc bei Bewertungen 51 ? exprz : expr-i wird zunächst expr\ berechnet. Ist der Wert nicht null, trifft also die Bedingung zu, dann wird der Ausdruck expr^ berechnet und dessen Wert ist dann das Resultat des bedingten Ausdrucks. Andernfalls wird exprj berechnet und das ist das Resultat. Nur einer der Ausdrücke expr-i und exprj wird bewertet. Um also an z das Maximum von a und b zu- zuzuweisen, formulieren wir z * (a > b) ? a : b; /* z « max(a, b) */ Zu beachten ist, daß der bedingte Ausdruck tatsächlich ein Ausdruck ist und wie jeder andere Ausdruck verwendet werden kann. Haben expr^ und expr^ verschiedene Typen, wird der Resultattyp durch die Umwandlungsregeln festgelegt, die früher in die- diesem Kapitel beschrieben wurden. Ist zum Beispiel f ein float-Wert und n ein int-Wert, dann hat der Ausdruck (n > 0) ? f : n als Resultat einen float-Wert, und zwar unabhängig davon, ob n positiv ist oder nicht. In einem bedingten Ausdruck muß der erste Ausdruck, also die Bedingung, nicht in Klammern eingeschlossen werden, da der Vorrang von ?: sehr niedrig ist, gerade noch höher als der Vorrang von Zuweisungen. Trotzdem sollten wir Klammern benutzen, da sie die Bedingung leichter sichtbar machen. Ein bedingter Ausdruck führt oft zu knappem Code. Beispielsweise gibt die fol- folgende Schleife n Elemente eines Vektors aus, und zwar 10 Elemente pro Zeile, mit ei- einem Leerzeichen zwischen Spalten und mit einem Zeilentrenner nach jeder Zeile (auch nach der letzten). for (i = 0; i < n; i++) printf("X6dXc", a[i], (iX10==9 || i==n-1> ? '\n' : ' '); Ein Zeilentrenner wird nach jedem zehnten Element ausgegeben und auch nach dem letzten Element. Nach jedem anderen Element steht ein Leerzeichen. Dies sieht viel- vielleicht verzwickt aus, aber es ist kompakter als die äquivalente Formulierung mit if-else. Ein weiteres gutes Beispiel ist printfC'Sie haben %d TeilXsAn", n, n==1 ? "" : "e"); Aufgabe 2-10. Ändern Sie die Funktion lower, die Großbuchstaben in Kleinbuchstaben umwandelt, so daß ein bedingter Ausdruck verwendet wird anstelle einer if-else- Anweisung. D 2.12 Vorrang und Reihenfolge bei Bewertungen Tabelle 2-1 faßt die Regeln für Vorrang und Assoziativität aller Operatoren zusam- zusammen; sie enthält auch Operatoren, die wir bisher nicht besprochen haben. Innerhalb ei- einer Zeile haben Operatoren den gleichen Vorrang. Die Zeilen sind mit abnehmendem Vorrang angeordnet, also haben zum Beispiel •, / und % den gleichen Vorrang, und die- dieser Vorrang ist größer als der des binären + und -. Der „Operator" () steht für Funkti- Funktionsaufrufe. Mit den Operatoren -> und . werden Komponenten einer Struktur ausge- ausgewählt; diese Operatoren werden, wie auch sizeof (Größe eines Objekts) in Kapitel 6 be- besprochen. In Kapitel 5 behandeln wir • (Inhalt bei Verweis mit einem Zeiger) und & (Adresse eines Objekts) und in Kapitel 3 wird der Komma-Operator behandelt.
52 2 Datentypen, Op *oren und Ausdrücke Tabelle 2-1. Vorrang und Assoziativität der Operatoren Operatoren O t) -> . 1 - ++ — + - * & (.type) sizeof * / X * - « » < « > >= »» ! = & A 1 && II ■ += -= •= /=%=&=*= |= «= »= Assoziativität von links her von rechts her von links her von links her von links her von links her von links her von links her von links her von links her von links her von links her von rechts her von rechts her von links her Unär haben +, - und * mehr Vorrang als binär. Zu beachten ist, daß der Vorrang der Operatoren zur Bit-Manipulation St, " und | geringer ist als der Vorrang der Vergleiche == und ! = . Daraus folgt, daß beim Überprü- Überprüfen bestimmter Bits, wie etwa if ((x & MASK) == 0) ... die Bit-Verknüpfungen in Klammern eingeschlossen werden müssen, damit korrekte Re- Resultate entstehen. Wie bei den meisten anderen Sprachen ist auch für C nicht definiert, in welcher Reihenfolge die Operanden eines Operators bewertet werden. (Ausnahmen sind &&, \ |, ?: und der Komma-Operator.) Beispielsweise kann in der Anweisung f vor oder nach g bewertet werden; wenn also f oder g eine Variable ändert, von der je- jeweils die andere Funktion abhängt, dann kann x von der Reihenfolge der Bewertungen abhängen. Zwischenresultate können in temporären Variablen gespeichert werden, um eine bestimmte Bewertungsreihenfolge zu erzwingen. Analog ist auch die Reihenfolge nicht festgelegt, in der die Argumente für einen Funktionsaufruf bewertet werden, also kann die Anweisung printf("%d %d\n", ++n, powerB, n)>; /* FALSCH */ bei verschiedenen Übersetzern verschiedene Resultate liefern, je nachdem, ob n inkre- mentiert wird, bevor power aufgerufen wird. Die Lösung ist natürlich ++n; printf("%d %d\n", n, powerB, n>); Funktionsaufrufe, verschachtelte Zuweisungen und die Inkrement- und Dekre- Dekrement-Operatoren verursachen „Nebenwirkungen" - als Nebenprodukt der Berechnung eines Ausdrucks wird eine Variable verändert. Verursacht ein Ausdruck Nebenwirkun-
2.12 Vorrang i Reihenfolge bei Bewertungen 53 gen, so kann die Reihenfolge, in der die beteiligten Variablen verändert werden, sehr subtil das Resultat des Ausdrucks beeinflussen. Eine unglückliche Situation zeigt sich in ati] ■ i++; Die Frage ist, ob der alte Wert von i oder der neue Wert als Index benutzt wird. Über- Übersetzer können dies verschieden interpretieren und können folglich je nach Interpretation verschiedene Resultate liefern. Der Standard legt die meisten dieser Dinge absichtlich nicht fest. Es bleibt dem Übersetzer überlassen, wann Nebenwirkungen (Zuweisung an Variablen) stattfinden, da die bestmögliche Reihenfolge sehr stark von der Maschinenar- Maschinenarchitektur abhängt. (Der Standard legt fest, daß alle Nebenwirkungen der Argumente stattfinden, bevor eine Funktion aufgerufen wird, aber das würde im dem oben erwähn- erwähnten Aufruf von printf nicht helfen.) Die Moral ist, daß es schlechter Programmierstil in jeder Sprache ist, wenn man so formuliert, daß die Bewertungsreihenfolge eine Rolle spielt. Man muß natürlich wissen, was man vermeiden soll, aber wenn Sie nicht wissen, wie gewisse Dinge auf verschiede- verschiedenen Maschinen realisiert werden, werden Sie nicht in Versuchung geraten, aus einer be- bestimmten Implementierung einen Vorteil ziehen zu wollen.
55 3 Kontrollstrukturen Kontrollstrukturen definieren die Reihenfolge, in der Berechnungen durchgeführt werden. In früheren Beispielen haben wir bereits die häufigsten Kontrollstrukturen ken- kennengelernt; im vorliegenden Kapitel erklären wir den Rest und präzisieren die früher be- besprochenen. 3.1 Anweisungen und Blöcke Aus einem Ausdruck wie x = 0 oder i++ oder printf(...) wird eine Anweisung, wenn ein Semikolon folgt: x = 0; printf( ... ); In C steht ein Semikolon am Ende einer Anweisung; in Sprachen wie Pascal steht es nur zwischen Anweisungen. Die geschweiften Klammern { und } dienen dazu, Vereinbarungen und Anweisun- Anweisungen in einen Block zusammenzufassen. Ein solcher Block ist syntaktisch äquivalent zu ei- einer einzelnen Anweisung. Ein offensichtliches Beispiel sind die geschweiften Klammern, die die Anweisungen einer Funktion umgeben; andere Beispiele sind geschweifte Klam- Klammern, die mehrere Anweisungen nach if, else, while oder for zusammenfassen. (Varia- (Variablen kann man in jedem Block vereinbaren; dies werden wir in Kapitel 4 besprechen.) Nach der rechten geschweiften Klammer, die einen Block abschließt, steht kein Semiko- Semikolon. 3.2 if-else Mit der if-else-Anweisung werden Entscheidungen formuliert. Der else-Teil ist op- optional; formal gilt folgende Syntax: if ( expression ) statement i else statement 2 expression wird berechnet. Trifft die Bedingung zu (hat also expression einen von 0 ver- verschiedenen Wert), so wird Statement \ ausgeführt. Trifft die Bedingung nicht zu (hat also expression den Wert 0), so wird statement 2 ausgeführt, falls ein else-Teil vorhanden ist. Da bei if einfach der numerische Wert eines Ausdrucks getestet wird, sind be- bestimmte Abkürzungen möglich. Am offensichtlichsten ist dabei if ( expression ) anstelle von if ( expression != 0) Manchmal ist dies klar und übersichtlich, manchmal aber werden die Vorgänge eher ver- verschleiert. Da der else-Teil einer if-else-Anweisung optional ist, entsteht eine Mehrdeutigkeit, wenn ein else-Teil in einer verschachtelten Folge von if-Anweisungen fehlt. Dem wird
56 3 Kontrollstrukturen dadurch begegnet, daß der else-Teil immer mit dem letzten if verbunden wird, für das noch kein else-Teil existiert. Beispielsweise gehört in if (n > 0) if (a > b) z = a; eise z = b; der else-Teil zum inneren if, wie wir durch Einrücken angedeutet haben. Sollten Sie das nicht gemeint haben, dsuin müssen Sie geschweifte Klammern benutzen, um die entspre- entsprechenden Zugehörigkeiten zu erzwingen: if (n > if else 2 = 0) (a z b: t > b) = a; Der Konflikt ist in Situationen wie der folgenden besonders bösartig: if (n >= 0) for (i = 0; i < n; i++) if (sti) > 0) { printfC'...»); return i; > eise /* FALSCH */ printfC'Fehler ~ n ist negativ\n"); Einrücken zeigt zwar unmißverständlich, was wir möchten, läßt jedoch den Übersetzer kalt, und so wird der else-Teil mit der inneren if-Anweisung verknüpft. Diese Art von Fehler kann schwierig zu finden sein; es empfiehlt sich, geschweifte Klammern bei ver- verschachtelten if-Anweisungen zu verwenden. Beachten Sie übrigens, daß in if (a > b) z = a; eise z = b; nach z - a ein Semikolon steht. Dies liegt daran, daß grammatisch auf if eine Anweisung folgt, und ein Ausdruck als Anweisung wie „z = a;" muß immer mit einem Semikolon abgeschlossen werden. 3.3 else-if Die Konstruktion if ( expression ) statement else if ( expression ) statement else if ( expression ) statement else if ( expression ) statement else statement
3.3 else-if 57 ist so häufig, daß wir sie kurz gesondert besprechen wollen. Diese Folge von if- Anweisungen ist die allgemeinste Art, eine Entscheidung unter vielen Alternativen zu formulieren. In der angegebenen Reihenfolge wird eine expression nach der anderen be- bewertet; sobald eine dieser Bedingungen zutrifft, wird die abhängige Anweisung aus- ausgeführt, und damit ist die Ausführung der gesamten Kette beendet. Wie immer, steht Statement für eine einzelne Anweisung oder eine Gruppe von Anweisungen in geschweif- geschweiften Klammern. Der letzte else-Teil behandelt den Fall, daß keine der früheren Bedingungen zu- zutrifft. Ist in einem solchen Fall keine explizite Aktion notwendig, kann eise Statement auch fehlen, oder man kann diesen Teil dazu benutzen, eine „unmögliche" Bedingung als Fehler zu erkennen. Um" eine Entscheidung mit drei Alternativen zu illustrieren, zeigen wir hier eine Funktion für binäre Suche, die feststellt, ob ein bestimmter Wert x in dem sortierten Vek- Vektor v vorkommt. Dabei müssen die Elemente von v in aufsteigender Reihenfolge ange- angeordnet sein. Die Funktion liefert die Position, also eine Zahl zwischen 0 und n-1, bei der x in v vorkommt, und -1 falls nicht. Die binäre Suche vergleicht zuerst den Eingangswert x mit dem mittleren Element des Vektors v. Falls x kleiner als der mittlere Wert ist, beschränkt sich die Suche auf die untere Hälfte des Vektors, ansonsten auf die obere Hälfte. In jedem Fall wird im näch- nächsten Schritt x mit dem mittleren Element der gewählten Hälfte verglichen. Dieser Vor- Vorgang der Zweiteilung wird solange fortgesetzt, bis der Wert gefunden wird oder bis der Bereich leer ist. /* binsearch: x in vtO) « vtD <= ... <= vtn-1) finden */ int binsearch(int x, int v[), int n) t int low, high, mid; low = 0; high «n-1; while (low <= high) t mid = (low+high) / 2; if (x < v[mid]> high = mid - 1; else if (x > v[mid]) low = mid + 1; else /* gefunden */ return mid; > return -1; /* nicht gefunden */ > Die wesentliche Entscheidung ist bei jedem Schritt, ob x kleiner, größer oder gerade gleich dem mittleren Element v[mid] ist; dies ist eine natürliche Anwendung für eine else-if-Kette. Aufgabe 3-1. Für unsere binäre Suche finden zwei Vergleiche in der Schleife statt, wenn einer genügen würde (für den Preis weiterer Vergleiche außerhalb der Schleife).
58 3 Kontrollstrukturen Schreiben Sie eine Version mit nur einem Vergleich in der Schleife, und messen Sie den Unterschied in der Laufzeit, ü 3.4 switch Die switch-Anweisung ist eine Auswahl unter mehreren Alternativen, die unter- untersucht, ob ein Ausdruck einen von mehreren konstanten ganzzahligen Werten besitzt; ist dies der Fall, so wird entsprechend verzweigt. switch ( expression ) t case const—expr : Statements case const-expr : Statements def au 11: Statements > Jeder Alternative geht eine Reihe von case-Marken mit ganzzahligen Konstanten oder konstanten Ausdrücken voraus. Hat die expression bei switch den Wert einer der case- Konstanten, so wird die Ausführung des Programms bei dieser case-Marke fortgesetzt. Alle case-Konstanten müssen voneinander verschieden sein. Wenn keine der Konstanten paßt, wird bei der default-Marke fortgefahren. Die default-Marke ist optional; ist keine vorhanden und paßt keine der Konstanten, so findet im switch keine Aktion statt, case und default können in beliebiger Reihenfolge angeordnet werden. In Kapitel 1 haben wir ein Programm formuliert, das Ziffern, Zwischenräume und alle anderen Zeichen gezählt hat. Dazu haben wir eine Kette von if ... else if... eise ver- verwendet. Hier ist das gleiche Programm mit einem switch: #ine lüde <stdio.h> mainO /* Ziffern, Zwischenrseume und andere Zeichen zaehlen */ t int c, i, nuhite, nother, ndigitHO); nwhite = nother - 0; for (i = 0; i < 10; i++) ndigitti] ■ 0; while ((c = getchsrO) != EOF) t switch (c) { esse '0': esse 'I1: esse '2': esse '3': esse '41: esse '51: esse '6': esse '71: esse '81: esse '9': ndigit[c-'0']++; break; esse ■ ■: case '\n': esse '\t': nwhite++; break; default: nother++; break; > > printf("digits «">; for (i = 0; i < 10; i++) printf(" %d", ndigitti]); printfC, white space ■ %d, other = %d\n", nwhite, nother); return 0;
3.5 Schleifen - die und for 59 Die break-Anweisung sorgt dafür, daß der switch unmittelbar verlassen wird. Da case-Marken nur Textpositionen bezeichnen, wird ein Programm an einer case-Marke vorbei weiter ausgeführt, nachdem eine Alternative erledigt ist, es sei denn, wir formulie- formulieren eine explizite Aktion, um den switch zu verlassen, break und return sind die häufig- häufigsten Anweisungen, mit denen ein switch verlassen wird. Mit break kann man auch while-, for- oder do-Schleifen vorzeitig verlassen; dies wird später in diesem Kapitel be- besprochen. Daß von einer Alternative in switch unmittelbar zur nächsten übergegangen wird, hat Vor- und Nachteile. Vorteilhaft ist, daß dadurch mehrere case-Marken vor einer ein- einzelnen Alternative stehen können, wie im vorliegenden Beispiel bei den Ziffern. Dafür muß aber normalerweise jede Alternative mit einer break-Anweisung beendet werden, um zu verhindern, daß implizit die nächste Alternative erreicht wird. Aus einer Alterna- Alternative zur nächsten überzugehen, ist nicht unbedingt robust; wird das Programm modifi- modifiziert, ergeben sich leicht Probleme. Abgesehen von mehreren case-Marken für eine ein- einzelne Alternative, sollten implizite Übergänge sparsam eingesetzt werden, und zwar mit Kommentar. Sie sollten sich auch angewöhnen, eine break-Anweisung nach der letzten Alterna- Alternative, in unserem Beispiel also nach default, einzufügen, obgleich dies aus logischer Sicht unnötig ist. Wird später eine weitere Alternative am Ende angefügt, so kann sich dieses Stück defensiven Programmierens als Rettung erweisen. Aufgabe 3-2. Schreiben Sie eine Funktion escape(s,t), die Zeichen wie Zeilentrenner und Tabulatorzeichen durch entsprechende Ersatzdarstellungen wie \n und \t ersetzt, während sie die Zeichenkette t nach s kopiert. Benutzen Sie eine switch-Anweisung. Schreiben Sie auch für die umgekehrte Operation eine Funktion, die Ersatzdarstellungen in reale Zeichen umwandelt, ü 3.5 Schleifen - while und for Den while- und for-Schleifen sind wir bereits begegnet. Bei while ( expression > statement wird zunächst eine Bedingung bewertet. Ist expression von 0 verschieden, so wird Statement, die abhängige Anweisung, ausgeführt und anschließend expression erneut be- bewertet. Dieser Zyklus wiederholt sich, bis schließlich expression den Wert 0 hat. Danach wird die Ausführung des Programms hinter Statement fortgesetzt. Die for-Anweisung for ( expn ; expri ; expr$ ) Statement ist äquivalent zu while ( expn ) t statement expr3 ; > mit Ausnahme des Verhaltens von continue, das in Abschnitt 3.7 beschrieben wird.
60 3 Kontrollstrukturen Grammatisch betrachtet sind die drei Teile von for jeweils Ausdrücke. In aller Re- Regel sind expr\ und expr$ Zuweisungen oder Funktionsaufrufe und expr^ ist ein Vergleich. Jeder dieser drei Teile kann fehlen, jedoch müssen die Semikolons verbleiben. Fehlen expr\ oder expr-$, so werden sie einfach in der äquivalenten while-Formulierung ausgelas- ausgelassen. Fehlt die Bedingung expr2, so gilt sie immer als wahr, for (;;) t ist also eine „unendliche" Schleife, die vermutlich durch Anweisungen wie break oder return beendet wird. Ob man while oder for bevorzugt, beruht weitgehend auf persönlicher Vorliebe. Beispielsweise gibt es in white ((c = getcharO) « ' • 11 c « '\n' 11 c -= ■ \t') ; /* Zwischenraum ueberspringen */ weder Initialisierung noch Reinitialisierung, also erscheint while natürlicher. for ist zu bevorzugen, wenn eine einfache Initialisierung und Reinitialisierung vor- vorgenommen werden sollen, denn for faßt die kontrollierenden Anweisungen der Schleife zusammen und macht sie am Anfang der Schleife sichtbar. Dies ist besonders offensicht- offensichtlich bei for (i = 0; i < n; i++) Diese Ausdrucksweise ist üblich in C, um die ersten n Elemente eines Vektors zu bear- bearbeiten, ist also analog zur DO-Schleife in Fortran oder zum for in Pascal. Es gibt jedoch gewisse Unterschiede: Die kontrollierende Variable und die Grenzen einer for-Schleife in C können aus der Schleife heraus beeinflußt werden, und der Wert der kontrollieren- kontrollierenden Variablen i bleibt erhalten, wie auch immer die Schleife beendet wird. Da bei for beliebige Ausdrücke angegeben werden können, beruhen for-Schleifen nicht ausschließ- ausschließlich auf arithmetischen Folgen. Es ist jedoch trotzdem schlechter Stil, wenn man Berech- Berechnungen, die keine Beziehung zueinander haben, in die Initialisierung oder die Reinitiali- Reinitialisierung einer for-Anweisung packt; diese Teile von for reserviert man besser für Opera- Operationen zur Kontrolle der Schleife. Als größeres Beispiel ist hier eine weitere Fassung von atoi, der Funktion, die das numerische Äquivalent einer Zeichenkette liefert. Diese Fassung ist etwas allgemeiner, als die im Kapitel 2; am Beginn der Zeichenkette können Zwischenraumzeichen auftre- auftreten sowie ein optionales + oder - als Vorzeichen. (In Kapitel 4 folgt dann noch die Funktion atof, die die gleiche Umwandlung für Gleitpunktwerte vornimmt.) Die Programmstruktur reflektiert das Eingabeformat: Zwischenraum überlesen, falls vorhanden Vorzeichen festhalten, falls voriianden Zahlenwert anlesen und umwandeln Jeder Teil des Programms erledigt seine eigene Aufgabe und hinterläßt klare Verhältnis- Verhältnisse für den nächsten Schritt. Der ganze Vorgang wird beendet, sobald ein Eingabezeichen gefunden wird, das nicht mehr Teil einer Zahl sein kann.
3.5 Schleifen - He und for 61 «include <ctype.h> /* stoi: Ziffernkette s in int umwandeln; Version 2 */ int stoi (char s[)) t int i, n, sign; for (i = 0; isspace(sti]); i++) ; /* Zwischenraeume ueberlesen */ sign * (sti] « '-') ? -1 : 1; if (sti] «■ '+' || sti) «» '-') /* Vorzeichen */ i++; for (n * 0; isdigit(sti]); i++) n « 10 * n + (sti] - '0'); return sign * n; > Die Standard-Bibliothek enthält eine aufwendigere Funktion strtol zur Umwandlung von Zeichenketten in long-Werte; siehe Abschnitt 5 im Anhang B. Die Kontrollausdrücke für eine Schleife zusammenzuhalten, bringt Vorteile - ganz besonders, wenn mehrere Schleifen verschachtelt sind. Die nachfolgende Funktion be- benutzt Shell's Algorithmus, um einen Vektor von int-Werten zu sortieren. Die grundsätz- grundsätzliche Idee bei diesem Algorithmus, den D. L. Shell 1959 erfunden hat, ist es, in den frühen Phasen Elemente zu vergleichen, die weit auseinander liegen; einfachere Aus- Austausch-Sortierverfahren vergleichen meistens nur benachbarte Elemente. Shell's Algo- Algorithmus neigt dazu, große Störungen der Reihenfolge schnell zu eliminieren, damit haben die späteren Phasen der Sortierung weniger zu tun. Der Abstand zwischen zwei Elemen- Elementen, die verglichen werden, wird allmählich auf 1 verringert. In dieser letzten Phase wird aus Shell's Algorithmus ein Austauschverfahren für benachbarte Elemente. /* shellsort: v[0]...vtn-1) aufsteigend sortieren */ void shellsort(int vt), int n) t int gap, i, j, temp; for (gap ■ n/2; gap > 0; gap /<= 2) for (i ■ gap; i < n; i++) for (j=i-gap; j>=0 && v[j]>vtj+gap); j-=gap> t temp <= vCj); vtj] = vtj+gap]; vfj+gap) = temp; Es gibt hier drei verschachtelte Schleifen. Die äußerste Schleife kontrolliert den Abstand zwischen zwei Elementen, die jeweils verglichen werden; dieser Abstand wird von n/2 ausgehend bei jedem Durchgang halbiert, bis schließlich 0 erreicht ist. Die mittlere Schleife geht über die Elemente. Die innerste Schleife vergleicht alle Paare von Elemen- Elementen, die zueinander den Abstand gap besitzen, und vertauscht die Elemente der Paare, die noch nicht in der richtigen Reihenfolge angeordnet sind. Da gap schließlich den Wert 1 erreicht, sind zum Schluß alle Elemente korrekt sortiert. Man beachte, daß auch die äußerste Schleife in die allgemeine Form einer for-Schleife paßt, obgleich hier keine arithmetische Folge durchlaufen wird.
62 Kontrollstrukturen Ein letzter C-Operator ist das Komma, das am häufigsten in for-Anweisungen vor- vorkommt. Zwei Ausdrücke, die durch Komma getrennt sind, werden von links nach rechts bewertet; Datentyp und Resultatwert sind dann der Typ und Wert des rechten Operan- Operanden. Man kann also in einer for-Anweisung jeweils mehrere Ausdrücke in den verschie- verschiedenen Teilen unterbringen, um beispielsweise zwei Indizes gleichzeitig zu bearbeiten. Dies wird in der folgenden Funktion reverse(s) illustriert, die die Zeichenkette s an Ort und Stelle umkehrt. «include <string.h> /* reverse: s an Ort und Stelle umkehren */ void reverse(chsr s[)> t int c, i, j; for (i « 0, j • strlen(s)-1; i < j; i++, j—) t c » sti); s[i) = stj); stj) = c; Funktionsargumente, Variablen in Deklarationen usw. werden zwar durch Komma ge- getrennt; dabei liegt jedoch kein Komma-Operator vor und folglich wird auch nicht unbe- unbedingt von links nach rechts bewertet. Komma-Operatoren sollten sparsam verwendet werden. Am besten passen sie, wenn Konstruktionen eng zusammengehören, wie in der for-Schleife von reverse oder in Makros, wo eine Berechnung mit mehreren Teilen als ein einfacher Ausdruck erscheinen muß. Ein Komma-Operator wäre auch beim Tauschen von Elementen in reverse ange- angebracht, denn der Tausch kann als eine Operation angesehen werden: for (i = 0, j = strlen(s)-1; i < j; i++, j—) c = sti), sei] = stj), stj] = c.- Aufgabe 3-3. Schreiben Sie eine Funktion expand(sl,s2), die eine Abkürzung wie a-z aus der Zeichenkette sl in der Zeichenkette s2 durch die äquivalente vollständige Liste abc.xyz darstellt. Berücksichtigen Sie dabei Groß- und Kleinbuchstaben sowie Ziffern, und akzeptieren Sie auch Angaben wie a-b-c oder a-zO-9 und auch -a-z. Am An- Anfang oder Ende sollte - wörtlich genommen werden, ü 3.6 Schleifen - do-while Wie wir in Kapitel 1 besprochen haben, wird die Abbruchbedingung von while und for am Anfang der Schleife geprüft. Im Gegensatz dazu wird bei do-while, der dritten Schleifenkonstruktion in C, das Abbruchkriterium am Ende der Schleife nach jedem Durchgang geprüft; die abhängige Anweisung der Schleife wird daher immer wenigstens einmal ausgeführt. Für die do-Schleife gilt folgende Syntax: do Statement while ( expression ); Die abhängige Anweisung (statement) wird ausgeführt, anschließend wird die Bedingung (expression) bewertet. Trifft sie zu, so wird die abhängige Anweisung erneut ausgeführt
3.7 break und co ie 63 usw. Ist schließlich die Bedingung nicht mehr erfüllt, wird die Schleife beendet. Abgese- Abgesehen von der Interpretation der Bedingung, ist do-while äquivalent zur repeat-until- Anweisung in Pascal. Die Erfahrung zeigt, daß do-while wesentlich seltener benutzt wird, als while und for. Trotzdem ist diese Anweisung ab und zu nützlich, wie zum Beispiel in der folgenden Funktion itoa, die einen Zahlenwert in eine Zeichenkette verwandelt (also die Umkeh- Umkehrung von atoi). Die Aufgabe ist ein bißchen komplizierter als es zunächst den Anschein hat, da die einfachen Methoden die Ziffern in der falschen Reihenfolge generieren. Wir produzieren deshalb die umgekehrte Zeichenkette und drehen sie anschließend um. /* itoa: n in Ziffernkette s umwandeln */ void itoaO'nt n, char st)> t . int i, sign; if ((sign ■ n) < 0) /* Vorzeichen notieren */ n = -n; /* n positiv machen */ i = 0; do t /* Ziffern von rechts her generieren */ s[i++) = n X 10 + '01; /• naechste Ziffer */ > while ((n /= 10) > 0); /* entfernen */ if (sign < 0) sti++] = '-•; sti) = '\0'; reverse(s); > Die do-while-Schleife ist hier notwendig oder mindestens praktisch, da wenigstens ein Zeichen im Vektor s angelegt werden muß, auch wenn n den Wert Null hat. Wenn von do-while nur eine einzelne Anweisung abhängt, so braucht diese nicht in geschweifte Klammern eingeschlossen zu werden. Wir verwenden trotzdem geschweifte Klammern, damit beim flüchtigen Lesen while nicht für den Anfang einer while-Schleife gehalten wird. Aufgabe 3-4. Werden ganzzahlige Werte im 2-Komplement repräsentiert, dann funktio- funktioniert unsere Fassung von itoa für die größte negative Zahl nicht korrekt, das heißt für den Wert „BWort8röße-1). Erklären Sie warum nicht. Ändern Sie itoa so ab, daß auch dieser Wert korrekt ausgegeben wird, unabhängig von der jeweiligen Maschine. O Aufgabe 3-5. Schreiben Sie die Funktion itob(n,s,b), die die ganze Zahl n in Basis b in der Zeichenkette s ablegt. Zum Beispiel legt itob(n,s,16) die Zahl n als hexadezimale Ziffern in der Zeichenkette s ab. ü Aufgabe 3-6. Schreiben Sie eine Version von itoa, die drei Argumente statt nur zwei akzeptiert. Das dritte Argument legt die minimale Breite der Ausgabe fest; nach Um- Umwandlung muß die Zeichenkette links mit Leerzeichen aufgefüllt werden, falls nötig, um sie genügend breit zu machen, ü 3.7 break und continue Es ist gelegentlich nützlich, wenn man Schleifen anders verlassen kann als nur durch Abbruchkriterien am Anfang oder Ende. Mit der break-Anweisung kann man for-, while- und do-Schleifen vorzeitig verlassen, genau wie eine switch-Anweisung. break
64 3 Kontrollstrukturen sorgt dafür, daß die innerste umgebende Schleife oder switch-Anweisung sofort verlassen wird. Die folgende Funktion trim entfernt Leerzeichen, Tabulatorzeichen und Zeilen- Zeilentrenner vom Ende einer Zeichenkette; dabei wird break dazu benutzt, eine Schleife zu verlassen, wenn ganz rechts kein Zwischenraumzeichen mehr gefunden wird. /* trim: Zwischenräume am Ende entfernen */ int trim(char s[]> { int n; for (n = strlen(s)-1; n >■= 0; n—) if (sCn] I» ' ' M s[n] l= '\t' && s[n] l= "\n') break; s[n+1] * '\0'; return n; > strlen liefert die Länge der Zeichenkette. Die for-Schleife beginnt beim letzten Zeichen und sucht rückwärts nach dem ersten Zeichen, das weder Leerzeichen noch Ta- Tabulatorzeichen oder Zeilentrenner ist. Die Schleife wird abgebrochen, wenn ein entspre- entsprechendes Zeichen gefunden wird, oder wenn n negativ wird (das heißt, wenn die ganze Zeichenkette abgesucht wurde). Sie sollten sich vergewissern, daß dies auch dann kor- korrekt ist, wenn die Zeichenkette leer ist oder nur aus Zwischenraumzeichen besteht. Die continue-Anweisung gleicht break, wird jedoch weniger oft verwendet. continue sorgt dafür, daß die nächste Wiederholung der umgebenden for-, while- oder do-Schleife unmittelbar begonnen wird. Bei while und do bedeutet dies, daß sofort wie- wieder die Bedingung bewertet wird; bei for wird als nächstes die Reinitialisierung durch- durchgeführt, continue bezieht sich nur auf Schleifen, nicht auf die switch-Anweisung. continue innerhalb einer switch-Anweisung innerhalb einer Schleife sorgt dafür, daß die Schleife wiederholt wird. Beispielsweise behandelt das folgende Programmfragment nur die nicht-negativen Elemente im Vektor a; negative Werte werden übergangen. for (i = 0; i < n; i++) { if (a[i] < 0) /* negative Elemente ueberspringen */ continue; /• nicht-negative Elemente bearbeiten */ > Die continue-Anweisung wird oft benutzt, wenn der anschließend folgende Teil einer Schleife so kompliziert ist, daß die Umkehrung einer Bedingung und weiteres Einrücken zu einer sehr tiefen Verschachtelung im Programm führen würden. 3.8 goto und Marken C verfügt auch über eine beliebig zu mißbrauchende goto-Anweisung und Marken, zu denen gesprungen werden kann. Formal ist goto niemals notwendig, und man kann fast immer leicht ohne goto-Anweisungen auskommen. Wir haben in diesem Buch goto nicht verwendet. Trotzdem gibt es einige Situationen, in denen goto angebracht sein könnte. Am häufigsten kann man damit die Ausführung einer tief verschachtelten Struktur beenden,
3.8 goto und t ken 65 wie zum Beispiel zwei Schleifen auf einmal verlassen, break kann hierbei nicht benutzt werden, da break nur jeweils die innerste Schleife beendet. Es ergibt sich also: for ( ... ) for (...)< if ( Katastrophe ) goto error; error: Oberreste abräum en Diese Programmstruktur ist nützlich, wenn die Fehlerbehandlung nicht trivial ist, und wenn Fehler an verschiedenen Stellen auftreten können. Eine Marke hat die gleiche Form wie ein Variablenname; anschließend folgt ein Doppelpunkt. Die Marke kann vor jeder Anweisung in der gleichen Funktion wie goto stehen. Der Gültigkeitsbereich einer Marke ist die ganze Funktion. Betrachten wir als weiteres Beispiel das Problem, herauszufinden, ob zwei Vekto- Vektoren a und b ein gleiches Element haben. Eine mögliche Lösung ist for (i = 0; i < n; i++) for (j = 0; j < m; j++) if Ca[i] == b[j]) goto found; /* kein gleiches Element gefunden */ found: /* eines gefunden: a[i) == b[j] •/ Man kann ein Programm mit goto grundsätzlich auch ohne goto formulieren, aller- allerdings benötigt man dazu vielleicht eine zusätzliche Variable, oder man muß einige Bedin- Bedingungen mehrfach angeben. Beispielsweise kann man die Vektorsuche so umformulieren: found = 0; for (i =0; i < n && 'found; i++) for (j = 0; j < m && !found; j++) if (a[i] « b[j]) found = 1; if (found) /* eines gefunden: a[i] « b[j] */ eise /* kein gleiches Element gefunden */ Mit einigen wenigen Ausnahmen, wie den hier zitierten, ist Code mit goto- Anweisungen im allgemeinen schwieriger zu verstehen und zu pflegen, als Code ohne goto. Wir wollen zwar nicht dogmatisch werden, aber es scheint doch, daß goto- Anweisungen wenn überhaupt, dann selten benutzt werden sollten.
67 4 Funktionen und Programmstruktur Mit Hilfe von Funktionen zerlegen wir große Problemstellungen in kleinere. Da- Damit können wir auf dem aufbauen, was andere vor uns programmiert haben, und müssen nicht selbst wieder von neuem beginnen. Zweckmäßige Funktionen verbergen die De- Details einer Operation vor den Teilen eines Programms, die darüber nicht Bescheid wissen müssen; dadurch wird das Ganze klarer, und es ist weniger schwierig, Änderungen vorzu- vorzunehmen. C wurde so entworfen, daß Funktionen effizient werden und einfach zu benutzen sind. Im allgemeinen bestehen C-Programme aus vielen kleinen Funktionen und nicht aus wenigen großen. Ein Programm kann in einer oder mehreren Quelldateien angeord- angeordnet werden. Die Quelldateien können separat übersetzt und dann gebunden werden, zu- zusammen mit früher übersetzten Funktionen aus Bibliotheken. Wir wollen diesen Prozeß hier nicht besprechen, da die Details sich je nach System unterscheiden. Im Bereich der Deklaration und Definition von Funktionen wurde C im ANSI- Standard am sichtbarsten geändert. Wie wir bereits im Kapitel 1 sahen, kann man nun den Typ von Parametern bei der Deklaration einer Funktion vereinbaren. Die Syntax der Funktionsdefinition hat sich auch geändert, so daß nun Deklaration und Definition zu- zusammenpassen. Dadurch kann ein Übersetzer jetzt viel mehr Fehler entdecken als früher. Wenn die Parameter korrekt vereinbart sind, werden außerdem die entsprechen- entsprechenden Typumwandlungen automatisch vorgenommen. Der Standard macht die Regeln zum Gültigkeitsbereich von Namen klarer; insbe- sonders verlangt er, daß ein externes Objekt nur einmal definiert wird. Die Initialisie- Initialisierung wurde verallgemeinert: automatische Vektoren und Strukturen können nun initiali- initialisiert werden. Auch der C-Preprozessor wurde verbessert. Zu den neuen Fähigkeiten des Pre- prozessors gehören ein kompletterer Satz von Anweisungen zur bedingten Übersetzung, eine Möglichkeit, konstante Zeichenketten aus Makroargumenten zu erzeugen und eine genauere Kontrolle über den Ablauf der Makro-Expansion. 4.1 Grundbegriffe Als erstes wollen wir ein Programm entwerfen und schreiben, das jede Eingabezei- Eingabezeile druckt, die ein spezielles „Muster", eine Zeichenkette, enthält. (Dies ist ein Spezialfall des UNIX-Dienstprogramms grep.) Sucht man zum Beispiel die Buchstabenfolge „ould" in den folgenden Zeilen Ah Love! could you and I with Fate conspire To grasp this sorry Scheme of Things entire. Would not we shatter it to bits — and then Re-mould it nearer to the Heart's Desire! so erhält man Ah Love! could you and I with Fate conspire Would not we shatter it to bits — and then Re-mould it nearer to the Heart's Desire!
68 4 Funktionei d Programmstruktur Die Arbeit läßt sich leicht in drei Teile aufteilen: while ( es gibt noch eine Zeile ) if ( die Zeile enthält das Suchmuster ) Zeile ausgeben Man kann dies selbstverständlich alles in main codieren; es ist jedoch besser, die natürliche Struktur auszunutzen und jeden Teil als separate Funktion zu realisieren. Drei kleine Teile sind leichter zu bearbeiten als ein großer, denn irrelevante Details kann man in den Funktionen verbergen, und das Risiko von unerwünschten Wechselwirkungen wird minimiert. Schließlich können die Einzelteile auch noch in anderen Programmen nütz- nützlich sein. while ( es gibt noch eine Zeile ) ist getline, eine Funktion, die wir in Kapitel 1 geschrieben haben, und Zeile ausgeben ist printf, eine Funktion, die schon jemand für uns zur Verfügung gestellt hat. Folglich müssen wir nur eine Routine schreiben, die entscheidet, ob die Zeile das Muster enthält. Wir lösen dieses Problem, indem wir eine Funktion strindex(s,t) schreiben, die die Position (den „Index") in der Zeichenkette s liefert, wo die Zeichenkette t beginnt, oder auch -1, wenn s das Muster t nicht enthält. Da in C Vektoren bei Position 0 beginnen, sind Indexwerte 0 oder positiv und ein negativer Wert wie -1 ist praktisch, um Mißerfolg anzuzeigen. Wenn wir später raffiniertere Methoden zur Mustererkennung benötigen, dann müssen wir nur strindex ersetzen; das restliche Programm kann unverändert blei- bleiben. (In der Standard-Bibliothek gibt es eine Funktion strstr, die strindex ähnelt; sie lie- liefert jedoch einen Zeiger und keinen Index.) Mit diesem Entwurf sind die Details des Programms sehr einfach. Hier ist das Ganze, damit Sie sehen können, wie die Teile zusammenpassen. Momentan ist das Such- Suchmuster eine konstante Zeichenkette - nicht unbedingt ein sehr allgemeiner Mechanis- Mechanismus. Wir werden in Kürze besprechen, wie man Zeichenvektoren initialisiert, und in Ka- Kapitel 5 zeigen wir, wie man das Muster als Parameter bei der Programmausführung über- übergibt. Wir haben hier auch eine leicht veränderte Version von getline; Sie sollten sie mit der Version in Kapitel 1 vergleichen. «include <stdio.h> «define MAXLINE 1000 /* maximale Laenge der Eingabezeile */ int getline(char lined, int max); int strindex(char source[], char searchforC] ); char pattern!] = "ould"; /* Suchmuster */ /* alle Zeilen mit Suchmuster finden */ mainO C char line[MAXLINE]; int found » 0; while (getlinedine, MAXLINE) > 0) if (strindex(line, pattern) >= 0) i printf("Xs", line); found++; > return found;
4.1 Grundbeg 69 /* getline: Zeile an s, laenge als Resultat */ int getline(char s[), int lim) i int c, i; i = 0; while (—lim > 0 && (c=getchar()) != EOF && c != '\n') s[i++] = c; if (c == '\n') s[i++] - c; s[i] = '\0'; return i; > /* strindex: Position von t in s liefern, -1 falls nicht da */ int strfndex(char s[], char t[]) i int i, j, k; for (i = 0; sti] != '\0'; i++) i for (j*i, k=0; t[k]!='\O' && s[j]==t[k]; j++, k++) if (k > 0 && t[k] « "\0') return i; > return -1; > Jede Funktionsdefinition hat folgende Form: Funktionstyp Funktionsname ( Parameterdeklarationen ) C Vereinbarungen und Anweisungen > Verschiedene Teile können fehlen. Eine minimale Funktion ist dunmyo O Diese Funktion tut nichts und liefert nichts. Eine leere Funktion, wie diese, ist manchmal nützlich als Platzhalter während der Programmentwicklung. Wenn der Resultattyp weg- weggelassen wird, wird int angenommen. Ein Programm besteht nur aus Definitionen von Variablen und Funktionen. Die Funktionen tauschen untereinander Information aus mit Hilfe von Argumenten und Re- Resultatwerten und über externe Variablen. Die Funktionen können in beliebiger Reihen- Reihenfolge in der Quelldatei angeordnet sein, und das Programm kann aus mehreren Quellda- Quelldateien bestehen, wenn eine Funktion nicht auf mehrere Dateien aufgeteilt wird. Die return-Anweisung dient dazu, einen Wert von der aufgerufenen Funktion zum Aufrufer als Resultat zu liefern. Nach return kann ein beliebiger Ausdruck folgen: return expression • Falls nötig, wird expression in den Resultattyp der Funktion umgewandelt; expression wird oft mit Klammern umgeben, aber sie sind optional. Es steht dem Aufrufer frei, den Resultatwert zu ignorieren. Nach return braucht auch kein Ausdruck zu stehen; in diesem Fall wird dem Aufrufer kein Wert geliefert. Ebenso wird die Ausführung einer Funktion beendet, und dem Aufrufer wird kein Resul- Resultatwert geliefert, wenn bei Ausführung der Funktion die abschließende rechte geschweif-
te Klammer erreicht wird. Es ist nicht illegal, aber vermutlich problematisch, wenn eine Funktion an einer Stelle ein Resultat liefert und an einer anderen Stelle nicht. Auf alle Fälle ist der „Wert" einer Funktion, die keinen liefert, sicherlich nicht sinnvoll. Das Mustersuchprogramm liefert aus main als Statusinformation die Anzahl Tref- Treffer. Dieser Wert steht der Umgebung zur Verfügung, von der das Programm aufgerufen wurde. Wie man ein C-Programm übersetzt und bindet, das aus mehreren Quelldateien besteht, ist je nach System unterschiedlich. Bei einem UNIX-System beispielsweise erle- erledigt diese Aufgabe das cc -Kommando, das in Kapitel 1 erwähnt wurde. Angenommen, unsere drei Funktionen stehen in drei Dateien, genannt tnain.c, getline.c und strindex.c. Dann übersetzt das Kommando cc main.c getline.c strindex.c die drei Dateien, legt die sogenannten Objektmodule in den Dateien main.o, getline.o und strindex.o ab und bindet dann alle drei als ausführbare Datei namens a.oul. Wenn zum Beispiel main.c einen Fehler enthält, kann diese Datei allein nochmals übersetzt werden, und das Resultat kann mit den früher produzierten Objektmodulen mit folgen- folgendem Kommando gebunden werden: cc main.c getline.o strindex.o Nach Konvention unterscheidet cc an Hand der Endungen ,,.c" und ,,.o" Quelldateien von Montagecodedateien. Aufgabe 4-1. Schreiben Sie die Funktion strrindex(s,t), die die erste Position von rechts her liefert, bei der t in s vorkommt, oder auch -1, falls nicht. D 4.2 Funktionen ohne ganzzahliges Resultat Bisher haben unsere Beispielfunktionen entweder keinen Wert (void) oder einen int-Wert geliefert. Was ist, wenn eine Funktion einen anderen Resultattyp liefern muß? Viele numerische Funktionen wie sqrt, sin und cos liefern double; andere spezialisierte Funktionen liefern andere Resultattypen. Um zu illustrieren, wie man dies bewerkstel- bewerkstelligt, wollen wir die Funktion atof(s) schreiben und benutzen, die die Zeichenkette s in ihr doppelt genaues Gleitpunktäquivalent umwandelt, atof ist eine Erweiterung von atoi, von der wir verschiedene Versionen in Kapitel 2 und 3 gezeigt haben; atof verarbeitet op- optional ein Vorzeichen und einen Dezimalpunkt; die Ziffernfolgen vor und nach dem De- Dezimalpunkt können beliebig fehlen oder vorhanden sein. Unsere Version ist keine Einga- Eingabe-Umwandlungsroutine von hoher Qualität; dies würde mehr Platz in Anspruch nehmen als wir hier aufwenden möchten. Die Standard-Bibliothek enthält eine Funktion atof; sie ist in der Definitionsdatei <stdlib.h> deklariert. Als erstes muß atof seinen Resultattyp selbst deklarieren, da dieser nicht int ist. Der Typname steht vor dem Funktionsnamen: «include <ctype.h> /* atof: Ziffernkette s nach double umwandeln */ double atof (char s[]> i double val, power; int i, sign; for (i - 0; isspace(sti)); i++) /* Zwischenraum uebergehen */ sign = CsCi] == '-') ? -1 : 1;
4.2 Funktionc hne ganzzahliges Resultat 71 if (sm == '♦' || s[i] « '-•) '♦♦; for (val = 0.0; isdigit(s[i]); i++) val = 10.0 * val + (s[i] - '0'); if (s[i] == '.') i++; for (power « 1.0; isdigit(s[i]); i++) i val = 10.0 * val ♦ (s[i] - '0'); power *= 10.0; > return sign * val / power; > Als zweites, und dies ist genauso wichtig, muß der Aufrufer ebenfalls wissen, daß atof kein int-Resultat liefert. Eine Möglichkeit ist, atof explizit in der aufrufenden Funk- Funktion zu deklarieren. Eine solche Deklaration zeigt der folgende primitive Taschenrech- Taschenrechner (der gerade zur Überprüfung eines Bankkontos genügt). Er liest pro Zeile eine Zahl, der optional ein Vorzeichen vorausgehen kann, und addiert alle Zahlen auf. Nach jeder Eingabe wird die momentane Summe ausgegeben. «include <stdio.h> «define MAXLINE 100 /* rudimentaerer Taschenrechner */ mainO < double sum, atof (char []); char line[MAXLINE]; int getline(char linen, int max); sum = 0; while (getlineCline, MAXLINE) > 0) printf("\t%g\n", sum += atof(line)); return 0; > Die Vereinbarung double sum, atof (char []); legt fest, daß sum eine double-Variable ist und daß atof eine Funktion ist, die einen Para- Parameter vom Typ char[ ] hat und double als Resultat liefert. Die Funktion atof muß konsistent deklariert und definiert werden. Wenn atof selbst und der Aufruf in main verschiedene Typen in der gleichen Quelldatei haben, wird der Fehler vom Übersetzer erkannt. Wird jedoch (was wahrscheinlicher ist) atof separat übersetzt und in main nicht explizit deklariert, so würde der Unterschied nicht entdeckt werden: atof würde ein double-Resultat liefern, das main dann (ohne Deklaration) als int-Wert behandelt und sinnlose Resultate würden folgen. In Anbetracht dessen, daß wir gesagt haben, daß Deklarationen zu Definitionen passen müssen, könnte dies überraschend sein. Eine Inkonsistenz kann aber dadurch entstehen, daß ohne einen Funktionsprototyp eine Funktion implizit deklariert wird, wenn sie erstmals in einem Ausdruck vorkommt: sum ♦= atof(line) Wenn ein Name, der bisher noch nicht vereinbart wurde, in einem Ausdruck vorkommt und wenn ihm eine Unke Klammer folgt, wird er durch diesen Kontext als Funktionsname
72 4 Funktionen Programmstruktur deklariert. Es wird angenommen, daß die Funktion int als Resultat liefert. Keine An- Annahmen werden über die Parameter getroffen. Auch wenn eine Funktionsdeklaration keine Parameterangaben enthält, wie in double atofO; bedeutet das ebenfalls, daß keine Annahmen über die Parameter von atof getroffen wer- werden sollen; die Überprüfung der Parameter wird für atof ganz ausgeschaltet. Diese spe- spezielle Bedeutung der leeren Parameterliste soll ermöglichen, daß man ältere C-Program- me mit neuen Übersetzern bearbeiten kann. Aber es ist keine gute Idee für neue Pro- Programme. Wenn eine Funktion Argumente akzeptiert, sollten Parameter deklariert wer- werden; wenn sie keine Argumente akzeptiert, benutzt man void. Mit Hilfe von atof, korrekt deklariert, können wir atoi schreiben (also eine Zei- Zeichenkette in int umwandeln): /* atoi: Ziffernkette s nach int unMandeln, mit atof */ int atoi(char s[)) i double atof (char s[]); return (int) atof(s); > Beachten Sie die Struktur der Vereinbarungen und die return-Anweisung. Der Wert des Ausdrucks bei return expression ; wird in den Resultattyp der Funktion umgewandelt, bevor die Ausführung der Funktion beendet wird. Daher wird der Wert von atof, also ein double-Wert, automatisch in int umgewandelt, wenn dieser Wert bei return auftritt, da die Funktion atoi ein int-Resultat liefert. Diese Operation führt jedoch eventuell zu Informationsverlust, deshalb warnen einige Übersetzer davor. Die Umwandlungsoperation (int) sagt explizit, daß diese Ope- Operation beabsichtigt ist, und unterdrückt jede Warnung. Aufgabe 4-2. Erweitern Sie atof so, daß auch Werte der Form 123.45e-6 akzeptiert werden, bei denen einem Gleitpunktwert optional der Buchstabe e oder E und ein Exponent, wieder optional mit Vorzeichen, folgen kann. D 4.3 Externe Variablen Ein C-Programm besteht aus einer Reihe von externen Objekten, die entweder Va- Variablen oder Funktionen sind. Dabei wird das Adjektiv „extern" als Kontrast zu „intern" benutzt. Letzteres beschreibt die Parameter und Variablen, die innerhalb von Funktio- Funktionen definiert sind. Externe Variablen sind außerhalb von allen Funktionen definiert und stehen daher potentiell vielen Funktionen zur Verfügung. Funktionen selbst sind immer extern, da C nicht erlaubt, daß Funktionen innerhalb von anderen Funktionen definiert werden. Nach Voreinstellung haben externe Variablen und Funktionen die Eigenschaft, daß alle Verweise auf sie mit gleichem Namen auch den gleichen Gegenstand bezeich- bezeichnen, sogar aus Funktionen heraus, die separat übersetzt wurden. (Der Standard nennt diese Eigenschaft externe Bindung.) In dieser Hinsicht verhalten sich externe Variablen wie die COMMON-Regionen in Fortran oder wie Variablen im äußersten Block in Pascal.
4.3 Externe Vari jn 73 Wir werden später,sehen, wie man externe Variablen und Funktionen definiert, die nur innerhalb einer einzigen Quelldatei sichtbar sind. Da man auf externe Variablen global zugreifen kann, sind sie eine Alternative zu Funktionsargumenten und Resultatwerten, um Daten zwischen Funktionen zu übertra- übertragen. Jede Funktion kann auf eine externe Variable zugreifen, indem sie ihren Namen verwendet, wenn dieser Name irgendwie vereinbart wurde. Müssen sehr viele Variablen mehreren Funktionen zur Verfügung stehen, sind ex- externe Variablen bequemer und effizienter als lange Argumentlisten. Wie wir jedoch schon in Kapitel 1 erklärt haben, sollte man dieses Konzept vorsichtig verwenden, denn es kann sich negativ auf die Programmstruktur auswirken und kann zu Programmen mit zu vielen Datenpfaden zwischen Funktionen führen. Externe Variablen sind auch nützlich wegen ihres größeren Gültigkeitsbereichs und längerer Lebensdauer. Automatische Variablen existieren intern in einer Funktion; sie werden erzeugt, wenn die Funktion aufgerufen wird, und verschwinden, wenn die auszu- auszuführende Funktion beendet ist. Externe Variablen sind dagegen permanent, deshalb be- behalten sie ihre Werte von einem Funktionsaufruf zum nächsten. Wenn also zwei Funktio- Funktionen Daten gemeinsam benutzen müssen und wenn keine Funktion die andere aufruft, ist es oft am einfachsten, wenn die gemeinsam benutzten Daten in externen Variablen ste- stehen und nicht als Argumente herumgereicht werden. Wir wollen dies mit einem größeren Beispiel vertiefen. Wir beschreiben dazu einen Taschenrechner, der die Operatoren +, -, * und / realisiert. Da es einfacher zu implementieren ist, benutzt dieser Taschenrechner umgekehrte polnische Notation und nicht Infix. (Umgekehrte polnische Notation wird von einigen Taschenrechnern benutzt sowie in Sprachen wie Forth und Postscript.) In umgekehrter polnischer Notation folgt jeder Operator seinen Operanden; ein Infix-Ausdruck wie O - 2) • D + 5) wird als 12-45 + * eingegeben. Klammern sind nicht notwendig; die Notation ist eindeutig, wenn wir wis- wissen, wieviele Operanden jeder Operator erwartet. Die Implementierung ist einfach. Jeder Operand wird auf einen Stack gebracht; kommt ein Operator an, so wird die entsprechende Anzahl Operanden (nämlich zwei bei binären Operatoren) vom Stack geholt, der Operator wird angewendet, und das Resultat wird wieder auf den Stack gebracht. Im obigen Beispiel werden 1 und 2 auf den Stack gebracht und dann durch ihre Differenz -1 ersetzt. Anschließend werden 4 und 5 auf den Stack gebracht und dann durch ihre Summe 9 ersetzt. Schließlich ersetzt das Pro- Produkt von -1 und 9, nämlich -9, diese zwei Werte auf dem Stack. Das oberste Element auf dem Stack wird entfernt und ausgegeben, wenn das Ende der Eingabezeile erreicht wird. Als Programmstruktur ergibt sich also eine Schleife, die die richtige Operation für jeden Operator und Operanden durchführt, wenn er ankommt:
74 4 Funktionen Programmstruktur while ( nächsterOperator oder Operand bedeutet nicht Dateiende ) if ( Zahl ) auf den Stack else if ( Operator ) Operanden vom Stack holen Operation ausfähren Resultat auf den Stack else if ( Zeilenende ) Wert vom Stack holen und ausgeben eise Fehler Objekte auf den Stack zu bringen (push) und vom Stack zu holen (pop) ist trivial, fügt man jedoch Fehlererkennung und Fehlerbehandlung dazu, so werden diese Opera- Operationen aufwendig genug, daß man lieber jede als eigene Funktionen realisiert und nicht den Code überall im Programm dupliziert. Außerdem sollte es eine separate Funktion geben, die den nächsten Operator oder Operanden aus der Eingabe liefert. Bisher noch nicht diskutiert wurde die wesentliche Entwurfsentscheidung: wo be- befindet sich der Stack, das heißt, welche Routinen greifen direkt auf ihn zu? Eine Mög- Möglichkeit ist, den Stack in main zu definieren, und den Stack und die momentane Position auf dem Stack an die Routinen zu übergeben, die den Stack ansprechen. Andrerseits braucht main nichts über die Variablen zu wissen, die den Stack kontrollieren; main soll- sollte nur push und pop verwenden. Wir haben deshalb den Stack und die zugehörige Infor- Information als externe Variablen realisiert, auf die nur die Funktionen push und pop, nicht aber main, zugreifen können. Diese Skizze nun in ein Programm umzusetzen, ist leicht genug. Wenn wir uns vor- vorläufig das Programm in einer einzigen Quelldatei vorstellen, wird sie etwa so aussehen: «includes «defines Funktionsdeklarationen für main mainO < ... > externe Variablen für push und pop void push (double f) { ... > double pop(void) i ... > int getop(char s[]) { ... > von getop aufgerufene Funktionen Später werden wir erklären, wie man das auf zwei oder mehr Quelldateien verteilen kann. Die Funktion main besteht aus einer Schleife mit einem großen switch, abhängig vom Typ des Operators oder Operanden; diese Verwendung von switch ist wahrschein- wahrscheinlich typischer als das Beispiel in Abschnitt 3.4. «include <stdio.h> «include <stdlib.h> /* fuer atofo */ «define MAXOP 100 /* max. Laenge von Operand oder Operator */ «define NUMBER '0' /* Anzeige: eine Zahl wurde entdeckt */ int getop(char [] ); void push(double); double pop(void);
4.3 Externe Vari m75 /* Taschenrechner mit umgekehrter polnischer Notation */ mainO < int type; double op2; char stMAXOP]; while ((type = getop(s)) != EOF) i switch (type) { case NUMBER: push(atof(s)); break; case '+': push(pop() + popO); break; case '*': push(pop() * popO); break; case '-': op2 = popO; push(pop() - op2); break; case '/': op2 = popO; if (op2 != 0.0) push(pop() / op2); eise printf("error: zero divisor\n"); break; case '\n': printf("\tX.8g\n», popO); break; default: printf("error: unknown command Xs\n", s); break; return 0; > Da + und * kommutative Operatoren sind, ist die Reihenfolge irrelevant, in der die vom Stack geholten Operanden kombiniert werden, bei den Operatoren - und / müssen je- jedoch die rechten und linken Operanden unterschieden werden. Bei push(pop<) - popO); /* falsch •/ ist die Reihenfolge Undefiniert, in der die zwei Aufrufe von pop ausgeführt werden. Um die richtige Reihenfolge zu garantieren, muß man den ersten Wert an eine temporäre Variable zuweisen, wie wir das in main gemacht haben. «define HAXVAL 100 /* maximale Stack-Laenge V int sp = 0; /* naechste freie Stack-Position */ double valCMAXVAL); /* Stack fuer die Operanden •/
76 4 Funktionen i Programmstruktur /• push: f auf den Stack bringen •/ void push(double f) < if (sp < HAXVAL) val[sp++] = f; eise printfCerror: Stack full, can't push Xg\n", f); > /* pop: Wert vom Stack holen und liefern */ double pop(void) i if (sp > 0) return val[—sp]; eise < printfC'error: Stack empty\n"); return 0.0; Eine Variable ist extern, wenn sie außerhalb jeder Funktion definiert wird. Also werden der Stack und der Stack-Index, die push und pop gemeinsam benutzen müssen, außerhalb von diesen Funktionen definiert, main selbst benutzt den Stack oder den Stack-Index nicht - die Repräsentierung kann verborgen werden. Betrachten wir jetzt die Implementierung von getop, die Funktion, die den näch- nächsten Operator oder Operanden bereitstellt. Die Aufgabe ist einfach. Zwischenräume müssen überlesen werden. Ist dann das nächste Zeichen weder eine Ziffer noch ein De- Dezimalpunkt, soll es als Operator geliefert werden. Andernfalls muß eine Folge von Zif- Ziffern (unter denen sich ein Dezimalpunkt befinden kann) gesammelt werden, und das Re- Resultat ist NUMBER, als Hinweis, daß eine Zahl aufgefunden wurde, «include <ctype.h> int getch(void); void ungetch(int); /* getop: naechsten Operator oder numerischen Operanden holen */ int getop(char s[]) int i, c; while C(s[O] = c = getcho) « ' ' 11 c == '\t') s[1] = '\0'; if (Üsdigit(c) && c !* '.') return c; /• keine Zahl */ i = 0; if (tsdigit(c)) /* ganzzahligen Teil sammeln */ while (isdigit(st++i] = c * getch())) * if (c =='.') /• Dezimalstellen sammeln */ while (isdigit(s[++i] = c = getchO)) s[i] = '\0'; if (c != EOF) ungetch(c); return NUMBER; >
4.3 Externe Varial 77 Was für Funktionen sind getch und ungetch? Oft stellt ein Programm, das eine Eingabe verarbeitet, erst dann fest, daß es genügend eingelesen hat, wenn bereits zuviel eingelesen wurde. Ein Beispiel dafür ist das Sammeln einer Ziffernfolge für eine Zahl: Der Zahlenwert ist erst vollständig, wenn das erste Zeichen eingelesen wird, das keine Ziffer mehr ist. Dann hat aber das Programm bereits ein Zeichen zuviel gelesen, ein Zeichen, mit dem das Programm im Augenblick nichts anfangen kann. Das Problem wäre einfach zu lösen, wenn man die unerwünschten Zeichen auch wieder „aus-lesen" könnte. Immer wenn das Programm ein Zeichen zuviel liest, könnte es dieses Zeichen in die Eingabe zurückstellen, damit der Rest des Programms so tun kann, als ob dieses Zeichen noch nicht gelesen wurde. Erfreulicherweise kann man diese Operation leicht dadurch simulieren, daß man ein Paar kooperierender Funktionen schreibt, getch liefert das nächste Eingabezeichen, das untersucht werden soll; ungetch stellt Zeichen in die Eingabe zurück, die dann die nächsten Aufrufe von getch liefern, be- bevor neue Eingabe gelesen wird. Wie diese Funktionen zusammenarbeiten, ist einfach, ungetch speichert die zurückgestellten Zeichen in einem gemeinsamen Puffer, einem Zeichenvektor, getch liest aus diesem Puffer, wenn sich dort Zeichen befinden; getchar wird aufgerufen, wenn der Puffer leer ist. Es muß dann noch eine Indexvariable geben, die die Position des ak- aktuellen Zeichens im Puffer angibt. Da der Puffer und die Indexvariable von getch und ungetch gemeinsam benutzt werden und ihre Werte zwischen Aufrufen beibehalten müssen, müssen diese Variablen extern für beide Funktionen sein. Also können wir getch, ungetch und ihre gemeinsamen Variablen folgendermaßen implementieren: «define BUFSIZE 100 char buf CBUFSIZE]; /* Puffer fuer ungetchO */ int bufp » 0; /* naechste freie Position in buf */ int getch(void) /* naechstes (eventuell zurückgestelltes) Zeichen holen */ i return (bufp > 0) ? buf[—bufp] : getcharO; > void ungetch(int c) /* Zeichen zurueckstellen */ C if (bufp >= BUFSIZE) printf("ungetch: too many characters\n"); else buf[bufp++] = c; > Die Standard-Bibliothek enthält eine Funktion ungetc, die ein Zeichen zurückstellen kann; wir werden sie in Kapitel 7 vorstellen. Wir haben einen Vektor als Puffer benutzt und nicht nur ein einzelnes Zeichen, um einen allgemeineren Ansatz zu zeigen. Aufgabe 4-3. Da der grundsätzliche Rahmen gegeben ist, kann der Taschenrechner leicht erweitert werden. Fügen Sie den Modulo-Operator % und Vorkehrungen für nega- negative Zahlen hinzu. G Aufgabe 4-4. Fügen Sie Kommandos hinzu, um das oberste Element des Stacks auszu- auszugeben ohne es zu entfernen, um das oberste Element zu duplizieren und um die obersten beiden Elemente zu vertauschen. Fügen Sie ein Kommando hinzu, das den Stack leert. G
78 4 Funktionen i Programmstruktur Aufgabe 4-5. Fügen Sie Zugriffe auf Bibliotheksfunktionen wie sin, exp und pow hinzu. Siehe < math Ji > im Anhang B, Abschnitt 4. D Aufgabe 4-6. Fügen Sie Kommandos zum Umgang mit Variablen hinzu. (Man kann leicht 26 Variablen mit Einzelbuchstaben als Namen unterstützen.) Fügen Sie eine Va- Variable für den zuletzt ausgegebenen Wert hinzu. D Aufgabe 4-7. Schreiben Sie eine Funktion ungets(s), die eine ganze Zeichenkette in die Eingabe zurückstellt. Soll ungets über buf und bufp Bescheid wissen oder sollte nur ungetch benutzt werden? D Aufgabe 4-8. Nehmen wir an, daß nie mehr als ein Zeichen zurückgestellt wird. Än- Ändern Sie getch und ungetch entsprechend. D Aufgabe 4-9. Unsere Funktionen getch und ungetch behandeln die Zurückstellung von EOF nicht korrekt. Entscheiden Sie, wie diese Funktionen sich verhalten sollen, wenn EOF zurückgestellt wird, und implementieren Sie Ihren Entwurf. D Aufgabe 4-10. Ein anderer Entwurf benutzt getline, um eine ganze Eingabezeile zu le- lesen; damit werden getch und ungetch unnötig. Ändern Sie den Taschenrechner so, daß er diesen Ansatz benutzt. G 4.4 Regeln zum Gültigkeitsbereich Die Funktionen und externen Variablen, aus denen ein C-Programm besteht, müs- müssen nicht alle gleichzeitig übersetzt werden; der Quelltext des Programms kann aus ver- verschiedenen Dateien bestehen, und früher übersetzte Routinen können aus Bibliotheken dazugebunden werden. Unter anderem interessieren folgende Fragen: • Wie schreibt man Vereinbarungen, damit die Variablen während der Überset- Übersetzung korrekt vereinbart werden? • Wie ordnet man die Vereinbarungen an, damit alle Teile beim Laden des Pro- Programms korrekt gebunden werden? • Wie werden Vereinbarungen organisiert, so daß es nur eine Kopie von ihnen gibt? • Wie werden externe Variablen initialisiert? Wir werden diese Punkte diskutieren, indem wir das Taschenrechnerprogramm umorga- umorganisieren und auf mehrere Quelldateien verteilen. Aus praktischer Sicht ist der Taschen- Taschenrechner zu klein, als daß sich Aufteilen lohnt, aber er illustriert die Probleme sehr schön, die bei größeren Programmen auftreten. Der Gültigkeitsbereich eines Namens ist der Teil des Programms, in dem der Name benutzt werden kann. Für eine automatische Variable, die am Anfang einer Funktion vereinbart ist, ist der Gültigkeitsbereich die Funktion, in der der Name vereinbart ist. Lokale Variablen mit gleichem Namen in verschiedenen Funktionen haben nichts mitein- miteinander zu tun. Gleiches gilt für die Parameter der Funktion, die im Endeffekt lokale Va- Variablen sind. Der Gültigkeitsbereich einer externen Variablen oder Funktion reicht von dem Punkt, wo sie vereinbart wird, bis zum Ende der Datei, die gerade übersetzt wird. Wer- Werden etwa main, sp, val, push und pop zusammen in einer Datei in der vorher gezeigten Reihenfolge definiert, das heißt
4.4 Regeln zum G ,keitsbereich 79 mainO i ... > int sp = 0; double vaUMAXVAL]; void pushCdouble f) i ... > double pop(void) < ... > dann können die Variablen sp und val in push und pop einfach dadurch benutzt werden, daß ihre Namen angegeben werden; keine weiteren Deklarationen sind nötig. Aber we- weder sind diese Namen in main sichtbar, noch etwa push und pop selbst. Soll jedoch eine externe Variable verwendet werden, bevor sie definiert wird, oder wird sie in einer anderen Quelldatei definiert, als in der, in der sie benutzt wird, dann ist eine extern-Deklaration notwendig. Man muß genau die Deklaration einer externen Variablen von ihrer Definition un- unterscheiden. Eine Deklaration legt die Eigenschaften einer Variablen (vor allem ihren Datentyp) fest; eine Definition sorgt auch noch für Speicherplatz. Erscheinen die Zeilen int sp; double vaUMAXVAl]; außerhalb von allen Funktionen, dann definieren sie die externen Variablen sp und val, sorgen für Speicherplatz und dienen außerdem als Deklaration für den Rest dieser Quelldatei. Andrerseits deklarieren die Zeilen extern int sp; extern double val[]; für den Rest der Quelldatei, daß sp eine int-Variable und val ein double-Vektor ist (des- (dessen Größe anderswo festgelegt wird); hier werden jedoch die Variablen nicht erzeugt, und es wird kein Speicherplatz reserviert. In allen Quelldateien für ein Programm zusammen darf es nur eine Definition für eine externe Variable geben; andere Dateien können extern-Deklarationen enthalten, um auf diese Variable zu verweisen. (In der Datei mit der Definition dürfen auch noch extern-Deklarationen stehen.) Vektorgrößen müssen bei der Definition angegeben wer- werden, bei einer extern-Deklaration sind sie optional. Eine externe Variable kann nur in ihrer Definition initialisiert werden. Bei dem vorliegenden Programm ist es unwahrscheinlich, aber die Funktionen push und pop könnten in einer Datei definiert werden, und die Variablen val und sp könnten in einer anderen definiert und initialisiert werden. Dann wären die folgenden Definitionen und Deklarationen notwendig, um sie zu verbinden: in der ersten Datei: extern int sp; extern double val[]; void pushCdouble f) < ... } double pop(void) < ... } in der zweiten Datei: int sp = 0; double val[MAXVAL];
80 4 Funktionen u Programmstruktur Da die extern-Deklarationen in der ersten Datei vor und außerhalb der Funktionsdefini- Funktionsdefinitionen liegen, gelten sie deshalb für alle Funktionen gemeinsam; ein Satz Deklarationen genügt für die ganze Datei. Die gleiche Anordnung würde auch gebraucht, wenn die De- Definitionen von sp und val erst nach deren Benutzung in einer gemeinsamen Datei stehen. 4.5 Definitionsdateien Überlegen wir jetzt, wie man das Taschenrechnerprogramm auf mehrere Quellda- Quelldateien verteilt, so wie man das machen würde, wenn jede seiner Komponenten wesentlich größer wäre. Die Funktion main würde in eine Datei kommen, die wir main.c nennen; push, pop und ihre Variablen kommen in eine zweite Datei staclcc; getop kommt in eine dritte getop.c. Schließlich kommen getch und ungetch in eine vierte Datei getch.c; wir trennen sie von den anderen, denn in einem realistischen Programm würden sie aus einer getrennt übersetzten Bibliothek stammen. Wir müssen noch etwas überlegen - die Definitionen und Deklarationen, die die Dateien gemeinsam enthalten. Wir wollen dies so gut wie möglich zentralisieren, so daß es nur ein Exemplar gibt, das man korrekt erzeugen und korrekt halten muß, wenn das Programm weiterentwickelt wird. Dementsprechend schreiben wir dieses gemeinsame Material in eine Definitionsdaiei (ein sogenanntes header file), calc.h, die bei Bedarf ein- eingefügt wird. (Die #include-Anweisung wird in Abschnitt 4.11 beschrieben.) Das resul- resultierende Programm sieht dann etwa so aus: calc.h «define NUMBER '0' void push(double); double pop(void); int getop(char []); int getch(void); void ungetch(int); main.c «include «include «include <stdio.h> <stdlib.h> ''calc.h" «define MAXOP 100 mainO i getop.c «include «include «include <stdio.h> <ctype.h> "calc.h" int getop(char s[]) { stocke getch.c «include <stdio.h> «define BUFSIZE 100 char buf[BUFSIZE]; int bufp ■ 0; int getch(void) { void ungetch(int c) i «include <stdio.h> «include "calc.h11 «define MAXVAL 100 int sp = 0; double val[MAXVAL]; void push(double f) { double pop(void) {
4.6 static 81 Man muß abwägen zwischen dem Wunsch, daß jede Datei nur Zugriff auf die In- Information hat, die sie für ihre Aufgabe benötigt, und der rauhen Wirklichkeit, daß es schwieriger ist, mehr Definitionsdateien zu unterhalten. Bis zu einer mittleren Pro- Programmgröße ist es vermutlich am besten, eine einzige Definitionsdatei zu haben, die alles enthält, was zwei beliebige Teile des Programms gemeinsam benötigen; das ist die Ent- Entscheidung, die wir hier getroffen haben. Für ein wesentlich größeres Programm brauchte man mehr Struktur und mehr Definitionsdateien. 4.6 static Die Variablen sp und val in stackx sowie buf und bufp in getch.c sind zur privaten Benutzung der Funktionen in ihren jeweiligen Quelldateien vorgesehen, und niemand sonst sollte auf sie zugreifen. Eine static-Vereinbarung, die auf eine externe Variable oder Funktion angewandt wird, begrenzt den Gültigkeitsbereich dieses Objekts auf den Rest der Quelldatei, die übersetzt wird, static dient also bei externen Vereinbarungen dazu, Namen wie buf und bufp in der getch-ungetch-Kombination zu verbergen; solche Namen müssen extern sein, damit sie gemeinsam benutzt werden können, sie sollten je- jedoch für Benutzer von getch und ungetch unsichtbar sein. Um solche Variablen zu erzeugen, stellt man der normalen Vereinbarung das Wort static voran. Werden die zwei Routinen und die zwei Variablen in einer Datei übersetzt, static char buf [BUFSIZE]; /* Puffer fuer ungetchO */ static int bufp » 0; /* naechste freie Position in buf */ int getch(void) { ... } void ungetch (int c) { ... } dann kann keine andere Routine buf und bufp benutzen und diese Namen kollidieren nicht mit den gleichen Namen in anderen Quelldateien für das gleiche Programm. Ge- Genauso können die Variablen verborgen werden, die push und pop für die Stack-Verarbei- Stack-Verarbeitung benutzen: sp und val werden static vereinbart. Eine externe static-Vereinbarung wird meistens für Variablen benutzt, aber sie kann genauso gut für Funktionen verwendet werden. Normalerweise sind Funktionsna- Funktionsnamen global und für jeden Teil des ganzen Programms sichtbar. Wenn eine Funktion je- jedoch static vereinbart wird, ist ihr Name außerhalb der Datei unsichtbar, in der sie ver- vereinbart ist. Eine static-Vereinbarung kann auch für interne Variablen verwendet werden. In- Interne static-Variablen sind in einer bestimmten Funktion lokal vorhanden, genauso wie automatische Variablen; anders als automatische Variablen bleiben sie jedoch erhalten und werden nicht für jeden Aufruf der Funktion neu erzeugt. Dies bedeutet, daß interne static-Variablen als private, permanente Speicher innerhalb einer Funktion dienen kön- können. Aufgabe 4-11. Verändern Sie getop, so daß ungetch nicht benötigt wird. Hinweis: ver- verwenden Sie eine interne static-Variable. O 4.7 register Eine register-Vereinbarung informiert den Übersetzer, daß die fragliche Variable häufig benutzt werden wird. Eigentlich sollen register-Variablen in den Registern der
82 4 Funktion ind Programmstruktur Maschine unterhalten werden, dies kann zu kleineren und schnelleren Programmen führen. Übersetzer dürfen diesen Hinweis aber ignorieren. Eine register-Vereinbarung hat folgende Form: register int x; register char c; register kann sich nur auf automatische Variablen und die Parameter einer Funktion be- beziehen. Im letzteren Fall sieht eine Deklaration so aus: f(register unsigned m, register long n) i register int i; In der Praxis gibt es Einschränkungen für register-Variablen, die die Realitäten der Maschine reflektieren. Nur wenige Variablen jeder Funktion können in Registern ange- angelegt werden, und nur bestimmte Datentypen sind dabei erlaubt. Überzählige register- Vereinbarungen sind jedoch harmlos, da das Wort register bei überzähligen oder verbo- verbotenen Vereinbarungen ignoriert wird. Man kann auch nicht die Adresse einer register- Variablen berechnen (dieser Komplex wird in Kapitel 5 behandelt), unabhängig davon, ob eine Variable wirklich in einem Register angelegt ist. Die speziellen Einschränkungen bezüglich Anzahl und Typen von register-Variablen sind von Maschine zu Maschine ver- verschieden. 4.8 Blockstruktur C hat keine Blockstruktur im Sinne von Pascal oder ähnlichen Sprachen, da Funk- Funktionen nicht innerhalb von anderen Funktionen definiert werden dürfen. Andrerseits können Variablen wie bei einer Blockstruktur innerhalb einer Funktion definiert werden. Unmittelbar nach der linken geschweiften Klammer am Anfang eines Blocks können Va- Variablen vereinbart und initialisiert werden. Dies gilt für jeden Block, nicht nur für den äußersten Block, aus dem jeweils eine Funktion besteht. Derartig vereinbarte Variablen verbergen alle Variablen gleichen Namens aus äußeren Blöcken, und sie existieren bis zur entsprechenden rechten geschweiften Klammer. Beispielsweise ist bei if (n > 0) { int i; /* neues "i" definieren */ for (i = 0; i < n; i++) der Gültigkeitsbereich der Variablen i der „wahre" Teil der if-Anweisung; dieses i ist un- unabhängig von allen anderen Variablen namens i außerhalb dieses Blocks. Eine automati- automatische Variable, die in einem Block definiert und initialisiert wird, wird jedesmal initiali- initialisiert wenn der Block neu ausgeführt wird. Eine static-Variable wird nur bei der ersten Ausführung des Blocks initialisiert. Automatische Variablen, wie auch formale Parameter, verbergen externe Variablen und Funktionen mit gleichem Namen. Nach den folgenden Vereinbarungen
4.9 Initialisierurl 83 int x; int y; «double x) C double y; bezieht sich innerhalb der Funktion f der Name x auf den Parameter, der als double ver- vereinbart ist; außerhalb von f bezieht sich der Name x auf die extern als int vereinbarte Va- Variable. Gleiches gilt für die Variable y. Es ist guter Stil, wenn man Variablennamen vermeidet, die Namen in einem weiter außen liegenden Gültigkeitsbereich verbergen; das führt sehr leicht zu Durcheinander und Fehlern. 4.9 Initialisierung Initialisierungen wurden bisher nebenbei schon oft erwähnt, aber immer am Rand von anderen Diskussionen. In diesem Abschnitt werden einige der Regeln zusammenge- zusammengefaßt, nachdem wir jetzt die verschiedenen Speicherklassen kennengelernt haben. Externe und static-Variablen, die nicht explizit initialisiert werden, haben bei Be- Beginn der Programmausführung den Wert 0; automatische und register-Variablen haben Undefinierte, das heißt, sinnlose Werte. Einfache Variablen können in einer Definition initialisiert werden; dabei folgt dem Namen ein Gleichheitszeichen und ein Ausdruck: int x = 1; char squote « '\"; long day « 10001 * 601 • 60L * 241; /* Millisekunden/Tag •/ Für externe und static-Variablen muß die Initialisierung ein konstanter Ausdruck sein; sie werden einmal initialisiert, im Prinzip bevor das Programm abläuft. Automatische und register-Variablen werden jedesmal initialisiert, wenn die Funktion oder der Block neu ausgeführt werden. Bei automatischen und register-Variablen braucht die Initialisierung keine Kon- Konstante zu sein: hier kann ein beliebiger Ausdruck angegeben werden, der sich nur auf früher definierte Werte bezieht, und sogar Funktionsaufrufe sind möglich. Beispielsweise könnten die Initialisierungen im binären Suchprogramm im Abschnitt 3.3 so formuliert werden int binsearchcint x, int v[], int n) C int low = 0; int high « n - 1; int mid; anstelle von int low, high, mid; low « 0; high » n - 1;
84 4 Funktione id Programmstruktur Die Initialisierungen von automatischen Variablen sind effektiv nur eine Abkürzung für Zuweisungsanweisungen. Was man bevorzugt, ist hauptsächlich eine Geschmacksfrage. Wir haben im allgemeinen explizite Zuweisungen verwendet, da Initialisierungen bei De- Definitionen schwerer erkennbar und weiter weg von dem Punkt sind, wo sie benutzt wer- werden. Ein Vektor kann dadurch initialisiert werden, daß seiner Definition eine Liste von Initialisierungswerten folgt, die mit geschweiften Klammern umgeben und durch Komma getrennt ist. Um zum Beispiel einen Vektor days mit der Anzahl Tage jedes Monats zu initialisieren, schreibt man: int dayst] = < 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 >; Ist die Größe eines Vektors nicht angegeben, so berechnet sie der Übersetzer, indem er die Initialisierungen zählt, hier sind das 12. Werden weniger Initialisierungswerte angegeben als für einen Vektor nötig sind, so werden die restlichen Elemente bei externen, statischen und automatischen Vektoren auf 0 gesetzt. Zu viele Initialisierungen sind ein Fehler. Leider kann man nicht angeben, daß ein Initialisierungswert wiederholt verwendet werden soll; auch kann ein Element in der Mitte eines Vektors nicht initialisiert werden, ohne daß alle vorhergehenden Werte auch angegeben werden. Für Zeichenvektoren gibt es eine besondere Initialisierung; statt der geschweiften Klammern und Kommas kann eine konstante Zeichenkette angegeben werden: char pattern!] = "ould"; ist eine Abkürzung für die längere, aber äquivalente Formulierung char pattern!] = { •<>■, "u1, 'l', 'd1, '\0• >; Hier ist die Vektorgröße fünf (vier Zeichen und das abschließende '\0'). 4.10 Rekursion C-Funktionen können rekursiv benutzt werden; das heißt, eine Funktion kann sich selbst entweder direkt oder indirekt aufrufen. Betrachten wir das Ausgeben einer Zahl als Zeichenkette. Wie wir früher erwähnt haben, werden die Ziffern in der verkehrten Reihenfolge erzeugt: die Ziffern am rechten Ende kennt man vor den Ziffern am Unken Ende, aber sie müssen genau umgekehrt ausgegeben werden. Es gibt zwei Lösungen für dieses Problem. Einmal kann man die Ziffern in der Reihenfolge in einem Vektor speichern, in der sie generiert werden, und sie dann in um- umgekehrter Reihenfolge ausgeben, wie wir dies im Abschnitt 3.6 bei itoa getan haben. Die Alternative dazu ist eine rekursive Lösung, bei der printd zuerst sich selbst aufruft, um alle führenden Ziffern abarbeiten zu lassen, und dann selbst die nachfolgende Ziffer aus- ausgibt. Auch diese Version kann allerdings bei der größten negativen Zahl schiefgehen, «include <stdio.h> /* printd: n dezimal ausgeben */ void printd(int n) t if (n < 0) { putcharC-1); n = -n;
4.10 Rekursion 85 if (n / 10) printd(n / 10); putchar(n X 10 ♦ '0'); > Ruft eine Funktion sich selbst rekursiv auf, dann erhält jede Aktivierung einen neu- neuen Satz aller ihrer automatischen Variablen, unabhängig vom vorhergehenden Satz. Bei printdA23) erhält die erste Aktivierung von printd das Argument n = 123. Sie übergibt 12 an einen zweiten Aufruf von printd, der seinerseits 1 an einen dritten übergibt. Die dritte Aktivierung von printd gibt 1 aus und kehrt zum zweiten Aufruf zurück. Dieses printd gibt 2 aus und kehrt zum ersten Aufruf zurück. Dieser gibt 3 aus und hört auf. Ein weiteres gutes Beispiel zur Rekursion ist Quicksort, ein Sortieralgorithmus, der 1962 von C. A. R. Hoare entwickelt wurde. Aus einem vorgegebenen Vektor wird ein Element ausgewählt und die anderen werden in zwei Untermengen aufgeteilt - eine Menge mit Elementen, die kleiner als das ausgewählte Element sind, und eine mit Ele- Elementen, die größer oder gleich sind. Das gleiche Verfahren wird dann rekursiv auf die beiden Untermengen angewandt. Hat eine Untermenge weniger als zwei Elemente, muß sie nicht mehr sortiert werden; dadurch hört die Rekursion auf. Unsere Version von Quicksort ist nicht die schnellstmögliche, aber sie eine der ein- einfachsten. Wir benutzen das mittlere Element jedes Teilvektors zur Aufteilung. /* qsort: sortiere v[left3...v[right3 in aufsteigende Reihenfolge */ void qsort(int v[3, int left, int right) int i, last; void swap(int v[], int i, int j); if (left >= right) /* nichts zu tun, wenn der Vektor */ return; /* weniger als zwei Elemente enthaelt */ swap(v, left, (left + right)/?); /* bewege gewaehltes Element */ last = left; /* nach v[0] */ for (i = left+1; i <= right; i++) /* aufteilen */ if (v[i] < vtleft]) swap(v, ++last, i); swap(v, left, last); /* hole gewaehltes Element zurueck */ qsort(v, left, last-1); qsort(v, last+1, right); > Wir haben die Tauschoperation in einer eigenen Funktion swap untergebracht, da sie in qsort dreimal vorkommt. /* swap: vertausche v[i] und v[j] */ void swapd'nt v[], int i, int j) t int temp; temp = v[i]; v[i] = v[j3; v[j] = temp; > Die Standard-Bibliothek enthält eine Version von qsort, die Objekte mit beliebigem Typ sortieren kann.
86 4 Funktionen i Programmstruktur Rekursion spart vielleicht keinen Speicherplatz, da es irgendwo einen Stack für die bearbeiteten Werte geben muß. Rekursion ist auch nicht schneller. Aber eine rekursive Lösung ist kompakter und oft wesentlich leichter zu schreiben und zu verstehen als eine nicht-rekursive Lösung. Rekursion ist besonders bequem bei rekursiv definierten Daten- Datenstrukturen wie Bäumen; wir werden in Abschnitt 6.5 ein gutes Beispiel dazu sehen. Aufgabe 4-12. Verwenden Sie die Ideen von printd dazu, eine rekursive Version von itoa zu schreiben; das heißt, verwandeln Sie einen ganzzahligen Wert in eine Zeichenket- Zeichenkette mit Hilfe einer rekursiven Funktion. G Aufgabe 4-13. Schreiben Sie eine rekursive Version der Funktion reverse(s), die die Zeichenkette s am gleichen Platz umkehrt. G 4.11 Der C-Preprozessor C realisiert bestimmte Sprachkonzepte mit einem Preprozessor, den man sich als selbständigen ersten Schritt der Übersetzung vorstellen kann. Zwei Eigenschaften wer- werden am häufigsten benutzt: #include, um den Inhalt einer Datei während der Überset- Übersetzung einzufügen, und #define, um einen Namen durch eine beliebige Folge von Symbo- Symbolen zu ersetzen. Weitere Fähigkeiten, die in diesem Abschnitt beschrieben werden, sind bedingte Übersetzung und Makros mit Argumenten. 4.11.1 Deflnitionsdateien einfügen Durch Einfügen von Dateien kann man leicht mit Sammlungen von #define- Anweisungen und Deklarationen (und anderen Dingen) umgehen. Eine Quellzeile wie #include "filename" oder #include <filcnamc> wird durch den Inhalt der Datei filename ersetzt.* Wenn filename in Doppelanführungs- Doppelanführungszeichen steht, beginnt die Suche nach der Datei typischerweise dort, wo das Quellpro- Quellprogramm steht; wenn sie dort nicht gefunden wird, oder wenn filename in spitzen Klam- Klammern < > angegeben ist, geht die Suche nach implementierungsabhängigen Regeln wei- weiter. Eine eingefügte Datei kann selbst auch #include's enthalten. Oft schreibt man mehrere #include-Zeilen am Anfang einer Quelldatei, um ge- gemeinsame #define-Anweisungen und extern-Deklarationen einzufügen oder um auf Funktionsprototypen von Bibliotheksfunktionen zuzugreifen, die in Definitionsdateien wie < stdio.h > stehen. (Genau genommen müssen das keine Dateien sein; die Details, wie auf Definitionsdateien zugegriffen wird, sind implementierungsabhängig.) Mit #include werden bevorzugt die Deklarationen eines großen Programms zu- zusammengehalten. Damit ist garantiert, daß alle Quelldateien mit den gleichen Definitio- Definitionen und Variablendeklarationen arbeiten; so wird eine besonders unangenehme Fehler- Fehlerquelle vermieden. Wenn eine eingefügte Datei verändert wird, müssen natürlich alle Da- Dateien, die davon abhängen, neu übersetzt werden. * Wir nennen diese dann eine Definitionsdatei. A.d.Ü.
4.11 Der C-Preproz. £ 87 Textersatz Eine Definition hat die Form «define name ersatztext Sie sorgt für die einfachste Art von Textersatz - tritt anschließend das Symbol name auf, wird es durch ersatztext ersetzt. Der name bei #deilne hat die gleiche Form wie ein Va- Variablenname; der Ersatztext ist beliebig. Normalerweise ist der Ersatztext der Rest der Zeile; eine lange Definition kann über mehrere Zeilen fortgesetzt werden, indem man \ an das Ende jeder Zeile stellt, die fortgesetzt werden soll. Der Gültigkeitsbereich eines mit #deflne vereinbarten Namens erstreckt sich von der Definition bis zum Ende der Quelldatei. Eine Definition kann frühere Definitionen verwenden. Textersatz findet nur für Namen und nicht innerhalb von Zeichenketten statt; ist also etwa YES ein definierter Name, dann wird innerhalb von printf("YES") oder in YESMAN nicht ersetzt. Jeder Name kann mit beliebigem Ersatztext definiert werden. Zum Beispiel «define forever for (;;) /* Endlosschleife */ definiert ein neues Wort forever für eine endlose Schleife. Man kann auch Makros mit Parametern definieren, so daß der Ersatztext bei ver- verschiedenen Aufrufen des Makros verschieden sein kann. Beispielsweise kann man einen Makro max folgendermaßen definieren: «define max(A, B) ((A) > (B) ? (A) : (B)) Auch wenn er wie ein Funktionsaufruf aussieht, wird ein Aufruf von max direkt im Pro- Programmtext expandiert. Überall wo ein Parameter im ersatztext steht (hier A oder B), wird er durch das entsprechende Argument ersetzt. Also wird die Zeile x « max(p+q, r+s); ersetzt durch die Zeile x = ((p*q) > (r+s) ? (p*q) : (r+s>); Werden die Parameter konsistent behandelt, dann funktioniert dieser Makro für beliebi- beliebige Datentypen; anders als bei Funktionen genügt eine einzige Definition von max für ver- verschiedene Datentypen. Studiert man die Expansion von max genauer, entdeckt man einige Fallstricke. Die Ausdrücke werden zweimal bewertet; das ist ungünstig, wenn sie Nebenwirkungen wie Inkrement-Operatoren oder Eingabe und Ausgabe enthalten. Zum Beispiel wird bei max(i++, j++) /* FALSCH */ der größere Wert zweimal inkrementiert. Der Ersatztext muß sorgfältig geklammert sein, damit die Reihenfolge von Bewertungen erhalten bleibt. Überlegen Sie was pas- passiert, wenn der Makro «define square(x) x * x /* FALSCH */ mit square(z+l) aufgerufen wird. Makros sind trotzdem wertvoll. Ein praktisches Beispiel stammt aus <stdio.h>, wo getchar und putchar oft als Makros definiert sind, um zu vermeiden, daß für jedes bearbeitete Zeichen ein Funktionsaufruf erfolgen muß. Die Funktionen in < ctype.h > sind normalerweise auch als Makros implementiert.
88 4 Funktionen i Programmstruktur Definitionen können mit #undef gelöscht werden, normalerweise um sicherzustel- sicherzustellen, daß eine Routine wirklich eine Funktion und kein Makro ist. #undef getchar int getchar(void) { ... > Parameter werden innerhalb von konstanten Zeichenketten nicht ersetzt. Wenn je- jedoch # im Ersatztext vor dem Parameternamen steht, wird beides zusammen als kon- konstante Zeichenkette mit Doppelanführungszeichen expandiert, wobei der Parameter durch das Argument ersetzt wird. Dies kann man mit Verkettung von Zeichenketten kombinieren, um zum Beispiel einen Makro zur Fehlerverfolgung zu konstruieren: #define dprint(expr) printf(#expr " = %g\n", expr) Ruft man dies zum Beispiel so auf dprint(x/y); wird der Makro zu printf("x/y" •' = %g\n", x/y); expandiert und die Zeichenketten werden aneinandergehängt; also ist der Effekt printf("x/y = %g\n", x/y); Im Argument wird " durch \" und \ durch \\ ersetzt, so daß das Resultat eine gültige konstante Zeichenkette ist. Mit dem Preprozessor-Operator ## kann man Argumente während der Makro- MakroExpansion aneinanderhängen. Steht ein Parameter im Ersatztext neben ##, wird der Parameter durch das Argument ersetzt, ## und der umgebende Zwischenraum werden entfernt, und das Ergebnis wird nochmals (auf Makroaufrufe) untersucht. Zum Beispiel verkettet der Makro paste seine beiden Argumente: «define paste(front, beck) front ## back paste(name, 1) erzeugt das Symbol namel. Die Regeln für verschachtelte ## sind mysteriös; die Details kann man in Anhang A finden. Aufgabe 4-14. Definieren Sie einen Makro swap(t,x,y), der zwei Argumente vom Typ t vertauscht. (Blockstruktur sollte sich hilfreich erweisen.) D 4.113 Bedingte Übersetzung Der Preprozessor selbst kann mit bedingten Anweisungen kontrolliert werden, die während seiner Ausführung bewertet werden. Damit kann man Text abhängig vom Wert von Bedingungen einfügen, die während der Übersetzung bewertet werden. Die #if-Anweisung berechnet einen konstanten ganzzahligen Ausdruck, (in dem sizeof, Umwandlungsoperationen oder Aufzählungskonstanten nicht vorkommen dür- dürfen). Ist der Wert des Ausdrucks nicht null, werden die folgenden Zeilen bis #endif, #elif oder #else eingefügt. (Die #elif-Anweisung des Preprozessors funktioniert wie else if.) Der Ausdruck defined(nawe) in einer #if-Anweisung liefert 1, wenn name mit dem Preprozessor definiert wurde, und sonst 0.
4.11 DerC-Preproz £ 89 Um zum Beispiel sicherzustellen, daß der Inhalt der Datei hdr.h nur einmal ein- eingefügt wird, wird der Inhalt der Datei mit einer Bedingung umgeben: «if Idefined(HDR) «define HDR /* Inhalt von hdr.h */ #endif Beim ersten Einfügen von hdr.h wird der Name HDR definiert; wird die Datei nochmals eingefügt, ist HDR definiert und bis #endif wird alles übersprungen. Ähnlich kann man vermeiden, daß man Dateien überhaupt mehrfach einfügt. Wird dieser Stil konsistent verwendet, dann kann jede Definitionsdatei selbst die Definitionsdateien einfügen, von denen sie abhängt, ohne daß sich der Benutzer einer Definitionsdatei um solche Abhän- Abhängigkeiten kümmern muß. Der folgende Code überprüft den Namen SYSTEM, um zu entscheiden, welche Version einer Definitionsdatei einzufügen ist: «if SYSTEM « SYSV «define HDR "sysv.h" #etif SYSTEM « BSD «define HDR "bsd.h" «elif SYSTEM -- MSDOS «define HDR "msdos.h" «eise «define HDR "default.h" «endif «include HDR Die Zeilen #ifdef und #ifndef dienen speziell zur Überprüfung, ob ein Name defi- definiert ist. Unser erstes Beispiel mit #if hätte auch so geschrieben werden können: «ifndef HDR «define HDR /* Inhalt von hdr.h */ «endif
91 5 Zeiger und Vektoren Ein Zeiger ist eine Variable, die die Adresse einer Variablen enthält. Zeiger wer- werden in C häufig benutzt: zum Teil, weil sie manchmal die einzige Möglichkeit sind, um eine Berechnung auszudrücken, zum Teil, weil sie normalerweise zu kompakteren und effizienteren Programmen führen als andere Methoden. Zeiger und Vektoren sind eng verwandt; dieses Kapitel untersucht auch diese Verwandtschaft und zeigt, wie man Vor- Vorteile daraus zieht. Zeiger wurden mit der goto-Anweisung in einen Topf geworfen, als eine ausge- ausgezeichnete Technik, um unverständliche Programme zu formulieren. Dies gilt sicher, wenn Zeiger sorglos verwendet werden, und man kann leicht Zeigerwerte erzeugen, die „irgendwohin" zeigen. Mit einer gewissen Disziplin können Zeiger aber auch benutzt werden, um klar und einfach zu programmieren. Genau diesen Aspekt wollen wir im fol- folgenden herausarbeiten. Als hauptsächliche Neuerung legt ANSI-C die Regeln explizit fest, wie Zeiger mani- manipuliert werden können; letztlich wird vorgeschrieben, was gute Programmierer schon im- immer praktizieren und was gute Übersetzer schon immer erzwingen. Außerdem ersetzt der Datentyp void * (Zeiger auf void) den Typ char • als korrekten Typ für unspezifische (generische) Zeiger. 5.1 Zeiger und Adressen Beginnen wir mit einem vereinfachten Bild der Speicherorganisation. Eine typi- typische Maschine hat einen Vektor von Speicherzellen, die fortlaufend numeriert oder adressierbar sind und die einzeln oder in zusammenhängenden Gruppen bearbeitet wer- werden können. Bei einer üblichen Architektur kann jedes Byte einen char-Wert darstellen, ein Paar von Ein-Byte-Zellen kann als short behandelt werden, und vier benachbarte Bytes sind long. Ein Zeiger ist eine Gruppe von Speicherzellen (oft zwei oder vier), die eine Adresse aufnehmen kann. Hat also c den Typ char und p ist ein Zeiger, der darauf verweist, dann könnten wir das folgendermaßen darstellen: Der unäre Adreß-Operator & liefert die Adresse eines Objekts. Die Anweisung P ■ &c; weist also die Adresse von c an die Variable p zu, und man sagt „p zeigt auf c". Der Adreß-Operator & kann nur auf Objekte im Speicher angewendet werden, also auf Va- Variablen und Vektorelemente. Auf Ausdrücke, Konstanten oder register-Variablen kann er nicht angewandt werden. Der unäre Operator • ist der Inhalts -Operator (indirection, dereferencing). Wird er auf einen Zeiger angewendet, so greift er auf das Objekt zu, auf das der Zeiger verweist. Angenommen, x und y sind ganzzahlig und ip ist ein Zeiger auf int; der folgende, fiktive
92 feiger und Vektoren Programmtext zeigt, wie man einen Zeiger vereinbart und wie die Operatoren & und • benutzt werden: int int ip « y = *ip ip = x = 1, y *ip; = &x; *ip; ■ 0; = &z[0); = 2, zMO]; /* /* /* /* /* ip ist ein Zeiger ip zeigt nun y ist nun 1 ' x ist nun 0 ' ip zeigt nun auf 7 7 auf auf x •/ z[0] int •/ */ Die Definitionen von x, y und z haben wir so schon bisher gesehen. Die Definition des Zeigers ip, int *ip; soll als Muster verstanden werden; sie besagt, daß der Ausdruck *ip ein int-Wert ist. Die Syntax einer Variablenvereinbarung imitiert die Syntax von Ausdrücken, in denen die Va- Variable auftreten könnte. Dieser Gedanke gilt auch für Funktionsvereinbarungen. Bei- Beispielsweise besagt double *dp, atof(char *); daß in einem Ausdruck *dp und atof(s) Werte vom Datentyp double haben, und daß das Argument von atof ein Zeiger auf char ist. Beachten Sie auch, daß daraus folgt, daß ein Zeiger jeweils nur auf eine bestimmte Art von Objekt zeigen darf: jeder Zeiger zeigt auf einen festgelegten Datentyp. (Es gibt eine einzige Ausnahme: ein „Zeiger auf void" wird benutzt, um einen Zeiger beliebigen TVps aufzunehmen, aber er darf nicht selbst zum Zugriff verwendet werden. Wir werden in Abschnitt 5.11 darauf zurückkommen.) Zeigt ip auf die int-Variable x, dann darf *ip überall stehen, wo x stehen dürfte: •ip = *ip + 10; erhöht *ip (also x) um 10. Die unären Operatoren • und & haben höheren Vorrang als arithmetische Opera- Operatoren, also holt die Zuweisung y = *ip + 1 den Wert, auf den ip zeigt, addiert 1 und weist das Resultat an y zu; ebenso inkrementiert *ip += 1 den Wert, auf den ip zeigt, wie das auch ++* i p und (*ip)++ tun. Im letzten Beispiel sind die Klammern notwendig; ohne sie würde der Ausdruck den Zeiger ip inkrementieren und nicht das Objekt, auf das ip zeigt, da unäre Operatoren wie * und ++ von rechts nach links zusammengefaßt werden. Schließlich sind Zeiger auch Variablen und können direkt als solche benutzt wer- werden. Ist zum Beispiel iq ein weiterer Zeiger auf int, dann kopiert iq = ip den Inhalt von ip nach iq, womit dann iq auf das gleiche Objekt wie ip zeigt.
5.2 Zeiger und Fi ionsargumente 93 5.2 Zeiger und Funktionsargumente Da C an Funktionen die Werte von Argumenten übergibt, kann die aufgerufene Funktion Variablen beim Aufrufer nicht direkt ändern. Ein Sortierprogramm könnte zum Beispiel zwei Elemente mit einer Funktion namens swap tauschen und in die richti- richtige Reihenfolge bringen wollen. Dann reicht aber der Aufruf swap(a, b); nicht, wenn die swap-Funktion folgendermaßen definiert ist: void swapOnt x, int y) /* FALSCH •/ t int temp; temp = x; x = y; y • temp; Da nur Argumentwerte übergeben werden, kann swap die Argumente a und b beim Auf- Aufrufer nicht beeinflussen. Die obige Funktion vertauscht nur Kopien von a und b. Um den gewünschten Effekt zu erzielen, muß der Aufrufer Zeiger übergeben, die auf die Werte zeigen, die geändert werden sollen: swap(&a, &b); Da der Operator & die Adresse einer Variablen liefert, ist &a ein Zeiger auf a. In swap werden die Parameter als Zeiger deklariert, und auf die Operanden wird indirekt mit die- diesen Zeigen zugegriffen. void swapOnt *px, int *py) /* *px und *py austauschen */ t int temp; temp ■ *px; •px = *py; •py ■ temp; Als Bild: beim Aufrufer: in swap:
94 ' 'eiger und Vektoren Mit Zeigern als Argumenten kann eine Funktion auf Objekte in der aufrufenden Funktion zugreifen und sie ändern. Betrachten wir zum Beispiel eine Funktion getint, die bei jedem Aufruf einen ganzzahligen Wert liefert, den sie durch Zerlegen einer frei formatierten Eingabe gewinnt, getint muß sowohl den gefundenen Wert liefern als auch das Dateiende anzeigen, wenn keine Eingabedaten mehr zur Verfügung stehen. Diese Werte müssen auf verschiedenen Wegen zurückgeliefert werden, denn unabhängig davon, wie man EOF definiert, könnte dieser Wert auch eine Eingabezahl sein. Bei einer möglichen Lösung liefert getint eine Dateiende-Anzeige als Funktions- Funktionswert und verwendet ein Zeigerargument, um die umgewandelte Zahl bei der aufrufenden Funktion abzuspeichern. Dieses Schema wird auch von scanf verwendet; siehe Abschnitt 7.4. Die folgende Schleife füllt einen Int-Vektor durch Aufrufe von getint: int n, array[SI2E], get int(int *); for (n = 0; n < SIZE && getint(&array[n]) != EOF; n++) Jeder Aufruf speichert in arrayfn] die nächste Zahl aus der Eingabe; dann wird n inkre- mentiert. Man beachte, daß man unbedingt die Adresse von arrayfn] an getint überge- übergeben muß. Andernfalls hat getint keine Möglichkeit, dem Aufrufer die umgewandelte Zahl zurückzugeben. Unsere Version von getint liefert EOF am Dateiende, Null, wenn in der Eingabe keine Zahl ansteht, und einen positiven Wert, wenn in der Eingabe eine gültige Zahl vor- vorkommt. «include <ctype.h> int getch(void); void ungetch(int); /* getint: naechsten ganzzahligen Wert aus der Eingabe holen und in *pn ablegen */ int getinUint *pn) t int c, sign; while (isspace(c = getchO)) /* Zwischenraum ignorieren */ if (Hsdigit(c) && c 1= EOF && c ! = ' + • && c 1= '-') < ungetch(c); /* es ist keine Zahl */ return 0; > sign = (c == •-■) ? -1 : 1; if (c « •+• || c == '-') c = getchO; for (*pn = 0; isdigit(c); c = getchO) *pn = 10 * *pn + (c - '0'); *pn *■ sign; if (c != EOF) ungetch(c); return c;
5.3 Zeiger und ^ oren 95 Überall in getint wird *pn als normale int-Variable benutzt. Wir haben auch die in Ab- Abschnitt 4.3 beschriebenen Funktionen getch und ungetch benutzt, damit das eine Zeichen, das wir zuviel lesen müssen, wieder in die Eingabe zurückgestellt werden kann. Aufgabe 5-1. So wie getint geschrieben ist, behandelt die Funktion ein + oder -, dem keine Ziffer folgt, als gültige Repräsentierung von Null. Korrigieren Sie das so, daß ein solches Zeichen in die Eingabe zurückgestellt wird, ü Aufgabe 5-2. Konstruieren Sie get float, eine Funktion analog zu getint, die Gleitpunkt- Gleitpunktwerte extrahieren kann. Was für einen Datentyp liefert getfloat als Funktionswert? O 5.3 Zeiger und Vektoren In C besteht eine enge Beziehung zwischen Zeigern und Vektoren, so eng, daß man Zeiger und Vektoren gemeinsam behandeln sollte. Jede Operation mit Vektorindi- Vektorindizes kann auch mit Zeigern formuliert werden. Die Zeigerversion ist im allgemeinen effi- effizienter, aber zumindest für Uneingeweihte etwas schwerer zu verstehen. Die' Vereinbarung int a [10]; definiert einen Vektor a mit zehn Elementen, das heißt, einen Block aus zehn aufeinan- aufeinanderfolgenden Objekten, mit den Namen a[0], a[l],..., a[9]. a[0] a [9] a[i] bezeichnet das i-te Element im Vektor a. Vereinbart man pa als Zeiger auf einen int-Wert, int *pa; dann zeigt pa durch die Zuweisung pa = &a[0); auf das Element 0 von a; das heißt, pa enthält die Adresse von a[0J. a[0] Die Zuweisung x = *pa; kopiert dann den Wert von a[0] nach x. Zeigt pa auf ein bestimmtes Element eines Vektors, dann zeigt per Sprachdefiniti- Sprachdefinition pa+1 auf das nachfolgende Element, und allgemein zeigt pa+i auf das i-te Element hinter pa und pa-1 auf das i-te Element davor. Wenn also pa auf a[0] zeigt, dann be- bezeichnet *<pa+1)
96 5_ iger und Vektoren den Inhalt von a[l], die Adresse des Elements a[i] ist pa+i und *(pa + i) ist der Wert des Elements a [i]. pa+1: a[0] Dies gilt unabhängig vom Datentyp oder der Größe der Elemente im Vektor a. Die Bedeutung von „addiere 1 zu einem Zeiger", und als Verallgemeinerung die gesamte Arithmetik mit Zeigern, ist so definiert, daß pa+1 auf das nächste Objekt zeigt und daß pa+i auf das i-te Objekt nach pa zeigt. Es besteht also ein sehr enger Zusammenhang zwischen Vektorindizes und Zeiger- Zeigerarithmetik. Nach Sprachdefinition ist der Wert einer Variablen oder eines Ausdrucks vom Typ Vektor die Adresse des Elements 0 (des Anfangselements) des Vektors. Des- Deshalb sind nach der Zuweisung pa = &a[01; die Werte von pa und a identisch. Da der Name eines Vektors synonym zur Adresse des Anfangselements ist, kann man die Zuweisung pa=&a[0] auch so formulieren: pa = a; Wesentlich überraschender, wenigstens auf den ersten Blick, ist die Tatsache, daß statt a[i] auch *(a+i) geschrieben werden kann. Der C-Übersetzer wandelt a[i] sofort in *(a+i) um; die zwei Angaben sind äquivalent. Wendet man den Adreß-Operator & auf beide Teile dieser Äquivalenz an, so sieht man, daß &a[i] und a+i ebenfalls identisch sind: a+i ist die Adresse des i-ten Elements nach a. Die Kehrseite der Medaille ist, daß man Zeiger wie pa in Ausdrücken zusammen mit einem Index verwenden kann: pa[i] ist äquivalent zu *(pa+i). Kurz - ein Ausdruck aus Vektornamen und Index ist äquivalent zu einem Ausdruck aus Zeiger und Abstand. Einen Unterschied zwischen Vektornamen und Zeiger muß man sich allerdings merken. Ein Zeiger ist eine Variable, also sind pa=a und pa++ erlaubt. Ein Vektorna- Vektorname ist jedoch keine Variable: Ausdrücke wie a - pa oder a++ sind nicht erlaubt. Wird ein Vektorname an eine Funktion übergeben, so wird in Wirklichkeit die Adresse des Anfangselements übergeben. Innerhalb der aufgerufenen Funktion ist die- dieses Argument eine lokale Variable und folglich ist ein Vektorname als Parameter ein Zei- Zeiger, das heißt, eine Variable, die eine Adresse enthält. Damit können wir eine andere Version der Funktion strlen schreiben, die die Länge einer Zeichenkette berechnet: /* strlen: laenge der Zeichenkette s */ int strlen(char *s) int n; for (n = 0; *s I- '\0'; s++) n++; return n;
5.4 Adreß-Arithm 97 Da s eine Zeigervariable ist, darf man s natürlich inkrementieren. s++ hat keinen Ein- Einfluß auf die Zeichenkette in der Funktion, von der strlen aufgerufen wurde, sondern in- krementiert nur die lokale Kopie des Zeigers innerhalb von strlen. Das heißt, daß Aufru- Aufrufe wie strlenC'hello, world"); /* konstante Zeichenkette */ strlen(array); /* char arrayMOO]; */ strlen(ptr); /* char *ptr; */ alle funktionieren. Als Parameter bei der Definition einer Funktion sind char s[]; und char *s; äquivalent; wir ziehen die zweite Schreibweise vor, da sie besser klarmacht, daß der Para- Parameter ein Zeiger ist. Wird ein Vektorname an eine Funktion übergeben, kann die Funk- Funktion je nach Belieben annehmen, daß ein Vektor oder ein Zeiger übergeben wurde, und den Parameter entsprechend verwenden. Eine Funktion kann sogar beide Schreibweisen verwenden, wenn dies zweckmäßig und klar scheint. Man kann auch einen Teil eines Vektors an eine Funktion übergeben, indem man einen Zeiger auf den Anfang des Teilvektors übergibt. Ist beispielsweise a ein Vektor, dann liefern sowohl f(Sa[2]) als auch f(a+2) an die Funktion f die Adresse des Teilvektors, der mit dem Element a [2] beginnt. Inner- Innerhalb von f kann man als Parameterdeklaration sowohl f(int arr[]> { ... > als auch f(int *arr) t ... > schreiben. Aus der Sicht von f spielt keine Rolle, daß der Parameter nur Teil eines größeren Vektors ist. Ist man sicher, daß die Elemente existieren, kann man auch rückwärts in einem Vektor indizieren; p[-l], p[-2] usw. sind syntaktisch erlaubt und bezeichnen die Ele- Elemente, die unmittelbar vor p[0] liegen. Natürlich darf man nicht auf Objekte zugreifen, die nicht innerhalb der Vektorgrenzen liegen. 5.4 Adreß-Arithmetik Ist p ein Zeiger auf ein Element eines Vektors, dann inkrementiert p++ den Zei- Zeiger p so, daß er auf das nächste Element zeigt, und p+= i inkrementiert ihn so, daß er 1 Elemente weiter zeigt. Diese und ähnliche Konstruktionen sind die einfachsten Arten von Zeiger- oder Adreß-Arithmetik. Adreß-Arithmetik ist in C konsistent und einheitlich definiert; die Integration von Zeigern, Vektoren und Adreß-Arithmetik ist eine der Stärken der Sprache. Wir illustrie- illustrieren dies am Beispiel einer primitiven Speicherverwaltung. Sie besteht aus zwei Funktio-
98 5 ,-iger und Vektoren nen: alloc(n) liefert einen Zeiger p auf n aufeinanderfolgende Speicherzellen für Einzel- Einzelzeichen, die der Aufrufer von alloc zum Speichern von Zeichen verwenden kann; afree(p) gibt den so reservierten Speicher wieder frei, damit er später neu vergeben wer- werden kann. Diese Routinen sind „rudimentär", da afree in umgekehrter Reihenfolge wie alloc aufgerufen werden muß. Das heißt, alloc und afree verwalten Speicher als Stack, also mit einer last-in,first-out-Diszip]in. Die Standard-Bibliothek enthält ähnliche Funk- Funktionen namens malloc und free, die nicht so eingeschränkt sind; im Abschnitt 8.7 zeigen wir, wie sie implementiert werden können. Bei der einfachsten Implementierung liefert alloc Abschnitte eines großen Zei- Zeichenvektors, den wir allocbuf nennen wollen. Dieser Vektor wird nur von alloc und afree verwaltet. Da die Funktionen mit Zeigern und nicht mit Vektorindizes operieren, braucht keine andere Routine den Vektornamen zu kennen; wir können den Vektor also in der Quelldatei, die alloc und afree enthält, als static definieren, und er ist deshalb außerhalb dieser Datei unsichtbar. Bei realistischen Implementierungen hat der Vektor möglicherweise nicht einmal einen Namen; er könnte statt dessen durch Aufruf von malloc entstehen, oder indem man vom Betriebssystem einen Zeiger auf einen unbe- unbenannten Speicherbereich verlangt. Wir müssen auch noch wissen, wieviel von allocbuf bereits verteilt wurde. Wir be- benutzen einen Zeiger allocp, der auf das nächste freie Element zeigt. Werden n Zeichen von alloc verlangt, so wird zunächst überprüft, ob noch genügend Platz in allocbuf vor- vorhanden ist. Falls ja, liefert alloc den aktuellen Wert von allocp, das heißt, die Anfangs- Anfangsadresse des freien Bereichs, und inkrementiert allocp um n, damit der Zeiger auf den nächsten freien Bereich zeigt. Ist kein Platz mehr frei, liefert alloc Null, afree(p) setzt einfach allocp auf p, wenn p auf eine Position in allocbuf zeigt. vor Aufruf von alloc: ^__^ allocp: v allocbuf: I I T] * benutzt *■ * frei nach Aufruf von alloc: allocp: \ allocbuf: * benutzt *•< frei «define ALLOCSIZE 10000 /* verfuegbarer Platz */ static char allocbuf[ALLOCSIZE]; /* Speicherplatz fuer alloc */ static char *allocp = allocbuf; /* naechste freie Position */ char *alloc(int n) /* liefert Zeiger auf Platz fuer n Zeichen */ t if (allocbuf + ALLOCSIZE - allocp >= n) { /* passt */ allocp += n; return allocp - n; /* alter Zeiger •/ > eise /* nicht genug Platz */ return 0;
5.4 Adreß-Arithm 99 void afree(char *p) /* Speicher ab p freigeben */ t if (p >= attocbuf &4 p < altocbuf + ALLOCSIZE) allocp = p; > Im allgemeinen kann ein Zeiger wie jede andere Variable initialisiert werden; nor- normalerweise sind jedoch die einzig sinnvollen Werte entweder Null oder ein Ausdruck mit Adressen von früher definierten Objekten mit geeignetem Datentyp. Die Vereinbarung static char *aüocp = aüocbuf; definiert allocp als Zeiger auf char und initialisiert diesen Zeiger so, daß er auf den An- Anfang von allocbuf zeigt; bei Programmbeginn ist dies die nächste freie Position. Das könnte man auch so schreiben: static char 'allocp = Sallocbuf[0]; denn der Vektorname ist die Adresse des Elements in Position 0. Die Bedingung if (allocbuf + ALLOCSIZE - allocp >= n) { /* passt */ überprüft, ob es noch genügend freien Platz gibt, um eine Anforderung von n Zeichen zu erfüllen. Falls ja, dann kann der neue Wert von allocp höchstens auf die erste Position nach dem Ende von allocbuf verweisen. Kann die Anforderung erfüllt werden, dann lie- liefert alloc einen Zeiger auf den Beginn eines Blocks für Zeichen. (Beachten Sie die Ver- Vereinbarung der Funktion selbst). Kann die Anforderung nicht erfüllt werden, muß alloc als Resultat anzeigen, daß kein Platz mehr zur Verfügung steht. In C ist sichergestellt, daß Null niemals eine gültige Datenadresse ist; deshalb kann Null als Resultatwert dazu dienen, einen Fehler anzuzeigen, in diesem Fall also, daß kein Platz mehr frei ist. Zeiger und ganze Zahlen sind nicht austauschbar. Null ist die einzige Ausnahme: die Konstante Null kann an einen Zeiger zugewiesen werden, und ein Zeiger kann mit der Konstante Null verglichen werden. Die symbolische Konstante NULL wird oft statt Null als Gedächtnisstütze benutzt, um hervorzuheben, daß dies ein spezieller Wert für einen Zeiger ist. NULL ist in < stdioJi > definiert. Wir werden ab jetzt NULL verwenden. Bedingungen wie if (allocbuf + ALLOCSIZE - allocp >= n) < /* passt */ und if (p >= allocbuf &4 p < allocbuf + ALLOCSIZE) zeigen mehrere wichtige Aspekte von Zeigerarithmetik. Zum einen können Zeiger unter bestimmten Umständen verglichen werden. Wenn p und q auf Elemente im gleichen Vektor zeigen, dann funktionieren Vergleiche wie ==, ! = , <, >= usw. Zum Beispiel trifft p < q zu, wenn p auf ein früheres Element im Vektor zeigt als q. Für jeden Zeiger ist die Be- Bedingung sinnvoll, ob er gleich oder nicht gleich Null ist. Der Ablauf ist jedoch Undefiniert bei Arithmetik oder Vergleichen zwischen Zeigern, die nicht auf Elemente des gleichen Vektors verweisen. (Es gibt eine Ausnahme: die Adresse des ersten Elements nach dem Ende eines Vektors kann für Zeigerarithmetik verwendet werden.)
100 5 ,iger und Vektoren Zum andern haben wir schon festgestellt, daß ein Zeiger und ein ganzzahliger Wert addiert oder subtrahiert werden können. Die Konstruktion p ♦ n bezeichnet die Adresse des n-ten Objekts nach dem Objekt, auf das p momentan zeigt. Dies gilt unabhängig von der Art des Objekts, auf das p zeigt; n wird in Abhängigkeit von der Größe der Objekte skaliert, auf die p zeigt. Die Objektgröße ergibt sich aus der Ver- Vereinbarung von p. Hat zum Beispiel ein int-Wert vier Bytes, wird bei int-Zeigern mit dem Faktor vier skaliert. Subtraktion von Zeigern ist ebenfalls erlaubt: Zeigen p und q auf Elemente des gleichen Vektors und ist p<q, dann ist q-p+1 die Anzahl der Elemente von p bis q ein- einschließlich. Damit kann man noch eine weitere Version von strlen schreiben: /* strlen: Laenge der Zeichenkette s */ int strlen(char *s) t char *p = s; while (*p != '\0') P++; return p - s; > In der Definition wird p mit dem Wert von s initialisiert, das heißt p zeigt auf das erste Zeichen der Zeichenkette. In der while-Schleife wird ein Zeichen nach dem anderen un- untersucht, bis '\0* am Ende der Zeichenkette entdeckt wird. Da p auf Zeichen zeigt, setzt p++ jedesmal p auf das nächste Zeichen, und p-s liefert die Anzahl Zeichen, über die weitergegangen wurde, eben die Länge der Zeichenkette. (Die Anzahl Zeichen in der Zeichenkette könnte zu groß sein, um sie in einer int-Variable zu speichern. In der Defi- Definitionsdatei < stddef.h > wird ein Typ ptrdiff t definiert, der groß ist für die Differenz zweier Zeigerwerte samt Vorzeichen. Wären wir jedoch ganz vorsichtig, würden wir size t als Resultattyp von strlen verwenden, wie das strlen aus der Standard-Bibliothek macht. size_t ist der vorzeichenlose, ganzzahlige Typ, den der sizeof-Operator liefert.) Zeigerarithmetik ist konsistent: Hätten wir float-Objekte manipuliert, die mehr Speicherplatz benötigen als char-Objekte, und wäre p ein Zeiger auf float, dann würde p++ zum nächsten float-Wert gehen. Wir könnten also eine neue Fassung von alloc schreiben, die float-Werte anstelle von char-Werten manipuliert, indem wir überall in alloc und afree den Typ char durch float ersetzen. Alle Zeigeroperationen berücksichti- berücksichtigen automatisch die Größe der Objekte, auf die gezeigt wird. Die erlaubten Operationen mit Zeigern sind: Zuweisung von Zeigern des gleichen Typs, Addition oder Subtraktion einer ganzen Zahl zu einem Zeiger, Subtraktion oder Vergleich zweier Zeiger auf Elemente des gleichen Vektors sowie Zuweisung oder Ver- Vergleich mit Null. Jede andere Zeigerarithmetik ist verboten. Zwei Zeiger dürfen nicht addiert werden, sie dürfen weder multipliziert noch dividiert, noch mit Shift oder anderen logischen Operatoren behandelt werden. Gleitpunktwerte dürfen nicht zu Zeigern ad- addiert werden. Abgesehen von void *, darf ohne explizite Umwandlungsoperation kein Zeiger auf einen Datentyp an einen Zeiger auf einen anderen Datentyp zugewiesen wer- werden.
5.5 char-Zeiger ur unktionen 101 5.5 cÄar-Zeiger und Funktionen Eine konstante Zeichenkette (string constant), wie "Ich bin eine konstante Zeichenkette11 ist ein Zeichenvektor. Intern wird der Vektor mit dem Nullzeichen '\0' beendet, damit Programme das Ende der Zeichenkette finden können. Die Vektorlänge ist im Speicher daher um 1 größer als die Anzahl Zeichen zwischen den Doppelanführungszeichen. Am häufigsten kommen konstante Zeichenketten wohl als Funktionsargumente vor, wie bei printf("hello, world\n"); Erscheint eine Zeichenkette wie diese in einem Programm, dann wird auf sie über einen Zeiger zugegriffen; printf erhält als Argument einen Zeiger auf den Anfang des Zeichen- Zeichenvektors. Auf eine konstante Zeichenkette wird also mit einem Zeiger auf das erste Ele- Element zugegriffen. Konstante Zeichenketten brauchen nicht nur Funktionsargumente zu sein. Wird pmessage als char 'pmessage; vereinbart, dann wird in pmessage = "now is the time"; an pmessage ein Zeiger auf den Zeichenvektor zugewiesen. Dabei wird die Zeichenkette nicht kopiert; an der Operation sind nur Zeiger beteiligt. C hat keine Operatoren, die ei- eine Zeichenkette als Einheit behandeln. Zwischen den folgenden beiden Definitionen besteht ein wichtiger Unterschied: char amessageU = "now is the time"; /* ein Vektor */ char 'pmessage = "now is the time"; /* ein Zeiger */ amessage ist ein Vektor, der gerade groß genug ist, um die Folge von Zeichen und das Nullzeichen '\0' aufzunehmen, mit denen er initialisiert wird. Einzelne Zeichen im Vek- Vektor können geändert werden, aber amessage wird immer auf den gleichen Speicherplatz verweisen. Andrerseits ist pmessage ein Zeiger, der so initialisiert ist, daß er auf eine konstante Zeichenkette zeigt; der Zeiger kann später so verändert werden, daß er auf et- etwas anderes zeigt. Versucht man aber, den Inhalt der konstanten Zeichenkette zu än- ändern, ist das Resultat Undefiniert. pmessage: •— » now is the tima\0 amessage: now is the tima\0 Wir illustrieren weitere Aspekte von Zeigern und Vektoren, indem wir verschiede- verschiedene Versionen von zwei nützlichen Funktionen aus der Standard-Bibliothek betrachten. strcpy(s,t), die erste Funktion, kopiert die Zeichenkette t in den Zeichenvektor s. Es wä- wäre schön, wenn man einfach s=t schreiben könnte, aber da wird der Zeiger kopiert, nicht der Inhalt. Um die Zeichen zu kopieren, brauchen wir eine Schleife. Hier ist die Vektor- Vektorfassung von strepy:
/* strcpy: t nach s kopieren; Version mit Vektorindex */ void strcpy(char *s, char *t) t int i; i = 0; while ((s[i] = t[i]> 1= '\0") > Als Gegensatz ist hier eine Version von strcpy mit Zeigern: /* strcpy: t nach s kopieren; 1. Version mit Zeigern */ void strcpy(char *s, char *t) t while ((*s = *t) I« '\0') { Da nur Argumentwerte übergeben werden, kann strcpy die Parameter s und t beliebig benutzen. Hier sind sie zweckmäßig initialisierte Zeigervariablen, die Element für Ele- Element die Vektoren entlanggeführt werden, bis '\0' als Abschluß von t nach s kopiert wur- wurde. In der Praxis würde man strcpy nicht so schreiben, wie wir das oben gezeigt haben. Erfahrene C-Programmierer würden folgendes vorziehen: /* strcpy: t nach s kopieren; 2. Version mit Zeigern •/ void strcpy(char *s, char *t) t while ((*s++ = *t++) !» 'X01) Die Inkrementierung von s und t wurde hier in die Bedingung der Schleife verlagert. Der Wert von *t++ ist das Zeichen, auf das t gezeigt hat, bevor t inkrementiert wurde; der nachgestellte Inkrement-Operator ++ ändert t erst, nachdem dieses Zeichen abge- abgeholt wurde. Analog wird das Zeichen an der alten Zielposition s abgespeichert, bevor s inkrementiert wird. Das Zeichen ist auch noch der Wert, der mit '\0' verglichen wird, um die Schleife zu kontrollieren. Im Endergebnis werden Zeichen von t nach s kopiert, unter Einschluß des abschließenden Nullzeichens '\0\ Als letzte Abkürzung halten wir fest, daß ein Vergleich mit '\0' redundant ist, da es nur darum geht, ob der Ausdruck Null ist. Die Funktion wird deshalb wahrscheinlich so formuliert: /* strcpy: t nach s kopieren; 3. Version mit Zeigern */ void strcpy(char *s, char *t) while (*s++ = *t++) Dies mag auf den ersten Blick unübersichtlich scheinen; es ist jedoch eine sehr bequeme Formulierung, und Sie sollten sich diese Ausdrucksweise aneignen, weil Sie sie häufig in C-Programmen sehen werden. Die Funktion strcpy aus der Standard-Bibliothek (<string.h>) liefert als Funkti- Funktionswert einen Zeiger auf das Kopierziel.
5.5 c/iar-Zeiger i Funktionen 103 Als zweite Funktion betrachten wir strcmp(s,t), die die Zeichenketten s und t ver- vergleicht und ein Resultat kleiner, gleich oder größer Null liefert, je nachdem, ob s alpha- alphabetisch kleiner, gleich oder größer t ist. Der Resultatwert entsteht als Differenz der Zei- Zeichen an der ersten Stelle, an der sich s und t unterscheiden. /* strcmp: liefert <0 wenn s<t, 0 wenn s==t, >0 wenn s>t */ int strcmp(char *s, char *t) t int i; for (i = 0; s[i] == t[i]; i++) if <s[i] == '\0') return 0; return s[i] - t [f]; > Die Zeigerfassung von strcmp: /* strcmp: liefert <0 wenn s<t, 0 wenn s==t, >0 wenn s>t */ int strcmp(char *s, char *t) t for ( ; *s == *t; s++, t++) if (*s == '\0') return 0; return *s - *t; > Da ++ und — vor oder nach ihren Operanden geschrieben werden können, sind andere Kombinationen von • und ++ und — möglich, wenn auch weniger häufig. Zum Beispiel dekrementiert *-p p, bevor das Zeichen geholt wird, auf das p zeigt. Tatsächlich sind *p++ = val; /* val auf den Stack speichern */ val = *—p; /* oberstes Stack-Element entfernen und an val zuweisen */ die Standard-Ausdrucke, um etwas auf einen Stack zu legen oder vom Stack zu holen; siehe Abschnitt 4.3. Die Definitionsdatei < string.h > enthält Deklarationen für die in diesem Abschnitt erwähnten Funktionen, sowie für eine Sammlung von anderen Zeichenketten-Funktionen aus der Standard-Bibliothek. Aufgabe 5-3. Schreiben Sie eine Zeigerversion der Funktion strcat, die wir in Kapitel 2 gezeigt haben: strcat(s.t) kopiert die Zeichenkette t an das Ende der Zeichenkette s. D Aufgabe 5-4. Schreiben Sie die Funktion strend(s.t), die 1 liefert, wenn die Zeichenket- Zeichenkette t am Ende der Zeichenkette s steht, und Null sonst. D Aufgabe 5-5. Schreiben Sie Versionen der Bibliotheksfunktionen strncpy, stmcat und strncmp, die höchstens die ersten n Zeichen der als Argumente übergebenen Zeichen- Zeichenketten bearbeiten. Zum Beispiel kopiert strncpy(s,t,n) höchstens n Zeichen von t nach s. Vollständige Beschreibungen befinden sich im Anhang B. O Aufgabe 5-6. Formulieren Sie geeignete Programme aus früheren Kapiteln und Aufga- Aufgaben mit Hilfe von Zeigern anstelle von Vektorindizes neu. Gut geeignet dazu sind getline (Kapitel 1 und 4), atoi, itoa und ihre Varianten (Kapitel 2, 3 und 4), reverse (Kapitel 3) sowie strindex und getop (Kapitel 4). D
104 feiger und Vektoren 5.6 Vektoren von Zeigern; Zeiger auf Zeiger Da Zeiger selbst Variablen sind, können sie genau wie andere Variablen in Vekto- Vektoren zusammengefaßt werden. Wir wollen dies anhand eines Programms illustrieren, das eine Anzahl Textzeilen alphabetisch sortiert; es ist eine vereinfachte Fassung des UNIX- Programms sort. In Kapitel 3 haben wir eine Sortierfunktion nach Shell vorgestellt, die einen Vektor von ganzen Zahlen sortiert hat. In Kapitel 4 haben wir dies auf der Basis von Quicksort verbessert. Die gleichen Algorithmen funktionieren wieder, nur müssen wir jetzt mit Textzeilen operieren, die verschieden lang sind und die, anders als ganze Zahlen, nicht in einer einzigen Operation verglichen oder verschoben werden können. Wir müssen die Daten so repräsentieren, daß wir effizient und bequem mit variabel langen Textzeilen umgehen können. Hier kommt ein Vektor von Zeigern ins Spiel. Wenn wir die zu sortierenden Text- Textzeilen nacheinander in einem langen Zeichenvektor abspeichern, können wir auf jede Zeile mit Hilfe eines Zeigers auf ihr erstes Zeichen zugreifen. Die Zeiger selbst können in einem Vektor gespeichert werden. Zwei Zeilen können verglichen werden, indem ihre Zeiger an strcmp übergeben werden. Müssen zwei Textzeilen ausgetauscht werden, da sie sich nicht in der richtigen Reihenfolge befinden, dann werden einfach die Zeiger im Zeigervektor ausgetauscht, nicht die Textzeilen selbst. •— •— • i »I defghi I »I jktmnopqrst 1 *] abc | — -—»j defghi 1^—J jktmnopqrst ^—*| abc~~| So vermeiden wir das zweifache Problem, daß die Speicherverwaltung kompliziert und der Rechenaufwand hoch wird, wenn wir die Textzeilen selbst verlagern würden. Der Sortiervorgang hat nun drei Phasen: alle Eingabezeilen lesen sortieren in neuer Reihenfolge ausgeben Wie üblich ist es am einfachsten, wenn wir das Programm in Funktionen aufteilen, die dieser natürlichen Unterteilung entsprechen, wobei das Hauptprogramm die anderen Funktionen kontrolliert. Stellen wir die Sortierphase für den Augenblick zurück und konzentrieren wir uns auf die Datenstruktur und Eingabe und Ausgabe. Die Eingabefunktion muß die Zeichen jeder Zeile sammeln und speichern und einen Vektor von Zeigern auf die Zeilen konstruieren. Außerdem müssen die Eingabe- Eingabezeilen gezählt werden, denn diese Information braucht man zum Sortieren und Ausge- Ausgeben. Da die Eingabefunktion nur mit einer endlichen Anzahl Eingabezeilen fertig wird, kann sie eine „unmögliche" Zeilenzahl wie -1 liefern, wenn zu viele Eingabezeilen vor- vorliegen. Die Ausgabefunktion muß nur die Zeilen in der Reihenfolge ausgeben, in der sie in dem Zeigervektor stehen.
5.6 Vektoren voi -igern; Zeiger auf Zeiger 105 «include <stdio.h> «include <string.h> «define MAXLINES 5000 /• maximale Anzahl Zeilen V char *lineptr[MAXLlNES3; /* Zeiger auf die Textzeilen */ int readlines(char *lineptrü, int nlines); void writelines(char *lineptr£], int nlines); void qsort(char *lineptr[], int left, int right); /* Eingabezeilen sortieren */ mainO t int nlines; /* Anzahl eingelesener Zeilen */ if ((nlines = readl inesd inept r, MAXLINES)) >= 0) { qsortdineptr, 0, nlines-1); writelinesdineptr, nlines); return 0; > eise < printf("error: input too big to sort\n"); return i; «define MAXLEN 1000 /* maximale Laenge jeder Eingabezeile */ int getline(char *, int); char *alloc(int); /* readlines: Eingabezeilen einlesen */ int readlines(char *tineptr[], int max lines) t int len, nlines; char *p, line[MAXLEN]; nlines = 0; while ((len = getline(line, MAXLEN)) > 0) if (nlines >= max lines 11 (p = allocden)) == NULL) return -1; eise t line[len-1] = '\0'; /* Zeilentrenner entfernen */ strcpy(p, line); lineptr[nlines++] « p; > return nlines; > /* writelines: Zeilen ausgeben */ void writelines(char *lineptr[], int nlines) { int i; for (i ■ 0; i < nlines; i++) printf("Xs\n", lineptrti]); Die Funktion getline ist aus Abschnitt 1.9. Die wesentliche Neuerung ist die Definition für lineptr: char * I ineptr[MAXLINES]
106 . *eiger und Vektoren Dadurch ist lineptr ein Vektor mit MAXLINES Elementen; jedes Element ist ein Zeiger auf einen char Wert. Das heißt, lineptr[i] ist ein Zeiger auf char und *lineptr[i] ist das Zeichen, auf das er zeigt, nämlich das erste Zeichen der i-ten gespeicherten Textzeile. Da lineptr ein Vektorname ist, kann er selbst auch als Zeiger behandelt werden, genau wie in unseren früheren Beispielen, writelines kann deshalb auch so formuliert werden: /* writetines: Zeilen ausgeben */ void writelines(char Mineptrü, int nlines) { white (ntines— > 0) printf("Xs\n", *lineptr++); > Am Anfang zeigt *lioeptr auf die erste Zeile; mit jeder Inkrementierung rückt lineptr auf den nächsten Zeilenzeiger vor, während nlines auf 0 zurückgezählt wird. Nachdem Eingabe und Ausgabe abgehandelt sind, können wir uns nun mit Sortie- Sortieren beschäftigen. Der Quicksort aus Kapitel 4 muß leicht geändert werden: die Deklara- Deklarationen müssen modifiziert werden, und der Vergleich muß durch Aufruf von strcmp er- erfolgen. Der Algorithmus bleibt gleich, was uns hoffen läßt, daß er noch immer funktio- funktioniert. /* qsort: sortiere v[left]...vlright] in aufsteigende Reihenfolge */ void qsort(char *v[], int left, int right) < int i, last; void swap(char *v[], int i, int j); if (left >= right) /* nichts machen, wenn der Vektor */ return; /* weniger als 2 Elemente enthaelt */ swap(v, left, (left + right)/2); last = left; for (i = left+1; i <= right; i++) if (strcmp(v[i], v[left]) < 0) swap(v, ++last, i); swap(v, left, last); qsort(v, left, last-1); qsort(v, last+1, right); > Genauso braucht auch die Funktion swap nur triviale Änderungen: /* swap: vertausche vtiJ und v[j] */ void swap(char *v[], int i, int j) < char *temp; temp = vlU; vCi 3 = v[jl; v[j] • temp; > Jedes einzelne Element von v (alias lineptr) ist ein Zeiger auf char, also wird temp auch so definiert, damit entsprechend zugewiesen werden kann. Aufgabe 5-7. Schreiben Sie readlioes so um, daß die Textzeilen in einem Vektor abge- abgelegt werden, den main zur Verfügung stellt, so daß alloc nicht zur Speicherverwaltung verwendet wird. Wieviel schneller wird das Programm? D
5.7 Mehrdimensio. . Vektoren 107 5.7 Mehrdimensionale Vektoren C verfügt über rechteckige, mehrdimensionale Vektoren, obgleich sie in der Praxis wesentlich seltener verwendet werden als Vektoren von Zeigern. In diesem Abschnitt werden wir einige ihrer Eigenschaften vorführen. Betrachten wir das Problem der Umrechnung eines Datums, aus Tag und Monat in den Tag im Jahr und umgekehrt. Beispielsweise ist der 1. März der 60. Tag in einem nor- normalen Jahr und der 61. Tag in einem Schaltjahr. Wir definieren zwei Funktionen, die die Umrechnung vornehmen: day of_year wandelt Monat und Tag in den Tag im Jahr um, und month_day wandelt den Tag im Jahr in Monat und Tag um. Da die letztere Funkti- Funktion zwei Werte berechnet, sind Monat und Tag jeweils Zeigerargumente: month_dayA988, 60, &m, &d) setzt m auf 2 und d auf 29 (nämlich den 29. Februar). Diese Funktionen brauchen beide die gleiche Information, nämlich eine Tabelle der Anzahl von Tagen in jedem Monat. Da die Anzahl der Tage je Monat in Schalt- und normalen Jahren verschieden ist, ist es einfacher, sie auf zwei Zeilen eines zweidimensio- nalen Vektors zu verteilen, als während der Berechnung zu verfolgen, was im Februar passiert. Der zweidimensionale Vektor und die Umrechnungsfunktionen lauten folgen- folgendermaßen: static char daytab[2][13] = t tO, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31>, tO, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31> >; /* day_of_year: Tag im Jahr aus Monat und Tag bestimmen */ int day_of_year(int year, int month, int day) t int i, leap; leap - year%4 == 0 && yearXIOO != 0 || yearXAOO « 0; for (i = 1; i < month; i++) day += daytabUeap] [i]; return day; > /* monthday: Honat und Tag aus Tag im Jahr bestimmen */ void month_day(int year, int yearday, int *pmonth, int *pday) t int i, leap; leap = year%4 == 0 && yearXIOO != 0 || yearXAOO == 0; for (i = 1; yearday > daytabüeap] [i]; i++) yearday -= daytablleap][i]; *pmonth * i; *pday = yearday; > Zur Erinnerung: der arithmetische Wert, der sich aus einer logischen Verknüpfung er- ergibt, wie etwa der Wert von leap, ist entweder Null (für falsch) oder Eins (für wahr) und kann deshalb als Index in den Vektor daytab verwendet werden.
108 5_ ger und Vektoren Der Vektor daytab muß außerhalb von beiden Funktionen definiert werden, damit beide ihn benutzen können. Wir haben ihn mit char vereinbart, um zu illustrieren, daß man in char legitimerweise auch kleine ganze Zahlen speichern kann, die keine Zeichen sind. daytab ist der erste zweidimensionale Vektor, dem wir begegnen. In C ist ein zwei- dimensionaler Vektor in Wirklichkeit ein eindimensionaler Vektor, bei dem jedes Ele- Element wieder ein Vektor ist. Deshalb schreibt man Indizes so daytab[i][j] /* [Zeile] [Spalte] */ und nicht so daytab[i,j] /* FALSCH */ Von der unterschiedlichen Schreibweise abgesehen, kann ein zweidimensionaler Vektor fast so verwendet werden wie in anderen Programmiersprachen. Elemente werden zei- zeilenweise abgespeichert, deshalb ändert sich der Index, der am weitesten rechts steht (der Spaltenindex), am schnellsten, wenn die Elemente in der abgespeicherten Reihenfolge angesprochen werden. Ein Vektor wird mit einer Liste von Initialisierungen in geschweiften Klammern in- initialisiert; jede Zeile eines zweidimensionalen Vektors wird mit einer entsprechenden Teilliste initialisiert. Wir haben den Vektor daytab mit einer Spalte mit 0 begonnen, so daß die Monatsnummern im natürlichen Bereich 1 bis 12 und nicht im Bereich 0 bis 11 liegen können. Da der Speicherbedarf hier unwesentlich ist, ist dies klarer, als die Indi- Indizes entsprechend abzuändern. Wenn ein zweidimensionaler Vektor an eine Funktion übergeben werden soll, dann muß die Parameterdeklaration in der Funktion die Anzahl Spalten enthalten; die Anzahl Zeilen ist nicht relevant, da wie vorher ein Zeiger übergeben wird, und zwar ein Zeiger auf einen Vektor von Zeilen, die jeweils 13 int-Elemente enthalten. Hier liegt also ein Zeiger auf Objekte vor, die Vektoren von 13 iot-Elementen sind. Wenn der Vektor daytab an die Funktion f übergeben werden soll, dann muß f so vereinbart werden: f(int daytab[2][13]) { ... > Man könnte auch f(int daytab[][13]) { ... > schreiben, da die Anzahl der Zeilen unerheblich ist. Schließlich könnte man auch f(int (*daytab)[13]> t ... > angeben: so ist der Parameter ein Zeiger auf einen Vektor mit 13 int-Elementen. Die Klammern sind notwendig, denn die eckigen Klammern [ ] haben Vorrang vor *. Ohne Klammern ist die Vereinbarung int *daytab[13] ein Vektor mit 13 Zeigern auf int-Werte. Allgemeiner formuliert: nur die erste Dimensi- Dimensionsangabe (der erste Index) eines Vektors darf weggelassen werden; alle anderen müssen angegeben werden. In Abschnitt 5.12 gibt es weitere Erklärungen zu komplizierten Vereinbarungen. Aufgabe 5-8. day of year und month day enthalten keine Fehlerprüfungen. Entfernen Sie diesen Defekt. D
5.8 Initialisierung \ Zeigervektoren 109 5.8 Initialisierung von Zeigervektoren Betrachten Sie das Problem, eine Funktion month_name(n) zu schreiben, die einen Zeiger auf eine Zeichenkette liefert, die den Namen des n-ten Monats enthält. Dies ist eine ideale Anwendung für einen internen static-Vektor. month_name enthält einen pri- privaten Vektor mit Zeichenketten und liefert einen Zeiger auf die richtige davon. In die- diesem Abschnitt sehen wir, wie dieser Vektor von Namen initialisiert wird. Die Syntax gleicht früheren Initialisierungen: /* month_name: liefert Name des n-ten Monats */ char *month_name(int n) t static char *name[] * { "Illegal month", "January", "February", "March", "April", "May", "June", "July", »August", "September", "October", "November", "December" >; return (n < 1 || n > 12) ? nameCO] : name[n]; > name ist ein Vektor von Zeigern auf char und wird wie lineptr im Sortierbeispiel verein- vereinbart. Die Initialisierung ist eine Liste von Zeichenketten; jede Zeichenkette wird an die entsprechende Position im Vektor zugewiesen. Die Zeichen des i-ten Monatsnamens werden irgendwo aufbewahrt, und ein Zeiger auf die Zeichenkette wird in name[i] abge- abgelegt. Da die Größe des Vektors name nicht angegeben ist, zählt der Übersetzer die In- Initialisierungen und dimensioniert den Vektor entsprechend. 5.9 Zeiger kontra mehrdimensionale Vektoren Neulinge in Sachen C verwechseln manchmal einen zweidimensionalen Vektor und einen Vektor von Zeigern, wie name im obigen Beispiel. Mit den Definitionen int a£10] [203; int *b[103; sind sowohl a[3][4] als auch b[3][4] legitime Verweise auf einen einzelnen int-Wert. Aber a ist ein echter zweidimensionaler Vektor: für 200 int-Werte ist Speicher bereitge- bereitgestellt worden, und die übliche Rechteckformel 20 xzeile + spalte wird angewendet, um das Element a[zeile][spalte] aufzufinden. Für b reserviert die (lokale) Definition jedoch nur Speicher für 10 Zeiger und initialisiert sie nicht; die Initialisierung muß explizit erfol- erfolgen, entweder statisch oder durch Ausführung von Anweisungen. Angenommen, jedes Element von b zeigt auf einen Vektor mit 20 Elementen, dann braucht man dazu Platz für 200 int-Werte und zusätzlich für 10 Zeiger. Der wichtige Vorteil des Zeigervektors ist, daß die Zeilen des Vektors verschieden lang sein können. Das heißt, nicht jedes Ele- Element von b muß auf einen Vektor mit 20 Elementen zeigen; manche Elemente könnten auf Vektoren mit 2 Elementen zeigen, andere auf Vektoren mit 50 und wieder andere überhaupt nicht auf Vektoren. Wir haben dies zwar auf int-Vektoren bezogen; am häufigsten werden Zeigervek- Zeigervektoren jedoch dazu benutzt, Zeichenketten unterschiedlicher Längen zu speichern, wie in
110 _5_ ;iger und Vektoren der Funktion monthname. Vergleichen Sie die Vereinbarung und die schematische Dar- Darstellung für einen Vektor von Zeigern char *name[] = { »Illegal month", »Jan, "Feb", "Mar" >; mit denen für einen zweidimensionalen Vektor: char aname[][15] = { "Illegal month", "Jan", "Feb", "Mar" >; Illegal month\o Jan\0 Feb\0 Mar\o 15 30 Aufgabe 5-9. Ändern Sie die Funktionen day_of_year und month_day, so daß Zeiger statt Indexausdrücke verwendet werden. D 5.10 Argumente aus der Kommandozeile In Programmierumgebungen für C kann man normalerweise Argumente aus der Kommandozeile beim Start an ein Programm übergeben. Wenn main aufgerufen wird, werden zwei Argumente übergeben. Das erste Argument (nach Konvention arge für argument ccount) ist die Anzahl der Argumente auf der Kommandozeile, mit der das Programm aufgerufen wurde; das zweite Argument (argv für argume/i/ vector) ist ein Zeiger auf einen Vektor von Zeigern auf Zeichenketten, die die Argumente enthalten, ein Argument pro Zeichenkette. Üblicherweise verwenden wir mehrere Ebenen von Zeigern, um diese Zeichenketten zu bearbeiten. Das einfachste Beispiel ist das Programm echo, das seine Argumente aus der Kom- Kommandozeile auf einer einzelnen Zeile, durch Leerzeichen getrennt, ausgibt. Das heißt, das Kommando echo hello, world erzeugt die Ausgabe hello, world Nach Konvention ist argv[0] der Name, mit dem das Programm aufgerufen wurde, also ist arge wenigstens 1. Hat arge den Wert 1, gibt es nach dem Programmnamen keine Ar- Argumente auf der Kommandozeile. Im obigen Beispiel hat arge den Wert 3 und argv[0], argv[l] und argv[2] sind entsprechend "echo", "hello," und "world". Das erste optionale Argument ist argv[l] und das letzte argv [arge -1]; zusätzlich verlangt der Standard, daß argv [arge] ein Nullzeiger ist.
5.10 Argumente der Kommandozeile 111 argv: Die erste Version von echo behandelt argv als Vektor von Zeigern auf Zeichen: «include <stdio.h> /* Echo der Konmandozeilenargumente; 1. Version */ main(int arge, char *argv[]> t int i; for (i « 1; i < arge; i++) printfC'XsXs», argv[i], (i < argc-1) ? " '■ : ""); printf(»\nM); return 0; > Da argv ein Zeiger auf einen Vektor von Zeigern ist, kann man den Zeiger manipulieren statt Indexoperationen zu verwenden. Die nächste Version beruht auf der Inkrementie- rung von argv, der ein Zeiger auf einen Zeiger auf char ist; gleichzeitig wird arge herun- heruntergezählt. «include <stdio.h> /* Echo der Komnanozeilenargumente; 2. Version */ main(int arge, char *argv[3) t while (—arge > 0) printfC'XsXs", *++argv, (arge > 1) ? •■"); printf(»\n"); return 0; argv ist ein Zeiger auf den Anfang des Vektors mit den Kommandoargumenten; inkre- mentiert man den Zeiger um 1 (++argv), so zeigt er auf das ursprüngliche Element argv[l] und nicht mehr auf argv[0]. Jede weitere Inkrementierung bewegt den Zeiger zum nächsten Argument; *argv ist dann der Zeiger auf das Argument. Gleichzeitig wird arge dekrementiert; wird dieser Wert 0, sind keine Argumente mehr übrig zur Ausgabe. Man könnte die printf-Anweisung auch so schreiben: printf((arge > 1) ? "Xs Xs", *++argv); Dies illustriert, daß die Format-Zeichenkette von printf auch ein Ausdruck sein kann. Als zweites Beispiel wollen wir das Mustersuchprogramm aus Abschnitt 4.1 etwas erweitern. Wie Sie sich erinnern, haben wir das Suchmuster tief im Programm vergra- vergraben, eine ganz offensichtlich ungenügende Lösung. Analog zum UNIX-Programm grep wollen wir das Programm so ändern, daß das Suchmuster als erstes Argument auf der Kommandozeile angegeben wird.
112 . siger und Vektoren «include <stdio.h> «include <string.h> «define MAXLINE 1000 int getline(char "line, int max); /* find: Zeilen mit Suchmuster aus dem 1. Argument ausgeben */ main(int arge, char *argvH) { char line[MAXLINE]; int found - 0; if (arge ! = 2) printfC'Usage: find pattern\n"); eise while (getlinedine, MAXLINE) > 0) if (strstrdine, argvM» ! = NULL) t printfC'Xs", line); found++; > return found; > Die Funktion strstr(s,t) aus der Standard-Bibliothek liefert einen Zeiger auf die erste Kopie der Zeichenkette t in der Zeichenkette s oder NULL, wenn t in s nicht vorkommt, strstristin <string.h> deklariert. Das Modell kann jetzt erweitert werden, um weitere Zeigerkonstruktionen zu illu- illustrieren. Angenommen, wir wollen zwei optionale Argumente erlauben: eines verlangt „Gib alle Zeilen aus, außer denen, die das Suchmuster enthalten"; das andere verlangt „Gib vor jeder Zeile auch ihre Nummer aus". Eine übliche Konvention für C-Programme bei UNIX-Systemen ist, daß ein Argu- Argument, das mit einem Minuszeichen beginnt, eine Option bezeichnet. Wählen wir etwa -x (für „ausgenommen", except), um die Umkehrung der Auswahl zu signalisieren, und - d, um die Numerierung der Ausgabezeilen zu verlangen, dann wird das Kommando find -x -n pattern jede Zeile samt Zeilennummer ausgeben, die das Muster pattern nicht enthält. Optionale Argumente sollten in beliebiger Reihenfolge erlaubt sein, und der Rest des Programms sollte unabhängig von der Anzahl der angegebenen Argumente sein. Außerdem ist es für die Benutzer bequem, wenn Optionen kombiniert werden können, wie zum Beispiel find -nx pattern Hier ist das Programm: «include <stdio.h> «include <string.h> «define MAXLINE 1000 int getline(char *line, int max);
/* find: Zeilen mit Suchmuster aus dem 1. Argument ausgeben */ main(int arge, char *argv[]) { char line [MAXLINE]; long lineno = 0; int c, except = 0, number = 0, found = 0; white (--arge > 0 && (*++argv)[0] == '-') while (c = *++argv[0]) switch (c) < case 'x1: except = 1; break; case 'n1: number ■ 1; break; default: printfC'find: illegal option Xc\n", c); arge = 0; found = -1; break; > if (arge != 1) printf("Usage: find -x -n pattern\n"); else while (getlinedine, MAXLINE) > 0) { lineno++; if ((strstrdine, *argv) 1= NULL) != except) { if (number) printfC'Xld:", lineno); printfC'Xs", line); found++; return found; > Vor jedem optionalen Argument wird arge dekrementiert und argv inkrementiert. Verlief alles fehlerfrei, dann gibt am Ende der Schleife arge an, wieviele Argumente un- unbearbeitet geblieben sind, und argv zeigt auf das erste davon, arge sollte deshalb 1 sein und *argv sollte auf das Suchmuster zeigen. Man beachte, daß *++argv auf eine Argu- Argument-Zeichenkette zeigt, also ist (*++argv)[0] das erste Zeichen des Arguments. (Ebenso korrekt wäre **++argv.) Da [ ] stärker bindet als * und ++, sind die Klammern notwendig, denn ohne sie wäre der Ausdruck gleichbedeutend mit *++(argv[0]). Das haben wir tatsächlich in der inneren Schleife verwendet, wo wir eine bestimmte Argu- Argument-Zeichenkette entlanggehen müssen. In der inneren Schleife inkrementiert der Aus- Ausdruck *++argv[0] den Zeiger argv[0]! Es kommt selten vor, daß man kompliziertere Zeigerausdrücke als diese benutzt; solche werden klarer, wenn man sie in zwei oder drei Einzelschritte zerlegt. Aufgabe 5-10. Schreiben Sie das Programm expr, das einen Ausdruck in umgekehrter polnischer Notation aus der Kommandozeile bewertet, wobei jeder Operator und jeder Operand ein separates Argument ist. Beispielsweise sollte expr 2 3 4 + * den Ausdruck 2 x C+4) bewerten. D
114 5 iger und Vektoren Aufgabe 5-11. Ändern Sie die Programme entab und detab (die als Aufgaben im ersten Kapitel entwickelt wurden), so daß sie eine Liste von Tabulatorpositionen als Argumente akzeptieren. Gibt es keine Argumente, dann sollen die normalen Tabulatorpositionen benutzt werden. D Aufgabe 5-12. Erweitern Sie entab und detab, so daß die Abkürzung entab -m +n akzeptiert wird; sie soll bedeuten, daß Tabulatorpositionen alle n Spalten, ausgehend von Spalte m, anzunehmen sind. Sorgen Sie für eine (aus der Sicht des Benutzers) praktische Voreinstellung. D Aufgabe 5-13. Schreiben Sie ein Programm tail, das die letzten n Zeilen seiner Eingabe ausgibt. Nach Voreinstellung soll n zum Beispiel 10 sein, aber das kann mit einem optio- optionalen Argument verändert werden, so daß tail -n die letzten n Zeilen ausgibt. Das Programm soll sich vernünftig verhalten, unabhängig davon, wie unsinnig die Eingabe oder auch der Wert von n ist. Schreiben Sie das Pro- Programm so, daß es den verfügbaren Speicherplatz optimal ausnützt; Zeilen sollen wie beim Sortierprogramm vod Abschnitt 5.6 gespeichert werden, nicht etwa in einem zweidi- mensionalen Vektor von fester Größe. D 5.11 Zeiger auf Funktionen In C ist eine Funktion selbst keine Variable, aber man kann Zeiger auf Funktionen definieren, die zugewiesen, in Vektoren eingetragen, an Funktionen übergeben, als Re- Resultatwert von Funktionen geliefert werden können usw. Wir illustrieren dies, indem wir das Sortierprogramm modifizieren, das wir früher in diesem Kapitel geschrieben haben, so daß ein optionales Argument - n dafür sorgt, daß die Eingabezeilen numerisch und nicht alphabetisch sortiert werden. Sortieren besteht oft aus drei Teilen - ein Vergleich, der die Anordnung von je- jeweils zwei Objekten bestimmt, ein Tausch, der die Anordnung umkehrt, und ein Sortier- Sortieralgorithmus, der Vergleiche und Tauschoperationen durchführt, bis sich die Objekte in der richtigen Reihenfolge befinden. Der Sortieralgorithmus ist von Vergleich und Tauschoperation unabhängig; wenn wir ihm verschiedene Vergleichs- und Tauschfunktio- Tauschfunktionen übergeben, können wir nach verschiedenen Kriterien sortieren. Diesen Ansatz ver- verwenden wir bei unserem neuen Sortierprogramm. Der Textvergleich von zwei Zeilen erfolgt wie bisher mit Hilfe von strcmp; zusätz- zusätzlich benötigen wir eine Funktion numcmp, die zwei Zeilen auf der Basis von numerischen Werten vergleicht und ein Resultat liefert, das strcmp entspricht. Diese Funktionen wer- werden vor main deklariert und ein Zeiger auf die passende Funktion wird an qsort überge- übergeben. Wir behandeln fehlerhafte Argumente kaum, um uns auf die hauptsächlichen Punk- Punkte zu konzentrieren. «include <stdio.h> «include <string.h> «define MAXLINES 5000 /* maximale Anzahl Zeilen */ char *lineptr[MAXLlNES]; /* Zeiger auf Textzeilen */
5.11 Zeiger auf Fur men 115 int readlines(char *lineptr[], int nlines); void writelines(char *lineptr[], int nlines); void qsort(void *lineptr[], int left, int right, int (*comp)(void *, void *)); int numctnp(char *, char *); /* Eingabezeilen sortieren */ main(int arge, char *argvü) t int nlines; /* Anzahl eingelesener Zeilen */ int numeric = 0; /* 1 fuer numerisches Sortieren */ if (arge > 1 && strcmp(argv[1], "-n") == 0) numeric * 1; if ((nlines = readlines(lineptr, MAXLINES)) >= 0) { qsort((void **) lineptr, 0, nlines-1, (int (*)(void*,void*))(numeric ? numetnp : stremp)); writelines(lineptr, nlines); return 0; > else t printfC'input too big to sort\n"); return 1; > > Im Aufruf von qsort sind stremp und numemp Adressen von Funktionen. Da bekannt i^t^aß_es_sichi tier uinjFunktioj^i^ handelt, ist der Adreß-Operator & nicht nötig, wie er auch vor einem Vektornamen nicht benötigtjwird. Wir haben qsort so formuliert, daß jeder Datentyp bearbeitet werden kann, nicht nur Zeichenketten. Durch den Funktionsprototyp wird angedeutet, daß qsort einen Vek- Vektor von Zeigern, zwei ganze Zahlen und eine Funktion mit zwei Zeigerargumenten er- erwartet. Der generische Zeigertyp void * wird für die Zeigerargumente verwendet. Jeder Zeiger kann in den Typ void * und zurück umgewandelt werden, ohne daß Information verloren geht; deshalb können wir qsort aufrufen und dabei die Argumente explizit in den Typ void * umwandeln. Die aufwendige Umwandlungsoperation für den Funktions- Funktionsnamen wandelt die Argumente der Vergleichsfunktion um. Diese Umwandlungen haben zwar im allgemeinen keinen Einfluß auf die tatsächliche Repräsentierung, aber sie über- überzeugen den Übersetzer, daß alles in Ordnung ist. /* qsort: sortiere vCleft]...vCright] in aufsteigende Reihenfolge */ void qsort(void *v[], int left, int right, int (*comp)(void *, void *)) int i, last; void swap(void *v[], int, int); if (left >= right) /* nichts machen, wenn der Vektor */ return; /* weniger als 2 Elemente enthaelt */ swap(v, left, (left + right)/2); last * left; for (i = left+1; i <= right; i++) if ((*comp)(vCi], vCleft]) < 0) swap(v, ++last, i); swap(v, left, last); qsort(v, left, last-1, comp); qsort(v, last+1, right, comp); >
116 Zeiger und Vektoren Die Parameterdeklarationen sollten sorgfältig betrachtet werden. Der vierte Parameter von qsort ist int CcompKvoid *, void *) comp ist also ein Zeiger auf eine Funktion, die zwei void * Argumente hat und ein int- Resultat liefert. In der Zeile if ((*comp)(v[i], vUeft]) < 0) wird der Zeiger comp entsprechend seiner Deklaration benutzt: comp ist ein Zeiger auf eine Funktion, *comp ist die Funktion, und (*comp)(v[i], vUeft]) ist der Aufruf. Die Klammern sind notwendig, damit die einzelnen Teile korrekt zusam- zusammengefaßt werden. Ohne diese Klammern würde int *comp(void *, void *) /* FALSCH */ ausdrücken, daß comp eine Funktion ist, die einen Zeiget auf einen int-Wert liefert, und dies ist natürlich etwas ganz anderes. Wir haben bereits die Funktion strcmp vorgeführt, die zwei Zeichenketten ver- vergleicht. Hier ist die Funktion numcmp, die zwei Zeichenketten in bezug auf führende numerische Werte vergleicht, die mit atof berechnet werden: «include <stdlib.h> /* numcmp: s1 und s2 numerisch vergleichen */ int numcmp(char *s1, char *s2) { double v1, v2; v1 = atof(si); v2 = atof(s2); if (vi < v2) return -1; else if (v1 > v2) return 1; eise return 0; > Die swap-Funktion, die zwei Zeiger vertauscht, ist identisch zu dem, was wir früher in diesem Kapitel vorgeführt haben, nur werden die Deklarationen auf void * abgeän- abgeändert. void swap(void *v[], int i, int j) void *temp; temp = v[f]; v[i] = v[j]; v[j] = temp; > Zum Sortierprogramm kann man eine Vielzahl anderer Optionen hinzufügen; eini- einige sind anspruchsvolle Aufgaben.
5.12 Komplizic Vereinbarungen 117 Aufgabe 5-14. Ändern Sie das Sortierprogramm, so daß eine Option -r akzeptiert wird, die die Reihenfolge der Sortierung umkehrt. Sorgen Sie dafür, daß -r auch zu- zusammen mit -n funktioniert. □ Aufgabe 5-15. Fügen Sie die Option -f (told) hinzu, bei der Groß- und Kleinschrei- Kleinschreibung beim Sortieren keinen Unterschied macht; zum Beispiel sind dann a und A gleich. D Aufgabe 5-16. Fügen Sie die Option -d (dictionary) hinzu, bei der nur Buchstaben, Ziffern und Leerzeichen verglichen werden. Sorgen Sie dafür, daß dies auch zusammen mit - f funktioniert. D Aufgabe 5-17. Fügen Sie eine Möglichkeit hinzu, einzelne Felder zu unterscheiden, so daß innerhalb der Zeilen in bezug auf verschiedene Felder sortiert werden kann, wobei jedes Feld noch mit voneinander unabhängigen Optionen sortiert werden soll. (Das Sachverzeichnis dieses Buches wurde mit -df in bezug auf die Stichwörter und mit - n in bezug auf die Seitenzahlen sortiert.) D 5.12 Komplizierte Vereinbarungen C wird gelegentlich wegen der Syntax seiner Vereinbarungen kritisiert, insbesonde- insbesondere, wenn Zeiger auf Funktionen beteiligt sind. Die Syntax ist ein Versuch, Vereinbarung und Aufruf gleich aussehen zu lassen; das gelingt in einfachen Fällen, aber bei den schwierigeren kann es verwirren, denn man kann Vereinbarungen nicht von links nach rechts lesen und Klammern nehmen Überhand. Der Unterschied zwischen int *fO; /* f: Funktion, die einen Zeiger auf int liefert */ und int (*pf)O; /* pf: Zeiger auf Funktion, die int liefert */ verdeutlicht das Problem: * ist ein vorangestellter Operator und hat geringeren Vorrang als (), also braucht man Klammern, um die richtige Zuordnung zu erzwingen. Obwohl wirklich komplizierte Vereinbarungen in der Praxis selten auftreten, ist es wichtig zu wissen, wie man sie versteht und nötigenfalls aufbaut. Ein guter Weg zum Aufbau von Vereinbarungen ist die Technik kleiner Schritte mit typedef, die in Abschnitt 6.7 erläutert wird. Als Alternative zeigen wir in diesem Abschnitt ein Paar von Program- Programmen, die korrekte Vereinbarungen von C in (deutschen) Text und wieder zurück verwan- verwandeln. Der Text wird von links nach rechts gelesen. Das erste Programm, del, ist das kompliziertere. Es wandelt eine Vereinbarung von C in Worte um, wie in diesen Beispielen:* char **argv argv: Zeiger auf Zeiger auf char int (*daytab)[13] daytab: Zeiger auf Vektor[13] mit int int *daytab[13] daytab: Vektor[13] mit Zeiger auf int void *comp() comp: Funktion mit Resultat Zeiger auf void * In diesem Abschnitt haben wir auch den Text übersetzt, der in den Programmen verwendet wird, dd funk- funktioniert auch im Deutschen verblüffend gut A.d.Ü.
118 Zeiger und Vektoren void (*comp)() comp: Zeiger auf Funktion mit Resultat void char (*(*x())[])O x: Funktion mit Resultat Zeiger auf Vektor[) mit Zeiger auf Funktion mit Resultat char char (*<*x[3])O)t5] x: Vektor[3] mit Zeiger auf Funktion mit Resultat Zeiger auf Vektor[5] mit char del beruht auf dem Grammatikbegriff declarator, der exakt im Anhang A § A.8.5 definiert ist; hier ist eine vereinfachte Form: del: optionale *-e direct-del direct-del: name (del) direct-del O direct-del [optionale Größe] Oder in Worten: del ist direct-del, wobei *-e (Sterne) vorausgehen können, direct-del ist ein Name oder del in Klammern oder direct-del gefolgt von Klammern oder direct-dcl ge- gefolgt von eckigen Klammern mit einer optionalen Größenangabe. Diese Grammatik kann zur Analyse von Vereinbarungen verwendet werden. Be- Betrachten Sie zum Beispiel folgende Vereinbarung: <*pfa[])O pfa wird als name erkannt und folglich als direct-dcl. Dann paßt pfa[ ] wieder zu direct- dcl. Damit wird *pfa[ ] als del erkannt, deshalb gilt (*pfa[ ]) als direct-dcl. Danach paßt dann (*pfa[])() zu direct-dcl und folglich zu del. Wir können die Analyse als Syntax- bauin folgendermaßen darstellen {direct-dcl wurde dabei mit dir-dcl abgekürzt): Das Herz des Programms del bilden zwei Funktionen del und dirdcl, die eine Ver- Vereinbarung gemäß dieser Grammatik analysieren. Da die Grammatik rekursiv definiert ist, rufen sich die Funktionen gegenseitig rekursiv auf, während sie Teile einer Vereinba-
5.12 Komplizier' * 'ereinbarungen 119 rung erkennen; ein solches Programm nennt man recursive-descent parser (Methode des rekursiven Abstiegs). /* del: "del" erkennen */ void dcl(void) t int ns; for (ns » 0; gettokenO =='*';) /* *-e zaehlen */ ns++; dirdclO; while (ns— > 0) strcat(out, " Zeiger auf"); > /* dirdcl: "dir-det" erkennen */ void dirdcl(void) t int type; if (tokentype «'(')< /* ( del ) */ dclO; if (tokentype != ')') printfC'Fehler: ) fehlt\n"); > else if (tokentype == NAME) /* Variablenname */ strcpy(name# token); else printfC'Fehler: Name oder (del) erwartet\n"); while ((type=gettoken()) == PARENS || type == BRACKETS) if (type == PARENS) strcat(out, " Funktion mit Resultat"); eise < strcat(out, " Vektor"); strcat(out, token); strcat(out, " mit"); Da diese Programme illustrativ und nicht unbedingt kugelsicher sein sollen, gibt es für del beachtliche Einschränkungen. Das Programm kann nur einfache Typen wie char oder int verarbeiten. Parametertypen bei Funktionen oder Attribute wie const werden nicht verarbeitet. Unerwartete Leerzeichen bringen del durcheinander. Es gibt nur we- wenig Fehlerbehandlung, deshalb stiften auch falsche Vereinbarungen Verwirrung. Die nötigen Verbesserungen bleiben als Übungsaufgaben. Hier sind die globalen Variablen und das Hauptprogramm: «include <stdio.h> «include <string.h> «include <ctype.h> «define MAXTOKEN 100 enum { NAME, PARENS, BRACKETS >; /* Name, Klammern, eckige Klammern */ void del(void); void dirdcl(void); int gettoken(void);
120 "*£Jger und Vektoren int tokentype; /* Typ des letzten Symbols */ char token[MAXTOKEN]; /* Zeichenkette mit letztem Symbol */ char nametMAXTOKEN]; /* Variablenname */ char datatype[MAXTOKEN]; /* Datentyp: char, int, usw. */ char out[1000]; /* Ausgabetext */ mainO /* verwandle Vereinbarung in Worte */ t while (gettokenO I» EOF) t /* 1. Symbol auf der Zeile V strcpy(datatype, token); /* ist der Datentyp */ out[0] « '\0'; dctO; /* Rest der Zeile analysieren */ if (tokentype I« '\n') printfCSyntaxfehler\nM); printfC'Xs: Xs Xs\n", name, out, datatype); > return 0; > Die Funktion gettoken überspringt Leerzeichen und Tabulatorzeichen und fmdet dann das nächste Symbol in der Eingabe; ein „Symbol" ist ein Name, ein Paar von Klam- Klammern, ein Paar von eckigen Klammern, eventuell mit einer Zahl dazwischen, oder ein an- anderes einzelnes Zeichen. int gettoken(void) /* liefere naechstes Symbol */ t int c, getch(void); void ungetch(int); char *p = token; white ((c = getchO) ■• ■ ' 11 c ■* '\t') if (c == '(•) t if «c = getchO) « ')') t strcpy(token, "()»); return tokentype * PARENS; > eise { ungetch(c); return tokentype • '('; > > else if (c «« •[•) t for (*p++ » c; (*p»+ « getchO) I" '3'; ) •p = >\0'; return tokentype ■ BRACKETS; > else if (isalpha(c)) { for (*p++ = c; isalnum(c ■ getchO); ) *p++ • c; *p . '\0'; ungetch(c); return tokentype « NAME; > eise return tokentype = c; > getch und ungetch wurden in Kapitel 4 besprochen.
5.12 Komplizierte ' inbarungen 121 Die umgekehrte Richtung ist einfacher, insbesondere, wenn uns überflüssige Klam- Klammern nicht stören. Das Programm undcl übersetzt eine verbale Beschreibung, wie „x ist eine Funktion mit Resultat Zeiger auf einen Vektor von Zeigern auf Funktionen mit Re- Resultat char". Wir verwenden folgende Eingabe: x ()•[]•() char und erhalten folgende Ausgabe: char (*(*x())[])O Dank der abgekürzten Eingabe-Syntax können wir die Funktion gettoken wiederverwen- wiederverwenden, undcl benutzt auch die gleichen externen Variablen wie del. I* undcl: verwandle verbale Beschreibung in Vereinbarung */ mainO t int type; char tempWAXTOKEtO; while (gettokenO !- EOF) t strcpy(out, token); while ((type = gettokenO) != '\n') if (type == PARENS || type == BRACKETS) strcat(out, token); else if (type == '*') t sprintfUemp, "(*Xs)», out); strcpy(out, temp); > else if (type « NAME) { sprintfdemp, "Xs Xs", token, out); strcpy(out, temp); > else printfCungueltige Eingabe bei Xs\n", token); printf("Xs\n", out); > return 0; > Aufgabe 5-18. Sorgen Sie dafür, daß sich dd von Eingabefehlern erholt. D Aufgabe 5-19. Ändern Sie undcl so, daß keine überflüssigen Klammern zu Vereinba- Vereinbarungen hinzugefügt werden. D Aufgabe 5-20. Erweitern Sie del so, daß Vereinbarungen mit Parametertypen und At- Attributen wie const usw. verarbeitet werden. D
6 Strukturen Eine Struktur ist eine Ansammlung von einer oder mehreren Variablen, möglicher- möglicherweise mit verschiedenen Typen, die unter einem einzigen Namen zur bequemen Handha- Handhabung zusammengefaßt sind. (In manchen Sprachen, insbesondere in Pascal, wird eine Struktur als „record" bezeichnet.) Strukturen sind nützlich, um komplizierte Daten zu organisieren, insbesondere in großen Programmen, denn sie ermöglichen, eine Gruppe von zusammengehörigen Variablen als Einheit statt separat zu behandeln. Ein traditionelles Beispiel einer Struktur ist ein Eintrag in einer Datei für Lohn- Lohnbuchhaltung: Ein Mitarbeiter wird durch einen Satz von Attributen beschrieben, wie zum Beispiel Name, Adresse, Sozialversicherungsnummer, Gehalt usw. Einige dieser At- Attribute können selbst wieder Strukturen sein: Ein Name hat mehrere Komponenten, ge- genau wie eine Adresse oder sogar ein Gehalt. Ein anderes Beispiel, das für C typischer ist, hat mit Grafik zu tun: ein Punkt ist ein Koordinatenpaar, ein Rechteck ist ein Punk- Punktepaar usw. Als hauptsächliche Änderung wurde im ANSI-Standard die Zuweisung von Struktu- Strukturen definiert - Strukturen können jetzt kopiert und zugewiesen sowie an Funktionen übergeben und von Funktionen als Resultat geliefert werden. Dies haben die meisten Übersetzer seit vielen Jahren unterstützt, aber die Eigenschaften sind nun genau defi- definiert. Automatische Strukturen und Vektoren dürfen jetzt auch initialisiert werden. 6.1 Die Grundbegriffe Wir wollen ein paar Strukturen für Grafikanwendungen erzeugen. Das elementare Objekt ist ein Punkt, und wir nehmen an, daß er eine x-Koordinate und eine y- Koordinate hat, die beide ganzzahlig sind. y , D,3) @,0) Die zwei Komponenten können in einer Struktur zusammengefaßt werden: struct point t int x; int y; >; Das reservierte Wort struct steht am Anfang einer Strukturvereinbarung, die aus einer Liste von Deklarationen besteht, die in geschweifte Klammern eingeschlossen sind. Ein sogenanntes Etikett (structure tag) kann dem Wort struct folgen (hier das Etikett point). Das Etikett steht dann für diese Art von Struktur und kann anschließend als Ab- Abkürzung für den Teil der Vereinbarung in geschweiften Klammern verwendet werden. Die Variablen, die in einer Struktur angegeben werden, nennen wir Komponenten (members). Ein Etikett oder eine Komponente können den gleichen Namen besitzen wie eine gewöhnliche Variable, ohne daß dadurch ein Konflikt entsteht, denn sie können im-
124 6 Strukturen mer aus dem Kontext unterschieden werden. Außerdem kann der gleiche Komponen- Komponentenname in verschiedenen Strukturen auftreten; aus stilistischen Gründen wird man aber normalerweise gleiche Namen nur für eng verwandte Objekte verwenden. Eine struct-Vereinbarung definiert einen Datentyp. Nach der rechten geschweif- geschweiften Klammer am Ende der Komponentenliste kann eine Liste von Variablen stehen, ge- genau wie bei jedem elementaren Datentyp. Das heißt: struct { ... > x, y, i; ist syntaktisch analog zu int x, y, z; Jede dieser beiden Definitionen vereinbart x, y und z als Variablen des angegebenen Typs und reserviert Speicherplatz. Folgt einer Strukturdeklaration keine Variablenliste, wird auch kein Speicherplatz reserviert; eine solche Deklaration beschreibt lediglich die Form einer Struktur. Hat die Deklaration jedoch ein Etikett, dann kann das Etikett später zur Definition von Objekten mit dieser Struktur verwendet werden. Wurde etwa point so wie oben deklariert, dann definiert struct point pt; eine Variable pt, die eine Struktur vom Typ struct point ist. Eine Struktur kann initiali- initialisiert werden; dazu folgt der Definition eine Liste von Initialisierungen, also konstanten Ausdrücken, für die Komponenten: struct point maxpt « { 320, 200 }; Eine automatische Struktur kann auch durch Zuweisung initialisiert werden oder durch Aufruf einer Funktion, die eine Struktur mit dem richtigen Typ als Resultat liefert. Ein Verweis auf eine Komponente einer bestimmten Struktur hat in einem Aus- Ausdruck folgende Form: Struktur- Variablenname. Komponente Dabei verbindet der Operator „." zur Auswahl von Strukturkomponenten den Namen der Strukturvariablen und den Namen der Komponente. Zum Beispiel gibt printfC'Xd.Xd", pt.x, pt.y); die Koordinaten des Punkts pt aus und double dist, sqrt(double); dist = sqrt((double)pt.x * pt.x + (double)pt.y * pt.y); berechnet den Abstand von pt zum Ursprung @,0). Strukturen können Strukturen als Komponenten enthalten. Ein Rechteck kann als Paar von Punkten dargestellt werden, die die diagonal gegenüberliegenden Ecken be- bezeichnen: y ,Pt2 pt1
6.2 Strukturen un .nktionen 125 struct rect { struct point ptl; struct point pt2; >; Die rect-Struktur enthält zwei point-Strukturen. Wenn wir eine Variable screen vereinba- vereinbaren struct rect screen; dann bezeichnet screen.pti.x die ^-Koordinate der Komponente ptl von screen. 62 Strukturen und Funktionen Strukturen darf man nur kopieren oder als Ganzes zuweisen; außerdem kann man die Adresse einer Struktur mit & bestimmen und auf die Komponenten zugreifen. Ko- Kopieren und Zuweisen beinhaltet auch das Übergeben von Argumenten an Funktionen und das Zurückliefern von Funktionsresultaten. Strukturen können aber nicht miteinan- miteinander verglichen werden. Eine Struktur kann mit einer Liste von konstanten Werten für die Komponenten initialisiert werden; auch eine automatische Struktur kann durch Zuwei- Zuweisung initialisiert werden. Wir wollen Strukturen untersuchen, indem wir einige Funktionen zur Bearbeitung von Punkten und Rechtecken schreiben. Es gibt wenigstens drei Möglichkeiten: Kompo- Komponenten getrennt übergeben, eine gesamte Struktur übergeben oder einen Zeiger auf eine Struktur übergeben. Jeder Ansatz hat seine guten und schlechten Seiten. Die erste Funktion, makepoint, akzeptiert zwei ganze Zahlen und liefert eine point-Struktur: /* makepoint: einen Punkt aus x- und y-Koordinate erzeugen */ struct point makepoinUint x, int y) { struct point temp; temp.x » x; temp-y ■ y; return temp; > Man beachte, daß zwischen den Namen der Parameter und den gleichen Namen der Komponenten kein Konflikt entsteht; tatsächlich betont die Wiederverwendung der Na- Namen den Zusammenhang. Mit makepoint kann man nun eine Struktur dynamisch initialisieren oder Struktu- Strukturen als Argumente für eine Funktion konstruieren: struct rect screen; struct point middle; struct point makepointO'nt, int); screen.ptl =■ makepoint@. On- Onscreen.pt2 = makepoinUXMAX, YHAX); middle = makepoint((screen.pt1.x + screen.pt2.x)/2, (screen.ptl.y + screen.pt2.y)/2);
Als nächsten Schritt entwickeln wir einen Satz von Funktionen für Arithmetik mit Punkten. Zum Beispiel /* addpoint: zwei Punkte addieren */ struct point addpoinUstruct point pi, struct point p2) { pl.x += p2.x; pl.y *= p2.y; return pi; > Hier sind beide Parameter und der Resultatwert Strukturen. Anstatt eine explizite tem- temporäre Variable zu benutzen, haben wir direkt zu den Komponenten von pl addiert; dies soll ganz deutlich machen, daß Strukturparameter wie alle anderen Parameter als Werte übergeben werden. Als weiteres Beispiel untersucht die Funktion ptinrect, ob ein Punkt in einem Rechteck liegt, wobei wir die Konvention getroffen haben, daß die linke und untere Kan- Kante zum Rechteck dazugehören, nicht aber die obere und rechte Kante. /• ptinrect: Resultat ist 1 falls p in r, 0 falls nicht •/ int ptinrecUstruct point p, struct rect r) { return p.x >- r.pti.x && p.x < r.pt2.x && p.y >= r.pti.y && p.y < r.pt2.y; > Dabei wird angenommen, daß das Rechteck in einer Normalform vorliegt, bei der die ptl-Koordinaten kleiner als die pt2-Koordinaten sind. Die folgende Funktion liefert ein Rechteck in dieser Normalform: #define minCa, b) CCa) < (b) ? (a) : <b>) «define max(a, b) <<a) > (b) ? (a) : (b>> /* canonrect: Normal form der Koordinaten eines Rechtecks liefern */ struct rect canonrect(struct rect r) { struct rect temp; temp.pt1.x = min(r.pt1.x, r.pt2.x); temp.pti.y = min(r.pt1.y, r.pt2.y); temp.pt2.x - max(r.pt1.x, r.pt2.x>; temp.pt2.y = max(r.pt1.y, r.pt2.y); return temp; > Wenn eine große Struktur an eine Funktion übergeben werden soll, ist es im allge- allgemeinen effizienter, einen Zeiger zu übergeben und nicht die ganze Struktur zu kopieren. Zeiger auf Strukturen verhalten sich genau wie Zeiger auf gewöhnliche Variablen. Die Vereinbarung struct point *pp; legt fest, daß pp ein Zeiger auf eine Struktur von Typ struct point ist. Wenn pp auf eine point-Struktur zeigt, dann ist *pp die Struktur und ('pp)Jt oder ('pp).y sind die Kompo- Komponenten. Um pp zu benutzen, könnten wir zum Beispiel folgendes schreiben: struct point origin, *pp; pp = Sorigin; printfC'origin is <Xd,Xd>\n", <*pp).x, (*pp).y);
6.3 Vektoren von jkturen 127 Die Klammern bei ('pp)jt sind notwendig, da der Vorrang des Operators „." zur Aus- Auswahl von Strukturkomponenten höher ist als der des Inhaltsoperators *. Der Ausdruck •ppjc bedeutet '(ppjc); das ist hier ungültig, denn x ist kein Zeiger. Zeiger auf Strukturen werden so häufig benutzt, daß es eine andere Schreibweise als Abkürzung gibt. Ist p ein Zeiger auf eine Struktur, dann verweist p->Komponente - einer- Struktur auf die angegebene Komponente. (Der Operator -> besteht aus einem Minuszeichen, dem eine rechte spitze Klammer > folgt.) Wir könnten deshalb auch schreiben printf("origin is Etd,5td)\n", pp->x, pp->y); Beide Auswahloperatoren . und -> werden von links nach rechts zusammengefaßt, also sind nach der Vereinbarung struct rect r, *rp » r; diese vier Ausdrücke äquivalent: r.pti.x rp->pt1.x (r.pti).x (rp->pt1).x Die Auswahloperatoren . und -> haben zusammen mit () für Funktionsaufrufe und [ ] für Indexoperationen den höchsten Vorrang aller Operatoren und binden daher sehr eng. Nach der Vereinbarung struct { int Ien; char *str; > *p; vergrößert zum Beispiel ++p->len len und nicht p, da implizit als ++(p->len) geklammert wird. Mit Klammern kann man die Bindung ändern: (++p)->len inkrementiert p, bevor dann auf len zugegriffen wird, und (p++)->len inkrementiert p hinterher. (In diesem letzten Beispiel sind die Klam- Klammern unnötig.) Ebenso wird mit dem Ausdruck *p->str auf das Objekt zugegriffen, auf das str zeigt; *p->str++ inkrementiert str nach Zugriff auf das Objekt, auf das str zeigt (genau wie *s++); (*p->str)++ inkrementiert das Objekt, auf das str zeigt; und *p++->str schließlich inkrementiert p nach Zugriff auf das Objekt, auf das str zeigt. 6.3 Vektoren von Strukturen Betrachten wir ein Programm, das die in C reservierten Worte in einem Text zählt. Wir benötigen einen Vektor von Zeichenketten, um die Namen aufzubewahren, und einen Vektor von ganzzahligen Werten für die Zähler. Eine Möglichkeit wäre, zwei par- parallele Vektoren keyword und keycount zu verwenden, die folgendermaßen definiert wer- werden könnten: char «keyword[NKEYS]; int keycount[NKEYS];
128 6 Strukturen Die Tatsache jedoch, daß die Vektoren parallel zu betrachten sind, deutet auf eine ande- andere Organisation: einen Vektor mit Strukturen. Jeder Eintrag für ein reserviertes Wort besteht aus zwei Informationen: char *word; int count; und es gibt einen Vektor von solchen Informationspaaren. Die Strukturdefinition struct key { char »word; int count; > keytab[NKEYS]; deklariert einen Strukturdatentyp key, definiert einen Vektor keytab von entsprechenden Strukturen und reserviert Speicher für diesen Vektor. Jedes Element des Vektors ist eine Struktur. Eine alternative Formulierung wäre struct key { char »word; int count; >; struct key keytabCNKEYS]; Da die Struktur keytab eine konstante Liste von Namen enthält, ist es am einfach- einfachsten, sie als externe Variable zu vereinbaren und sie ein für alle Mal bei der Definition gleich zu initialisieren. Die Initialisierung der Struktur erfolgt analog wie früher - der Definition folgt eine Liste von Initialisierungen in geschweiften Klammern: struct key { char «word; int count; > keytab [] = { "auto", 0, "break", 0, "case", 0, "char", 0, "const", 0, "continue", 0, "default", 0, /• ... V "unsigned", 0, "void", 0, "volatile", 0, "while", 0 >; Die Initialisierungen werden paarweise angegeben, den Komponenten der Struktur ent- entsprechend. Präziser sollte man dabei die Initialisierungen für jede „Zeile" oder Einzel- Einzelstruktur in geschweifte Klammern einschließen, also in folgender Form: { "auto", 0 >, t "break", 0 >, < "case", 0 >, Die inneren geschweiften Klammern sind jedoch nicht notwendig, wenn die Initialisierun- Initialisierungen einfache Variablen oder Zeichenketten sind und wenn alle angegeben werden. Wie
6.3 Vektoren von i sturen 129 üblich berechnet der Übersetzer die Anzahl der Einträge im Vektor keytab, wenn Initia- Initialisierungen angegeben sind und die Dimensionierung in [ ] ausgelassen wird. Das Programm zum Zählen von reservierten Worten beginnt mit der Definition von keytab. Das Hauptprogramm liest seine Eingabe, indem wiederholt eine Funktion getword aufgerufen wird, die jedesmal ein Wort holt. Jedes Wort wird dann in keytab ge- gesucht, und zwar mit einer Variante der Funktion zur binären Suche, die wir in Kapitel 3 geschrieben haben. Die Liste der reservierten Worte muß in der Tabelle aufsteigend sor- sortiert sein. «include <stdio.h> #ine lüde <ctype.h» #ine lüde <string.h> «define MAXWORD 100 int getword(char *, int); int binsearch(char *, struct key *, int); /* reservierte Uorte in C zaehlen •/ mainO t int n; char wordtMAXWORD]; while (getword(word, HAXWORD) !- EOF) if (isalpha{word[0}>> if (<n = binsearch(word, keytab, NKEYS)) >= 0) keytab[n].count++; for <n = 0; n < NKEYS; n++) if (keytab[n].count > 0) printf("%4d Xs\n", keytab[n].count, keytabtn].word); return 0; > /* bfnsearch: word in tab[0]. ..tabf.n-1] finden */ int binsearch(char *word, struct key tab[], int n) t int cond; int low, high, mid; low - 0; high = n - 1; while (low <= high) { mid * (low+high) / 2; if ((cond = strcmp(word, tab[mid].word)) < 0) high = mid - 1; else if (cond > 0) tow = mid + 1; else return mid; > return -1; > Wir zeigen die Funktion getword später; im Augenblick genügt es, wenn wir davon ausge- ausgehen, daß jeder Aufruf von getword ein Wort findet, das in den Zeichenvektor kopiert wird, der getword als erstes Argument übergeben wird.
130 6 Strukturen Die Größe NKEYS ist die Anzahl der reservierten Worte in keytab. Wir könnten diese Zahl natürlich von Hand auszählen, es ist jedoch wesentlich leichter und sicherer, wenn dies die Maschine übernimmt, insbesondere, falls die Liste später geändert wird. Eine Möglichkeit hierzu wäre, an das Ende der Initialisierungsliste einen NULL-Zeiger zu stellen und dann keytab mit Hilfe einer Schleife bis zu diesem Zeiger abzusuchen. Dies ist jedoch zu aufwendig, denn die Größe des Vektors kann vollständig vom Übersetzer bestimmt werden. Die Größe des Vektors ist bestimmt durch die Größe ei- eines Eintrags multipliziert mit der Anzahl Einträge, deshalb ist die Anzahl Einträge gera- gerade Größe von keytab / Größe von struct key In C gibt es den unären Operator sizeof, der vom Übersetzer bewertet wird und der dazu benutzt werden kann, die Größe eines beliebigen Objekts festzustellen. Die Ausdrücke sizeof Objekt und sizeof( Typname ) liefern jeweils eine ganze Zahl, nämlich die Größe des angegebenen Objekts oder Daten- Datentyps gemessen in „Byte". (Genaugenommen erzeugt sizeof einen ganzzahligen Wert oh- ohne Vorzeichen, dessen Typ size t in der Definitionsdatei < stddef.h > vereinbart ist.) Ein Objekt kann dabei eine Variable, ein Vektor oder eine Struktur sein. Der Datentypname kann der Name eines elementaren Typs wie int oder double sein, oder auch der Name ei- eines abgeleiteten Typs, wie etwa einer Struktur oder eines Zeigers. In unserem Fall ist die Anzahl reservierter Worte gleich der Größe des Vektors, di- dividiert durch die Größe eines Vektorelements. Dies wird in einer #define-Anweisung dazu benutzt, um den Wert von NKEYS festzulegen: «define NKEYS (sizeof keytab / sizeof(struct key)) Man kann auch die Vektorgröße durch die Größe des ersten Elements teilen: «define NKEYS (sizeof keytab / sizeof keytabtO]) Diese Formulierung hat den Vorteil, daß sie unabhängig vom Datentyp ist. sizeof kann nicht in einer #if-Zeile verwendet werden, da der Preprozessor die Typnamen nicht erkennt. Der Ausdruck in #deflne wird dagegen nicht vom Preprozes- Preprozessor bewertet, also ist die Formulierung erlaubt. Betrachten wir jetzt die Funktion getword. Wir haben getword allgemeiner formu- formuliert als nötig für dieses Programm, aber die Funktion ist nicht kompliziert, getword holt das nächste „Wort" aus der Eingabe. Dabei ist ein Wort entweder eine Kette von Buch- Buchstaben und Ziffern, die mit einem Buchstaben beginnt, oder ein einzelnes Zeichen, das kein Zwischenraumzeichen ist. Das Funktionsresultat ist der erste Buchstabe des Worts, oder EOF am Dateiende oder das Zeichen selbst, wenn es kein Buchstabe ist.
6.4 Zeiger auf Strt. en 131 /* getword: naechstes Wort oder Zeichen aus der Eingabe holen */ int getwordCchar "word, int lim) { int c, getch(void); void ungetch(int); char *w * word; while (isspaceCc = getch()>> if (c 1= EOF) *w++ » c; if (lisalpha(O) { *w » '\0'; return c; > for ( ; --lim > 0; w++) if (lisalnun(*w = getchO)) { ungetch(*w>; break; > *w = '\0'; return word[0]; > getword benutzt die Funktionen getch und ungetch, die wir in Kapitel 4 entwickelt haben. Wird das Ende einer Folge von Buchstaben und Ziffern erreicht, dann hat getword ein Zeichen zuviel eingelesen. Durch einen Aufruf von ungetch wird dieses Zeichen in die Eingabe zurückgestellt, so daß es beim nächsten Aufruf wieder gelesen werden kann. getword benutzt auch isspace, um Zwischenraumzeichen zu überspringen, isalpha, um Buchstaben und isalnum, um Buchstaben oder Ziffern zu erkennen; alle stammen aus der Standard-Definitionsdatei < ctype.h >. Aufgabe 6-1. Unsere Version von getword behandelt Unterstrich „_", konstante Zei- Zeichenketten (in Doppelanführungszeichen), Kommentare und Zeilen für den Preprozes- sor nicht richtig. Schreiben Sie eine bessere Version. D 6.4 Zeiger auf Strukturen Wir illustrieren einige der Überlegungen im Zusammenhang mit Zeigern auf Strukturen und Vektoren von Strukturen, indem wir das Programm zum Zählen von re- reservierten Worten nochmals schreiben, und zwar diesmal mit Zeigern anstelle von Vek- Vektorindizes. Die globale Definition von keytab kann unverändert bleiben, aber main und binsearch müssen modifiziert werden.
132 6 Strukturen flinclude <stdio.h> #include <ctype.h> #ine lüde <string.h> «define MAXWORD 100 int getword(char •, int); struct key *binsearch(char *, struct key *, int); /* reservierte Worte in C zaehlen; Version mit Zeigern •/ mainO t char word[MAXWORD]; struct key *p; while (getword(word, MAXWORD) 1= EOF) if (isalpha(word[0})) if ((p=binsearch(word, keytab, NKEYS)) 1 = NULL) p->count++; for (p = keytab; p < keytab + NKEYS; p++) if (p->count > 0) printf("%4d Xs\n", p->count, p->word); return 0; > /• binsearch: word in tabCO]...tab[n-1] finden •/ struct key *binsearch(char »word, struct key *tab, int n) { int cond; struct key "low = &tab[0]; struct key *high = &tab[n]; struct key *mid; while (low < high) { mid = low + (high-low) / 2; if ((cond = strcmp(word, mid*>word)) < 0) high s mid; else if (cond > 0) low = mid + 1; else return mid; > return NULL; > Hier ist einiges erwähnenswert. Zunächst müssen die Vereinbarungen von binsearch zeigen, daß als Resultat ein Zeiger auf struct key geliefert wird und kein int- Wert; dies geschieht im Funktionsprototyp und in binsearch. Findet binsearch das ge- gesuchte Wort, dann wird ein Zeiger auf den zugehörigen Tabelleneintrag geliefert; andern- andernfalls ist der Resultatwert NULL. Zum zweiten wird auf Elemente von keytab nun mit Hilfe von Zeigern zugegriffen. Dies bedingt wesentliche Änderungen in binsearch. Die Initialisierungswerte für low und high sind nun Zeiger auf den Anfang und di- direkt hinter das Ende der Tabelle. Das mittlere Element kann nicht mehr als mid = <low+high) / 2 /• FALSCH •/
6.5 Rekursive S .turen 133 berechnet werden, da die Addition zweier Zeiger nicht erlaubt ist. Subtraktion ist jedoch erlaubt, high - low ist die Anzahl der Elemente, und durch mid » low + (high-low) / 2 wird mid ein Zeiger auf das Element, das in der Mitte zwischen low und high liegt. Am wichtigsten aber ist, daß der Algorithmus geändert wird, um sicherzustellen, daß kein ungültiger Zeiger erzeugt wird, und daß nicht versucht wird, auf ein Element außerhalb des Vektors zuzugreifen. Das Problem besteht darin, daß &tab[-l] und &tab[n] beide außerhalb der Grenzen des Vektors tab liegen. Das erste Verweis ist defi- definitiv nicht erlaubt, und mit dem zweiten Zeigerwert darf nicht auf ein Element zugegrif- zugegriffen werden. Die Sprachdefinition garantiert jedoch, daß Zeigerarithmetik korrekt arbei- arbeitet, auch wenn sie sich auf das erste Element nach dem Ende eines Vektors (also &tab[n]) stützt. In main schrieben wir for (p • keytab; p < Iceytab + NKEYS; p*+) Ist p ein Zeiger auf eine Struktur, dann wird bei jeder Arithmetik mit p die Größe der Struktur berücksichtigt; p++ inkrementiert daher p mit einem entsprechenden Wert, da- damit p dann auf das nächste Element im Vektor von Strukturen zeigt, und der Vergleich hält die Schleife rechtzeitig an. Nehmen Sie jedoch nicht an, daß die Größe einer Struktur die Summe der Größe der Komponenten ist. Je nachdem, wie verschiedene Objekte ausgerichtet sein müssen, kann die Struktur namenlose „Löcher" enthalten. Wenn zum Beispiel ein char ein Byte belegt und ein int-Wert vier Bytes, dann kann die Struktur struct { char c; int i; >; durchaus acht Bytes benötigen, und nicht nur fünf. Der Operator sizeof liefert den richti- richtigen Wert. Zum Schluß noch eine Bemerkung zum Programm form at. Liefert eine Funktion einen komplizierten Resultattyp, wie zum Beispiel einen Zeiger auf eine Struktur, struct key *binsearch(char *word, struct key "tab, int n) dann kann der Funktionsname schwer zu sehen oder mit einem Texteditor aufzufinden sein. Deshalb wird oft folgende andere Schreibweise benutzt: struct key * binsearchCchar *word, struct key *tab, int n) Dies ist eine Frage des persönlichen Programmierstils; wählen Sie das Format, das Ihnen gefällt, und behalten Sie es bei. 6.5 Rekursive Strukturen Nehmen wir an, daß wir das allgemeinere Problem lösen wollen, die Häufigkeit aller Wörter in einem Text zu zählen. Da die Liste der Wörter von vornherein nicht be- bekannt ist, können wir sie nicht einfach sortieren und binäre Suche verwenden. Andrer- Andrerseits können wir auch nicht jedes eingegebene Wort linear suchen, um zu sehen, ob das
134 6 Strukturen Wort bereits früher aufgetreten ist; das Programm würde zu lange laufen. (Präziser ge- gesagt, die Laufzeit des Programms würde wahrscheinlich quadratisch mit der Anzahl der Eingabewörter wachsen.) Wie können wir die Daten repräsentieren, um effizient mit ei- einer Liste beliebiger Wörter fertig zu werden? Eine mögliche Lösung ist, die bereits eingegebenen Wörter grundsätzlich sortiert aufzubewahren, indem man jedes Wort unmittelbar nach der Eingabe an die richtige Stelle speichert. Dazu sollte man allerdings nicht Wörter in einem linearen Vektor ver- verschieben - auch dies benötigt zuviel Zeit. Wir werden statt dessen eine Datenstruktur verwenden, die man einen binären Baum nennt. Der Baum enthält einen „Knoten" für jedes unterschiedliche Wort; ein Knoten enthält folgende Information: einen Zeiger auf den Text des Worts einen Zähler für die Häufigkeit einen Zeiger auf den linken Nachkommen einen Zeiger auf den rechten Nachkommen Ein Knoten kann dabei nicht mehr als zwei Nachkommen besitzen; ein oder gar kein Nachkomme kann allerdings auftreten. Die Knoten werden so angeordnet, daß bei jedem Knoten der Unke Unterbaum nur Wörter enthält, die alphabetisch vor dem Wort im Knoten stehen, und der rechte Unterbaum nur die „größeren" Wörter. Für den Satz „now is the time for all good men to come to the aid of their party" entsteht folgender Baum, wenn man die Wörter in Rei- Reihenfolge der Eingabe einträgt: now / \ is the / \ / \ for men of time / \ \ / \ all good party their to / \ aid come Um festzustellen, ob sich ein neues Wort bereits im Baum befindet, beginnt man an der Wurzel und vergleicht das neue Wort mit dem Wort, das im Wurzelknoten gespeichert ist. Handelt es sich um das gleiche Wort, ist die Frage bereits positiv beantwortet. Ist das neue Wort kleiner als das Wort im Baum, wird die Suche mit dem Unken Nachkom- Nachkommen fortgesetzt; andernfalls wird beim rechten Nachkommen weiter untersucht. Gibt es in der gewünschten Richtung keine Nachkommen, befindet sich das neue Wort nicht im Baum, und tatsächlich ist die unbesetzte Position die richtige Stelle, um das neue Wort als Nachkomme einzutragen. Der Suchvorgang ist rekursiv, da die Suche von einem Knoten aus eine Suche von einem der Nachkommen aus verwendet. Rekursive Funktio- Funktionen sind deshalb für Einfügen und Ausgeben wohl am natürlichsten. Zurück zur Beschreibung eines Knotens; er wird am einfachsten als Struktur mit vier Komponenten repräsentiert:
6.5 Rekursive Struk n 135 struct tnode { /* der Knoten eines Baumes: •/ char *word; /• zeigt auf den Text •/ int count; /• Haeufigkeit */ struct tnode »left; /* linker Nachkomme */ struct tnode *right; /• rechter Nachkomme •/ >; Diese „rekursive" Vereinbarung eines Knotens sieht vielleicht riskant aus, sie ist aber korrekt. Eine Struktur darf sich zwar selbst nicht enthalten, aber struct tnode *left; vereinbart left als Zeiger auf einen Knoten tnode, nicht als Knoten tnode selbst. Manchmal braucht man eine andere Art von rekursiven Strukturen: zwei Struktu- Strukturen, die gegenseitig aufeinander verweisen. Dies kann folgendermaßen erreicht werden: struct t { struct s *p; /• p zeigt auf ein s •/ >; struct s ( struct t *q; /• q zeigt auf ein t •/ >; Das Programm selbst ist überraschend klein, wenn wir eine Handvoll Hilfsfunktio- nen wie getword benutzen, die wir bereits früher geschrieben haben. Das Hauptpro- Hauptprogramm liest die Wörter mit getword und fügt sie in den Baum mit addtree ein. «include <stdio.h> #ine lüde <ctype.h> #ine lüde <string.h> «define MAXWORD 100 struct tnode *addtree(struct tnode *, char •); void treeprintCstruct tnode •); int getword<char *, int); /* Haeufigkeit von Worten zaehlen */ main() { struct tnode *root; char word[MAXWORD]; root = NULL; while CgetwordCword, HAXWORD) 1= EOF) if (isalpha{word[0}>> root = addtree(root, word); t reepr i nt(root); return 0; > addtree ist rekursiv, main präsentiert ein Wort an der obersten Ebene (der Wur- Wurzel) des Baumes. Auf jeder Ebene wird dieses Wort mit dem Wort verglichen, das im Knoten bereits gespeichert ist, und wird mit einem rekursiven Aufruf von addtree entwe- entweder zum Unken oder zum rechten Unterbaum durchgereicht. Schließlich wird das Wort entweder im Baum gefunden (dann wird count inkrementiert) oder wir finden einen Nullzeiger, wo dann ein Knoten erzeugt und zum Baum hinzugefügt werden muß. Wird
136 6 Strukturen ein neuer Knoten erzeugt, dann liefert addtree einen Zeiger auf diesen Knoten, der dann beim vorhergehenden Knoten eingefügt wird. struct tnode *talloc(void); char *strdup(char *); /* addtree: einen Knoten mit w bei oder nech p einfuegen */ struct tnode *addtree(struct tnode *p, char *w) { int cond; if (p =» NULL) { /* ein neues Wort */ p ■ tallocO; /* neuen Knoten erzeugen •/ p->word « strdup(w); p->count » 1; p->left « p->right » NULL; > else if ((cond « strcmp(w, p->word)) « 0) p->count++; /* Uort ist schon vorgekommen •/ else if (cond < 0) /* kleiner: links darunter */ p->left = addtree(p->left, w); else /* groesser: rechts darunter */ p->right « addtree(p->right, w); return p; > Eine Funktion talioc beschafft Speicherplatz für den neuen Knoten. Sie liefert einen Zeiger auf einen freien Speicherbereich, der geeignet ist, einen Knoten aufzuneh- aufzunehmen. Das neue Wort wird mit Hilfe von strdup an eine verborgene Stelle kopiert. (Wir werden diese Funktionen in Kürze noch besprechen.) Der Häufigkeitszähler wird initia- initialisiert, und die zwei Zeiger auf die Nachkommen werden auf NULL gesetzt. Dieser Teil der addtree-Funktion wird nur an den Blättern des Baumes ausgeführt, wenn ein neuer Knoten hinzugefügt wird. Wir haben (unvorsichtigerweise) auf Fehlerbehandlung bei entsprechenden Resultatwerten von strdup und talioc verzichtet. treeprint gibt den Baum sortiert aus; bei jedem Knoten wird zunächst der Unke Unterbaum ausgegeben (alle Wörter, die dem momentanen Wort vorausgehen), dann das Wort selbst, und dann der rechte Unterbaum (alle Wörter, die noch nachfolgen). Macht Rekursion Sie unsicher, dann sollten Sie nachvollziehen wie treeprint den oben gezeigten Baum abarbeitet. /• treeprint: Baum p sortiert ausgeben (Inorder) */ void treeprint(struct tnode *p) { if (p 1 = NULL) { treeprint(p->left); printf("%4d Xs\n", p->count, p->word); treeprint(p->right); Ein praktischer Hinweis: Wird der Baum „unbalanciert", weil die Wörter nicht durcheinander ankommen, dann kann die Laufzeit des Programms zu schnell wachsen. Im schlimmsten Fall sind die Wörter bereits sortiert, und dann ist dieses Programm die aufwendige Simulation einer linearen Suche. Es gibt Verallgemeinerungen von binären Bäumen, die nicht derartig in eine lineare Liste ausarten, aber wir wollen diese Bäume hier nicht beschreiben.
6.5 Rekursive Stru en 137 Bevor wir dieses Beispiel verlassen, wollen wir noch kurz abschweifen und ein Pro- Problem diskutieren, das bei Speicherverwaltung auftritt. Es ist sicher wünschenswert, daß es in einem Programm nur eine Routine zur Speicherverwaltung gibt, auch, wenn diese Routine Speicherbereiche für verschiedenartige Objekte liefern soll. Wenn aber die glei- gleiche Funktion etwa Zeiger auf char und Zeiger auf struct tnode liefern soll, ergeben sich zwei Fragen. Erstens: Wie erfüllt die Funktion die Bedingung, daß bei den meisten Ma- Maschinen Objekte mit bestimmten Datentypen auch geeignet ausgerichtet sein müssen (beispielsweise müssen ganzzahlige Werte sehr oft geradzahlige Adressen besitzen)? Zweitens: Welche Vereinbarungen lösen das Problem, daß eine Funktion zur Speicher- Speicherverwaltung zwangsweise verschiedenartige Zeiger als Resultat liefert? Bedingungen zur Ausrichtung kann man im allgemeinen leicht erfüllen, wenn man eine gewisse Platzverschwendung in Kauf nimmt: Man sorgt dafür, daß die Funktion zur Speicherverwaltung grundsätzlich einen Zeigerwert liefert, der allen Anforderungen an Ausrichtung genügt. Die Funktion alloc aus Kapitel 5 garantiert keine bestimmte Aus- Ausrichtung; wir verwenden deshalb die Funktion malloc aus der Standard-Bibliothek, die das tut. In Kapitel 8 werden wir eine Implementierung von malloc vorführen. Die Frage der Typdeklaration für eine Funktion wie malloc ist bei jeder Sprache diffizil, die ihre Typüberprüfung ernst nimmt. In C vereinbart man korrekterweise, daß malloc einen Zeiger auf void liefert; dieser Zeiger wird dann jeweils explizit mit einer Umwandlungsoperation in den gewünschten Typ umgewandelt, malloc und verwandte Routinen sind in der Standard-Definitionsdatei < stdlib.h > vereinbart, talloc kann des- deshalb so geschrieben werden: #ine lüde <stdlib.h> /* talloc: tnode erzeugen •/ struct tnode *talloc(void) (. return (struct tnode *) mallocCsizeof(struct tnode)); > strdup kopiert einfach die als Argument übergebene Zeichenkette an eine sichere Stelle, die ein Aufruf von malloc liefert: char *strdup(char *s) /* Duplikat von s erzeugen •/ { char *p; p « (char *) malloc(strlen(s)+1); /* +1 fuer '\0' */ if (p 1 = NULL) strcpy(p, s); return p; > malloc liefert NULL, wenn kein Speicherplatz verfügbar ist; strdup gibt diesen Wert wei- weiter und überläßt seinem Aufrufer die Fehlerbehandlung. Speicherplatz, den man von malloc erhält, kann man durch Aufruf von free zur Wiederverwendung freigeben; siehe Kapitel 7 und 8. Aufgabe 6-2. Schreiben Sie ein Programm, das ein C-Programm liest und in alphabeti- alphabetischer Reihenfolge jede Gruppe von Variablennamen ausgibt, die sich erst nach den er- ersten 6 Zeichen unterscheiden. Berücksichtigen Sie Wörter in Zeichenketten und Kom-
138 6 Strukturen mentaren nicht. Machen Sie 6 zu einem Parameter, der von der Kommandozeile aus ge- gesetzt werden kann. D Aufgabe 6-3. Schreiben Sie ein Referenzlistenprogramm, das eine Liste aller Wörter in einem Text ausgibt und bei jedem Wort zusätzlich die Nummern der Zeilen, in denen dieses Wort auftritt. Entfernen Sie Füllwörter wie „der", „und", „die", „oder", „das" und so weiter. D Aufgabe 6-4. Schreiben Sie ein Programm, das die verschiedenen Wörter aus der Ein- Eingabe in abnehmender Häufigkeit ihres Auftretens ausgibt. Geben Sie vor jedem Wort seine Anzahl aus. D 6.6 Suchen in Tabellen In. diesem Abschnitt werden wir die wesentlichen Funktionen zur Tabellensuche konstruieren, um weitere Aspekte von Strukturen zu illustrieren. Die Funktionen sind ty- typisch dafür, was man als Symboltabellen-Management in einem Makroprozessor oder Übersetzer findet. Betrachten wir etwa die #define-Anweisung. Sieht der Preprozessor eine Zeile wie «define IN 1 dann müssen der Name IN und der Ersatztext 1 in einer Tabelle gespeichert werden. Er- Erscheint dann später der Name IN in einer Anweisung wie state = IN; so muß er durch den Text 1 ersetzt werden. Es gibt zwei Funktionen, die die Namen und Ersatztexte verwalten. install(s,t) trägt den Namen s und den Ersatztext t in eine Tabelle ein; s und t sind einfach Zeichen- Zeichenketten, lookup(s) sucht die Zeichenkette s in der Tabelle und liefert einen Zeiger auf den entsprechenden Tabelleneintrag oder NULL, wenn s nicht vorhanden ist. Als Algorithmus wird eine Hash -Technik verwendet - der betroffene Name wird in eine kleine natürliche Zahl verwandelt, die dann als Index in einen Zeigervektor ver- verwendet wird. Jedes Vektorelement zeigt auf den Anfang einer verketteten Liste von Da- Datenblöcken, die alle Namen beschreiben, die denselben //as/i-Wert besitzen. Ein Vektor- Vektorelement ist NULL, wenn noch keine Namen diesen //as/i-Wert ergaben. name defn Ein Datenblock in der Liste ist eine Struktur, die Zeiger auf den Namen, den Er- Ersatztext und den nächsten Block in der Liste beinhaltet. Ein NULL-Zeiger markiert dabei das Ende der Kette. •— 0 0 •— 0 , > •— •— •— 0 •— 0 •— —»• name —*■ defn
6.6 Suchen in T Jen 139 struct nlist { /* Tabelleneintrag: */ struct nlist »next; /• naechster Eintrag in der verketteten Liste •/ char *name; /* definierter Name */ char *defn; /• Ersatztext */ >; Der Zeigervektor ist einfach «define HASHSIZE 101 static struct nlist *hashtab[HASHSIZE]; /* Zeigertabelle */ Die Hash -Funktion, die von lookup und install benutzt wird, addiert jedes Zeichen im Namen zu einer verwürfelten Kombination der vorhergehenden und liefert dann den Rest nach Division durch die Anzahl der Vektorelemente. Dies ist zwar nicht die best- bestmögliche Hash -Funktion, dafür aber kurz und effektiv. /* hash: Hash-Uert fuer Zeichenkette s liefern */ unsigned hash(char *s) { unsigned hashval; for (hashval ■ 0; «s I» '\0'; s++) hashval « *s + 31 * hashval; return hashval X HASHSIZE; > Die vorzeichenlose Arithmetik stellt sicher, daß der //asÄ-Wert nicht negativ ist. hash liefert einen Index für den Vektor hashtab; ist die gesuchte Zeichenkette überhaupt vorhanden, dann befindet sie sich in der Liste von Datenblöcken, die in die- diesem Element beginnt. Die Suche erfolgt in der Funktion lookup. Findet lookup den ge- gesuchten Eintrag, wird ein entsprechender Zeiger als Resultatwert geliefert; gibt es den Eintrag nicht, so ist das Resultat NULL. /* lookup: s in hashtab suchen */ struct nlist *lookup(char *s) t struct nlist *np; for (np * hashtabChash(s)]; np 1= NULL; np = np->next) if (strcmp(s, np->name) «» 0) return np; /• gefunden */ return NULL; /* nicht gefunden •/ > Die for-Schleife in lookup ist die Standard-Formulierung zum Durchlaufen einer verket- verketteten Liste: for (ptr ■ head; ptr 1= NULL; ptr = ptr->next) install benutzt lookup, um zu entscheiden, ob der neue Name bereits vorhanden ist; wenn ja, ersetzt die neue Definition die alte. Andernfalls wird ein neuer Eintrag kon- konstruiert, install liefert NULL, falls etwa kein Platz für einen neuen Eintrag vorhanden ist.
140 6 Strukturen struct nlist *lookup(char *); char *strdup(char *); /* install: (name, defn) in hashtab eintragen */ struct nlist *install(char "name, char *defn) { struct nlist *np; unsigned hashval; if (<np » lookup(name)) « NULL) { /* nicht da •/ np * (struct nlist *) malloc(sizeof(*np)); if (np =■= NULL || (np->name = strdup(name)) =» NULL) return NULL; hashval * hash(name); np->next • hashtabChashval]; hashtabChashval] » np; > else /* bereits vorhanden */ free((void *) np->defn); /* alte defn auflassen */ if ((np->defn • strdup(defn)) «« NULL) return NULL; return np; > Aufgabe 6-5. Schreiben Sie eine Funktion undef, die einen Namen und eine Definition aus der Tabelle entfernt, die lookup und install unterhalten. D Aufgabe 6-6. Implementieren Sie einen einfachen #define-Prozessor (das heißt, ohne Argumente), der für C-Programme geeignet ist, unter Verwendung der Funktionen aus diesem Abschnitt. Vielleicht finden Sie auch getch und ungetch nützlich. D 6.7 typedef Neue Typnamen können in C mit Hilfe von typedef vereinbart werden. Beispiels- Beispielsweise wird durch die Vereinbarung typedef int Length; der Name Length synonym zu int. Der Typ Length kann bei Vereinbarungen, Umwand- Umwandlungsoperationen usw. genauso verwendet werden wie der Typ int: Length len, maxien; Length »lengths C]; Entsprechend vereinbart typedef char »String; String als Synonym für char *, also als einen Zeiger auf Zeichen, der dann in Vereinba- Vereinbarungen und Umwandlungsoperationen benutzt werden kann: String p, I ineptr[MAXLINES], alloc(int); int strcmp(String, String); p = (String) malloc(IOO); Man beachte, daß der in typedef vereinbarte Typname in der Position eines Varia- Variablennamens auftritt und nicht unmittelbar nach dem Wort typedef. Syntaktisch verwendet man typedef wie eine Speicherklasse, also wie extern, static etc. Wir haben Namen bei typedef großgeschrieben, um sie hervorzuheben. Als komplizierteres Beispiel könnten wir einen Knoten für den früher in diesem Kapitel beschriebenen binären Baum mit typedef vereinbaren:
6.8 Unionen 141 typedef struct tnode »Treeptr; typedef struct tnode { /* der Knoten eines Baumes: */ char «word; /* zeigt auf den Text •/ int count; /* Haeufigkeit •/ Treeptr left; /• linker Nachkomme •/ Treeptr right; /• rechter Nachkomme */ > Treenode; Hier werden zwei neue Typnamen eingeführt, nämlich TVeenode (eine Struktur) und TVeeptr (ein Zeiger auf die Struktur), talloc könnte dann folgendermaßen formuliert werden: Treeptr talloc(void) t return (Treeptr) maltocCsizeof(Treenode)>; > Wir müssen betonen, daß eine typedef-Vereinbarung in keiner Hinsicht einen neu- neuen Datentyp konstruiert; es wird lediglich ein zusätzlicher Name für einen existenten Typ eingeführt. Ebenso gibt es keine neue Semantik: Variablen, die mit Hilfe von typedef- Typen vereinbart werden, haben genau die gleichen Eigenschaften wie Variablen, deren Vereinbarungen explizit formuliert wurden, typedef verhält sich effektiv wie #define, ab- abgesehen davon, daß typedef vom Übersetzer selbst verarbeitet wird und folglich Texter- Textersatz vornehmen kann, der die Möglichkeiten des C-Preprozessors übersteigt. Zum Bei- Beispiel erzeugt typedef int («PFIXchar «, char •); den Datentyp PFI: „Zeiger auf eine Funktion (mit zwei char • Argumenten), die ein int- Resultat liefert". PFI kann dann beispielsweise im Sortierprogramm von Kapitel 5 fol- folgendermaßen benutzt werden: PFI strcmp, numcmp; Neben rein ästhetischen Gründen wird typedef hauptsächlich aus zwei Gründen be- benutzt. Einmal kann dadurch ein Programm gegen Portabilitätsprobleme parametrisiert werden. Führt man Datentypen, die maschinenabhängig sein könnten, mit Hilfe von typedef ein, dann muß man nur die typedef-Vereinbarungen ändern, wenn man das Pro- Programm auf eine andere Maschine überträgt. Häufig vereinbart man mit typedef Namen für verschiedene ganzzahlige Wertebereiche und wählt dann je nach Zielmaschine short, int und long dafür aus. Typdefinitionen wie size t und ptrdiff_t aus der Standard-Biblio- Standard-Bibliothek sind Beispiele. Zum zweiten kann man mit typedef ein Programm besser dokumentieren - einen Typ TVreeptr kann man vielleicht leichter verstehen als einen Typ, der nur als Zeiger auf eine komplizierte Struktur vereinbart ist. 6.8 Unionen Eine Union ist eine Variable, die (zu verschiedenen Zeitpunkten) Objekte mit ver- verschiedenen Datentypen und Größen enthält, wobei der Übersetzer die nötige Größe und Ausrichtung des Speicherbereichs überwacht. Mit Unionen können verschiedene Arten von Datenobjekten in einem einzigen Speicherbereich manipuliert werden, ohne daß da- dadurch maschinenabhängige Information in das Programm eingeht. Die Datenstruktur entspricht dem case-varianten record von Pascal.
Betrachten wir ein Beispiel, das man vielleicht in der Symboltabellen-Verwaltung eines Übersetzers findet: Nehmen wir an, daß Konstanten int-Werte, float-Werte oder auch Zeiger auf Zeichen sein können. Der Wert einer bestimmten Konstanten muß in einer Variablen von geeignetem Typ abgespeichert werden. Am bequemsten für die Ver- Verwaltung von Tabellen ist es jedoch, wenn der Wert unabhängig von seinem Typ jeweils gleichviel Speicher benötigt und auch im gleichen Speicherbereich abgelegt wird. Darin liegt der Sinn einer Union - eine einzelne Variable, die legitim erweise einen beliebigen von mehreren Typen aufnehmen kann. Die Syntax beruht auf Strukturen: union u_tag { int ival; float fval; char *sval; > u; Die Variable u ist groß genug, um den größten der drei Datentypen aufzunehmen, die Größe des benötigten Speicherplatzes ist implementierungsabhängig. Ein Wert aus jedem der drei Typen kann an u zugewiesen und dann in Ausdrücken benutzt werden, je- jedenfalls solange die Benutzung konsistent ist: der Datentyp, der entnommen wird, muß der Typ sein, der als letzter zuvor gespeichert wurde. Der Programmierer muß verfol- verfolgen, welcher Typ jeweils in einer Union gespeichert ist; die Resultate sind implementie- implementierungsabhängig, wenn ein Objekt mit einem Datentyp abgespeichert und mit einem ande- anderen Datentyp wieder entnommen wird. Syntaktisch werden, analog zu den Komponenten von Strukturen, die Alternativen einer Union folgendermaßen ausgewählt: Union - Variablenname .Alternative oder Union - Zeiger—> Alternative Benutzt man die Variable utype, um festzuhalten, welcher Datentyp im Augenblick in u abgespeichert ist, dann könnte man etwa folgenden Programmtext finden: if (utype =>= INT) printf(fd\n", u.ival); else if (utype « FLOAT) printf(»Xf\n", u.fval); else if (utype == STRING) printfC'XsXn", u.sval); eise printfC'bad type Xd in utype\n", utype); Unionen können innerhalb von Strukturen und Vektoren auftreten und umgekehrt. Eine Alternative innerhalb einer Union in einer Struktur (oder umgekehrt) wird genauso ausgewählt, wie das bei verschachtelten Strukturen geschieht. Beispielsweise wird im fol- folgenden Strukturvektor struct { char "name; int flags; int utype; union { int ival; float fval; char *sval; > u; } symtabCNSYM];
6.9 Bit-Felder 143 die Alternative ival folgendermaßen angesprochen symtabti].u.ival und für das erste Zeichen von sval gibt es zwei Möglichkeiten •symtabti].u.sval symtabm.u.sval [0] Eine Union ist praktisch eine Struktur, in der alle Teile die relative Adresse 0 besit- besitzen (bezogen auf die Basisadresse der Struktur). Die Struktur ist groß genug, um die „breiteste" Alternative aufzunehmen, und die Ausrichtung genügt allen Datentypen in der Union. Für eine Union gibt es die gleichen Operationen wie für eine Struktur: zu- zuweisen oder kopieren als Ganzes, berechnen der Adresse, Zugriff auf eine Alternative. Eine Union kann nur mit einem Wert initialisiert werden, der zum Typ der ersten Alternative paßt; die oben beschriebene Union u kann deshalb nur mit einem Integer- Wert initialisiert werden. Die in Kapitel 8 vorgeführte Speicherverwaltung zeigt, wie man mit einer Union ei- eine bestimmte Ausrichtung für eine Variable erzwingen kann. 6.9 Bit-Felder Wenn Speicherplatz sehr knapp ist, muß man vielleicht mehrere Objekte in einem einzelnen Maschinenwort zusammenfassen; ein übliches Beispiel ist eine Menge von Ein- Einzel-Bit-Werten bei Anwendungen wie der Symboltabelle eines Übersetzers. Auch bei Datenformaten, die von der Umgebung abhängen, wie etwa bei den Schnittstellen von Peripheriegeräten, ist es oft notwendig, auf Teile eines Worts zugreifen zu können. Stellen Sie sich einen Ausschnitt aus einem Übersetzer vor, bei dem eine Symbol- Symboltabelle verwaltet wird. Zu jedem Namen in einem Programm gehört bestimmte Informa- Information, etwa ob es sich um ein reserviertes Wort handelt, ob der Name global bekannt oder in der Speicherklasse static deklariert ist usw. Ganz kompakt codiert man diese Infor- Information in einer Menge von Einzel-Bit-Werten in einer Variablen vom Datentyp char oder int. Dies geschieht üblicherweise, indem man „Bit-Masken" definiert, die den relevan- relevanten Bit-Positionen entsprechen, also «define KEYWORD 01 «define EXTERNAL 02 «define STATIC 04 oder enun { KEYWORD = 01, EXTERNAL - 02, STATIC = 04 >; Die Zahlen müssen dabei Potenzen von 2 sein. Zugriff auf die Bits wird dann ein Pro- Problem, das mit den Operatoren für Bit-Manipulation gelöst werden muß, die in Kapitel 2 beschrieben wurden. Bestimmte Redewendungen sind sehr häufig. Die Zuweisung flags |= EXTERNAL | STATIC; setzt die EXTERNAL- und STATIC-Bits in flags auf Eins, flags &• -(EXTERNAL | STATIC); löscht genau diese Bits, und die Bedingung
144 6 Strukturen if ((flags & (EXTERNAL | STATIC)) == 0) ... ist genau dann erfüllt, wenn beide Bits gelöscht sind. Obgleich diese Redewendungen leicht zu behalten sind, verfügt C alternativ dazu über die Möglichkeit, Bit-Werte innerhalb eines Worts direkt zu definieren und zu ver- verwenden. Ein Bit-Feld ist eine Menge von nebeneinanderliegenden Bits innerhalb einer einzelnen implementierungsabhängigen Speichereinheit, die wir „Wort" nennen wollen. Die Syntax für Definition und Zugriff von Bit-Feldern beruht auf Strukturen. Beispiels- Beispielsweise könnten die oben gezeigten #define-Anweisungen für eine Symboltabelle durch folgende Definition von drei Bit-Feldern ersetzt werden: struct < unsigned int isjteyword : 1; unsigned int is_extern : 1; unsigned int isstatic : 1; > flags; Hier wird eine Variable flags definiert, die drei Einzel-Bit-Felder enthält. Die Zahl, die dem Doppelpunkt folgt, repräsentiert die Feldbreite in Bits. Die Bit-Felder werden unsigned int vereinbart, um sicherzustellen, daß es sich um vorzeichenlose Größen han- handelt. Der Zugriff auf einzelne Bit-Felder geschieht genau wie bei anderen Strukturkom- Strukturkomponenten: flags.is_keyword, flags.is_extern usw. Bit-Felder funktionieren wie kleine ganze Zahlen und können wie andere ganzzahlige Werte in arithmetischen Ausdrücken verwendet werden. Die vorhergehenden Beispiele könnte man etwas natürlicher so schreiben: flags.is_extern = flags.isstatic = 1; setzt die Bits, f lags.isextern =■ flags.is_static ■ 0; löscht sie und mit if (flags.isextern == 0 && flags.isstatic == 0) werden die Bits überprüft. Fast alles bei Bit-Feldern ist implementierungsabhängig. Ob ein Bit-Feld eine Wortgrenze überschreiten kann, hängt von der Implementierung ab. Bit-Felder müssen nicht unbedingt benannt werden; für Zwischenräume können unbenannte Bit-Felder ver- verwendet werden, die dann nur aus Doppelpunkt und Angabe zur Breite bestehen. Mit der besonderen Breite 0 kann man die Ausrichtung auf die nächste Wortgrenze verlangen. Bit-Felder werden bei manchen Maschinen von links nach rechts und bei anderen von rechts nach links angeordnet. Bit-Felder sind deshalb zwar nützlich, um intern defi- definierte Datenstrukturen zu repräsentieren; bildet man jedoch extern definierte Objekte ab, so muß man sorgfältig überlegen, in welcher Reihenfolge die Bit-Felder angelegt wer- werden. Programme, die von solchen Dingen abhängen, sind nicht portabel. Bit-Felder dür- dürfen auch nur als int vereinbart werden; zur Portabilität sollte jedoch explizit signed oder unsigned angegeben werden. Es gibt keine Vektoren von Bit-Feldern und Bit-Felder ha- haben keine Adressen, so daß der Adreß-Operator & auf sie nicht angewendet werden kann.
7 Eingabe-und Ausgabe Eingabe und Ausgabe sind nicht als Teil der Sprache C selbst definiert, und wir ha- haben dies deshalb bisher weitgehend ausgeklammert. Nichtsdestoweniger reagieren Pro- Programme mit ihrer Umgebung, und zwar wesentlich komplizierter, als wir dies bisher ge- gezeigt haben. Im vorliegenden Kapitel beschreiben wir die Standard-Bibliothek. Sie be- besteht aus einem Satz von Punktionen für Eingabe und Ausgabe, zum Umgang mit Zei- Zeichenketten und zur Speicherverwaltung. Außerdem enthält sie mathematische Routinen und viele andere Hilfsfunktionen für C-Programme. Wir werden uns auf die Beschrei- Beschreibung von Eingabe und Ausgabe konzentrieren. Der ANSI-Standard definiert diese Funktionen sehr exakt, so daß sie in kompatibler Form auf jedem System, auf dem C zur Verfügung steht, existieren können. Programme, die ihr Zusammenspiel mit dem umgebenden System auf die Funktionalität beschränken, die von der Standard-Bibliothek angeboten wird, können unverändert von einem System auf ein anderes gebracht werden. Die Eigenschaften der Bibliotheksfunktionen sind in mehr als einem Dutzend De- Definitionsdateien beschrieben; wir haben bereits einige davon gesehen, darunter <stdio.h>, <string.h> und <ctype.h>. Wir werden hier nicht die gesamte Bibliothek vorstellen, da wir mehr am Schreiben von C-Programmen interessiert sind, die sie benut- benutzen. Die Bibliothek wird im Detail im Anhang B beschrieben. 7.1 Standard-Eingabe und Standard-Ausgabe Wie wir bereits in Kapitel 1 erwähnten, implementiert die Bibliothek ein einfaches Modell für Texteingabe und Textausgabe. Ein Textstrom ist eine Folge von Zeilen; jede Zeile endet mit einem Zeilentrenner. Wenn das System selbst nicht so arbeitet, sorgt die Bibliothek dafür, daß es wenigstens so scheint. Zum Beispiel kann die Bibliothek die Zeichenfolge Wagenrücklauf und Zeilenvorschub in einen Zeilentrenner bei der Eingabe abbilden, und dies bei der Ausgabe umkehren. Der einfachste Eingabemechanismus besteht darin, ein einzelnes Zeichen von der Standard-Eingabe, normalerweise von der Tastatur, mit Hilfe von getchar zu lesen: int getchar(void) getchar liefert bei jedem Aufruf das nächste Eingabezeichen oder den Wert EOF am Da- Dateiende. Die symbolische Konstante EOF ist in <stdio.h> definiert. EOF hat typischer- typischerweise den Wert -1, aber Bedingungen sollten mit EOF formuliert werden, damit die Pro- Programmtexte unabhängig vom spezifischen Wert bleiben. Bei vielen Systemen kann man eine Datei anstelle der Tastatur angeben, wenn man die Eingabe mit < umlenkt: Wenn ein Programm prog die Routine getchar benutzt, dann sorgt die Kommandozeile prog <infile dafür, daß prog statt dessen Zeichen aus der Datei infile liest. Die Eingabe wird dabei so umgeschaltet, daß prog selbst die Änderung nicht bemerkt; insbesondere ist die Zei- Zeichenkette „< infile" nicht Teil der Kommandoargumente in argv. Die Umschaltung der Eingabe ist ebenfalls unsichtbar, wenn die Eingabe von einem anderen Programm über eine Pipe angeliefert wird: bei einigen Systemen führt die Kommandozeile
146 '_ ingabe und Ausgabe otherprog | prog zwei Programme otherprog und prog aus und lenkt die Standard-Ausgabe von otherprog zur Standard-Eingabe für prog. Die Funktion int putchar(int) wird zur Ausgabe benutzt: putchar(c) gibt das Zeichen c zur Standard-Ausgabe aus, nach Voreinstellung ist das der Bildschirm, putchar liefert als Resultatwert das ausgege- ausgegebene Zeichen oder EOF, wenn ein Fehler auftritt. Auch die Ausgabe kann normalerwei- normalerweise in eine Datei gelenkt werden, und zwar mit >dateiname: wenn das Programm prog die Funktion putchar benutzt, sorgt prog >outfile dafür, daß die Standard-Ausgabe in der Datei outfile abgelegt wird. Gibt es Pipes, dann lenkt prog | anotherprog die Standard-Ausgabe von prog zur Standard-Eingabe von anotherprog. Eine Ausgabe, die mit printf erzeugt wird, taucht ebenfalls als Standard-Ausgabe auf. Aufrufe von putchar und printf können beliebig durcheinander erfolgen - die Aus- Ausgabe erscheint in Reihenfolge der Aufrufe. In jeder Quelldatei, in der Eingabe- oder Ausgabefunktionen aus der Bibliothek verwendet werden, muß die Zeile «include <stdio.h> vor dem ersten Aufruf stehen. Wenn der Name in spitzen Klammern < > eingeschlossen ist, wird die Definitionsdatei an Standardstellen gesucht (bei UNIX-Systemen zum Bei- Beispiel typischerweise im Katalog /usr/include). Viele Programme lesen nur einen einzigen Eingabestrom und schreiben nur einen einzigen Ausgabestrom; für solche Programme kann Eingabe und Ausgabe mit Hilfe von getchar, putchar und printf völlig ausreichen, und diese Funktionen genügen sicherlich für den Anfang. Dies gilt ganz besonders, wenn Standard-Eingabe und Standard-Ausga- Standard-Ausgabe umgelenkt werden, um die Ausgabe von einem Programm mit der Eingabe des näch- nächsten zu verbinden. Betrachten Sie zum Beispiel das Programm lower, das seine Eingabe in Kleinbuchstaben umwandelt: #ine lüde <stdio.h> #include <ctype.h> mainO /* lower: Eingabe in Kleinbuchstaben umwandeln */ { int c; while <(c = getcharO) ! = EOF) putchar(tolower(c)); return 0; > Die Funktion tolower ist in <ctype.h> definiert; sie verwandelt einen Großbuch- Großbuchstaben in einen Kleinbuchstaben,* und liefert die anderen Zeichen unverändert. Wie be- • allerdings nicht die Umlaute Ä, Ö und Ü. A.d.Ü.
7.2 Formatierte i jabe - print/ 147 reits früher erwähnt, sind „Funktionen" wie getchar und putchar in <stdio.h> und tolower in <ctype.h> oft Makros, um den Aufwand eines Funktionsaufrufs für jedes ein- einzelne Zeichen zu vermeiden. In Abschnitt 8.5 werden wir vorführen, wie man diese Ma- Makros definiert. Unabhängig davon, wie die Funktionen von <ctype.h> für eine bestimm- bestimmte Maschine implementiert sind, müssen Programme, die sie benutzen, den Zeichensatz selbst nicht kennen. Aufgabe 7-1. Schreiben Sie ein Programm, das Großbuchstaben in Kleinbuchstaben oder Kleinbuchstaben in Großbuchstaben umwandelt, und zwar abhängig vom Pro- Programmnamen, mit dem es aufgerufen wird, und der sich in argv[0] befindet. G 12 Formatierte Ausgabe - printf Die Ausgabefunktion printf übersetzt interne Werte in Zeichenfolgen. Wir haben printf ohne formale Beschreibung in den vorhergehenden Kapiteln benutzt. Die Er- Erklärung hier beschreibt die meisten typischen Anwendungen, aber sie ist nicht vollstän- vollständig; die vollständige Beschreibung finden Sie im Anhang B. int printf(char »format, arg\, argi, ...) Unter Kontrolle der Zeichenkette format wandelt die Funktion printf ihre Argumente um, formatiert sie und gibt sie als Standard-Ausgabe aus. Als Resultat liefert printf die Anzahl der ausgegebenen Zeichen. Die Format-Zeichenkette enthält zwei Arten von Objekten: Gewöhnliche Zeichen, die in die Ausgabe kopiert werden, und Umwandlungsangaben, die jeweils die Umwand- Umwandlung und Ausgabe des nächstfolgenden Arguments von printf veranlassen. Jede Um- Umwandlungsangabe beginnt mit dem Zeichen % und endet mit einem Umwandlungszei- Umwandlungszeichen. Zwischen % und Umwandlungszeichen kann, der Reihe nach, folgendes angege- angegeben werden: • Ein Minuszeichen, damit das umgewandelte Argument nach links ausgerichtet wird. • Eine Zahl, die eine minimale Feldbreite festlegt. Das umgewandelte Argument wird in einem Feld ausgegeben, das mindestens so breit ist und bei Bedarf auch breiter. Hat das umgewandelte Argument weniger Zeichen als die Feldbreite verlangt, wird links (oder rechts, wenn Ausrichtung nach links verlangt wurde) auf die Feldbreite aufgefüllt. • Ein Punkt, der die Feldbreite von der Genauigkeit {precision) trennt. • Eine Zahl, die Genauigkeit, die die maximale Anzahl von Zeichen festlegt, die von einer Zeichenkette ausgegeben werden sollen, oder die Anzahl Ziffern, die nach dem Dezimalpunkt bei einem Gleitpunktwert ausgegeben werden, oder die minimale Anzahl von Ziffern, die bei einem ganzzahligen Wert ausgegeben werden sollen. • Der Buchstabe h, wenn short ausgegeben werden soll, oder der Buchstabe 1 wenn das Argument long ist. Die Umwandlungszeichen erklärt Tabelle 7-1. Wenn das Zeichen nach % kein Umwand- Umwandlungszeichen ist, ist der Verlauf Undefiniert.
148 7 igabe und Ausgabe Zeichen Tabelle 7-1. Elementare printf Umwandlungen Argument; Ausgabe als d, i o x,X u c s f e, E 9,G int; dezimale Zahl. int; oktale Zahl ohne Vorzeichen (ohne führende Null). int; hexadezimale Zahl ohne Vorzeichen (ohne führendes Ox oder OX), mit abcdef oder ABCDEF für 10,..., 15. int; dezimale Zahl ohne Vorzeichen int; einzelnes Zeichen. char •; aus der Zeichenkette werden Zeichen ausgegeben bis vor '\0\ oder so viele Zeichen, wie die Genauigkeit verlangt. double; \-\n.dddddd, wobei die Genauigkeit die Anzahl der d festlegt (Voreinstellung ist 6). double; [-]m.dddddde±xx oder [-]m.ddddddE±xx, wobei die Genauigkeit die Anzahl der d festlegt (Voreinstellung ist 6). double; %t oder %E wird verwendet, wenn der Exponent kleiner als - 4 oder nicht kleiner als die Genauigkeit ist; sonst wird %f benutzt. Null und Dezimalpunkt am Schluß werden nicht ausgegeben. void •; als Zeiger (Darstellung hängt von Implementierung ab). es wird kein Argument umgewandelt; ein % wird ausgegeben. Als Feldbreite oder Genauigkeit kann jeweils • angegeben werden; dann wird der Wert durch Umwandlung des nächsten Arguments festgelegt (das ein int-Wert sein muß). Zum Beispiel gibt der folgende Aufruf höchstens max Zeichen von einer Zeichen- Zeichenkette s aus: printf("X.*s», max, s); Die meisten Umwandlungen wurden in früheren Kapiteln illustriert. Eine Ausnah- Ausnahme ist die Genauigkeit bei Zeichenketten. Die folgende Tabelle zeigt den Effekt einer Reihe von Umwandlungsangaben, wenn „hello, world" A2 Zeichen) ausgegeben wird. Wir haben jedes Feld mit Doppelpunkten umgeben, damit Sie den Bereich sehen kön- können: Xs: X10s: X.10s: X-IOs: X.1Ss: X-15s: X1S.10s: X-15.10s: :hello, world: :hello, world: :hello, wor: :hello, world: :hello, world: :hello, world : hello, wor :hello, wor Eine Warnung: printf benutzt sein erstes Argument, um zu entscheiden, wieviele weitere Argumente folgen und um welche Datentypen es sich handelt, printf wird sehr verwirrt, und Sie erhalten falsche Antworten, wenn nicht genügend Argumente vorhan- vorhanden sind oder wenn die Datentypen nicht stimmen. Sie sollten sich auch den Unterschied zwischen folgenden zwei Aufrufen klarmachen: printf(s); printfC'Xs" s); /* FEHLER, wem s ein X enthaelt */ /* SICHER V
7.3 Variable Arg jntlisten 149 Die Funktion sprintf nimmt die gleichen Umwandlungen vor wie printf, aber die Ausgabe wird in einer Zeichenkette abgelegt: int sprintf(char "string, char »format, argi, argi, ...) sprintf wandelt die Argumente argi, arg2, usw. gemäß dem format wie vorher beschrie- beschrieben um, legt aber das Resultat in der Zeichenkette string ab, anstatt es als Standard- Ausgabe auszugeben; string muß groß genug für das Resultat sein. Aufgabe 7-2. Schreiben Sie ein Programm, das beliebige Eingaben sinnvoll ausgibt. Mindestens sollten nicht-druckbare Zeichen, je nach lokalen Gebräuchen, oktal oder he- hexadezimal ausgegeben werden und lange Textzeilen sollten getrennt werden. D 7.3 Variable Argumentlisten Dieser Abschnitt enthält eine Implementation einer minimalen Version von printf, um zu zeigen, wie man eine Funktion schreibt, die eine variable Argumentliste portabel bearbeitet. Da wir hauptsächlich an der Bearbeitung der Argumente interessiert sind, wird minprintf nur die kontrollierende Zeichenkette und die Argumente bearbeiten, da- danach aber die echte Funktion printf aufrufen, um die Formatumwandlungen auszu- auszuführen. Die korrekte Vereinbarung von printf ist int printf(char *fmt, ...) wobei die Deklaration ... bedeutet, daß Anzahl und Typen dieser Argumente variieren können. Die Deklaration ... darf nur am Ende einer Argumentliste auftreten. Unsere Funktion minprintf wird mit void minprintf(char *fmt, ...) vereinbart: anders als printf liefern wir die Anzahl der ausgegebenen Zeichen nicht als Resultat. Der interessante Teil besteht darin, wie minprintf die Argumentliste abarbeitet, wenn die Liste nicht einmal einen Namen hat. Die Standard-Definitionsdatei <stdarg.h> enthält einen Satz Makros, die definieren, wie man durch eine Argumentli- Argumentliste läuft. Die Implementierung dieser Definitionsdatei ist von Maschine zu Maschine verschieden, aber die Schnittstelle, die sie anbietet, ist gleich. Mit dem Typ va_iist vereinbart man eine Variable, die der Reihe nach auf jedes Argument verweist; in minprintf wird diese Variable ap genannt, als Abkürzung für argument pointer. Der Makro va_start initialisiert ap so, daß die Variable auf das erste unbenannte Argument zeigt. Dieser Makro muß einmal aufgerufen werden, bevor ap benutzt wird. Es muß mindestens einen Parameter mit Namen geben; va_start benutzt den letzten Parameternamen um anzufangen. Jeder Aufruf von va_arg liefert ein Argument und bewegt ap auf das nächste; va_acg benutzt einen Typnamen, um zu entscheiden, welcher Datentyp geliefert und wie ap fortgeschrieben wird. Zum Schluß erledigt va_end eventuell notwendige Aufräumar- Aufräumarbeiten, va end muß vor dem Verlassen der Funktion aufgerufen werden.
150 7 ngabe und Ausgabe Diese Eigenschaften bilden die Basis unserer vereinfachten Version von printf: #ine lüde <stdarg.h> /* minprintf: vereinfachtes printf mit variabler Argumentliste */ void minprintf(char »fmt, ...) < va_list ap; /* zeigt nacheinander auf jedes unbenannte Argument */ char *p, »sval; int ival; double dval; va_start(ap, fmt); /* ap zeigt auf 1. unbenanntes Argument */ for (p = fmt; *p; p++) { if <*p != 'X') { putchar(*p); continue; > switch (*++p) { case 'd1: ival = va_arg(ap, int); printfC'Xd", ival); break; case 'f: dval = va_arg(ap, double); printf("Xf", dval); break; case 's1: for (sval = va_arg(ap, char •); »sval; sval++) putchar(*sval); break; default: putchar(*p); break; vaend(ap); /* hinterher aufraeumen */ > Aufgabe 7-3. Ändern Sie minprintf, so daß noch mehr Umwandlungen von printf bear- bearbeitet werden. Q 7.4 Formatierte Eingabe - scanf Die Funktion scanf ist die zu printf analoge Eingabefunktion, die praktisch die gleichen Umwandlungen in der umgekehrten Richtung zur Verfügung stellt, int scanf(char "format, ...) ' scanf liest Zeichen aus der Standard-Eingabe, interpretiert sie unter Kontrolle von format und legt die Resultate mit Hilfe der übrigen Argumente ab. Das Formatargu- Formatargument wird nachstehend beschrieben; die anderen Argumente, die alle Zeiger sein müssen, geben an, wo das zugehörige umgewandelte Eingabefeld gespeichert werden soll. Wie bei printf ist der vorliegende Abschnitt eine Zusammenfassung der nützlichsten Möglich- Möglichkeiten, aber keine vollständige Liste.
7.4 Formatierte ' gäbe - scanf 151 scanf hört auf, wenn die Format-Zeichenkette abgearbeitet ist, oder wenn ein Ein- Eingabefeld nicht zur Umwandlungsangabe paßt. Als Funktionsresultat wird die Anzahl er- erfolgreich erkannter und zugewiesener Eingabefelder geliefert. Damit kann man ent- entscheiden, wieviele Eingaben gefunden wurden. Am Eingabeende wird EOF geliefert; man beachte, daß das von 0 verschieden ist - 0 bedeutet, daß bereits das erste Eingabe- Eingabezeichen nicht zur ersten Angabe im Format paßt. Der nächste Aufruf von scanf beginnt seine Suche unmittelbar nach dem zuletzt umgewandelten Zeichen. Es gibt auch eine Funktion sscanf, die aus einer Zeichenkette statt von der Stan- Standard-Eingabe liest: int sscanf (char "string, char "format, argx, arg^, ...) sscanf bearbeitet die Zeichenkette string nach den Umwandlungsangaben in format und speichert die Resultatwerte mit Hilfe von arg\, argi usw. Diese Argumente müssen Zei- Zeiger sein. Die Format-Zeichenkette enthält normalerweise Umwandlungsangaben, die zur Interpretation der Eingabe verwendet werden. Die Format-Zeichenkette kann folgendes enthalten: • Leerzeichen oder Tabulatorzeichen, die ignoriert werden. • Gewöhnliche Zeichen (nicht aber %), die dem nächsten Zeichen nach Zwischenraum im Eingabestrom entsprechen müssen. • Umwandlungsangaben, bestehend aus %; einem optionalen Zeichen *, das die Zuwei- Zuweisung an ein Argument verhindert; einer optionalen Zahl, die die maximale Feldbreite festlegt; einem optionalen Buchstaben h, 1 oder L, der die Länge des Ziels beschreibt; und einem Umwandlungszeichen. Eine Umwandlungsangabe bestimmt die Umwandlung des nächsten Eingabefelds. Normalerweise wird das Resultat in der Variablen abgelegt, auf die das zugehörige Argu- Argument zeigt. Wenn jedoch • die Zuweisung verhindern soll, dann wird das Eingabefeld Übergängen; eine Zuweisung findet nicht statt. Ein Eingabefeld ist als Folge von Zeichen definiert, die keine Zwischenraumzeichen sind; es reicht entweder bis zum nächsten Zwi- Zwischenraumzeichen oder bis eine explizit angegebene Feldbreite erreicht ist. Daraus folgt, daß scanf über Zeilengrenzen hinweg liest, um seine Eingabe zu finden, denn Zeilentren- Zeilentrenner sind Zwischenraumzeichen. (Zwischenraumzeichen sind Leerzeichen, Tabulatorzei- Tabulatorzeichen \t, Zeilentrenner \n, Wagenrücklauf \r, Vertikal-Tabulator \v und Seitenvorschub V). Das Umwandlungszeichen gibt die Interpretation des Eingabefelds an. Das zu- zugehörige Argument muß ein Zeiger sein, mit Rücksicht auf die Semantik von C, wonach nur Argumentwerte übergeben werden. Umwandlungszeichen zeigt die Tabelle 7-2. Den Umwandlungszeichen d, i, o, u und x sollte h vorausgehen, wenn das Argu- Argument ein Zeiger auf short statt int ist, oder der Buchstabe 1, wenn das Argument ein Zei- Zeiger auf long ist. Analog sollte vor den Umwandlungszeichen e, f und g der Buchstabe 1 stehen, wenn ein Zeiger auf double und nicht auf float in der Argumentliste steht.
152 ■ingabe und Ausgabe Zeichen Tabelle 7-2. Elementare scanf Umwandlungen Eingabedaten; Argumenttyp c, f,g x dezimal, ganzzahlig; int •. ganzzahlig; int •. Der Wert kann oktal (mit 0 am Anfang) oder hexadezimal (mit Ox oder OX am Anfang) angegeben sein. oktal ganzzahlig (mit oder ohne 0 am Anfang); int *. dezimal ohne Vorzeichen; unsigned int *. hexadezimal ganzzahlig (mit oder ohne Ox oder OX am Anfang); int *. ein oder mehrere Zeichen; char *. Die nachfolgenden Eingabezeichen werden im angegebenen Vektor abgelegt, bis die Feldbreite erreicht ist; Voreinstellung ist 1. In diesem Fall wird Zwischenraum nicht überlesen; das nächste Zeichen nach Zwischenraum liest man mit %ls. Zeichenkette (ohne Doppelanführungszeichen); char •, der auf einen Vektor zeigen muß, der die Zeichenkette und das abschließende '\0' aufnehmen kann, das angehängt wird. Gleitpunktzahl mit optionalem Vorzeichen, optionalem Dezimalpunkt und optionalem Exponenten; float*. erkennt %; eine Zuweisung findet nicht statt. Als erstes Beispiel kann der primitive Taschenrechner aus Kapitel 4 jetzt mit scanf zur Eingabe-Umwandlung formuliert werden: «include <stdio.h> mainO /* elementarer Taschenrechner */ t double sum, v; sum = 0; while (scanf("Xlf", &v) == 1) printf("\tX.2f\n", sum += v); return 0; > Angenommen, wir wollen Eingabezeilen verarbeiten, die Datumsangaben wie 25 Dec 1988 enthalten. Die scanf-Anweisung dazu ist int day, year; char monthname[20]; scanf("Äd Xs Xd», &day, roonthname, &year>; Der Adreß-Operator & wird vor monthname nicht angegeben, da ein Vektorname be- bereits ein Zeiger ist. Die Format-Zeichenkette von scanf kann gewöhnliche Zeichen enthalten; sie müs- müssen dann genauso in der Eingabe vorkommen. Datumsangaben wie mm/dd/yy könnten wir mit dem folgenden Aufruf von scanf verarbeiten: int day, month, year; scanf("Xd/Xd/Xd", fanonth, &day, &year);
7.5 Dateizugriff 153 scanf ignoriert Leerzeichen und Tabulatorzeichen in der Format-Zeichenkeite. Außerdem werden Zwischenraumzeichen (Leerzeichen, Tabulatorzeichen, Zeilentrenner usw.) in der Eingabe überlesen, wenn nach Eingabewerten gesucht wird. Um eine Ein- Eingabe zu verarbeiten, deren Format nicht exakt festgelegt ist, liest man oft am besten eine Zeile auf einmal und zerlegt sie dann mit sscanf. Wenn wir beispielsweise Zeilen verar- verarbeiten möchten, die Datumsangaben in jeder der beiden oben gezeigten Arten enthalten, dann könnten wir das folgendermaßen formulieren: while (getlineUine, sizeof(line)) > 0) { if (sscanfdine, "%d Xs %d", &day, monthname, &year) == 3) printf("valid: Xs\n", line); /* Art: 25 Dec 1988 */ else if (sscanfdine, "Xö/Xö/Xö", Smonth, &day, &year) == 3) printf("valid: Xs\n", line); /* Art: mn/dd/yy */ eise printf("invalid: Xs\n", line); /* ungueltig •/ > Aufrufe von scanf und Aufrufe anderer Eingabefunktionen können durcheinander erfolgen. Der nächste Aufruf irgendeiner Eingabefunktion beginnt damit, daß das erste Zeichen gelesen wird, das scanf noch nicht gelesen hat. Eine Warnung zum Schluß: Die Argumente von scanf und sscanf müssen Zeiger sein. Bei weitem der häufigste Fehler ist, daß man scanf("*d", n); anstelle von scanf("«d", &n); schreibt. Dieser Fehler wird im allgemeinen nicht bei der Übersetzung erkannt. Aufgabe 7-4. Schreiben Sie eine eigene Version von scanf analog zu minprintf aus dem letzten Abschnitt. D Aufgabe 7-5. Ändern Sie den Postfix-Taschenrechner aus Kapitel 4 so ab, daß scanf be- beziehungsweise sscanf für Eingabe und Umwandlung verwendet werden, a 7.5 Dateizugriff Bisher haben alle Beispiele von der Standard-Eingabe gelesen und in die Stan- Standard-Ausgabe geschrieben, die automatisch für ein Programm vom lokalen Betriebssy- Betriebssystem vordefiniert sind. Als nächsten Schritt schreiben wir ein Programm, das auf eine Datei zugreift, die noch nicht mit dem Programm verbunden ist. Ein Programm, das die Notwendigkeit sol- solcher Operationen illustriert, ist cat*, das eine Reihe von namentlich angegebenen Datei- Dateien nacheinander als Standard-Ausgabe ausgibt. Mit cat gibt man Dateien auf dem Bild- Bildschirm aus und produziert Eingabe für Programme, die selbst nicht per Name auf Datei- Dateien zugreifen können. Beispielsweise gibt das Kommando cat x.c y.c den Inhalt der Dateien x.c und^.c (und sonst nichts) als Standard-Ausgabe aus. * cat steht Tür catenate, also „verketten" A.d.Ü.
154 'ingabe und Ausgabe Die Frage ist, wie man dafür sorgt, daß die namentlich angegebenen Dateien gele- gelesen werden - das heißt, wie verbindet man die externen Namen, die der Benutzer wählt, mit den Anweisungen, die die Daten lesen. Die Regem sind einfach. Bevor eine Datei gelesen oder geschrieben werden kann, muß der Zugriff mit der Bibliotheksfunktion fopen eröffnet werden, fopen akzeptiert einen externen Namen, wiexc oder_y.c, führt Buch und verhandelt mit dem Betriebssy- Betriebssystem (wobei die Details uns nicht zu interessieren brauchen) und liefert als Funktions- Funktionswert einen Zeiger, der anschließend beim Lesen oder Schreiben der Datei verwendet wird. Dieser sogenannte FILE-Zeiger zeigt auf eine Struktur, die Information über die Datei enthält, wie zum Beispiel die Adresse eines Puffers, die aktuelle Zeichenposition im Puffer sowie Angaben, ob die Datei gelesen oder geschrieben wird, ob Fehler aufge- aufgetreten sind und ob das Dateiende erreicht wurde. Die Benutzer brauchen die Details nicht zu wissen, da zu den Vereinbarungen aus < stdio.h > auch eine Strukturdefinition für FILE gehört. Die allein nötigen Vereinbarungen für einen FILE-Zeiger sieht man an folgendem Beispiel: FILE *fp.- FILE *fopen(char »name, char *mode); Diese Vereinbarung legt fest, daß fp ein Zeiger auf FILE ist und daß fopen einen entspre- entsprechenden Zeiger liefert. Man beachte, daß FILE ein Datentypname wie int ist und nicht das Etikett einer Struktur; FILE ist mit Hilfe von typedef definiert. (Details darüber, wie fopen im UNIX-System implementiert werden kann, befinden sich im Abschnitt 8.5.) fopen wird in einem Programm folgendermaßen aufgerufen: fp = fopenCname, mode); Das erste Argument von fopen ist eine Zeichenkette, die den Namen der Datei enthält. Das zweite Argument ist die Zugriffsart, ebenfalls eine Zeichenkette, die angibt, wie man auf die Datei zugreifen will. Erlaubt sind unter anderem V für Lesen, V für Schreiben und "a" für Anfügen. Manche Systeme unterscheiden zwischen Text- und Binärdateien; bei letzteren muß "b" an mode angehängt werden. Wenn eine nicht-existente Datei zum Schreiben oder Anfügen eröffnet wird, so wird sie erzeugt, falls das möglich ist. Wird eine existente Datei zum Schreiben eröffnet, geht der alte Inhalt verloren; beim Öffnen zum Anfügen bleibt der Inhalt erhalten. Ein Versuch, eine Datei zu lesen, die nicht existiert, ist ein Fehler, und es gibt auch andere Fehlermöglichkeiten, wie zum Beispiel den Versuch, eine Datei zu lesen, wenn man dazu nicht berechtigt ist. Bei Fehlern liefert fopen den Wert NULL. (Ein Fehler kann genauer identifiziert werden; siehe dazu die Erklärungen der Funktionen zur Fehlerbehandlung im Abschnitt B.1.7 im Anhang B.)
7.5 Dateizugriff 155 Als nächstes benötigen wir eine Möglichkeit, um eine Datei zu lesen oder zu schreiben, nachdem der Zugriff eröffnet wurde. Es gibt dazu verschiedene Funktionen, am einfachsten sind getc und putc. Die Funktion getc liefert das nächste Zeichen aus ei- einer Datei; dazu benötigt die Funktion den FILE-Zeiger, um die richtige Datei anzuspre- anzusprechen. int getc(FILE *fp) getc liefert das nächste Zeichen aus dem Datenstrom, den fp bezeichnet; am Dateiende oder bei Fehler ist das Resultat EOF. putc ist eine Ausgabefunktion: int putc(int c, FILE *fp) putc gibt das Zeichen c in die Datei fp aus und liefert das ausgegebene Zeichen als Re- Resultatwert oder EOF, wenn ein Fehler auftritt. Wie getchar und putchar können auch getc und putc Makros statt Funktionen sein. Wenn die Ausführung eines C-Programms beginnt, muß die Betriebssystem-Um- Betriebssystem-Umgebung drei Dateiverbindungen eröffnen und entsprechende FILE-Zeiger zur Verfügung stellen. Diese Dateiverbindungen sind Standard-Eingabe, Standard-Ausgabe und Dia- Diagnose-Ausgabe. Die zugehörigen FILE-Zeiger heißen stdin, stdout und stderr; sie sind in <stdio.h> vereinbart. Normalerweise ist stdin mit der Tastatur verbunden, und stdout sowie stderr führen zum Bildschirm, aber stdin und stdout können in Dateien oder Pipes umgelenkt sein, wie das in Abschnitt 7.1 beschrieben wurde. getchar und putchar können mit Hilfe von getc, putc, stdin und stdout folgender- folgendermaßen definiert werden: #define getcharO getc(stdin) #define putchar(c) putc((c), stdout) Für formatierte Eingabe oder Ausgabe bei Dateien können die Funktionen fscanf und fprintf benutzt werden. Diese Funktionen werden exakt wie scanf und printf ver- verwendet, abgesehen davon, daß das erste Argument jeweils ein FILE-Zeiger ist, der die Datei bezeichnet, die gelesen oder geschrieben werden soll; die Format-Zeichenkette ist das zweite Argument. int fscanf(FILE *fp, char «format, ...) int fprintf(FILE *fp, char «format, ...) Nach diesen Vorbemerkungen können wir jetzt das Programm cat schreiben, das Dateien aneinanderreiht. Der Entwurf hat sich schon für viele Programme als nützlich erwiesen: Gibt es Argumente in der Kommandozeile, dann werden sie als Dateinamen interpretiert und der Reihe nach bearbeitet. Gibt es keine Argumente, dann wird die Standard-Eingabe bearbeitet.
156 7_ gäbe und Ausgabe #ine lüde <stdio.h> /* cat: Dateien nacheinander ausgeben, Version 1 */ maind'nt arge, char *argvü) t FILE *fp; void filecopy(FILE *, FILE •); if (arge « 1) /* kein Argument; Standardeingabe kopieren */ filecopy(stdin, stdout); eise while (—arge > 0) if ((fp = fopen(*++argv, "r")) *= NULL) { printfC'cat: can't open Xs\n", *argv); return 1; > eise { filecopy(fp, stdout); fclose(fp); > return 0; > /* filecopy: kopiere Datei ifp in Datei ofp */ void filecopy(FILE *ifp, FILE *ofp) t int c; while ((c = getc(ifp)) 1 = EOF) putc(c, ofp); > Die FILE-Zeiger stdin und stdout sind Objekte vom Datentyp FILE •. Sie sind jedoch Konstanten, keine Variablen, deshalb kann man nicht an sie zuweisen. Die Funktion int fclose(FILE *fp) ist das Gegenteil von fopen; sie löst die Verbindung zwischen einem FILE-Zeiger und dem externen Namen, die fopen eingerichtet hat; der FILE-Zeiger kann für eine andere Datei verwendet werden. Da die meisten Betriebssysteme die Anzahl Dateien begren- begrenzen, die ein Programm gleichzeitig eröffnet haben kann, ist es eine gute Idee, FILE- Zeiger freizugeben, wenn sie nicht länger benötigt werden, wie wir dies in cat getan ha- haben. Es gibt noch einen Grund für fclose bei einer Ausgabedatei - der Puffer wird aus- ausgegeben, in dem putc die Ausgabe sammelt. Geht ein Programm normal zu Ende, wird fclose am Schluß automatisch für jede offene Datei aufgerufen. (Sie können stdin und stdout freigeben, wenn sie nicht gebraucht werden. Mit der Bibliotheksfunktion freopen können sie auch mit einer anderen Datei verbunden werden.) 7.6 Fehlerbehandlung - stderr und exit Die Fehlerbehandlung in cat ist nicht ideal. Kann aus irgendeinem Grund auf eine der Dateien nicht zugegriffen werden, so wird die Fehlermeldung leider am Ende der bis- bisherigen Ausgabe angefügt. Dies könnte akzeptabel sein, wenn die Ausgabe zum Bild- Bildschirm geht, jedoch nicht, wenn sie in eine Datei oder per Pipe zu einem anderen Pro- Programm geht.
7.6 Fehlerbehand i - stderr und exit 157 Um dieser Situation besser begegnen zu können, erhält jedes Programm einen zweiten Ausgabestrom für Diagnosen namens stderr, zusätzlich zu stdin und stdout, vom Betriebssystem zugewiesen. Ausgabe für stderr erscheint normalerweise auch dann am Bildschirm, wenn die Standard-Ausgabe umgelenkt wurde. Ändern wir cat, so daß die Fehlermeldungen als Diagnose-Ausgabe erscheinen: #includ« <stdio.h> /* cat: Dateien necheinander ausgeben, Version 2 */ mainCint arge, char *argvü) t FILE *fp; void filecopy(FILE *, FILE •); char *prog = argv[0]; /* Progranmname fuer Fehlermaldungen */ if (arge «« 1) /* kein Argument; Standard Eingabe kopieren */ filecopy(stdin, stdout); eise while (—arge > 0) if (<fp = fopen(*++argv, "r")) ■" NULL) { fprintf(stderr, "Xs: can't open Xs\n", prog, *argv); exitd); } eise { filecopy(fp, stdout); fclose(fp); > if (ferror(stdout)) { fprintf(stderr, "Xs: error writing stdout\n", prog); exit<2); > exit@); > Das Programm teilt Fehler auf zwei Arten mit: Erstens erscheinen Fehlermeldun- Fehlermeldungen, die mit fprintf erzeugt werden, als stderr und werden deshalb am Bildschirm ausge- ausgegeben; sie verschwinden nicht in einer Pipe oder Ausgabedatei. Wir haben den Pro- Programmnamen aus argv[0] mit in die Meldung aufgenommen; wenn das Programm zu- zusammen mit anderen ausgeführt wird, kann die Fehlerquelle identifiziert werden. Zweitens benutzt das Programm die Funktion exit aus der Standard-Bibliothek, die bei Aufruf die Programmausführung beendet. Das Argument von exit steht dem Prozeß zur Verfügung, der den vorliegenden Prozeß aufgerufen hat; auf diese Weise kann Erfolg oder Mißerfolg unseres Programms von einem anderen Programm abgeprüft werden, das unser Programm als Teilprozeß benutzt. Nach Konvention bedeutet 0 als Resultat, daß alles funktioniert hat; von 0 verschiedene Werte signalisieren normalerweise unvor- unvorhergesehene Umstände, exit ruft fclose für jede offene Ausgabedatei auf, um etwa ge- gepufferte Ausgaben noch zu schreiben. In der Funktion main ist return expr äquivalent zu exit(expr). Die Funktion exit hat den Vorteil, daß sie von anderen Funktionen aufgerufen werden kann, und daß Auf- Aufrufe von exit mit Suchprogrammen wie denen in Kapitel S gefunden werden können.
158 Eingabe und Ausgabe Die Funktion ferror liefert einen von Null verschiedenen Wert, wenn beim Strom fp ein Fehler aufgetreten ist. int ferror(FILE *fp) Obwohl Ausgabefehler selten sind, kommen sie doch vor (zum Beispiel, wenn eine Platte voll wird), also sollte ein Produktionsprogramm das auch prüfen. Analog zu ferror liefert die Funktion feof(FILE •) einen von Null verschiedenen Wert, wenn das Dateiende bei der angegebenen Datei gefunden wurde, int feof(FlLE *fp) In unseren kleinen Beispielprogrammen haben wir haben uns im allgemeinen nicht um den exit-Status gekümmert, aber jedes ernsthafte Programm sollte sich bemühen, sinnvolle und nützliche Statuswerte zu liefern. 7.7 Zeilen-Eingabe und -Ausgabe In der Standard-Bibliothek gibt es eine Eingabefunktion fgets, die der getline- Funktion gleicht, welche wir in früheren Kapiteln verwendet haben: char *fgets(char *line, int maxi ine, FILE *fp) fgets liest die nächste Eingabezeile (und auch den Zeilentrenner) aus der Datei fp in den Zeichenvektor line; dabei werden höchstens maxiine-1 Zeichen gelesen. Die resultie- resultierende Zeile wird mit '\0' abgeschlossen. Normalerweise liefert fgets den Zeiger line als Resultat; am Dateiende oder bei Fehlern ist der Resultatwert aber NULL. (Unsere getline-Funktion liefert die Länge der eingelesenen Zeile, was nützlicher ist; 0 bedeutet Dateiende.) Als Ausgabefunktion schreibt fputs eine Zeichenkette (die keinen Zeilentrenner enthalten muß) in eine Datei: int fputs(char Mine, FILE *fp) Sie liefert EOF, wenn ein Fehler auftritt, und sonst 0. Die Bibliotheksfunktionen gets und puts funktionieren ähnlich wie fgets und fputs, aber sie verwenden stdin und stdout. Ein bißchen verwirrt, daß gets den abschließenden Zeilentrenner '\n' entfernt, während puts ihn hinzufügt. Um zu demonstrieren, daß Funktionen wie fgets und fputs nichts Besonderes sind, zeigen wir sie hier, kopiert aus der Standard-Bibliothek unseres Systems: /* fgets: hoechstens n Zeichen von iop einlesen */ char *fgets(char *s, int n, FILE *iop) t register int c; register char *cs; es = s; while (—n > 0 && (c = getc(iop)) != EOF) if ((*cs++ = c) == 'W) break; •es = '\0'; return (c == EOF && es « s) ? NULL : s;
/* fputs: Zeichenkette s an iop ausgeben */ int fputs(char *s, FILE *iop) t int c; while (c = *s++) putc(c, iop); return ferror(iop) ? EOF : 0; > Der Standard legt fest, daß ferror bei Fehler einen von Null verschiedenen Wert liefert; fputs liefert EOF bei Fehler und einen nicht-negativen Wert sonst. Unser getline kann man leicht mit fgets implementieren: /* getline: eine Zeile lesen, Laenge liefern */ int getline(char Mine, int max) t if (fgetsdine, max, stdin) == NULL) return 0; eise return strlen(line); > Aufgabe 7-6. Schreiben Sie ein Programm, das zwei Dateien vergleicht und die erste Zeile ausgibt, wo sie verschieden sind. D Aufgabe 7-7. Ändern Sie das Programm zur Mustersuche aus Kapitel 5 so ab, daß die Eingabe aus einer Reihe von explizit angegebenen Dateien kommt; sind keine Dateien als Argumente angegeben, soll die Standard-Eingabe verwendet werden. Sollte der Da- Dateiname ausgegeben werden, wenn eine gesuchte Zeile gefunden wird? G Aufgabe 7-8. Schreiben Sie ein Programm, das eine Reihe von Dateien ausgibt, wobei für jede Datei eine neue Seite begonnen wird; zu jeder Datei soll es einen laufenden Sei- Seitentitel und eine Seitennumerierung geben. D 7.8 Weitere Funktionen Die Standard-Bibliothek stellt eine Vielzahl von Funktionen zur Verfügung. In die- diesem Abschnitt werden die nützlichsten kurz beschrieben. Mehr Details und viele andere Funktionen sind im Anhang B zu finden. 7.8.1 Operationen mit Zeichenketten Wir haben die Zeichenketten-Funktionen strlen, strcpy, strcat und strcmp bereits erwähnt, die man in < string.h > findet. Im folgenden sind s und t Zeiger auf char und c und n sind int. strcat(s.t) hängt t an das Ende von s an strncat(s,t,n) hängt n Zeichen von t an s an strcmp(s,t) liefert negativ oder Null oder positiv je nachdem ob s < t, s == t, oder s > t ist strncmp(s,t,n) wie strcmp, aber nur für die ersten n Zeichen strcpy(s.t) kopiert t nach s strncpy(s, t,n) kopiert höchstens n Zeichen von t nach s strien(s) liefert Länge von s ohne '\0* am Schluß strchr(s,c) liefert Zeiger auf erstes c in s, oder NULL falls nicht da strrchr(s.c) liefert Zeiger auf letztes c in s, oder NULL falls nicht da
160 'j_ ngabe und Ausgabe 7.8.2 Tests für Zeichenklassen und Umwandlung Eine Reihe von Funktionen aus <ctype.h> dienen zur Klassifikation und Um- Umwandlung von Zeichen.* Im folgenden ist c ein Int-Wert, der als unsigned char repäsen- tiert werden kann, oder EOF. Die Funktionen liefern int. isalpha(c) nicht Null, wenn c ein Buchstabe ist, sonst 0 isupper(c) nicht Null, wenn c ein Großbuchstabe ist, sonst 0 i s iower( c) nicht Null, wenn c ein Kleinbuchstabe ist, sonst 0 isdigi t(c) nicht Null, wenn c eine Ziffer ist, sonst 0 isainum(c) nicht Null, wenn isalpha(c) oder isdigit(c) gilt, sonst 0 isspace(c) nicht Null, wenn c ein Leerzeichen, Tabulatorzeichen, Zeilentrenner, Wagenrücklauf, Seitenvorschub oder Vertikal-Tabulator ist, sonst 0 toupper(c) liefert c, umgewandelt in Großbuchstaben toiower(c) return c, umgewandelt in Kleinbuchstaben 7.83 ungetc In der Standard-Bibliothek gibt es auch eine sehr eingeschränkte Fassung der Funktion ungetch, die wir im Kapitel 4 geschrieben haben; hier heißt die Funktion ungetc. int ungetc(int c, FILE *fp) stellt das Zeichen c in die Datei fp zurück und liefert entweder c oder EOF bei Fehler. Nur ein Zeichen kann garantiert für jede Datei zurückgestellt werden, ungetc kann in Verbindung mit allen Eingabefunktionen wie scanf, getc oder getchar verwendet werden. 7.8.4 Kommandoausführung Die Funktion system (char *s) führt das Kommando aus, das in der Zeichenkette s enthalten ist, und setzt dann die Ausführung des momentanen Programms fort. Der In- Inhalt der Zeichenkette s hängt stark vom lokalen Betriebssystem ab. Als triviales Beispiel führt bei UNIX-Systemen der Aufruf systemO'date"); das date-Programm aus; dieses Programm schreibt Datum und Uhrzeit in seine Stan- Standard-Ausgabe, system liefert einen systemabhängigen ganzzahligen Statuswert vom aus- ausgeführten Kommando. Bei einem UNIX-System ist der Statuswert der Wert, der von exit geliefert wird. 7.8.5 Speicher-Verwaltung Die Funktionen malloc und calloc beschaffen Speicherblöcke dynamisch, void *malloc(size_t n) liefert einen Zeiger auf einen (uninitialisierten) Speicherbereich von n Bytes, oder NULL, wenn nicht genügend Speicher vorhanden ist. * Nationale Zeichen wie Umlaute oder Akzente zahlen nicht zu den Buchstaben. A.d.Ü.
7.8 Weitere Fun nen 161 void *calloc(size_t n, size_t size) liefert einen Zeiger auf genügend Speicher für einen Vektor von n Objekten der angege- angegebenen Größe size. Ist nicht genügend Speicher vorhanden, so ist der Resultatwert NULL. Der Speicher wird mit 0 initialisiert. Der Zeiger, den malloc oder calloc liefern, hat die korrekte Ausrichtung für das gewünschte Objekt, muß aber noch mit einer Umwandlungsoperation in den richtigen Datentyp umgewandelt werden, zum Beispiel int *ip; ip = (int *) calloc(n, sizeof(int)); free(p) stellt den Speicher wieder zur Verfügung, auf den p zeigt; dabei stammt p ursprünglich von einem Aufruf von malloc oder calloc. Speicher kann zwar in beliebiger Reihenfolge freigegeben werden; es ist jedoch ein entsetzlicher Fehler, wenn etwas frei- freigegeben wird, was nicht von calloc oder malloc stammt. Es ist auch falsch, wenn etwas benutzt wird, das schon freigegeben wurde. Ein ty- typisches, aber fehlerhaftes Programmfragment ist folgende Schleife, die Elemente aus ei- einer Liste freigibt: for (p « head; p I» NULL; p » p->next) /* FALSCH •/ free(p); Korrekterweise kopiert man das, was noch benötigt wird, vor der Freigabe: for (p = head; p ! = NULL; p = q) { q = p->next; free(p); > Ein Mechanismus zur Speicherverwaltung wie malloc, bei dem Speicher in beliebi- beliebiger Reihenfolge wieder freigegeben werden kann, wird in Abschnitt 8.7 vorgeführt. 7.8.6 Mathematische Funktionen Mehr als zwanzig mathematische Funktionen sind in <math.h> deklariert; hier sind einige, die häufiger benutzt werden. Jede benötigt ein oder zwei double-Argumente und liefert ein double-Resultat. sinto Sinus vonx,x im Bogenmaß costo Kosinus von x,xwa. Bogenmaß atan2(yr*> Arcustangens von y/x, y/x im Bogenmaß expto Exponentialfunktion ex logU) natürlicher Logarithmus (Basise) von* (*>0) log 10CO gewöhnlicher Logarithmus (Basis 10) vonx (x>0) pouoc.y) xy sqrtto Quadratwurzel vonx (x>0) fabsto absoluter Wert von x 7.8.7 Zufallszahlengenerator Die Funktion rand() berechnet eine Folge von ganzzahligen Pseudo-Zufallszahlen im Bereich Null bis RAND MAX; der Grenzwert RAND MAX ist in <stdlib.h> definiert.
162 7. igabe und Ausgabe Gleitpunkt-Zufallszahlen, die größer oder gleich Null und kleiner als Eins sind, kann man folgendermaßen erzeugen: «define frandO ((double) randO / (RAMDMAX+1.0)) (Wenn Ihre Bibliothek bereits eine Funktion für Gleitpunkt-Zufallszahlen enthält, hat sie vermutlich bessere statistische Eigenschaften als diese.) Die Funktion srand(unslgned) hinterlegt den Ausgangswert für rand. Die portable Implementierung von rand und srand, die der Standard vorschlägt, steht im Abschnitt 2.7. Aufgabe 7-9. Funktionen wie isupper können so implementiert werden, daß sie entwe- entweder Platz oder Zeit sparen. Untersuchen Sie beide Möglichkeiten. D
8 Die Schnittstelle zum UNIX-Betriebssystem Das UNIX-Betriebssystem stellt seine Dienste als Satz von Systemaufrufen zur Ver- Verfugung, die praktisch Funktionen im Betriebssystem sind, die ein Benutzerprogramm auf- aufrufen kann. Dieses Kapitel beschreibt, wie man einige der wichtigsten Systemaufrufe in C-Programmen benutzt. Wenn Sie UNIX verwenden, sollte dies für Sie eine unmittelbare Hilfe sein, denn manchmal muß man Systemaufrufe benutzen, um die größtmögliche Ef- Effizienz zu erzielen oder um auf Dienste zuzugreifen, die nicht in der Standard-Bibliothek enthalten sind. Aber auch wenn Sie C mit einem anderen Betriebssystem verwenden, sollten Sie weitere Einblicke in die C-Programmierung durch das Studium dieser Beispie- Beispiele gewinnen können; obwohl die Details verschieden sind, findet man ähnliche Pro- Programmtexte bei jedem Betriebssystem. Da die ANSI-C-Bibliothek sich in vielen Fällen an den Möglichkeiten von UNIX orientiert, können diese Beispiele auch zu Ihrem Verständ- Verständnis der Bibliothek beitragen. Das Kapitel ist in drei wesentliche Teile unterteilt: Eingabe und Ausgabe, Dateisy- Dateisystem und Speicherverwaltung. In den ersten beiden Teilen nehmen wir an, daß Sie ein wenig mit den externen Charakteristika von UNIX-Systemen vertraut sind. Kapitel 7 beschäftigte sich mit einer Schnittstelle für Ein- und Ausgabe, die über Betriebssysteme hinweg einheitlich zur Verfügung steht. Bei jedem einzelnen System müssen die Funktionen der Standard-Bibliothek mit Hilfe der Möglichkeiten implemen- implementiert werden, über die das Betriebssystem verfügt. In den nächsten Abschnitten beschrei- beschreiben wir die UNIX-Systemaufrufe für Eingabe und Ausgabe und zeigen, wie man damit Teile der Standard-Bibliothek implementieren kann. 8.1 File-Deskriptoren Im UNIX-Betriebssystem erfolgt die gesamte Ein- und Ausgabe durch Lesen oder Schreiben von Dateien, denn alle Peripheriegeräte, sogar Tastatur und Bildschirm, sind Dateien im Dateisystem. Dies bedeutet, daß die gesamte Kommunikation zwischen ei- einem Programm und peripheren Geräten über eine einzige, einheitliche Schnittstelle er- erfolgt. Im allgemeinsten Fall müssen Sie, bevor Sie eine Datei lesen oder schreiben, Ihre Absicht dem System mitteilen; dies wird als Eröffnen der Datei bezeichnet. Wenn Sie in eine Datei schreiben wollen, muß diese Datei möglicherweise erst erzeugt werden, oder ihr bisheriger Inhalt muß beseitigt werden. Das System prüft, ob Sie dies dürfen: Exi- Existiert die Datei? Sind Sie berechtigt, auf diese Datei zuzugreifen? Ist alles in Ordnung, so erhält das Programm eine kleine, nicht-negative Zahl, einen sogenannten File- Deskriptor. Für jede Eingabe von oder Ausgabe zu dieser Datei wird dann der File-De- File-Deskriptor anstelle des Namens benutzt, um die Datei zu identifizieren. (Ein File-Deskrip- File-Deskriptor entspricht dem FILE-Zeiger der Standard-Bibliothek oder dem file handle von MS- DOS.) Alle Informationen über eine geöffnete Datei werden vom System unterhalten; das Benutzerprogramm verweist auf die Datei nur mit dem File-Deskriptor. Da Ein- und Ausgabe mit Tastatur und Bildschirm so häufig vorkommen, wurden besondere Vorkehrungen getroffen, um dies bequem zu machen. Wenn der Kommando- Kommandoprozessor (die „Shell") ein Programm ausführen läßt, sind bereits drei Dateiverbindun- Dateiverbindungen eröffnet, mit den File-Deskriptoren 0, 1 und 2, die als Standard-Eingabe, Standard-
164 8 Die Schnittstelle zun flX-Betriebssystem Ausgabe und Diagnose-Ausgabe bezeichnet werden. Wenn ein Programm vom File-De- File-Deskriptor 0 liest und auf 1 oder 2 schreibt, kann es ein- und ausgeben, ohne selbst Dateien eröffnen zu müssen. Der Benutzer eines Programms kann Standard-Eingabe und Standard-Ausgabe zu Dateien mit Hilfe von < und > umlenken: prog <infile >outfile In diesem Fall ändert die Shell die Voreinstellung der File-Deskriptoren 0 und 1 und ver- verknüpft sie mit den angegebenen Dateien. Der File-Deskriptor 2 bleibt normalerweise mit dem Bildschirm verbunden, damit Fehlermeldungen dorthin gehen. Ahnliches gilt, wenn Standard-Eingabe oder Standard-Ausgabe mit einer pipe verbunden werden. In al- allen Fällen werden die Dateiverbindungen von der Shell und nicht vom Programm selbst geändert. Das Programm weiß nicht, woher seine Eingabe kommt oder wohin seine Ausgabe geht, wenn die File-Deskriptoren 0 für Eingabe und 1 und 2 für Ausgabe ver- verwendet werden. 8.2 Elementare Ein- und Ausgabe - read und write Zur Ein- und Ausgabe werden die Systemaufrufe read und write benutzt, auf die C-Programme mit zwei Funktionen namens read und write zugreifen. Bei beiden ist das erste Argument ein File-Deskriptor. Das zweite Argument ist ein Zeichenvektor in Ih- Ihrem Programm, in den oder aus dem Daten transferiert werden sollen. Das dritte Argu- Argument ist die Anzahl Bytes, die transferiert werden soll. int n_read ■ read(int fd, char »buf, int n); int n_written = write(int fd, char *buf, int n); Jeder Aufruf liefert die Anzahl Bytes, die transferiert wurden. Beim Lesen kann der Re- Resultatwert kleiner sein als die gewünschte Anzahl. Liefert read das Resultat 0, bedeutet dies, daß das Dateiende erreicht wurde. Ist das Resultat -1, so ist irgendein Fehler pas- passiert. Beim Schreiben ist der Resultatwert die Anzahl Bytes, die geschrieben wurden; ein Fehler liegt vor, wenn das nicht die verlangte Anzahl ist. Jede Anzahl Bytes kann mit einem einzigen Aufruf gelesen oder geschrieben wer- werden. Die häufigsten Werte sind 1, also ein Zeichen auf einmal („nicht gepuffert"), oder eine Anzahl wie 1024 oder 40%, die der physikalischen Blocklänge von Peripheriegeräten entspricht. Größere Werte sind effizienter, da weniger Systemaufrufe gemacht werden. Wenn wir dies alles zusammenfassen, können wir ein einfaches Programm schrei- schreiben, das seine Eingabe zur Ausgabe kopiert, also das Äquivalent des Dateikopierpro- Dateikopierprogramms, das wir in Kapitel 1 geschrieben haben. Dieses Programm kopiert von überall nach überall, da Eingabe und Ausgabe zu beliebigen Dateien oder Geräten umgelenkt werden können. «include "syscalls.h" mainO /* Eingabe in die Ausgabe kopieren */ i char buf[BUFSIZJ; int n; while ((n = read@, buf, BUFSIZ)) > 0) writed, buf, n); return 0;
8.3 open, creat, cL unlink 165 Wir haben Funktionsprototypen für die Systemaufrufe in einer Datei namens syscallsJi gesammelt, damit wir sie in die Programme in diesem Kapitel einfügen kön- können. Dieser Name ist jedoch nicht Teil des Standards. Der Parameter BUFS1Z ist auch in syscalls.h definiert; sein Wert eignet sich als Puffergröße für das lokale System.* Ist die Dateigröße kein Vielfaches von BUFSIZ, so liefert ein Aufruf von read schließlich eine kleinere Anzahl Bytes, die von write ausgege- ausgegeben werden sollen; danach liefert der nächste Aufruf von read den Wert 0. Es ist instruktiv, wenn man sieht, wie man mit read und write komplexere Funktio- Funktionen wie getchar, putchar usw. konstruiert. Hier ist zum Beispiel eine Version von getchar, bei der die Eingabe nicht gepuffert ist. Von der Standard-Eingabe wird ein Zei- Zeichen auf einmal gelesen. #include "syscalls.h" /* getchar: ungepufferte Eingabe einzelner Zeichen */ int getchar(void) { char c; return (read@, &c, 1) == 1) ? (unsigned char) c : EOF; > c muß als char vereinbart werden, da read einen Zeiger auf char als zweites Argument erwartet. Wenn man c in der return-Anweisung in den Typ unsigned char umwandelt, vermeidet man Probleme durch Propagierung des Vorzeichens. Die zweite Version von getchar liest ein größeres Stück der Eingabe und verteilt als Resultatwert die einzelnen Zeichen der Reihe nach, «include "syscalls.h" /* getchar: gepufferte Eingabe einzelner Zeichen, einfache Version */ int getchar(void) { static char bufCBUFSIZ] ; static char *bufp » buf; static int n = 0; if (n == 0) t /* Puffer ist leer •/ n * read@, buf, sizeof buf); bufp = buf; > return (—n >= 0) ? (unsigned char) *bufp++ : EOF; > Übersetzte man diese Versionen von getchar mit der Definitionsdatei <stdio.h>, so müßte man den Namen getchar mit #undef entfernen, denn er könnte als Makro imple- implementiert sein. 8.3 open, creat, close, unlink Abgesehen von der Standard-Eingabe, Standard-Ausgabe und Diagnose-Ausgabe müssen Sie Dateien explizit eröffnen, um sie zu lesen oder zu schreiben. Dazu gibt es zwei Systemaufrufe, nämlich open und creat.* * Ken Thompson wurde gefragt, was er beim nächsienmal bei unk ändern wurde: „Ich würde creal mit e buchstabieren." A.d.Ü.
166 8 Die Schnittstelle ; UNIX-Betriebssystem open gleicht ziemlich der fopen-Funktion, die in Kapitel 7 besprochen wurde, nur daß der Resultatwert kein FILE-Zeiger sondern ein File-Deskriptor ist, also ein int-Wert, open liefert -1, falls ein Fehler auftritt. «include <fcntl.h> int fd; int open(char *name, int flags, int perms); fd = open(name, flags, perms); Wie bei fopen ist name eine Zeichenkette, die den Dateinamen enthält. Das zweite Ar- Argument, flags, ist ein int-Wert, der beschreibt, wie zugegriffen werden soll; die wichtig- wichtigsten Werte sind o_rdonly Öffnen nur zum Lesen o_uronly Öffnen nur zum Schreiben ordwr Öffnen zum Lesen und Schreiben Bei UNIX System V sind diese Konstanten in <fcntIJi> und bei Berkeley-Versionen (BSD) in <sys/fileJi> definiert. Eine existente Datei wird folgendermaßen zum Lesen eröffnet: fd = open(name, O_RDONLY, 0); Bei der Art von open, die wir beschreiben, ist perms immer Null. Es ist ein Fehler, wenn man auf eine Datei mit open zugreift, die nicht existiert. Der Systemaufruf creat dient dazu, neue Dateien zu erzeugen oder existierende Dateien zu überschreiben: int creat(char *name, int perms); fd = creat(name, perms); Dieser Aufruf liefert einen File-Deskriptor, wenn die angegebene Datei erzeugt werden konnte, und -1 sonst. Wenn die Datei bereits existiert, reduziert creat ihre Länge auf 0 und zerstört damit den bisherigen Inhalt; es ist kein Fehler, wenn man auf eine Datei mit creat zugreift, die schon existiert. Wenn die Datei noch nicht existiert, so erzeugt sie creat mit dem Zugriffsschutz, der als Argument perms angegeben ist. Im UNIX-Dateisystem gibt es neun Bits für den Zugriffsschutz einer Datei, die Zugriff zum Lesen, Schreiben und Ausführen für den Be- Besitzer der Datei, für seine Gruppe und für alle anderen Teilnehmer kontrollieren. Mit ei- einer Oktalzahl mit drei Ziffern kann man deshalb den Zugriffsschutz bequem angeben. Beispielsweise erlaubt 0755 Lesen, Schreiben und Ausführen für den Besitzer und Lesen und Ausführen für die „Gruppe" und alle „Anderen". Zur Illustration ist hier eine vereinfachte Fassung des UNIX-Programms cp, das ei- eine Datei in eine andere kopiert. Unsere Version kopiert nur eine Datei, sie akzeptiert als zweites Argument keinen Katalog, und sie erfindet den Zugriffsschutz, statt ihn zu ko- kopieren. «include <stdio.h> «include <fcntl.h> «include "syscalls.h" «define PERMS 0666 /* RU fuer Besitzer, Gruppe und Andere */ void error(char *, ...);
8.3 open,creat se, unlink 167 /* cp: fi nach f2 kopieren */ mainO'nt arge, char *argvt]) i int fi, f2, n; char buf[BUFSIZ]; if (arge != 3) errorC'Usage: cp from to"); if ((f1 = open(argv[1], O_RDONLY, 0)) == -1) errorC'cp: can't open Xs", argv[1]); if (<f2 = creat(argv[2], PERMS)) == -1) errorC'cp: can't create Xs, mode %03o", argvB], PERMS); white ((n = read(f1, buf, BUFSIZ)) > 0) if (write(f2, buf, n) != n) errorC'cp: write error on file Xs", argvB]); return 0; > Dieses Programm erzeugt die Ausgabedatei mit einem unveränderlichen Zugriffsschutz von 0666. Mit dem Systemaufruf stat, der in Abschnitt 8.6 beschrieben wird, kann man den Zugriffsschutz einer existenten Datei feststellen und folglich der Kopie den gleichen Zugriffsschutz geben. Beachten Sie, daß die Funktion error ähnlich wie printf mit variablen Argumentli- Argumentlisten aufgerufen wird. Die Implementierung von error zeigt, wie man ein anderes Mit- Mitglied der printf-Familie benutzt. Die Funktion vprintf aus der Standard-Bibliothek funk- funktioniert wie printf, aber die variable Argumentliste ist durch ein einzelnes Argument er- ersetzt, das durch einen Aufruf des Makros va start initialisiert wurde. Ahnlich passen vfprintf und vsprintf zu fprintf und sprintf. ^include <stdio.h> «include <stdarg.h> /* error: gib eine Fehlermaldung aus und verende */ void error(char *fmt, ...) { va_list args; va_start(args, fmt); fprintf(stderr, "error: "); vfprintf(stderr, fmt, args); fprintf(stderr, "\n"); va_end(args); exitA); > Die Anzahl Dateien, die ein Programm gleichzeitig eröffnen kann, ist (häufig auf etwa 20) begrenzt. Wenn ein Programm viele Dateien bearbeiten will, muß man Vorkeh- Vorkehrungen treffen, um File-Deskriptoren wiederzuverwenden. Die Funktion close(int fd) löst die Verbindung zwischen einem File-Deskriptor und einer eröffneten Datei und gibt den File-Deskriptor zur Wiederverwendung mit einer anderen Datei frei; sie entspricht fclose aus der Standard-Bibliothek, nur daß es keine Puffer gibt, die geleert werden müs- müssen. Wird ein Programm mit exit oder durch return aus der main-Funktion beendet, so werden alle eröffneten Dateien implizit geschlossen. Die Funktion unlink(char *name) entfernt den Dateinamen name aus dem Datei- Dateisystem. Sie entspricht der Funktion remove aus der Standard-Bibliothek.
168 8 Die Schnittstelle zum TIX-Betriebssystem Aufgabe 8-1. Ändern Sie das Programm cat aus Kapitel 7, und benutzen Sie read, write, open und close anstelle der äquivalenten Funktionen aus der Standard-Bibliothek. Experimentieren Sie, um die relative Geschwindigkeit der zwei Programmversionen fest- festzustellen. D 8.4 Random-Zugriff - Iseek Eingabe und Ausgabe erfolgen normalerweise sequentiell: Jeder Aufruf von read oder write bezieht sich auf die Dateiposition unmittelbar nach dem vorhergehenden Auf- Aufruf. Falls notwendig, kann eine Datei jedoch in jeder beliebigen Reihenfolge gelesen oder geschrieben werden. Mit dem Systemaufruf Iseek kann man sich in einer Datei be- bewegen, ohne Daten zu lesen oder zu schreiben: long Iseek(int fd, long offset, int origin); Dieser Aufruf setzt die Bearbeitungsposition in der durch den File-Deskriptor fd be- beschriebenen Datei auf offset, relativ zu dem durch origin beschriebenen Punkt. Der nächste Aufruf von read oder write beginnt dann bei dieser Position, origin kann 0, 1 oder 2 sein und legt fest, daß offset vom Anfang der Datei, von der aktuellen Dateipositi- Dateiposition oder vom Dateiende aus gemessen wird. Um zum Beispiel an eine Datei anzufügen, (das entspricht der Umlenkung mit >> in der UNIX-Shell oder "a" bei fopen), positioniert man vor dem Schreiben zum Dateiende: lseek(fd, 0L, 2); Zum Dateianfang („zurückspulen") kommt man mit dem Aufruf: lseek(fd, 01, 0); Beachten Sie das Argument 0L; dafür könnte man auch (long) 0 schreiben oder nur 0, wenn für Iseek ein korrekter Funktionsprototyp vorhanden ist. Mit Iseek kann man Dateien mehr oder weniger wie große Vektoren behandeln, al- allerdings mit langsamerem Zugriff. Die folgende Funktion liest beispielsweise von einer beliebigen Stelle an beliebig viele Bytes aus einer Datei. Sie liefert die Anzahl gelesener Zeichen, oder -1 bei Fehler. «include "syscatts.h" /* get: n Bytes ab Position pos einlesen */ int get(int fd, long pos, char *buf, int n) { if (lseek(fd, pos, 0) >= 0) /* auf pos einstellen */ return read(fd, buf, n); eise return -1; > Der Funktionswert von Iseek ist long und gibt die neue Position in der Datei an, oder er ist -1L bei einem Fehler. Die Funktion fseek der Standard-Bibliothek gleicht Iseek, nur hat das erste Argument den Typ FILE •, und das Funktionsresultat ist nicht-null bei Feh- Fehler. 8.5 Beispiel: Eine Implementierung von fopen und getc Wie einige dieser Teile zusammenpassen, illustrieren wir mit einer Implementie- Implementierung von fopen und getc aus der Standard-Bibliothek.
8.5 Beispiel: Eine Zementierung von fopen und getc 169 Erinnern wir uns daran, daß die Standard-Bibliothek Dateien mit FILE-Zeigern und nicht mit File-Deskriptoren beschreibt. Ein FILE-Zeiger ist ein Zeiger auf eine Struktur, die verschiedene Informationen über die Datei enthält: einen Zeiger auf einen Puffer, damit die Datei in großen Stücken gelesen werden kann; die Anzahl Zeichen, die noch im Puffer sind; einen Zeiger auf die Position des nächsten Zeichens im Puffer; den File-Deskriptor; und einige Bits, die Zugriff (lesen/schreiben), Fehlerstatus usw. be- beschreiben. Die Datenstruktur, die eine Datei beschreibt, steht in < stdio.h >. Diese Definiti- Definitionsdatei muß (mit #include) in jede Quelldatei eingefügt werden, die Funktionen aus der Standard-Bibliothek für Ein- und Ausgabe verwendet. < stdio.h > wird auch von den Funktionen in dieser Bibliothek selbst verwendet. Im folgenden Auszug aus einer typi- typischen Definitionsdatei <stdioJi> beginnen Namen, die nur von Bibliotheksfunktionen verwendet werden sollten, mit einem Unterstrich, damit sie sich nach Möglichkeit von Namen im Benutzerprogramm unterscheiden. «define NULL 0 (»define EOF (-1) «define BUFSIZ 1024 «define FOPENJtAX 20 /* maximale Anzahl gleichzeitig eroeffneter Dateien •/ typedef struct _iobuf < int cnt; /* Anzahl verbleibender Zeichen */ char *ptr; /* Position des naechsten Zeichens */ char *base; /* Adresse des Puffers */ int flag; /* Art des Dateizugriffs */ int fd; /• File-Deskriptor */ > FILE; extern FILE _iob[FOPEN_MAX]; «define stdin (&_iobfOJ) «define stdout (&_iob[1]) «define stderr (&_iob[2]) /* Datei zum Lesen eroeffnet V /* Datei zum Schreiben eroeffnet •/ /* nicht gepuffert •/ /* Dateiende wurde bereits erreicht */ /* ein Fehler ist schon passiert bei dieser Datei */ >; int _fillbuf(FILE •); int _flushbuf(int, FILE *); «define feof(p) <((p)->flag & EOF) != 0) «define ferror(p) (((p)->flag & ERR) ! = 0) «define fileno(p) ((p)->fd) «define getc(p) (—(p)->cnt >= 0 \ ? (unsigned char) *(p)->ptr++ : fitlbuf(p)) «define putc(x.p) (—(p)->cnt >= 0 \ ? (unsigned char) *(p)->ptr++ = (x) : _ftushbuf((x),p)) «define getcharO getc(stdin) «define putchar(x) putc((x), stdout) enum flags _READ WRITE UNBUF _£0F ERR { = 01, ■ 02. = 04, • 010, ' 020
170 8 Die Schnittstelle zun JIX-Betriebssystem Normalerweise dekrementiert der getc-Makro die Anzahl im Puffer verbleibender Zeichen, rückt den Zeiger vor und liefert das Zeichen als Resultat. (Zur Erinnerung: ei- eine längere #define-Anweisung wird mit einem Gegenschrägstrich auf einer neuen Zeile fortgesetzt.) Wenn die Anzahl der im Puffer verbleibenden Zeichen jedoch negativ wird, ruft getc die Funktion _fillbuf auf, um den Puffer neu zu füllen, die Datenstruktur wieder zu initialisieren und ein Zeichen zu liefern. Die Zeichen werden als unsigned geliefert, damit sicher ist, daß alle Zeichen positiv sind. Wir wollen zwar die Details nicht beschreiben, aber wir haben auch die Definition von putc beigefügt, putc funktioniert praktisch wie getc und ruft eine Funktion fiushbuf auf, wenn der Puffer gefüllt ist. Der Auszug aus < stdioJi > enthält auch Makros zum Zugriff auf die Fehler- und Dateiende-Bits sowie auf den File-Deskriptor. Wir können jetzt die Funktion fopen schreiben. Der größte Teil von fopen befaßt sich damit, Zugriff zur Datei zu eröffnen, richtig zu positionieren und die Bits in der FIUE-Struktur dem Zugriff entsprechend zu setzen, fopen stellt noch keinen Puffer be- bereit; dies erfolgt durch _fillbuf beim ersten Lesezugriff auf die Datei. «include <fcntt.h> «include "syscalts.h" «define PERMS 0666 /* RU fuer Besitzer, Gruppe und Rest */ /* fopen: Datei eroeffnen, liefert FILE-Zeiger */ FILE *fopen(char »name, char *mode) t int fd; FILE *fp; if (*mode !» 'r' && *mode 1= 'W && *mode != '«') return NULL; for (fp = _iob; fp < _iob + OPEN_MAX; fp++) if ((fp->ftag & (READ | _URITE)) « 0) break; /• frei FILE-Struktur gefunden */ if (fp >= _iob + OPEN_MAX) /* nichts mahr frei V return NULL; if («mode == 'w') fd = creat(name, PERMS); else if («mode « 'a1) < if ((fd = open(name, O_WRONLY, 0)) == -1) fd = creat(name, PERMS); lseek(fd, 0L, 2); > eise fd = open(name, O_RDONLY, 0); if (fd == -1) /• Zugriff nicht moeglich •/ return NULL; fp->fd = fd; fp->cnt = 0; fp->bese = NULL; fp->flag = («mode == 'r') ? _READ : _WRITE; return fp; > Diese Version von fopen behandelt nicht alle Zugriffsarten des Standards; allerdings wür- würde man nicht viel Programmtext brauchen, um sie hinzuzufügen. Insbesondere erkennt
8.5 Beispiel: Eine plementierung von/qpgn und getc 171 unser fopen weder „b", für binären Zugriff, weil das bei UNIX-Systemen bedeutungslos ist, noch „+", was sowohl Lesen als auch Schreiben erlaubt. Der erste Aufruf von getc für eine Datei entdeckt Null als Anzahl der verbleiben- verbleibenden Zeichen im Puffer; folglich wird fillbuf aufgerufen. Entdeckt fillbuf, daß die Datei nicht zum Lesen eröffnet wurde, so wird sofort das Resultat EOF geliefert. Andernfalls versucht _Rllbuf, einen Puffer anzulegen (falls gepuffert gelesen werden soll). Wenn ein Puffer eingerichtet ist, ruft _fillbuf dann read auf, um ihn zu füllen, setzt die Anzahl der verbleibenden Zeichen und die Zeiger entsprechend und liefert das Zei- Zeichen am Anfang des Puffers als Resultat. Spätere Aufrufe von _fillbuf finden dann einen Puffer vor. «include "syscalls.h" /• _fillbuf: Eingabepuffer anlegen und fuellen •/ int _fillbuf(FILE *fp) { int bufsize; if ((fp->flag&(_READ|_EOF|_ERR>) != _READ) return EOF; bufsize = (fp->flag & JJNBUF) ? 1 : BUFSIZ; if (fp->bese == NULL) /* noch kein Puffer */ if ((fp->base = (char •) malloc(bufsize)) == NULL) return EOF; /* kein Puffer zu erhalten */ fp->ptr » fp->bese; fp->cnt = read(fp->fd, fp->ptr, bufsize); if (~fp->cnt < 0) { if (fp->cnt == -1) fp->flag |= EOF; eise fp->flag |= ERR; fp->cnt = 0; return EOF; > return (unsigned char) *fp->ptr++; > Als einziges Problem bleibt übrig, wie alles initialisiert wird. Der Vektor _iob muß definiert und für stdin, stdout und stderr initialisiert werden: FILE _iOb[OPEN_MAX] = t /* stdin, stdout, stderr: V { 0, (char *) 0, (char *) 0, _READ, 0 >, { 0, (char *) 0, (char •) 0, _URITE, 1 >, t 0, (char *) 0, (char *) 0, _URITE | _UNBUF, 2 > >; Die Initialisierung der fiag-Komponente der Struktur zeigt, daß stdin gelesen, stdout ge- geschrieben und stderr ungepuffert geschrieben werden soll. Aufgabe 8-2. Schreiben Sie fopen und fillbuf so um, daß Bit-Felder anstelle von expli- expliziter Bit-Manipulation verwendet werden. Vergleichen Sie Programmgröße und Aus- Ausführungsgeschwindigkeit. D Aufgabe 8-3. Entwerfen und schreiben Sie die Funktionen _flushbuf, fflush und fclose. D
172 8 Die Schnittstelle zun. JIX-Betriebssystem Aufgabe 8-4. Die Funktion int fseek(FILE *fp, long offset, int origin) in der Standard-Bibliothek ist identisch zu lseek, abgesehen davon, daß fp ein FILE- Zeiger und kein File-Deskriptor ist, und daß als Resultat ein int-Status und keine Positi- Positionsangabe geliefert wird. Schreiben Sie fseek. Sorgen Sie unbedingt dafür, daß Ihre fseek-Funktion im Zusammenhang mit den Pufferoperationen korrekt funktioniert, die die anderen Bibliotheksfunktionen durchführen. D 8.6 Beispiel: Kataloge ausgeben Manchmal braucht man eine andere Dateioperation - wenn man Information über eine Datei selbst und nicht über ihren Inhalt benötigt. Ein Beispiel dafür ist ein Programm zum Ausgeben von Katalogen, wie das UNIX-Kommando Is Qist directory). Dieses Programm gibt die Dateinamen in einem Katalog aus und zusätzlich - wahlweise - auch andere Informationen, wie Dateigröße, Zugriffsberechtigungen usw. Is gleicht dem Kommando DIR bei MS-DOS. Da ein UNIX-Katalog einfach eine Datei ist, braucht Is ihn nur zu lesen, um die Da- Dateinamen zu erhalten. Aber man muß einen Systemaufruf benutzen, um auf andere In- Information über eine Datei zuzugreifen, wie zum Beispiel auf die Dateigröße. Bei ande- anderen Betriebssystemen benötigt man unter Umständen sogar einen Systemaufruf, um an die Dateinamen zu kommen; dies gilt zum Beispiel bei MS-DOS. Wir wollen erreichen, daß der Zugriff zu dieser Information möglichst systemunabhängig erfolgen kann, auch wenn die Implementierung sehr systemabhängig sein mag. Wir illustrieren einige Aspekte, indem wir ein Programm fsize schreiben, fsize ist eine spezielle Version von Is und gibt die Größen aller Dateien aus, die als Argumente in der Kommandozeile angegeben werden. Ist eine dieser Dateien selbst ein Katalog, so durchläuft fsize diesen Katalog rekursiv. Ohne Argument aufgerufen, bearbeitet fsize den aktuellen Katalog. Beginnen wir mit einer kurzen Wiederholung der Struktur des UNIX-Dateisystems. Ein Katalog ist eine Datei, die eine Liste von Dateinamen enthält, zusammen mit Anga- Angaben, wo sich diese Dateien befinden. Eine solche Angabe ist ein Index in eine weitere Tabelle, die sogenannte Inode -Tabelle. In der Inode befindet sich alle Information über eine Datei, mit Ausnahme des Dateinamens. Ein Katalogeintrag enthält im allgemeinen nur zwei Teile, den Dateinamen und die Inode -Nummer. Bedauerlicherweise sind Format und exakter Inhalt eines Katalogs nicht bei allen Versionen des UNIX-Betriebssystems gleich. Wir teilen deshalb die Arbeit in zwei Teile, um die nicht-portablen Teile möglichst einzukapseln. Die äußere Ebene definiert eine Struktur namens Dirent und drei Funktionen opendir, readdir und closedir, um einen sy- systemunabhängigen Zugriff auf Namen und Inode -Nummer aus einem Katalogeintrag zu erreichen. Mit dieser Schnittstelle schreiben wir fsize. Dann zeigen wir, wie man diese Funktionen bei Systemen implementiert, die die gleiche Katalogstruktur wie UNIX Versi- Version 7 oder System V haben; Abarten bleiben als Übungsaufgaben. Die Dirent-Struktur enthält die Inode -Nummer und den Namen. Die maximale Länge einer Komponente eines Dateinamens ist NAME_MAX, ein systemabhängiger Wert, opendir liefert einen Zeiger auf eine Struktur namens DIR, analog zu FILE, die
8.6 Beispiel: Kata ausgeben 173 von readdir und closedir benutzt wird. Diese Information ist in einer Datei dirent.h zu- zusammengefaßt. «define NAMEMAX U / typedef struct { long ino; char name[NAME_MAX+1]; > Dirent; laengste Dateinamenskomponente ; /* systemabhaengig */ /* portabler Katalogeintrag: */ /* inode-Nummer */ /* Name + '\0' am Ende */ DIR minimal: ungepuffert, etc. */ /* File-Deskriptor fuer Katalog */ /* der Katalogeintrag */ typedef struct < int fd; Dirent d; > DIR; DIR *opendir(char *dirname); Dirent *readdir(DIR *dfd); void closadir(DIR *dfd); Der Systemaufruf stat akzeptiert einen Dateinamen als Argument und liefert alle Information aus der Inode für diese Datei; bei Fehlern ist das Funktionsresultat -1, sonst 0. char »name; struct stat stbuf; int stat(char *, struct stat *); stat(name, &stbuf); Dieser Aufruf füllt die Struktur stbuf mit der Inode -Information für die Datei name. Die Struktur, die das Resultat von stat beschreibt, befindet sich in < sys/stat.h > und sieht ty- typischerweise so aus: struct stat /* von stat gelieferte Inode-Information */ dev_t st_dev; /* Geraet der Inode */ ino_t st_ino; /* Inode-Nummer */ short stjnode; /* Datei typ, Zugriffsschutz */ short st_nlink; /* Anzahl Pfade zur Datei */ short st_uid; /* Nummer des Dateibesitzers */ short stgid; /* Nummer der Gruppe des Dateibesitzers */ dev_t st_rdev; /* bei Geraeten */ off_t st_sjze; /* Dateigroesse (in Bytes) */ tima_t st_atima; /* Zeitpunkt des letzten Zugriffs */ tima_t stjntima; /* Zeitpunkt der letzten Aenderung */ tima_t st_ctima; /* Zeitpunkt der letzten Inode-Aenderung */ Die meisten Werte werden in den Kommentaren erklärt. Datentypen wie dev_t und ino_t sind in < sys/types.h > definiert; diese Definitionsdatei muß auch eingefügt werden. Die Komponente st mode enthält eine Reihe von Bits, die die Datei beschreiben. Die Definitionen dieser Bits befinden sich auch in < sys/stat.h >; wir brauchen nur den Teil, der sich mit dem Dateityp befaßt: «define #define #define «define #define S_IFMT 0160000 SJFDIR 0040000 S IFCHR 0020000 s'lFBLK 0060000 s'lFREG 0100000 /* Datei typ: */ /* Katalog V /* Geraet mit Zeichenprotokoll */ /* Geraet mit Blockprotokoll */ /* normale Datei */
174 8 Die Schnittstelle zuc >1IX-Betriebssystem Jetzt können wir das Programm fsize schreiben. Wenn aus der Komponente stjnode nach dem Systemaufruf stat hervorgeht, daß die Datei kein Katalog ist, dann haben wir die Dateigröße und können sie direkt ausgeben. Ist die Datei jedoch ein Kata- Katalog, dann müssen wir sie, eine Datei auf einmal, bearbeiten; dabei können wiederum Ka- Kataloge auftreten, so daß der Vorgang rekursiv ist. Das Hauptprogramm bearbeitet die Argumente aus der Kommandozeile; jedes Argument wird an die Funktion fsize übergeben. «include <stdio.h» «include <string.h> «include "syscalls.h" «include <fcntt.h> /* Bits fuer Lesen und Schreiben •/ «include <sys/types.h> /* Systemtypen */ «include <sys/stat.h> /* stat-Resultatstruktur */ «include "dirent.h" void fsize(char *); /* Dateigroessen ausgeben */ main(int arge, char **argv) { if (arge == 1) /* Voreinstellung: momentaner Katalog */ fsize("."); eise while (—arge > 0) fsize(*++argv); return 0; > Die Funktion fsize gibt die Dateigröße aus. Ist die Datei jedoch ein Katalog, so ruft fsize zuerst dirwalk auf, um alle Dateinamen im Katalog zu bearbeiten. Beachten Sie, wie die Bit-Definitionen S_IFMT und S IFDIR aus < sys/stat.h > verwendet werden, um zu entscheiden, ob die Datei ein Katalog ist. Die Klammern sind wichtig, da der Vor- Vorrang von & kleiner ist als der von ==. int stat(char *, struct stat *); void dirwalk(char *, void (*fcn)(char *)); /* fsize: gib Groesse der Datei "nama" aus */ void fsize(char *name) { struct stat stbuf; if (stat(name, &stbuf) " -1) < fprintf(stderr, "fsize: can't access Xs\n", name); return; > if ((stbuf.stjnode & S_IFMT) =» SJFDIR) dirwaUCname, fsize); printf("X8ld Xs\n", stbuf.st_size, name); > Die Funktion dirwalk ist eine allgemeine Routine, die eine Funktion auf jede Datei in einem Katalog anwendet. Sie eröffnet den Katalog, durchläuft alle Dateinamen darin und ruft die Funktion für jeden auf, schließt dann den Katalog und hört auf. Da fsize dirwalk für jeden Katalog aufruft, rufen sich die beiden Funktionen gegenseitig rekursiv auf.
8.6 Beispiel: Katal •»■ ausgeben 175 «define MAXPATH 1024 /* dirwalk: fcn auf alle Dateien in dir anwenden */ void dirwalk(char «dir, void (*fcn)(char •)) t char nametMAX_PATH3; Di rent *dp; DJR *dfd; if ((dfd * opendir(dir)) == NULL) i fprintf(stderr, "dirwalk: can't open Xs\n", dir); return; > while ((dp • readdir(dfd)) I* NULL) < /* diesen Katalog und Vorgaenger ueberspringen */ if (strcmp(dp->name, ".«) « 0 || strcmp(dp->name, "..") « 0) continue; if (strlen(dir)+strlen(dp->name)+2 > sizeof(name)) fprintf(stderr, "dirwalk: name Xs/Xs too long\n", dir, dp->name); eise < sprintf(name, "Xs/Xs", dir, dp->name); (*fcn)(name); closadir(dfd); > Jeder Aufruf von readdir liefert einen Zeiger auf Information über die nächste Datei oder NULL, wenn keine Dateien mehr übrig sind. Jeder Katalog enthält immer auch Ein- Einträge für sich selbst, nämlich „.", und für seinen Vorgänger „.."; diese Einträge müssen übersprungen werden, weil das Programm sonst ewig laufen würde. Bis zu dieser Ebene ist der Programmtext unabhängig davon, wie Kataloge ausse- aussehen. Als nächsten Schritt stellen wir minimale Verstonen von opendir, readdir und closedir für ein spezielles System vor. Die folgenden Funktionen sind für Version 7 und System V UNIX-Systeme; sie benutzen die Information über Kataloge aus der Definitions- Definitionsdatei < sys/dirJi >, die etwa so aussieht: tfifndef D1RSIZ «define DIRSIZ 14 «endif struct direct /* Katalogeintrag */ { ino_t d_ino; /* Inode-Nunmer */ char d_name[DIRSIZ]; /* ein langer Name hat kein '\0' */ >; Manche Versionen des Systems erlauben viel längere Namen und haben eine komplizier- kompliziertere Katalogstruktur. Der Datentyp ino t ist mit typedef vereinbart und beschreibt den Index in die Inode -Liste. Zufälligerweise ist er unsigned short bei dem System, das wir normalerwei- normalerweise benutzen, aber das ist nicht die Art von Information, die man in einem Programm ver- vergräbt; bei einem anderen System kann dies anders sein, also ist typedef besser. Einen kompletten Satz an „Systemtypen" findet man in < sys/types.h >.
176 8 Die Schnittstelle zu TNiX-Betriebssystem opendir eröffnet den Katalog, prüft, daß es sich um einen Katalog handelt (diesmal mit dem Systemaufruf fstat, der wie stat funktioniert, nur daß er auf einen File-Deskrip- File-Deskriptor angewandt wird), legt eine Katalogstruktur an und speichert die Information: int fstat(int fd, struct stat *); /* opendir: einen Katalog fuer readdir-Aufrufe eroeffnen */ DIR *opendir(char *dirname) { int fd; struct stat stbuf; DIR *dp; if ((fd = open(dirname, O_RDONLY, 0)) « -1 || fstat(fd, istbuf) « -1 || (stbuf.stjnode & SJFMT) != S_IFDIR II (dp « (DIR *) malloc(sizeof(DIR))) « NULL) return NULL; dp->fd ■ fd; return dp; > closedir schließt die Katalogdatei und gibt den Speicherplatz wieder frei: /* closedir: Katalog sehliessen, der von opendir eroeffnet wurde */ void closedir(DIR *dp) { if (dp) { close(dp->fd); free(dp); Schließlich liest readdir jeden Katalogeintrag mit read. Ist ein Katalogeintrag im Augenblick unbenutzt (da ein Dateiname gelöscht wurde), so ist die Inode -Nummer Null und diese Position im Katalog wird übersprungen. Andernfalls werden Inode -Nummer und Dateiname in einer static-Struktur abgelegt, und ein Zeiger auf die Struktur wird an den Aufrufer geliefert. Jeder neue Aufruf überschreibt die vorhergehende Information, «include <sys/dir.h> /* lokale Katalogstruktur */ /* readdir: Katalogeintraege der Reihe nach lesen */ Dirent *readdir(DIR *dp) i struct direct dirbuf; /* lokale Katalogstruktur */ static Dirent d; /* Resultat: portable Struktur */ while (read(dp->fd, (char *) &dirbuf, sizeof(dirbuf)) ** sizeof(dirbuf)) f if (dirbuf.d_ino == 0) /• Eintrag unbenutzt •/ continue; d.ino = dirbuf.d_ino; strncpy(d.nama, dirbuf.d nama, DIRSIZ); d.nameCDIRSIZ] = ■\0'; 7* unbedingt abschliessen */ return &d; > return NULL;
8.7 Beispiel: Funl aen zur Speicherverwaltung 177 Auch wenn das fsize -Programm ziemlich speziell ist, illustriert es doch ein paar wichtige Ideen. Zunächst sehen wir, daß viele Programme keine „Systemprogramme" sind; sie benutzen nur Information, die durch das Betriebssystem unterhalten wird. Für solche Programme ist entscheidend, daß die Repräsentierung der Information nur in Standard-Definitionsdateien vorkommt und daß Programme diese Dateien einfügen und die Vereinbarungen nicht in sich selbst vergraben. Außerdem sehen wir, daß man mit ei- einer gewissen Sorgfalt eine Schnittstelle zu systemabhängigen Objekten schaffen kann, die selbst relativ systemunabhängig ist. Die Funktionen der Standard-Bibliothek sind gute Beispiele dafür. Aufgabe 8-5. Ändern Sie das/nz^-Programm so, daß die andere Information aus dem Inode -Eintrag auch ausgegeben wird. D 8.7 Beispiel: Funktionen zur Speicherverwaltung In Kapitel 5 haben wir eine sehr einfache Speicherverwaltung vorgestellt, die mit einem Stack arbeitete. Die Version, die wir jetzt schreiben, unterliegt keinen Einschrän- Einschränkungen. Aufrufe von malloc und Tree können in beliebiger Reihenfolge erfolgen; malloc verlangt vom Betriebssystem mehr Speicher, wenn dies notwendig wird. Diese Funktio- Funktionen illustrieren einige der Überlegungen, die man anstellen muß, wenn man ein maschi- maschinenabhängiges Programm einigermaßen maschinenunabhängig schreiben will, und sie zeigen eine Anwendung mit Strukturen, Unionen und typedef aus dem wirklichen Leben. malloc vergibt Speicher nicht aus einem bei der Übersetzung festgelegten Vektor, sondern aus Bereichen, die bei Bedarf vom Betriebssystem angefordert werden. Da an- andere Teile eines Programms auch Speicher anfordern können, ohne diese Speicherver- Speicherverwaltung aufzurufen, braucht der Speicher, den malloc verwaltet, nicht unbedingt zusam- zusammenzuhängen. Freier Speicher wird daher als Liste von freien Blöcken verwaltet. Jeder Block enthält eine Größenangabe, einen Zeiger auf den nächsten Block und den freien Speicher selbst. Die Blöcke werden in Reihenfolge aufsteigender Speicheradressen an- angeordnet und der letzte Block (die höchste Adresse) zeigt auf den ersten, freie Liste nicht frei nicht frei nicht frei nicht frei frei, kontrolliert von malloc nicht frei I in Gebrauch, kontrolliert von malloc "TTTTTTTTTTI nicht kontrolliert von malloc Wird Speicherplatz angefordert, so wird die freie Liste durchlaufen, bis ein Block geeigneter Größe gefunden wird. Diesen Algorithmus nennt man first fit (paßt zuerst), im Gegensatz zu best fit (paßt am besten), wo nach dem kleinsten Block gesucht wird, der die Anforderung erfüllt. Hat der Block genau die notwendige Größe, wird er aus der freien Liste entfernt und an den Benutzer geliefert. Ist der Block zu groß, wird er geteilt, der angeforderte Speicher wird an den Benutzer geliefert und der Rest bleibt in der frei-
178 8 Die Schnittstelle UNIX-Betriebssystem en Liste. Findet man keinen Block geeigneter Größe, wird ein neues, großes Stück vom Betriebssystem angefordert und in die freie Liste aufgenommen. Auch bei Freigabe wird die freie Liste durchsucht, um den neuen freien Block an der richtigen Stelle einzufügen. Hat der neue freie Block als Nachbarn einen freien Block, so werden die Nachbarn in einen einzigen Block zusammengefaßt, damit der Spei- Speicher nicht zu sehr fragmentiert wird. Die Nachbarschaft kann leicht festgestellt werden, da die freie Liste in Reihenfolge aufsteigender Speicheradressen unterhalten wird. In Kapitel 5 haben wir angedeutet, daß malloc Speicher liefern muß, der für die Objekte korrekt ausgerichtet ist, die darin gespeichert werden. Obgleich sich Maschinen unterscheiden, gibt es für jede Maschine einen Datentyp, der am meisten eingeschränkt ist: wenn dieser Typ bei einer bestimmten Adresse gespeichert werden kann, können dort auch alle anderen Datentypen gespeichert werden. Bei manchen Maschinen ist double der am meisten eingeschränkte Datentyp; bei anderen genügen int oder long. Ein freier Block enthält einen Zeiger auf den nächsten Block in der Kette, eine An- Angabe zur Größe des Blocks und schließlich den freien Bereich selbst. Wir nennen die In- Information am Anfang des Blocks den Header. Um die Ausrichtung zu vereinfachen, sind alle Blöcke Vielfache der Header-Größe und der Header selbst ist korrekt ausgerichtet. Dies erreichen wir mit Hilfe einer Union, die die gewünschte Struktur enthält sowie eine Alternative mit dem in bezug auf Ausrichtung am meisten eingeschränkten Datentyp; wir haben hier willkürlich long gewählt: typedef long Align; /* zur Ausrichtung auf long */ union header { /* Blockbeschreibung: */ struct { union header *ptr; /* naechster Block, falls auf freier Liste */ unsigned size; /* Groesse dieses Blocks */ > s; Align x; /* Ausrichtung erzwingen */ >; typedef union header Header; Die Alternative Align wird nie benutzt; sie erzwingt nur, daß jeder Header korrekt ausge- ausgerichtet ist. malloc rundet die gewünschte Anzahl Bytes auf die nächstmögliche Anzahl Header-Einheiten auf; der Block, der verwendet wird, enthält eine Einheit mehr für den Header selbst, und dieser Wert wird in der size-Komponente im Header abgelegt. malloc liefert einen Zeiger, der auf die freie Fläche zeigt, nicht auf den Header. Der Auf- Aufrufer kann alles mit dem angeforderten Speicher machen; wird aber etwas außerhalb des zugewiesenen Speichers abgelegt, so wird die Liste wahrscheinlich zerstört. zeigt auf nächsten freien Block / Länge ^ Adresse für den Benutzer Ein Block, wie ihn malloc liefert
8.7 Beispiel: Fui inen zur Speicherverwaltung 179 Die size-Komponente ist notwendig, da die von malloc verwalteten Blöcke nicht zusam- zusammenhängen müssen - man kann die Größen nicht mit Zeigerarithmetik berechnen. Die Variable base wird als Anfang benutzt. Hat freep den Wert NULL (dies ist beim ersten Aufruf von malloc der Fall), dann wird eine leere freie Liste konstruiert; die- diese enthält einen Block der Größe Null und der Header zeigt auf sich selbst. In jedem Fall wird dann die freie Liste durchsucht. Die Suche nach einem freien Block geeigneter Größe beginnt an dem Punkt (freep), wo der letzte Block gefunden wurde; diese Strate- Strategie verteilt die Fragmentierung gleichmäßiger. Wird ein Block gefunden, der zu groß ist, so wird das Ende des Blocks an den Benutzer vergeben; dann muß im Header des ur- ursprünglichen Blocks nur die size-Komponente geändert werden. Auf jeden Fall erhält der Benutzer einen Zeiger auf die freie Fläche innerhalb des Blocks, die eine Einheit hin- hinter dem Header beginnt. static Header bese; /* leere Liste zur Initialisierung */ static Header *freep * NULL; /* Anfang der freien Liste */ /* malloc: allgemein verwendbare Speicherverwaltung */ void *malloc(unsigned nbytes) i Header *p, »prevp; Header *morecore(unsigned); unsigned nunits; nunits - (nbytes+sizeof(Header)-1)/sizeof(Header) + 1; if ((prevp = freep) == NULL) { /* noch keine freie Liste */ bese.s.ptr « freep - prevp = &bese; bese.s.size = 0; for (p * prevp->s.ptr; ; prevp = p, p = p->s.ptr) { if (p->s.size >» nunits) { /* gross genug */ if (p->s.size ** nunits) /* passt genau */ prevp->s.ptr = p->s.ptr; eise { /* vom Ende her vergeben */ p->s.size -= nunits; p +* p->s.size; p->s.size * nunits; freep = prevp; return' (void *)(p+1); if (p ei freep) /* einmal rund um die freie Liste */ if ((p ■= morecore(nunits)) == NULL) return NULL; /* kein Platz mahr */ > > Die Funktion morecore beschafft Speicher vom Betriebssystem. Wie dies im De- Detail geschieht, ist von System zu System verschieden. Da die Anforderung von Speicher beim Betriebssystem relativ aufwendig ist, wollen wir das nicht bei jedem Aufruf von malloc tun, deshalb fordert morecore mindestens NALLOC Einheiten an; dieser größere Block wird dann je nach Bedarf in kleinere Teile zerlegt, morecore notiert die Größe in der size-Komponente und fügt den zusätzlichen Speicher mit einem Aufruf von free in die Verwaltung ein.
180 8 Die Schnittstelle zi JNIX-Betriebssystem Der UNIX-Systemaufruf sbrk(n) liefert einen Zeiger auf weitere n Bytes Speicher. sbrk liefert -1, wenn kein Speicherplatz mehr verfügbar ist; NULL wäre dafür ein besse- besserer Resultatwert gewesen. -1 muß in den Typ char • umgewandelt werden, damit mit dem Funktionswert verglichen werden kann. Auch hier machen Umwandlungsoperatio- Umwandlungsoperationen die Funktion einigermaßen immun gegenüber den Details der Repräsentierung von Zeigern bei verschiedenen Systemen. Eine Annahme bleibt jedoch noch immer - daß nämlich Zeiger auf verschiedene Blöcke, die von sbrk geliefert werden, sinnvoll vergli- verglichen werden können. Dies garantiert der Standard nicht; er läßt nur Vergleiche von Zei- Zeigern innerhalb eines Vektors zu. Deshalb ist diese Version von malloc nur portabel zwi- zwischen Maschinen, bei denen allgemeine Vergleiche von Zeigern sinnvoll sind, «define NALLOC 1024 /* minimal anzufordernde Anzahl Header */ /• morecore: vom System mehr Speicher verlangen */ static Header *morecore(unsigned nu) ( char *cp, *sbrk(int); Header *up; if (nu < NALLOC) nu = NALLOC; cp = sbrk(nu * sizeof(Header)); if (cp « (char *) -1) /* kein Platz mahr */ return NULL; up * (Header *) cp; up->s.size = nu; free((void *)(up+1)); return freep; > Tree selbst ist die letzte Funktion. Ausgehend von freep durchläuft sie die freie Li- Liste, um den Punkt zu finden, wo der freie Block eingefügt werden soll. Dies geschieht entweder zwischen zwei existenten Blöcken oder an einem Ende der Liste. In jedem Fall werden benachbarte freie Blöcke zusammengefaßt. Man muß nur dafür sorgen, daß die Zeiger auf die richtigen Objekte zeigen und daß die size-Komponenten korrekt bleiben. /* free: Block bei ap in freie Liste aufnehmen */ void free(void *ap) { Header *bp, *p; bp = (Header *)ap - 1; /* auf Blockbeschreibung zeigen */ for (p = freep; l(bp > p && bp < p->s.ptr); p = p->s.ptr) if (p >= p->s.ptr && (bp > p || bp < p->s.ptr)) break; /• freier Block am einen oder anderen Ende */ if (bp + bp->s.size « p->s.ptr) { /* mit rechtem Nachbarn kombinieren */ bp->s.size +» p->s.ptr->s.size; bp->s.ptr * p->s.ptr->s.ptr; } eise bp->s.ptr = p->s.ptr; if (p + p->s.size •• bp) { /* mit linkem Nachbern kombinieren */ p->s.size += bp->s.size; p->s.ptr = bp->s.ptr; > eise p->s.ptr = bp; freep = p;
8.7 Beispiel: Fur >nen zur Speicherverwaltung 181 Obgleich Speicherverwaltung implizit maschinenabhängig ist, illustrieren diese Funktionen, wie man die Maschinenabhängigkeit kontrollieren und in einen sehr kleinen Programmteil einschließen kann. Mit typedef und union wird das Ausrichtungsproblem gelöst (ausgehend von der Tatsache, daß sbrk einen geeigneten Zeiger liefert). Um- Umwandlungsoperationen sorgen dafür, daß Zeigerumwandlungen explizit geschehen, und sie werden sogar mit einer schlecht definierten Systemschnittstelle fertig. Obgleich sich die Details hier auf Speicherverwaltung beziehen, so ist die allgemeine Technik auch auf andere Situationen anwendbar. Aufgabe 8-6. Die Funktion calloc(n,size) aus der Standard-Bibliothek liefert einen Zei- Zeiger auf n Objekte mit der Größe size, wobei der Speicherbereich auf 0 initialisiert ist. Schreiben Sie calloc, wobei malloc entweder aufgerufen oder modifiziert wird. D Aufgabe 8-7. malloc akzeptiert eine Speicheranforderung, ohne die Größe auf Plausibi- lität zu überprüfen; free nimmt an, daß der angebotene Block eine korrekte size- Komponente enthält. Verbessern Sie diese Funktionen, so daß sie Fehler besser erken- erkennen. G Aufgabe 8-8. Schreiben Sie eine Funktion bfree(p.n), die einen beliebigen Block p mit n Bytes in die freie Liste aufnimmt, die malloc und free unterhalten. Mit Hilfe von bfree kann ein Benutzer jederzeit einen Vektor aus der Speicherklasse static oder extern zur freien Liste hinzufügen. D
183 A C-Sprachbeschreibung A.1 Einführung Dieser Anhang beschreibt die Programmiersprache C, wie sie durch den Entwurf definiert wird, der am 31. Oktober 1988 dem ANSI eingereicht wurde, um als .American National Standard for Information Systems - Programming Language C, X3.159-1989" genehmigt zu werden. Dieser Anhang ist eine Interpretation des vorgeschlagenen Stan- Standards, nicht der Standard selbst, obwohl sorgfältig darauf geachtet wurde, einen verläßli- verläßlichen Führer für die Sprache zu gestalten. Im großen und ganzen folgt dieser Anhang der groben Linie des Standards, der seinerseits wieder der Ersten Ausgabe dieses Buches folgt, aber die Gliederung unter- unterscheidet sich im Detail. Abgesehen davon, daß einige Grammatikregeln umbenannt sind und daß die Definitionen der lexikalischen Symbole und der Preprozessor nicht formali- siert sind, ist die hier angegebene Grammatik für die eigentliche Sprache äquivalent zu der im Standard. In diesem ganzen Anhang werden Kommentare so wie hier eingerückt und kleiner ge- geschrieben. Meistens heben diese Kommentare hervor, wie ANSl-StandardC von der Sprache abweicht, die in der Ersten Ausgabe definiert wurde, oder auch von Erweiterun- Erweiterungen, die später in verschiedenen Übersetzern eingeführt wurden. A.2 Lexikalische Konventionen Ein Programm besteht aus einer oder mehreren Übersetzungseinheiten (translation units), die in Dateien gespeichert sind. Es wird in verschiedenen Phasen übersetzt, die in §A.12 beschrieben werden. Die ersten Phasen nehmen einfache lexikalische Umwand- Umwandlungen vor, führen die Anordnungen der Zeilen aus, die mit # beginnen, und definieren und expandieren Makros. Wenn die in § A.12 beschriebene Arbeit des Preprozessors ab- abgeschlossen ist, liegt das Programm als Folge von Eingabesymbolen (tokens) vor. A-2.1 Eingabesymbole Es gibt sechs Klassen von Eingabesymbolen: Namen, reservierte Worte, Konstan- Konstanten, konstante Zeichenketten (string literals), Operatoren und übrige Trenner. Leerzei- Leerzeichen, Tabulatorzeichen, Vertikal-Tabulatoren, Zeilentrenner, Seitenvorschub sowie Kom- Kommentare, die nachstehend beschrieben werden, insgesamt als Zwischenraum bezeichnet, werden ignoriert, abgesehen davon, daß sie Eingabesymbole voneinander trennen. Zwi- Zwischenraum muß direkt benachbarte Namen, reservierte Worte und Konstanten trennen. Nachdem die Eingabe bis zu einem bestimmten Zeichen schon in Eingabesymbole zerlegt ist, wird als nächstes Eingabesymbol die längstmögliche Zeichenfolge aufgefaßt, die ein Eingabesymbol darstellen kann. K12 Kommentare Ein Kommentar wird durch die Zeichen /• eingeleitet und durch die Zeichen */ beendet. Kommentare dürfen nicht verschachtelt werden, und sie dürfen nicht innerhalb von Zeichenkonstanten oder konstanten Zeichenketten auftreten.
184 A jprachbeschreibung A23 Namen Ein Name besteht aus einer Folge von Buchstaben und Ziffern. Das erste Zeichen muß ein Buchstabe sein; dabei zählt der Unterstrich _ zu den Buchstaben. Groß-^und Kleinbuchstaben werden unterschieden. Namen können beliebig lang sein. Bei intern verwendeten Namen werden wenigstens die ersten 31 Zeichen unterschieden; Implemen- Implementierungen dürfen auch mehr Zeichen als signifikant ansehen. Zu den internen Namen gehören Makronamen im Preprozessor und alle anderen Namen, die keine externe Bin- Bindung besitzen (§ A.11.2). Namen mit externer Bindung sind mehr eingeschränkt: Imple- Implementierungen könnten hier gerade noch sechs Zeicheni unterscheidenjjnd sie^dürften Groß- und Kleinbuchstaben als gleichwertig betrachten. A-2.4 Reservierte Worte Die folgenden Worte sind reserviert und können nur mit ihrer vordefinierten Be- Bedeutung verwendet werden: auto default float long sizeof union break do for register static unsigned case double goto return struct void char else if short switch volatile const enum int signed typedef while continue extern Manche Implementierungen reservieren auch die Worte fortran und asm. Die reservierten Worte const, signed und volatile werden durch den ANSI-Standard neu eingeführt; enum und void kamen in der (englischen) Ersten Ausgabe noch nicht vor, sind aber allgemein gebräuchlich; entry war früher reserviert, wurde aber nie benutzt und ist nicht mehr reserviert. A-2.5 Konstanten Es gibt verschiedene Arten von Konstanten. Jede Konstante hat einen Datentyp; § A.4.2 diskutiert die elementaren Typen. constant: integer-constant character-constant floating-constant enumeration-constant A-2.5.1 Ganzzahlige Konstanten Eine ganzzahlige Konstante (integer constant) besteht aus einer Ziffernfolge. Sie wird normalerweise dezimal interpretiert. Falls die Folge mit der Ziffer 0 beginnt, wird sie oktal interpretiert, also zur Basis 8. Oktale konstanten dürfen die Ziffern 8 und 9 nicht enthalten. Falls die Folge^ mit Ox oder OX (Ziffer 0, Buchstabe x) beginnt, wird sie hexadezimal interpretiert, also zur Basis 16. Zu den hexadezimalen Ziffern gehören a (oder A) bis f (oder F) mit den Werten 10 bis 15. An die ganzzahlige Konstante kann der Buchstabe u oder U angehängt werden, der. angibt, daß die Konstante vorzeichenlos ist (unsigned). Außerdem kann der Buchstabe 1 oder L angehängt werden, der angibt, daß sie long ist.
A.2 Lexikalische I Petitionen 185 Der Typ einer ganzzahligen Konstanten hängt von ihrer Form, ihrem Wert und den angehängten Suffixen ab. (Typen werden in §A.4 diskutiert.) Ohne Suffixe und dezimal angegeben, hat die Konstante den ersten der folgenden Typen, in denen sie dargestellt werden kann: int, long Int oder unsigned long int. Ohne Suffixe und oktal oder hexade- hexadezimal angegeben, hat sie den ersten möglichen der folgenden Typen: int, unsigned int, long int oder unsigned long int. Mit dem Suffix u oder U ist sie unsigned int oder unsigned long int. Mit dem Suffix 1 oder L ist sie long int oder unsigned long int. Die Auslegung der Typen für ganzzahlige Konstanten geht wesentlich über die Erste Ausgabe hinaus, in der lediglich große ganzzahlige Konstanten als long angesehen wur- wurden. Die Suffixe n und U sind neu. AJ.5.2 Zeichenkonstanten Eine Zeichenkonstante (character constant) ist eine Folge von einem oder mehre- mehreren Zeichen, eingeschlossen in einfache Anführungszeichen, also zum Beispiel V. Der Wert einer Zeichenkonstanten mit einem einzigen Zeichen ist der numerische Wert des Zeichens im Zeichensatz der jeweiligen Maschine, auf der das Programm ausgeführt wird. Der Wert einer Zeichenkonstanten mit mehreren Zeichen ist implementierungsab- implementierungsabhängig. Zeichenkonstanten dürfen das Zeichen ' sowie Zeilentrenner nicht enthalten; um diese und bestimmte andere Zeichen darzustellen, können die folgenden Ersatzdarstel- Ersatzdarstellungen verwendet werden: Zeilentrenner NL(LF) \n Gegenschrägstrich \ w Tabulatorzeichen HT \t Fragezeichen ? \? Vertikal-Tabulator VT \v Anführungszeichen ' V backspace BS \b Doppelanführungszeichen " \" Wagenrücklauf CR \r oktale Zahl ooo \ooo Seitenvorschub FF \f hexadezimale Zahl hh \xhh Klingelzeichen BEL \a Die Ersatzdarstellung \ooo besteht aus einem Gegenschrägstrich \ gefolgt von 1, 2 oder 3 Oktalziffern, die als Wert des gewünschten Zeichens interpretiert werden. Ein häufiges Beispiel dieser Konstruktion ist das NUL-Zeichen \0 (dabei darf keine Ziffer folgen). Die Ersatzdarstellung \xhh besteht aus einem Gegenschrägstrich \ gefolgt von x und hexade- hexadezimalen Ziffern, die als Wert des gewünschten Zeichens interpretiert werden. Die An- Anzahl der Ziffern ist nicht beschränkt, aber das Resultat ist Undefiniert, wenn der resultie- resultierende Wert größer ist als der Wert des größten Zeichens. Wenn in einer Implementie- Implementierung der Typ char nicht vorzeichenlos ist, wird bei oktaler wie hexadezimaler Ersatzdar- Ersatzdarstellung das Vorzeichen des Werts so propagiert, als ob der Wert in char umgewandelt worden wäre. Wenn das Zeichen nach \ nicht eines der angegebenen ist, so ist das Re- Resultat Undefiniert. Bei manchen Implementierungen gibt es einen erweiterten Zeichensatz, der nicht im Typ char repräsentiert werden kann. Eine Konstante in diesem erweiterten Satz wird mit einem vorangestellten L geschrieben, zum Beispiel LV, und sie wird erweiterte Zei- Zeichenkonstante (wide character constant) genannt. Eine solche Konstante hat den Typ wchart, das ist ein ganzzahliger Typ, der in der Standard-Definitionsdatei <stddef.h> vereinbart ist. Wie bei gewöhnlichen Zeichenkonstanten können oktale oder hexadezi-
186 / -Sprachbeschreibung male Ersatzdarstellungen verwendet werden; das Resultat ist Undefiniert, wenn der ange- angegebene Wert nicht mit wchar_t repräsentiert werden kann. Manche dieser Ersatzdarstellungen sind neu, insbesondere die hexadezimale Zeichendar- Zeichendarstellung. Erweiterte Zeichenkonstanten sind ebenfalls neu. Die Zeichensätze, die nor- normalerweise in Amerika und West-Europa verwendet werden, können so codiert werden, daß sie in den char-Typ passen; wchar_t wurde hauptsächlich für asiatische Sprachen hinzugefügt. A-2.53 Gleitpunktkonstanten Eine Gleitpunktkonstante besteht aus einem ganzzahligen Teil, einem Dezimal- Dezimalpunkt, einem Dezimalbruch, dem Zeichen e oder E, einem ganzzahligen Exponenten mit optionalem Vorzeichen und einem optionalen Typ-Suffix, nämlich einem der Buchstaben f, F, I oder L. Ganzzahliger Teil und Dezimalbruch sind Ziffernfolgen. Entweder der ganzzahlige Teil oder der Dezimalbruch kann fehlen (aber nicht beide); entweder der Dezimalpunkt oder der Exponent beginnend mit e kann fehlen (aber nicht beide). Der Typ der Konstanten wird durch das Suffix bestimmt; F oder f machen sie zu float, die Suffixe L oder I machen sie zu long double; andernfalls hat sie den Typ double. Suffixe an Gleitpunktkonstanten sind neu. A-2.5.4 Aufzählungskonstanten Namen, die in Aufzählungen vereinbart werden (siehe §A.8.4), sind Konstanten vom Typ int. A-2.6 Konstante Zeichenketten Eine konstante Zeichenkette (string literal) ist eine Folge von Zeichen eingeschlos- eingeschlossen in Doppelanführungszeichen, also "...". Eine konstante Zeichenkette wirdjds Vektor von Zeichen mit Speicherklasse statte betrachtet (siehe unten § A.4) und mit den angege- angegebenen Zeichen initialisiert. Objjdentische_ konstante Zeichenketten verschieden ^ind, hängt von der Implementierung ab, und die Resultate eines Programms, das eine kon- konstante Zeichenkette zu ändern versucht, sind Undefiniert. Aufeinanderfolgende konstante Zeichenketten werden in eine einzige Zeichenkette zusammengefaßt. Nach allen Verkettungen wird ein NUL-Zeichen \0 an die Zeichenket- Zeichenkette angefügt, damit Programme, die die Zeichenkette absuchen, ihr Ende finden können. Um Zeilentrenner oder Doppelanführungszeichen in konstanten Zeichenketten darzu- darzustellen, gibt es die gleichen Ersatzdarstellungen wie bei Zeichenkonstanten; direkt dürfen sie nicht vorkommen. Wie Zeichenkonstanten werden auch konstante Zeichenketten in einem erweiter- erweiterten Zeichensatz mit einem vorangestellten L geschrieben, also L"...\ Erweiterte konstan- konstante Zeichenketten (wide-character string literals) werden als Vektoren von wchar_t be- betrachtet. Eine Verkettung von normalen und erweiterten konstanten Zeichenketten ist Undefiniert. Die Bestimmung, daß konstante Zeichenketten nicht verschieden sein müssen, und das Verbot, sie zu ändern, sind neu im ANSI-Standard, ebenso wie auch die Verkettung be- benachbarter konstanter Zeichenketten. Erweiterte konstante Zeichenketten sind neu.
A.3 Syntax-Schre: ise 187 A.3 Syntax-Schreibweise In der Syntax-Darstellung in diesem Anhang werden Grammatikbegriffe kursiv und reservierte Worte und Zeichen in Schreibmaschinenschrift geschrieben. Al- Alternative Formulierungen erscheinen normalerweise auf verschiedenen Zeilen; in ein paar Fällen steht auch eine längere Liste von kurzen Alternativen auf einer Zeile, vor der dann „eins von" steht. Ein optionales Symbol ist mit dem Index ,x>pt" markiert: { expression^ ) bezeichnet zum Beispiel die optionale Angabe von expression, umgeben von geschweiften Klammern. Die Syntax wird in § A.13 zusammengefaßt. Anders als in der Grammatik in der (englischen) Ersten Ausgabe, werden in der vorlie- vorliegenden Grammatik Vorrang und Assoziativität von Operatoren in Ausdrücken explizit dargestellt. A.4 Die Bedeutung von Namen Namen {identifier) bezeichnen eine Vielzahl von Dingen: Funktionen, Etiketten (tags) von Strukturen, Unionen und Aufzählungen; Komponenten von Strukturen und Alternativen von Unionen; Aufzählungskonstanten; Typnamen und schließlich Objekte. Ein Objekt, manchmal auch Variable genannt, ist ein Speicherbereich, und seine Inter- Interpretation hängt von zwei hauptsächlichen Attributen ab: der Speicherklasse und dem Typ. Die Speicherklasse bestimmt die Lebensdauer des Speicherbereichs, der mit einem Objekt verbunden ist; der Typ bestimmt die Bedeutung der Werte, die in diesem Speicherbereich gefunden werden. Ein Name hat auch einen Gültigkeitsbereich, das ist der Teil des Programms, in dem der Name bekannt ist, und eine Bindung (linkage), von der abhängt, ob sich der gleiche Name in einem anderen Gültigkeitsbereich auf das glei- gleiche Objekt oder die gleiche Funktion bezieht. Gültigkeitsbereich und Bindung werden in § A.ll besprochen. A.4.1 Speicherklasse Es gibt zwei Speicherklassen: automatisch und statisch. Zusammen mit dem Kon- Kontext der Vereinbarung eines Objekts bestimmen mehrere reservierte Worte die Speicher- Speicherklasse. Automatische Objekte existieren lokal in einem Block (§A.9.3) und werden bei Verlassen des Blocks aufgehoben. Definitionen in einem Block erzeugen automatische Objekte, wenn keine Speicherklasse angegeben ist oder wenn auto verwendet wird. Mit register definierte Objekte sind automatisch und werden (falls möglich) in schnellen Hardware-Registern gespeichert. Statische Objekte können lokal in einem Block oder auch außerhalb von allen Blöcken vereinbart werden, aber sie behalten in beiden Fällen ihre Werte bei Verlassen von und Wiedereintritt in Funktionen und Blöcke. In einem Block, und auch in dem äußersten Block einer Funktion, werden statische Objekte mit static definiert. Objekte, die außerhalb von allen Blöcken, auf der gleichen Ebene wie Funktionen, definiert wer- werden, sind immer statisch. Mit static können sie lokal für eine Übersetzungseinheit ver- vereinbart werden; dadurch erhalten sie interne Bindung (internal linkage). Für ein ganzes Programm werden sie global bekannt, wenn keine Speicherklasse explizit angegeben ist oder durch Verwendung von extern; dadurch erhalten sie externe Bindung (external linkage).
188 A_ 'prachbeschreibung A.4.2 Elementare Datentypen Es gibt mehrere elementare Datentypen. Die Standard-Definitionsdatei < limits.h >, die im Anhang B beschrieben wird, definiert die größten und kleinsten Wer- Werte für jeden Typ in der lokalen Implementierung. Die im Anhang B angegebenen Zahlen sind die kleinstmöglichen Größen. Als char vereinbarte Objekte sind groß genug für jedes Zeichen aus dem Zeichen- Zeichensatz der Maschine, auf der das Programm ausgeführt wird. Wenn wirklich ein Zeichen aus diesem Zeichensatz in einem char-Objekt gespeichert wird, entspricht sein Wert dem ganzzahligen Code dieses Zeichens und ist nicht negativ. In char-Variablen können auch andere Werte gespeichert werden, aber der mögliche Wertebereich und insbesondere, ob der Wert vorzeichenbehaftet ist, hängt von der Implementierung ab. Vorzeichenlose Zeichen, die als unsigned char vereinbart werden, benötigen eben- ebensoviel Platz wie einfache Zeichen, aber sie erscheinen niemals negativ; explizit mit Vor- Vorzeichen versehene Zeichen, die als signed char vereinbart werden, benötigen ebenfalls gleichviel Platz wie einfache Zeichen. unsigned char erscheint nicht in der Ersten Ausgabe, ist aber allgemein in Gebrauch. signed char ist neu. Neben den char-Typen gibt es bis zu drei Größen von ganzzahligen Objekten, im folgenden Integer genannt, die als short int, int und long int vereinbart werden. »Einfa- »Einfache" int-Objekte haben eine der jeweiligen Maschine entsprechende, natürliche Größe; die anderen Größen s_ind fürspeziellelErfordernisse gedacht. Längere Integer-Objekte bieten wenigstens soviel Speicherplatz wie die kürzeren, aber die Implementierung kann einfache Integer-Objekte äquivalent zu short int oder zu long int machen. Falls nicht an- anders angegeben, repräsentieren alle int-Typen Werte mit Vorzeichen. Vorzeichenlose Integer-Werte, die mit unsigned vereinbart werden, gehorchen den üblichen arithmetischen Regeln modulo 2", wobei n durch die Anzahl Bits in der Reprä- Repräsentierung festgelegt wird. Arithmetik mit vorzeichenlosen Werten kann deshalb niemals zu Overflow führen. Die Menge der nicht-negativen Werte, die in einem vorzeichenbe- vorzeichenbehafteten Objekt gespeichert werden kann, ist eine Untermenge der Werte, die im ent- entsprechenden vorzeichenlosen Objekt gespeichert werden können, und in beiden gemein- gemeinsam vorhandene Werte werden gleich repräsentiert. Jeder der Gleitpunkttypen, mit einfacher (float), doppelter (double) oder zusätzli- zusätzlicher Genauigkeit (long double), kann zu anderen synonym sein, aber die, die in dieser Liste später auftauchen, sind wenigstens so genau wie die früheren. long double ist neu. In der Ersten Ausgabe war long float äquivalent zu double, aber diese Umschreibung ist nicht mehr erlaubt. Aufzählungen {enumerations) sind eindeutige Typen, die ganzzahlige Werte besit- besitzen; zu jeder Aufzählung gehört eine Menge benannter Konstanten (§A.8.4). Aufzäh- Aufzählungen verhalten sich wie Integer-Werte, aber ein Übersetzer wird häufig eine Warnung ausgeben, wenn an ein Objekt mit einem bestimmten Aufzählungstyp etwas anderes zu- zugewiesen wird, als eine seiner Konstanten oder ein Ausdruck mit dem gleichen Typ. Die bisher besprochenen Typen werden als arithmetische Typen bezeichnet, da ihre Objekte als numerische Werte interpretiert werden können, char- und int-Typen aller
A.5 Objekte und" rerte 189 Größen, mit und ohne Vorzeichen, wie auch Aufzählungstypen, werden im folgenden ins- insgesamt als Integer-Typen bezeichnet, float, double und long double werden Gleitpunkttypen genannt. Der Typ void bezeichnet eine leere Menge von Werten. Er wird als Resultattyp von Funktionen verwendet, die keine Werte erzeugen. A.43 Abgeleitete Typen Zusätzlich zu den elementaren Typen kann eine praktisch unbegrenzte Zahl abge- abgeleiteter Typen aus den elementaren Typen folgendermaßen konstruiert werden: Vektoren (arrays) von Objekten mit einem bestimmten Typ; Funktionen, die Objekte eines bestimmten Typs liefern; Zeiger auf Objekte eines bestimmten Typs; Strukturen, die eine Folge von Objekten verschiedener Typen enthalten; Unionen, die jeweils ein Objekt aus einer Reihe verschiedener Typen enthalten. Diese Methoden zur Konstruktion von Objekten können überall rekursiv angewendet werden. A.4.4 Attribute für Typen Der Typ eines Objekts kann zusätzliche Attribute (qualifiers) besitzen. Wenn ein Objekt als const vereinbart wird, bedeutet das, daß sein Wert nicht verändert wird; volatile bedeutet, daß ein Objekt spezielle Eigenschaften besitzt, die bei Optimierung berücksichtigtwj^d«Ljnüssen. Kein Attribut beeinflußt den Wertebereich oder die arithmetischen Eigenschaften eines Objekts. Attribute werden in § A.8.2 besprochen. A.5 Objekte und L-Werte Ein Objekt ist ein benannter Speicherbereich; ein L-Wert ist ein Ausdruck, der ein Objekt bezeichnet. Ein triviales Beispiel für einen L-Wert-Ausdruck ist ein Name mit geeignetem Typ und passender Speicherklasse. Es gibt Operatoren, die L-Werte liefern: ist etwa E ein Ausdruck, der einen Zeigerwert liefert, dann ist *E ein L-Wert-Ausdruck, der das Objekt bezeichnet, auf das E zeigt. Die Bezeichnung „L-Wert" erinnert an die Zuweisung El = E2, bei der der linke Ojerand El^ein L-Wert-Ausdruck sein muß. Bei der Beschreibung der Operatoren ist jeweils angegeben, ob ein Operator L-Werte als Operanden erwartet und ob er einen L-Wert als Resultat liefert. A.6 Typumwandlungen Einige Operatoren können, je nach ihren Operanden, implizite Typumwandlungen verursachen, bei denen der Wert eines Operanden aus einem Datentyp in einen anderen Typ umgewandelt wird. In diesem Abschnitt wird das Resultat erklärt, das von diesen Typumwandlungen erwartet werden kann. §A.6.5 faßt die Typumwandlungen zusam- zusammen, die die meisten gewöhnlichen Operatoren benötigen; eventuell nötige Ergänzungen findet man bei der Definition der einzelnen Operatoren. A.6.1 Integer-Erweiterung Ein Zeichen, ein kurzer Integer-Wert oder ein Integer-Bit-Feld, alle mit oder ohne Vorzeichen, sowie ein Objekt mit einem Aufzählungstyp, kann überall in einem Ausdruck
190 ._ ^-Sprachbeschreibung verwendet werden, wo ein Integer-Objekt benötigt wird. Wenn int alle Werte des ur- ursprünglichen Typs darstellen kann, wird der Wert in int umgewandelt; andernfalls wird der Wert in unsigned Int umgewandelt. Dieser Vorgang wird als Integer-Erweiterung {integralpromotion) bezeichnet. A.6.2 Integer-Umwandlung Ein Integer-Wert wird folgendermaßen in einen vorgegebenen vorzeichenlosen Typ umgewandelt: Man findet den kleinsten, nicht-negativen Wert, der kongruent zum Integer-Wert ist. Als Modulus dient dabei der größte Wert, der im vorzeichenlosen Typ dargestellt werden kann, plus eins. Bei Darstellung im 2-Komplement ist das äquivalent zum Abschneiden am linken (signifikanten) Ende, wenn das Bit-Muster des vorzeichen- vorzeichenlosen Typs schmäler ist, und zum Auffüllen von vorzeichenlosen Werten mit Null-Bits und zur Propagierung des Vorzeichens bei Werten mit Vorzeichen, wenn der vorzeichen- vorzeichenlose Typ breiter ist. Wenn ein beliebiger Integer-Wert in einen vorzeichenbehafteten Typ umgewandelt wird, bleibt der Wert unverändert, wenn er im neuen Typ dargestellt werden kann; an- andernfalls ist das Resultat implementierungsabhängig. A.63 Integer- und Gleitpunktwerte Wenn ein Gleitpunktwert in einen Integer-Typ umgewandelt wird, wird der Dezi- Dezimalbruch abgeschnitten; kann der Resultatwert nicht im Integer-Typ dargestellt werden, ist der Effekt Undefiniert. Insbesondere ist das Resultat unbestimmt, wenn negative Gleitpunktwerte in vorzeichenlose Integer-Typen umgewandelt werden. Wenn ein Wert aus einem Integer-Typ in einen Gleitpunkttyp umgewandelt wird und wenn der Wert im darstellbaren Bereich liegt aber nicht exakt dargestellt werden kann, kann das Resultat entweder der nächsthöhere oder der nächstniedrigere darstellba- darstellbare Wert sein. Liegt das Resultat nicht im zulässigen Bereich, ist der Effekt Undefiniert. A.6.4 Gleitpunkttypen Wenn ein weniger genauer Gleitpunktwert in einen gleich oder höher genauen Gleitpunkttyp umgewandelt wird, bleibt der Wert unverändert. Wenn ein Gleitpunktwert höherer Genauigkeit in einen Gleitpunkttyp mit geringerer Genauigkeit umgewandelt wird, und wenn der Wert im darstellbaren Bereich liegt, kann das Resultat entweder der nächsthöhere oder der nächstniedrigere darstellbare Wert sein. Liegt das Resultat nicht im zulässigen Bereich, ist der Effekt Undefiniert. A.6.5 Arithmetische Umwandlungen Viele Operatoren verursachen Typumwandlungen und liefern Resultattypen auf die gleiche Weise. Ihr Effekt besteht darin, daß die Operanden in einen gemeinsamen Typ umgewandelt werden, der dann auch der Resultattyp ist. Dieses Muster wird als übliche arithmetische Umwandlungen bezeichnet. Zuerst wird, wenn einer der beiden Operanden long double ist, der andere in long double umgewandelt. Andernfalls wird, wenn einer der beiden Operanden double ist, der andere in double umgewandelt.
A.6 Typumwar, igen 191 Andernfalls wird, wenn einer der beiden Operanden float ist, der andere in float um- umgewandelt. Andernfalls werden beide Operanden der Integer-Erweiterung unterworfen; wenn dann einer der beiden Operanden unsigned long int ist, wird der andere in unsigned long int umgewandelt. Andernfalls, wenn ein Operand long int und der andere unsigned int ist, hängt der Effekt davon ab, ob long int alle Werte von unsigned int darstellen kann. Falls ja, wird der eine Operand von unsigned int in long int umgewandelt; falls nein, werden beide in unsigned long int umgewandelt. Andernfalls, wenn ein Operand long int ist, wird der andere in long int umgewandelt. Andernfalls, wenn einer der beiden Operanden unsigned int ist, wird der andere in unsigned int umgewandelt. Andernfalls haben beide Operanden den Typ int. Hier gibt es zwei Änderungen. Erstens kann Arithmetik für (loat-Operanden mit einfa- cher Genauigkeit erfolgen, statt mit doppelter, die Erste Ausgabe verlangte, daßüüe Gleitpunktarithmetik mit doppelter Genauigkeit erfolgte. Zweitens resultieren aus vor- vorzeichenlosen kürzeren Typen, die mit größeren vorzeichenbehafteten Typen kombiniert werden, keine vorzeichenlosen Resultattypen; in der Ersten Ausgabe setzten sich die vor- vorzeichenlosen immer durch. Die neuen Regeln sind ein bißchen komplizierter, aber sie verringern bis zu einem gewissen Grad die Überraschungen, die sich ergeben, wenn ein vorzeichenloser Wert auf einen Vorzeichen behafteten Wert trifft. Unerwartete Ergebnis- Ergebnisse kann es noch immer geben, wenn ein vorzeichenloser Ausdruck mit einem vorzeichen- behafteten^Äusdruck^ejcher Größe verglichen wird. A.6.6 Zeiger und Integer-Werte Ein Ausdruck mit einem Integer-Typ kann zu einem Zeiger addiert oder von ihm subtrahiert werden; der Integer-Ausdruck wird dabei so umgewandelt, wie bei der Be- Beschreibung der Addition (§ A.7.7) erklärt wird. Zwei Zeiger auf Objekte vom gleichen Typ, im gleichen Vektor, können voneinan- voneinander subtrahiert werden; das Resultat wird so in einen Integer-Wert umgewandelt, wie bei der Beschreibung der Subtraktion (§ A.7.7) erklärt wird. Ein konstanter Integer-Ausdruck mit Wert 0 oder ein entsprechender Ausdruck, der in den Typ void * umgewandelt wurde, kann durch einen Umwandlungsoperator (cast), durch Zuweisung oder durch Vergleich in einen Zeiger auf einen beliebigen Typ umgewandelt werden. Dadurch entsteht ein Nullzeiger, der gleich jedem anderen Null- Nullzeiger auf den gleichen Typ ist, aber verschieden von jedem Zeiger auf eine Funktion oder ein Objekt. Bestimmte andere Typumwandlungen, bei denen Zeiger beteiligt sind, sind erlaubt, haben aber implementierungsabhängige Aspekte. Sie müssen durch einen expliziten Umwandlungsoperator angegeben werden (§§ A.7.5 und A.8.8). Ein Zeigerwert darf in einen Integer-Typ umgewandelt werden, der für ihn groß genug ist; die nötige Größe ist implementierungsabhängig. Die Abbildungsfunktion ist ebenfalls implementierungsabhängig.
192 A_ iprachbeschreibung Ein Objekt mit Integer-Typ kann explizit in einen Zeiger umgewandelt werden. Die Abbildung ist zwar implementierungsabhängig, führt aber einen Zeiger, der in einen genügend großen Integer-Typ umgewandelt wurde, wieder in den gleichen Zeiger zurück. Ein Zeiger auf einen Typ darf in einen Zeiger auf einen anderen Typ umgewandelt werden. Der resultierende Zeiger kann Adressierungsfehler verursachen, wenn der ur- ursprüngliche Zeiger nicht auf ein Objekt zeigt, das geeignet im Speicher ausgerichtet ist. Es wird garantiert, daß ein Zeiger auf ein Objekt in einen Zeiger auf ein Objekt und zurück umgewandelt werden darf, ohne daß sich etwas ändert, wenn das zweite Objekt eine weniger oder gleich einschränkende Ausrichtung benötigt. Das Konzept der .Aus- .Ausrichtung" (io/(gimenf)_ist implementierungsabhängig, aber die Objekte der char-Typen _haben die geringsten Einschränkungen hinsichtlich Ausrichtung. Wie in § A.6.8 beschrie- beschrieben, darf ein Zeiger auch in den Typ void * und zurück ohne Änderung umgewandelt werden. Ein Zeiger darf in einen anderen Zeiger umgewandelt werden, der den gleichen Typ besitzt, abgesehen davon, daß Attribute (§§ A.4.4, A.8.2) zum Objekttyp, auf den der Zeiger verweist, hinzugefügt oder davon entfernt werden. Werden Attribute hinzugefügt, ist der neue Zeiger äquivalent zum alten, abgesehen von Einschränkungen durch die neu- neuen Attribute. Werden Attribute entfernt, unterliegen die Operationen für das betroffene Objekt immer noch den Attributen der ursprünglichen Vereinbarung. Ein Zeiger auf eine Funktion kann schließlich in einen Zeiger auf einen anderen Typ von Funktion umgewandelt werden. Wie die Funktion über den umgewandelten Zei- Zeiger aufgerufen wird, ist implementierungsabhängig; wenn aber der umgewandelte Zeiger wieder in seinen ursprünglichen Typ zurück umgewandelt wird, ist das Resultat identisch mit dem ursprünglichen Zeiger. A.6.7 void Der (nicht-existente) Wert eines Objekts mit Typ void kann in keiner Weise verar- verarbeitet werden, und er darf weder explizit noch implizit in einen anderen Typ umgewan- umgewandelt werden. Da ein Ausdruck mit Typ void einen nicht-existenten Wert bezeichnet, kann ein solcher Ausdruck nur dort verwendet werden, wo der Wert nicht benötigt wird, zum Beispiel als Anweisung (§A.9.2) oder als linker Operand eines Komma-Operators (§A.7.18). Mit einem Umwandlungsoperator kann ein Ausdruck in den Typ void umgewan- umgewandelt werden, (void) als Umwandlungsoperator dokumentiert zum Beispiel, daß der Re- Resultatwert eines Funktionsaufrufs nicht beachtet wird, der als Anweisung verwendet wird, void erschien nicht in der (englischen) Ersten Ausgabe, aber ist bereits gebräuchlich. A.6.8 Zeiger auf void Jeder Zeiger auf ein Objekt darf in den Typ void • umgewandelt werden, ohne daß Information verlorengeht. Wenn das Resultat zurück in den ursprünglichen Zeigertyp umgewandelt wird, wird der ursprüngliche Zeigerwert wiederhergestellt. Anders als bei den Zeiger-zu-Zeiger-Umwandlungen, die in § A.6.6 besprochen wurden und die einen expliziten Umwandlungsoperator benötigen, können Zeiger von und an Zeiger mit Typ void * zugewiesen und mit solchen verglichen werden.
A.7 Ausdrücke 193 Diese Interpretation von void * ist neu; davor spielte char * die Rolle des generischen Zeigers. Der ANSI-Standard segnet explizit ab, daß void * auf Objektzeiger in Zuweisun- Zuweisungen und Vergleichen trifft, während bei anderen Zeigermischungen explizite Umwand- Umwandlungsoperatoren verlangt werden. A.7 Ausdrücke Die Unterabschnitte in diesem Abschnitt sind in bezug auf abnehmenden Vorrang dej;^^eratoren_angeordnet. So sind zum Beispiel die Ausdrücke, die als Operanden von + angegeben werden, genau die Ausdrücke, die in §§ A.7.1-A.7.6 definiert werden. In je- jedem Unterabschnitt haben die Operatoren gleichen Vorrang. Ob Operatoren links- oder rechts-assoziativ sind, ist in jedem Unterabschnitt für die dort beschriebenen Operatoren angegeben. Die Grammatik beinhaltet Vorrang und Assoziativität der Operatoren in Ausdrücken; sie ist in § A.13 zusammengefaßt. Vorrang und_Assoziativität der Operatoren ist vollständig festgelegt, aber die Rei- Reihenfolge, in der Ausdrücke bewertet werden, ist, mit wenigen Ausnahmen, Undefiniert, und das selbst dann, wenn Teilausdrücke Nebenwirkungen verursachen. Das heißt, falls die Definition eines Operators nicht garantiert, daß seine Operanden in einer bestimm- bestimmten Reihenfolge bewertet werden, bleibt es der Implementierung freigestellt, Operanden in jeder beliebigen Reihenfolge zu bewerten oder gar ihre Bewertungen überlappend vorzunehmen. Jeder Operator verbindet allerdings die Werte, die seine Operanden pro- produzieren, in einer Form, die kompatibel mit der Erkennung des Ausdrucks ist, in dem der Operator auftritt. Spät in ihren Verhandlungen beschloß die ANSI-Kommission, die bisherige Freiheit ein- einzuschränken, daß Ausdrücke umstrukturiert werden dürfen, wenn sie Operatoren enthal- enthalten, die zwar in der Mathematik assoziativ und kommutativ sind, die aber numerisch nicht assoziativ sind. In der Praxis betrifft diese Änderung nur Gleitpunktberechnungen an der Grenze ihrer Genauigkeit sowie Situationen, in denen Overflow möglich ist. Die Behandlung von Overflow, DKjsion durch Null und anderen Ausnahmen bei der Bewertung von Ausdrücken ist in der Sprache nicht definiert. Die meisten existieren- existierenden C-Implementierungen ignorieren Overflow bei der Auswertung von vorzeichenbehaf- vorzeichenbehafteten Integer-Ausdrücken und Zuweisungen, aber dieses Verhalten wird nicht garantiert. Die Behandlung von Division durch Null und allen Ausnahmen bei Gleitpunktrechnung variiert zwischen verschiedenen Implementierungen; manchmal kann sie durch nicht- standardisierte Bibliotheksfunktionen beeinflußt werden. A.7.1 Erzeugung von Zeigerwerten Ist der Typ eines Ausdrucks oder Teilausdrucks „Vektor von 7*" für irgendeinen Typ T, dann ist der Wert des Ausdrucks ein Zeiger auf das erste Objekt im Vektor, und der Typ des Ausdrucks wird abgeändert in „Zeiger auf T". Diese Typumwandlung findet nicht statt, wenn der Ausdruck als Operand des unären Adreß-Operators & oder der Operatoren ++, —, sizeof oder als linker Operand eines Zuweisungsoperators oder des Strukturoperators . angegeben ist. Analog wird ein Ausdruck vom Typ „Funktion mit Resultattyp T", der nicht als Operand des Adreß-Operators & verwendet wird, umge- umgewandelt in „Zeiger auf Funktion, die T liefert". Ein Ausdruck, der derartig umgewandelt wurde, ist kein L-Wert.
194 C-Sprachbeschreibung A.7.2 PrimärausdrQcke Primärausdrücke sind Namen, Konstanten, konstante Zeichenketten oder Aus- Ausdrücke in Klammern. primary-expression: identifier constant string ( expression ) Ein Name ist ein Primärausdruck, vorausgesetzt, er wurde geeignet vereinbart, wie später noch ausgeführt wird. Sein Typ wird durch seine Vereinbarung festgelegt. Ein Name ist ein L-Wert, wenn er sich auf ein Objekt (§ A.5) bezieht und wenn der Typ arith- arithmetisch, eine Struktur, eine Union oder ein Zeiger ist. Eine Konstante ist ein Primärausdruck. Ihr Typ hängt von ihrer Form ab, wie in § A.2.5 besprochen. Eine konstante Zeichenkette ist ein Primärausdruck. Ihr Typ ist ursprünglich „char-Vektor" (bei erweiterten konstanten Zeichenketten „wchar t-Vektor"), aber auf Grund der Regel in § A.7.1 wird dies normalerweise in „Zeiger auf char" (wchar t) um- umgewandelt, und das Resultat ist ein Zeiger auf das erste Zeichen in der Zeichenkette. Die Typumwandlung erfolgt auch nicht bei bestimmten Initialisierungen; siehe § A.8.7. Ein Ausdruck in Klammern ist ein Primärausdruck, dessen Typ und Wert identisch zu denen des Ausdrucks ohne Klammern sind. Die Klammern beeinflussen nicht, ob der Ausdruck ein L-Wert ist. A.73 Postfix-Ausdrücke Postfix-Ausdrücke werden von links nach rechts zusammengefaßt. postfix-expression: primary-expression postfix-expression [ expression ] postfix-expression ( argument-expression-listopl ) postfix-expression . identifier postfix-expression -> identifier postfix-expression ++ postfix-expression — argument-expression-list: assignment-expression argument-expression-list , assignment-expression A.73.1 Vektorelemente Ein Postfix-Ausdruck gefolgt von einem Ausdruck in eckigen Klammern ist ein Postfix-Ausdruck und bezeichnet einen indizierten Vektorverweis {subscripted array reference). Einer der beiden Ausdrücke muß den Typ „Zeiger auf T" besitzen, dabei ist T irgendein Typ, und der andere Ausdruck muß einen Integer-Typ besitzen; der Typ des In- Indexausdrucks ist T. Der Ausdruck E1[E2] ist (nach Definition) identisch zu •((El) + (E2)). Dies wird in § A.8.6.2 weiter besprochen.
A.7 Ausdrücke 195 A.73.2 Funktionsaufrufe Ein Funktionsaufruf ist ein Postfix-Ausdruck, der sogenannte Funktionsbezeichner (function designator), gefolgt von einer Liste von Zuweisungsausdrücken (§A.7.17), die durch Komma getrennt und zusammen in Klammern eingeschlossen sind. Die Liste von Ausdrücken innerhalb der Klammern kann leer sein; die Werte der Ausdrücke sind die Funktionsargumente. Wenn der Postfix-Ausdruck aus einem Namen besteht, für den es im aktuellen Gültigkeitsbereich keine Vereinbarung gibt, wird der Name implizit so de- deklariert, als ob die Deklaration extern int idenrifieri); im innersten Block angegeben worden wäre, der den Funktionsaufruf enthält. Der Post- Postfix-Ausdruck (möglicherweise nach einer impliziten Deklaration und Erzeugung eines Zeigerwerts nach § A.7.1) muß den Typ „Zeiger auf Funktion, die T liefert" besitzen, für irgendeinen Typ T, und der Wert des Funktionsaufrufs hat den Typ T. In der Ersten Ausgabe war der Typ auf „Funktion" beschränkt, und ein expliziter Inhalts- Inhaltsoperator • war nötig, um Funktionen über Zeiger aufzurufen. Der ANSI-Standard segnet die Gepflogenheiten mancher existenter Übersetzer ab und erlaubt die gleiche Syntax zum Aufruf von Funktionen und von Funktionen, die durch Zeiger ausgewählt werden. Die ältere Syntax kann noch immer verwendet werden. Die Bezeichnung Argument wird für einen Ausdruck verwendet, der bei einem Funktionsaufruf übergeben wird; Parameter bezieht sich auf das Eingabeobjekt (oder sei- seinen Namen), das die Funktionsdefinition empfängt oder das in einer Funktionsdeklarati- Funktionsdeklaration beschrieben wird. Die Bezeichnungen Aktual- und Formalparameter dienen manch- manchmal zur gleichen Unterscheidung. In Vorbereitung für einen Funktionsaufruf wird von jedem Argument eine Kopie erzeugt; grundsätzlich werden nur Argumentwerte übergeben. Eine Funktion kann die Werte ihrer Parameterobjekte ändern, die Kopien der Argumentausdrücke sind, aber diese Änderungen können die Werte der Argumente nicht erreichen. Man kann jedoch einen Zeiger in der Absicht übergeben, daß die Funktion das Objekt ändert, auf das der Zeiger zeigt. Es gibt zwei Stile, in denen Funktionen deklariert werden können. Im neuen Stil sind die Typen der Parameter explizit angegeben, und sie sind Teil des Typs der Funktion; eine derartige Deklaration wird als Funktionsprototyp bezeichnet. Im alten Stil werden die Typen der Parameter nicht angegeben. Die Vereinbarung von Funktionen wird in §§ A.8.6.3 und A.10.1 besprochen. Wenn die für einen Aufruf geltende Funktionsdeklaration den alten Stil verwendet, wird eine voreingestellte Erweiterung der Argumente auf jedes Argument folgender- folgendermaßen angewendet: Integer-Erweiterung (§A.6.1) erfolgt für jedes Argument mit Integer-Typ, und jedes Argument mit Typ float wird in double umgewandelt. Der Effekt des Aufrufs ist Undefiniert, wenn die Anzahl der Argumente nicht mit der Anzahl der Pa- Parameter in der Funktionsdefinition übereinstimmt oder wenn der Typ eines Arguments nach Erweiterung nicht dem Typ des zugehörigen Parameters entspricht. Die Überein- Übereinstimmung der Typen hängt davon ab, ob die Funktion im neuen oder alten Stil definiert ist. Beim alten Stil werden der erweiterte Typ des Arguments aus dem Aufruf und der erweiterte Typ des Parameters verglichen; erfolgte die Definition im neuen Stil, muß der
196 / -Sprachbeschreibung erweiterte Typ des Arguments der Typ des Parameters selbst sein, der dann nicht erwei- erweitert wird._ Gilt für einen Aufruf eine Funktionsdeklaration im neuen Stil, dann werden die Ar- Argumente wie bei einer Zuweisung in die Typen der zugehörigen Parameter aus dem Funktionsprototyp umgewandelt. Die Anzahl der Argumente muß gleich der Anzahl der explizit beschriebenen Parameter sein, es sei denn, die Parameterliste in der Deklaration endet mit der Auslassung (, ...). In diesem Fall muß die Anzahl der Argumente gleich oder größer als die Anzahl der Parameter sein; auf Argumente, die nach den explizit be- beschriebenen Parametern folgen, wird die im vorigen Absatz beschriebene voreingestellte Argumenterweiterung angewendet. Verwendet die Definition der Funktion den alten Stil, dann muß der Typ jedes Parameters in dem Prototyp, der beim Aufruf sichtbar ist, mit dem entsprechenden Parameter in der Definition übereinstimmen, nachdem die Ar- Argumenterweiterung auf den Typ des Parameters in der Definition angewendet wurde. Diese Regeln sind außerordentlich kompliziert, denn sie müssen mit einer Mischung von Funktionen alten und neuen Stils fertigwerden. Wenn möglich, sollten Mischungen ver- vermieden werden. Die Reihenfolge, in der die Argumente bewertet werden, ist nicht festgelegt; man beachte, daß sich verschiedene Übersetzer hierin unterscheiden. Die Argumente und der Funktionsbezeichner werden jedoch vollständig bewertet, mit allen Nebenwirkungen, bevor die Ausführung der Funktion beginnt. Funktionen können immer auch rekursiv aufgerufen werden. A.733 Strukturverweise Ein Postfix-Ausdruck gefolgt von einem Punkt und einem Namen ist ein Postfix- Ausdruck. Der erste Postfix-Ausdruck muß eine Struktur oder eine Union sein und der Name muß eine Komponente in der Struktur oder eine Alternative in der Union benen- benennen. Der Wert ist der benannte Teil der Struktur oder Union, und sein Typ ist der Typ dieses Teils. Der Ausdruck ist ein L-Wert, wenn der erste Ausdruck ein L-Wert ist und wenn der Typ des zweiten Ausdrucks kein Vektortyp ist. Ein Postfix-Ausdruck gefolgt von einem Pfeil (konstruiert aus - und >) und einem Namen ist wieder ein Postfix-Ausdruck. Der erste Postfix-Ausdruck muß ein Zeiger auf eine Struktur oder Union sein, und der Name muß eine Komponente in der Struktur oder eine Alternative in der Union benennen. Das Resultat bezieht sich auf den benann- benannten Teil der Struktur oder Union, auf die der Zeigerausdruck zeigt, und sein Typ ist der Typ dieses Teils. Das Resultat ist ein L-Wert, wenn der Typ kein Vektortyp ist. Folglich ist der Ausdruck E1->MOS der gleiche wie (*E1).MOS. Struktur und Union werden in § A.8.3 besprochen. In der Ersten Ausgabe gab es bereits die Regel, daß ein Komponenten- oder Alternati- / venname in einem derartigen Ausdruck zu der Struktur oder Union gehören sollte, die im Postfix-Ausdruck erwähnt wurde; ein Hinweis gab allerdings zu, daß diese Regel nicht streng geprüft wurde. Neuere Übersetzer, und ANSI, erzwingen es. A.73.4 Postfix-Inkrementierung Ein Postfix-Ausdruck, dem der Operator ++ oder — folgt, ist ein Postfix-Aus- Postfix-Ausdruck. Der Wert des Ausdrucks ist der Wert des Operands. Nachdem der Wert festge-
A.7 Ausdrücke 197 halten ist, wird der Operand um 1 bei ++ inkrementiert oder bei — dekrementiert. Der Operand muß ein L-Wert sein; man beachte die Beschreibung der additiven Operatoren (§A.7.7) und der Zuweisung (§A.7.17) für weitere Einschränkungen in bezug auf den Operanden und Details der Operation. Das Resultat ist kein L-Wert. A.7.4 Unäre Operatoren Ausdrücke mit unären Operatoren werden von rechts nach links zusammengefaßt. unary-expression: postfix-expression ++ unary-expression — unary-expression unary-operator cast-expression sizeof unary-expression slzeof (type-name ) unary-operator: eins von &* + --! A.7.4.1 Präflx-Inkrementierung Ein unärer Ausdruck, dem der Operator ++ oder — vorausgeht, ist ein unärer Ausdruck. Der Operand wird bei ++ um 1 inkrementiert und bei — dekrementiert. Der Wert des Ausdrucks ist der Wert nach Inkrementierung (oder Dekrementierung). Der Operand muß ein L-Wert sein; man beachte die Beschreibung der additiven Opera- Operatoren (§A.7.7) und der Zuweisung (§A.7.17) für weitere Einschränkungen in bezug auf den Operanden und Details der Operation. Das Resultat ist kein L-Wert. A.7.4.2 Adreß-Operator Der unäre Operator & jjerechnet die Adresse seines Operanden. Der Operand muß ein L-Wert sein, der weder auf ein Bit-Feld noch auf ein Objekt verweist, das als register vereinbart wurde, oder er muß vom Typ Funktion sein. Das Resultat ist ein Zei- Zeiger auf das Objekt oder die Funktion, auf die der L-Wert verweist. Hat der Operand den Typ T, so hat das Resultat den Typ „Zeiger auf T". A.7.43 Inhaltsoperator Der unäre Operator • dient für Verweise und liefert das Objekt oder die Funktion, auf die sein Operand zeigt. Das Resultat ist ein L-Wert, wenn der Operand ein Zeiger auf ein Objekt ist, dessen Typ arithmetisch, eine Struktur, eine Union oder ein Zeigertyp ist. Hat der Operand den Typ „Zeiger auf T", so hat das Resultat den Typ T. A.7.4.4 Unärer Plus-Operator Der Operand des unären Operators + muß einen arithmetischen Typ besitzen und das Resultat ist der Wert des Operanden. Für einen Integer-Operanden findet Integer- Erweiterung statt. Der Typ des Resultats ist der Typ des erweiterten Operanden. Das unäre + ist neu im ANSI-Standard. Es wurde aus Symmetrie zum unären - hinzu- hinzugefügt.
198 "%Sprachbeschreibung A.7.4.5 Unärer Minus-Operator Der Operand des unären Operators - muß einen arithmetischen Typ besitzen und das Resultat ist der negative Wert des Operanden. Für einen Integer-Operanden findet Integer-Erweiterung statt. Der negative Wert einer vorzeichenlosen Größe wird dadurch berechnet, daß der erweiterte Wert vom größtmöglichen Wert des erweiterten Typs sub- subtrahiert und eins addiert wird; der negative Wert von Null ist jedoch Null. Der Typ des Resultats ist der Typ des erweiterten Operanden. A.7.4.6 1-KompIement-Operator Der Operand des Operators - muß einen Integer-Typ besitzen und das Resultat ist das 1-Komplement des Operanden. Integer-Erweiterung findet statt. Wenn der Ope- Operand vorzeichenlos ist, wird das Resultat dadurch berechnet, daß der Wert vom größt- größtmöglichen Wert des erweiterten Typs subtrahiert wird. Hat der Operand ein Vorzeichen, wird der erweiterte Operand in den zugehörigen vorzeichenlosen Typ umgewandelt, — wird angewandt und dann wird zurück in den Typ mit Vorzeichen umgewandelt. Der Typ des Resultats ist der Typ des erweiterten Operanden. A.7.4.7 Logische Negation Der Operand des Operators ! muß einen arithmetischen Typ besitzen oder ein Zei- Zeiger sein. Das Resultat ist 1, wenn der Wert des Operanden gleich 0 ist; andernfalls ist das Resultat 0. Der Typ des Resultats ist int. A.7.4.8 sizeof Der Operator sizeof liefert die Anzahl der Bytes, die benötigt werden, um ein Ob- Objekt mit dem Typ seines Operanden zu speichern. Der Operand ist entweder ein Aus- Ausdruck, der nicht bewertet wird, oder ein Typname in Klammern. Wird sizeof auf char angewendet, ist das Resultat 1; wird sizeof auf einen Vektor angewendet, ist das Resultat die Gesamtzahl der Bytes im Vektor. Wird sizeof auf eine Struktur oder Union ange- angewendet, ist das Resultat die Anzahl der Bytes im Objekt, inklusive etwaiger Bytes, die benötigt werden, damit das Objekt Element eines Vektors sein kann: die Größe eines Vektors mit n Elementen ist n mal die Größe eines Elements. Der Operator darf nicht auf ein Objekt vom Typ Punktion, auf einen unvollständigen Typ oder auf ein Bit-Feld angewendet werden. Das Resultat ist eine vorzeichenlose Integer-Konstante; der exakte Typ ist implementierungsabhängig. Die Standard-Definitionsdatei <stddef.h> (siehe Anhang B) definiert diesen Typ als sizet. A.7.5 Typumwandlungen Ein unärer Ausdruck, dem ein Typname in Klammern vorausgeht, wandelt den Wert des Ausdrucks in den angegebenen Typ um. cast-expression: unary-expression ( type-name ) cast-expression Diese Konstruktion wird als Umwandlungsoperation oder cast bezeichnet. Typnamen werden in § A.8.8 beschrieben. Die Effekte der Typumwandlungen werden in § A.6 be- beschrieben. Ein Ausdruck mit einer Umwandlungsoperation ist kein L-Wert.
A.7 Ausdrücke 199 A.7.6 Multiplikative Operatoren Die multiplikativen Operatoren *, / und % werden von links nach rechts zusam- zusammengefaßt. multiplicative-expression: cast-expression multiplicative-expression * cast-expression multiplicative-expression / cast-expression multiplicative-expression X cast-expression Die Operanden von • und / müssen arithmetische Typen besitzen; die Operanden von % müssen Integer-Typen besitzen. Für die Operanden finden die üblichen arithmeti- arithmetischen Umwandlungen statt und sie definieren den Typ des Resultats. Der binäre Operator • bezeichnet Multiplikation. Der binäre Operator / liefert den Quotienten und der Operator % liefert den Rest nach Division des ersten Operanden durch den zweiten; ist der zweite Operand 0, so ist das Resultat Undefiniert. Andernfalls gilt immer, daß (a/b)*b + a%b gleich a ist. Sind beide Operanden nicht-negativ, dann ist der Rest nicht-negativ und kleiner als der Divi- Divisor; falls nicht, wird nur garantiert, daß der absolute Wert des Rests kleiner ist als der ab- absolute Wert des Divisors. A.7.7 Additive Operatoren Die additiven Operatoren + und - werden von links nach rechts zusammengefaßt. Wenn die Operanden arithmetische Typen besitzen, werden die üblichen arithmetischen Umwandlungen vorgenommen. Für jeden Operator gibt es noch einige andere mögliche Typen. additive-expression: multiplicative-expression additive-expression + multiplicative-expression additive-expression - multiplicative-expression Der Operator + liefert die Summe seiner Operanden. Ein Zeiger, der auf ein Ob- Objekt in einem Vektor verweist, und ein Integer-Wert dürfen addiert werden. Dabei wird der Integer-Wert in eine relative Adresse verwandelt, indem er mit der Länge des Ob- Objekts multipliziert wird, auf das der Zeiger verweist. Die Summe ist ein Zeiger mit glei- gleichem Typ wie der ursprüngliche Zeiger, der auf ein anderes Objekt im gleichen Vektor verweist, das die entsprechende Adresse besitzt, relativ zum ursprünglichen Objekt. Wenn also P auf ein Objekt in einem Vektor zeigt, so zeigt P+l auf das nächste Objekt in diesem Vektor. Wenn die Summe nicht mehr in den Vektor zeigt und auch nicht auf die e^st^Position_nach_dem o^r^nJEnd^de_s_Y^kiprsLdajM ist das.Re^ Die Möglichkeit von Zeigern unmittelbar hinter das Ende eines Vektors ist neu. Sie le- legalisiert eine gebräuchliche Formulierung für eine Schleife über die Elemente in einem Vektor. Der Operator - liefert die Differenz seiner Operanden. Ein Integer-Wert darf von einem Zeiger subtrahiert werden, dann gelten Typumwandlungen und Bedingungen wie bei der Addition.
200 C-Sprachbeschreibung Wenn zwei Zeiger auf Objekte vom gleichen Typ subtrahiert werden, ist das Resul- Resultat ein Integer-Wert mit Vorzeichen, der den Abstand zwischen den Objekten repräsen- repräsentiert, auf die die Zeiger zeigen; Zeiger auf aufeinanderfolgende Objekte unterscheiden sich um 1. Der Resultattyp ist implementierungsabhängig, aber er ist als ptrdifl_t in der Standard-Definitionsdatei <stddef.h> vereinbart. Der Wert ist Undefiniert, wenn die Zeiger nicht auf Objekte im gleichen Vektor zeigen; wenn allerdings P auf das letzte Ob- Objekt im Vektor zeigt, dann hat (P+l) - P den Wert 1. A.7.8 SAj/f-Operatoren Die .r/wyi-Operatoren << und » werden von links nach rechts zusammengefaßt. Bei beiden Operatoren muß jeder Operand ein Integer-Wert sein und Integer-Erweite- rung findet statt. Der Resultattyp ist der Typ des erweiterten linken Operanden. Das Resultat ist Undefiniert, wenn der rechte Operand negativ ist oder wenn jder Wert des rechtenjOperanden^ichtJdeiner^ist ajsjdie Länge_desjinken Ogerandenjn Bits. shift-expression: additive-expression shift-expression « additive-expression shift-expression » additive-expression Der Wert von E1«E2 ist El (interpretiert als Bit-Muster) um E2 Bits nach links ver- verschoben; wenn kein Overflow eintritt, ist das äquivalent zu einer Multiplikation mit 2s2. Der Wert von E1>>E2 ist El um E2 Bit-Positionen nach rechts verschoben. Die Ver- Verschiebung nach rechts ist äquivalent zu einer Division durch 2s2, wenn El vorzeichenlos ist oder keinen negativen Wert hat; andernfalls ist das Resultat implementierungsabhän- implementierungsabhängig- A.7.9 Vergleiche Vergleiche werden zwar von links nach rechts zusammengefaßt, aber diese Eigen- Eigenschaft nutzt nichts; a<b<c wird als (a<b)<c bearbeitet und a<b hat entweder den Wert 0 oder 1. relational-expression: shift-expression relational-expression < shift-expression relational-expression > shift-expression relational-expression <- shift-expression relational-expression >— shift-expression Die Operatoren < (kleiner), > (größer), <= (kleiner oder gleich) und >= (größer oder gleich) liefern alle 0, wenn die angegebene Relation falsch ist, und 1, wenn die Relation vorliegt. Der Resultattyp ist Int. Die üblichen arithmetischen Umwandlungen werden auf Operanden mit arithmetischen Typen angewendet. Zeiger auf Objekte mit gleichem Typ (dabei werden Attribute ignoriert) dürfen verglichen werden; das Resultat wird durch die relative Position der Objekte im Adreßraum bestimmt, auf die die Zeiger verweisen. Zeigervergleich ist nur für Teile des gleichen Objekts definiert: wenn zwei Zeiger auf das gleiche einfache Objekt zeigen, sind sie gleich; wenn die Zeiger auf Komponenten der gleichen Struktur zeigen, gelten Zeiger auf später in der Struktur vereinbarte Komponen- Komponenten als größer; wenn die Zeiger auf Alternativen in der gleichen Union zeigen, sind sie
A.7 Ausdrücke 201 gleich; wenn Zeiger auf Elemente des gleichen Vektors zeigen, ist der Vergleich äquiva- äquivalent zum Vergleich der zugehörigen Indizes. Wenn P auf das letzte Element in einem Vektor zeigt, dann gilt P+l als größer, obgleich P+l nicht mehr in den Vektor zeigt. Andernfalls ist der Vergleich von Zeigern Undefiniert. Diese Regeln sind etwas weniger einschränkend als die Erste Ausgabe, denn sie erlauben Vergleiche von Zeigern auf verschiedene Teile einer Struktur oder Union. Sie erlauben außerdem den Vergleich mit einem Zeiger, der unmittelbar hinter einen Vektor zeigt. A.7.10 Äquivalenzvergleiche equality-expression: relational-expression equality-expression — relational-expression equality-expression ! - relational-expression Die Operatoren == (gleich) und != (nicht gleich) sind analog zu den anderen Ver- Vergleichsoperatoren in §A.7.9, abgesehen von ihrem geringeren Vorrang. (a<b== c<d liefert also 1 genau dann, wenn die beiden Vergleiche a<b und c<d das gleiche Resultat liefern.) Die Äquivalenzvergleiche folgen den gleichen Regeln wie die Vergleichsoperato- Vergleichsoperatoren, aber sie bieten zusätzliche Möglichkeiten: ein Zeiger darf mit einem konstanten Integer-Ausdruck mit Wert 0 oder mit einem Zeiger auf void verglichen werden. Siehe §A.6.6. A.7.11 UND-Verknüpfung von Bits AND-expression: equality-expression AND-expression & equality-expression Die üblichen arithmetischen Umwandlungen finden statt; für das Resultat werden die Bits der beiden Operanden in jeder Position der UND-Verknüpfung unterworfen. Der Operator darf nur auf Integer-Operanden angewendet werden. A.7.12 Exklusive ODER-Verknüpfung von Bits exclusive-OR-expression: AND-expression exclusive-OR-expression A AND-expression Die üblichen arithmetischen Umwandlungen finden statt; für das Resultat werden die Bits der beiden Operanden in jeder Position der exklusiven ODER-Verknüpfung unter- unterworfen. Der Operator darf nur auf Integer-Operanden angewendet werden. A.7.13 ODER-Verknüpfung von Bits inclusive-OR-expression: exclusive-OR-expression inclusive-OR-expression \ exclusive-OR-expression Die üblichen arithmetischen Umwandlungen finden statt; für das Resultat werden die Bits der beiden Operanden in jeder Position der inklusiven ODER-Verknüpfung unterwor- unterworfen. Der Operator darf nur auf Integer-Operanden angewendet werden.
202 / Sprachbeschreibung A.7.14 Logische UND-Verknüpfung logical-AND-expression: inclusive-OR-expression logical-AND-expression && inclusive-OR-expression Der Operator && wird von links nach rechts zusammengefaßt. Das Resultat ist 1, wenn beide Operanden nicht gleich 0 sind; sonst ist das Resultat 0. Anders als & werden &&- Operationen garantiert von links nach rechts durchgeführt: der erste Operand wird mit aflenTNeBenwirkungenTjerechnet; ist er 0, so ist der Wert de_s_Aj^racks_Öv_Ändernfklk wird der rechte Operand berechnet; wenn er 0 ist, ist der Wert des Ausdrucks 0 und sonst 1." " " "" " ' Die Operanden brauchen nicht den gleichen Typ zu besitzen, aber jeder Operand muß einen arithmetischen Typ besitzen oder ein Zeiger sein. Das Resultat ist int. A.7.15 Logische ODER-Verkniipfung logical-OR-expression: logical-AND-expression logical-OR-expression | | logical-AND-expression Der Operator 11 wird von links nach rechts zusammengefaßt. Das Resultat ist 1, wenn wenigstens einer der beiden Operanden nicht gleich 0 ist; sonst ist das Resultat 0. An- Anders als | werden 11 -Operationen garantiert von links nach rechts durchgeführt: der er- erste Operand wird mit allen Nebenwirkungen berechnet; ist er verschieden von 0, so ist der Wert des Ausdrucks 1. Andernfalls wird der rechte Operand berechnet; wenn er ver- verschieden von 0 ist, ist der Wert des Ausdrucks 1 und sonst 0. Die Operanden brauchen nicht den gleichen Typ zu besitzen, aber jeder Operand muß einen arithmetischen Typ besitzen oder ein Zeiger sein. Das Resultat ist int. A.7.16 Bedingter Ausdruck conditional-expression: logical-OR-expression logical-OR-expression 1 expression : conditional-expression Der erste Ausdruck wird mit allen Nebenwirkungen berechnet; falls er verschieden von 0 ist, ist das Resultat der Wert des zweiten Ausdrucks, sonst der des dritten Ausdrucks. In jedem Fall wird nur einer dieser beiden letzten Ausdrücke bewertet. Wenn der zweite und dritte Operand arithmetisch sind, werden die üblichen arithmetischen Umwandlun- Umwandlungen angewendet, um sie zu einem gemeinsamen Typ zu bringen, und dies ist der Typ des Resultats. Sind beide void, oder Strukturen oder Unionen mit gleichem Typ, oder Zeiger auf Objekte mit gleichem Typ, dann hat das Resultat den gemeinsamen Typ. Ist einer dieser Operanden ein Zeiger und der andere die Konstante 0, wird 0 in den Zeigertyp umgewandelt, und das Resultat hat diesen Typ. Ist einer dieser Operanden ein Zeiger auf void und der andere ist ein anderer Zeiger, wird dieser andere Zeiger in einen Zeiger auf void umgewandelt, und das ist der Typ des Resultats. Beim Vergleich der Typen für Zeiger sind Typattribute (§A.8.2) unerheblich im Typ, auf den der Zeiger zeigt, aber der Resultattyp erbt Attribute von beiden Zweigen der Bedingung.
A.7 Ausdrücke 203 A.7.17 Zuweisungen Es gibt verschiedene Zuweisungsoperatoren; alle werden von rechts nach links zu- zusammengefaßt. assignment-expression: conditional-expression unary-expression assignment-operator assignment-expression assignment-operator: eins von - *_ /- x~ +- — «- »- &- > |- Der linke Operand muß immer ein L-Wert sein, und der L-Wert muß modifizierbar sein: er darf kein_^ktor_sein, und er darf keinen unvollständigen Typ haben oder eine Funkti- Funktion sein. Sein Typ darf auch nicht das Attribut const haben; wenn er eine Struktur oder Union ist, darf er weder direkt noch rekursiv eine Komponente oder Alternative besit- besitzen, die das const-Attribut hat. Der Resultattyp einer Zuweisung ist der des linken Ope- Operanden, und der Wert ist der, der sich nach der Zuweisung lmTinken Operan3en~b"efin- det. Bei der einfachen Zuweisung mit = ersetzt der Wert des Ausdrucks den Wert des Objekts, das der L-Wert bezeichnet. Eine der folgenden Bedingungen muß gelten: bei- beide Operanden besitzen arithmetische Typen, dann wird der rechte Operand bei der Zu- Zuweisung in den Typ des linken Operanden umgewandelt; oder beide Operanden sind Strukturen oder Unionen gleichen Typs; oder ein Operand ist ein Zeiger und der andere ist ein Zeiger auf void; oder der linke Operand ist ein Zeiger und der rechte Operand ist ein konstanter Ausdruck mit Wert 0; oder beide Operanden sind Zeiger auf Funktionen oder Objekte, deren Typen gleich sind, abgesehen davon, daß beim rechten Operanden const oder volatile fehlen darf. Ein Ausdruck der Form El op = E2 ist äquivalent zu El = El op (E2), nur daß El nur einmal bewertet wird. A.7.18 Komma als Operator expression: assignment-expression expression , assignment-expression Zwei Ausdrücke, die durch Komma getrennt sind, werden von links nach rechts bewertet; der Wert des Unken Ausdrucks wird berechnet, aber nicht weiterverwendet. Typ und Wert des Resultats sind Typ und Wert des rechten Operanden. Alle Nebenwirkungen der Bewertung des Unken Operanden werden abgeschlossen, "bevor die Bewertung des rechten Operanden beginnt. In einem Kontext, in dem das Komma eine besondere Be- Bedeutung hat, wie etwa in der Liste von Argumenten für einen Funktionsaufruf (§ A.7.3.2) und in Listen von Initialisierungen (§ A.8.7), ist die notwendige syntaktische Gruppierung ein Zuweisungsausdruck, folglich kann das Komma als Operator nur innerhalb von Klammern auftreten; beispielsweise hat f(a, (t«3, t+2), o drei Argumente, von denen das zweite den Wert 5 hat.
204 / -Sprachbeschreibung A.7.19 Konstante Ausdrücke Syntaktisch ist ein konstanter Ausdruck ein Ausdruck, in dem nur eine Untermen- Untermenge der Operatoren verwendet werden darf: constant-expression: conditional-expression Ausdrücke, die ein konstantes Resultat liefern, werden an verschiedenen Stellen verlangt: nach case, zur Dimensionierung von Vektoren und als Längen von Bit-Feldern, als Wert einer Aufzählungskonstanten, bei Initialisierungen und in bestimmten Ausdrücken für den Preprozessor. Konstante Ausdrücke dürfen keine Zuweisungen, Inkrement- oder Dekrement- Operatoren, Funktionsaufrufe oder Komma-Operatoren enthalten, ausgenommen in ei- einem Operanden von sizeof. Wenn ein konstanter Ausdruck einen Integer-Wert besitzen muß, dann müssen seine Operanden Integer-, Aufzählungs-, Zeichen- oder Gleitpunkt- Gleitpunktkonstanten sein; Umwandlungsoperationen müssen Integer-Typen angeben und Gleit- Gleitpunktkonstanten müssen in Integer umgewandelt werden. Damit sind zwangsweise Vek- Vektor-, Inhalts-, Adreß- und Strukturkomponenten-Operationen nicht erlaubt. (Beliebige Operanden sind jedoch für sizeof erlaubt.) Konstante Ausdrücke in Initialisierungen sind weniger eingeschränkt; die Operan- Operanden können beliebige Konstanten sein, und der unäre Adreß-Operator & kann auf exter- externe oder statische Objekte sowie auf externe oder statische Vektoren mit einem konstan- konstanten Ausdruck als Index angewendet werden. Der unäre Adreß-Operator & kann auch implizit verwendet werden, dadurch daß Vektoren ohne Index sowie Funktionen auftre- auftreten. Der Wert einer Initialisierung muß entweder konstant sein oder die Adresse eines früher vereinbarten externen oder statischen Objekts plus oder minus eine Konstante. Konstante Integer-Ausdrücke nach #if sind mehr eingeschränkt: sizeof- Ausdrücke, Aufzählungskonstanten und Umwandlungsoperationen sind nicht erlaubt. Siehe §ÄTl2.5. A.8 Vereinbarungen Vereinbarungen legen die Interpretation der einzelnen vom Benutzer eingeführten Namen fest; sie reservieren nicht unbedingt den Speicherplatz, der zu einem Namen gehört. Vereinbarungen, die Speicherplatz reservieren, werden Definitionen genannt. Vereinbarungen haben folgende Form: declaration: declaration-specifiers init-declarator-listop, ; Die Deklaratoren in der init-declarator-list enthalten die Namen, die vereinbart werden; declaration-specifiers bestehen aus einer Folge von Angaben zu Typ und Speicherklasse. declaration-specifiers: storage-class-specifier declaration-specifiers op, type-specifier declaration-specifiersopl type-qualifier declaration-specifiers„p, init-declarator-list: init-declarator init-declarator-list , init-declarator
A.8 Vereinbart n 205 init-declarator: declarator declarator — initializer Deklaratoren werden später besprochen (§A.8.5); sie enthalten die Namen, die verein- vereinbart werden. Eine Vereinbarung muß wenigstens einen Deklarator enthalten, oder ihr type-specifier muß ein Struktur-Etikett (tag), ein Union-Etikett oder die Konstanten einer Aufzählung vereinbaren; leere Vereinbarungen sind nicht erlaubt. AJJ.l Speicherklassen Es gibt folgende Angaben zur Speicherklasse: storage-class-specifier: auto register static extern typedef Speicherklassen wurden in § A.4 erklärt. Die Angaben auto und register legen für die vereinbarten Objekte die automati- automatische Speicherklasse fest und können nur in Funktionen verwendet werden. Derartige Vereinbarungen sind gleichzeitig Definitionen und reservieren Speicherplatz. Eine register-Definition ist äquivalent zu einer auto-Definition, soll aber andeuten, daß häufig auf die vereinbarten Objekte zugegriffen wird. Nur wenige Objekte werden wirklich in Registern abgelegt, und nur bestimmte Typen eignen sich dazu; die Einschränkungen sind implementierungsabhängig. Wenn jedoch für ein Objekt register vereinbart wird, darf der unäre Adreß-Operator & weder explizit noch implizit darauf angewendet wer- werden. Die Regel, daß die Adresse eines Objekts nicht berechnet werden darf, für das register vereinbart wurde, das aber in Wirklichkeit als auto angelegt wurde, ist neu. Die Angabe static legt für die vereinbarten Objekte die statische Speicherklasse fest und kann innerhalb oder außerhalb von Funktionen verwendet werden. Innerhalb einer Funktion ist die Angabe eine Definition und sorgt für Speicherplatz; der Effekt außerhalb einer Funktion wird in § A.11.2 beschrieben. Innerhalb einer Funktion legt eine Vereinbarung mit extern fest, daß Speicherplatz für die vereinbarten Objekte an anderer Stelle definiert wird; der Effekt außerhalb einer Funktion wird in § A.11.2 beschrieben. Die Angabe typedef reserviert keinen Speicherplatz und wird nur zur Vereinfa- Vereinfachung der Syntax als Speicherklassenangabe bezeichnet; sie wird in § A.8.9 besprochen. In einer Deklaration darf höchstens ein storage-class-specifier angegeben werden. Ist nichts angegeben, gelten folgende Regem: für Objekte, die innerhalb einer Funktion vereinbart werden, wird auto angenommen; Funktionen, die innerhalb einer Funktion vereinbart werden, gelten als extern; Objekte und Funktionen, die außerhalb einer Funk- Funktion vereinbart werden, gehören zur statischen Speicherklasse mit externer Bindung. Sie- Siehe §§A.10-A.ll.
206 A ' T>rachbeschreibung AJU Typangaben Die Typangaben sind type-specifier. void char short Int long float double signed unsIgned struct-or-union-specifier enum-specifier typedef-name Höchstens eins der Worte long oder short darf zusammen mit int angegeben werden; die Bedeutung ist so, als ob int nicht angegeben wurde, long darf zusammen mit double an- angegeben werden. Höchstens eins der Worte signed oder unsigned darf zusammen mit int, oder mit einer seiner short oder long Varianten oder auch mit char angegeben wer- werden, unsigned oder signed können allein verwendet werden; dazu wird int angenommen. signed ist zweckmäßig, um zu erzwingen, daß char-Objekte vorzeichenbehaftet sind; bei anderen Integer-Typen ist signed zwar redundant, aber zulässig. Davon abgesehen darf höchstens ein type-specifier in einer Vereinbarung angege- angegeben werden. Fehlt ein type-specifier in einer Vereinbarung, so wird int angenommen. Typen können auch mit Attributen versehen werden, um besondere Eigenschaften der verebbarten Objekte auszudrücken. type-qualifier. const volatile Attribute können mit jeder Typangabe auftreten. Ein const-Objekt darf initialisiert wer- werden, darf aber anschließend nicht Ziel einer Zuweisung sein. Es gibt keine implementie- implementierungsunabhängige Semantik für volatile-Objekte. Die Eigenschaften const und volatile sind neu im ANSI-Standard. Mit const sollen Objek- Objekte eingeführt werden, die im schreibgeschützten Speicher angelegt werden lcönnen; außerdem können die Optimierungsmöglichkeiten verbessert werden, volatile dient da- dazu, in einer Implementierung ejine^^tjmiejTLinjj^verhindern zujcönnen, die sonst erfolgen könnte. Greift eine Maschine beispielsweise über spezielle Speicheradressen auf peri- phere Geräte zu, hat sie also memory-mapped input /output, so könnte ein Zeiger auf ein Geräteregister als volatile vereinbart werden, damit der Übersetzer nicht anscheinend redundante Verweise entfernt, die diesen Zeiger verwenden. Abgesehen davon, daß ein Übersetzer Fehlermeldungen ausgeben sollte, wenn explizit versucht wird, ein const- Objekt zu ändern, darf ein Übersetzer die Attribute ignorieren.
A.8 Vereinbari' n 207 AJJ3 Strukturen und Unionen Eine Struktur ist ein Objekt, das aus einer Folge von benannten Komponenten mit verschiedenen Typen besteht. Eine Union ist ein Objekt, das zu verschiedenen Zeiten ei- eine von mehreren Alternativen mit verschiedenen Typen enthält. Struktur- und Union- Vereinbarungen haben die gleiche Form. struct-or-union-specifier. struct-or-union identifieropl { struct-declaration-list } struct-or-union identifier struct-or-union: struct union Eine struct-declaration-list ist eine Folge von Deklarationen für die Komponenten der Struktur oder die Alternativen der Union: struct-declaration-list: struct-declaration struct-declaration-list struct-declaration struct-declaration: specifier-qualifier-list struct-declarator-list ; specifier-qualifier-list: type-specifier specifier-qualifier-list„p, type-qualifier specifier-qualifier-listgp, struct-declarator-list: struct-declarator struct-declarator-list , struct-declarator Normalerweise ist ein struct-declarator einfach ein Deklarator für eine Komponente oder Alternative. Eine Strukturkomponente darf auch aus einer explizit angegebenen Anzahl Bits bestehen. Eine solche Komponente wird auch als Bit-Feld (bit-field oder einfach field) bezeichnet. Die Feldlänge wird vom Deklarator für den Bit-Feld-Namen durch einen Doppelpunkt getrennt. struct-declarator: declarator declarator^ : constant-expression Eine Typangabe der Form struct-or-union identifier { struct-declaration-list ) vereinbart den identifier als Etikett (tag) der Struktur oder Union, die durch die Liste vereinbart wird. Eine nachfolgende Vereinbarung im gleichen oder einem inneren Gül- Gültigkeitsbereich kann auf den gleichen Typ bezugnehmen, indem nur noch das Etikett oh- ohne die Liste angegeben wird: struct-or-union identifier Erscheint_eine TiPaBgabe ™iLSJ55m Etikett aber ohne eine Liste, dann entsteht_ein un- unvollständiger Tjflp, wenn das Etikett nicht vereinbart ist. Objekte mit einem unvollständi-
208 A C rachbeschreibung gen Struktur- oder Union-Typ dürfen nur in Kontexten vorkommen, wo ihre Größe nicht benötigt wird, zum Beispiel in Deklarationen (aber nicht Definitionen), bei der Kon- Konstruktion eines Zeigers, oder wenn mit typedef ein Typ erzeugt wird, aber sonst nicht. Der Typ wird durch eine nachfolgende Typangabe mit dem Etikett vervollständigt, die ei- eine Liste von Deklarationen enthält. Auch in Typangaben mit einer Liste ist der Struktur- Strukturoder Union-Typ, der gerade vereinbart wird, innerhalb der Liste unvollständig, und er wird erst an der geschweiften Klammer } vollständig, die die Typangabe abschließt. Eine Struktur darf keine Komponente mit unvollständigem Typ enthalten. Deshalb kann keine Struktur oder Union vereinbart werden, die sich selbst enthält. Abgesehen davon, daß sie einen Struktur- oder Union-Typ mit einem Namen versehen, erlauben Eti- Etiketten die Definition von Strukturen, die auf sich selbst verweisen; eine Struktur oder Union kann einen Zeiger auf ihren eigenen Typ enthalten, denn Zeiger auf unvollständi- unvollständige Typen dürfen vereinbart werden. Eine ganz besondere Regel gilt für folgende Art von Deklaration stmct-or-union identifier ; die eine Struktur oder Union vereinbart, aber weder eine Liste von Deklarationen noch Deklaratoren enthält. Auch wenn der identifier ein Struktur- oder Union-Etikett ist, das bereits in einem äußeren Gültigkeitsbereich (§A.11.1) vereinbart wurde, macht diese Vereinbarung den Namen zum Etikett einer neuen, unvollständigen Struktur oder Union im aktuellen Gültigkeitsbereich. Diese spitzfindige Regel ist neu im ANSI-Standard. Sie soll dazu dienen, mit gegenseitig rekursiven Strukturen fertig zu werden, die in einem inneren Gültigkeitsbereich verein- vereinbart werden, deren Etiketten aber vielleicht schon im äußeren Gültigkeitsbereich verein- vereinbart wurden. Die Angabe einer Struktur oder Union mit einer Liste, aber ohne Etikett, erzeugt einen eindeutigen Typ; auf ihn kann direkt nur in der Vereinbarung bezug genommen werden, in der er vorkommt. Die Namen von Struktur- oder Union-Teilen und Etiketten können weder mitein- miteinander noch mit gewöhnlichen Variablen kollidieren. Ein Komponentenname darf nicht zweimal in der gleichen Struktur erscheinen, oder ein Alternativenname in einer Union, aber der gleiche Name kann in verschiedenen Strukturen oder Unionen benutzt werden. In der Ersten Ausgabe waren Namen in Strukturen oder Unionen nicht auf ihre Eltern bezogen. Diese Zuordnung wurde jedoch in Übersetzern lange vor dem ANSI-Standard üblich. Mit Ausnahme von Bit-Feldern können die Teile von Strukturen oder Unionen be- beliebige Typen besitzen. Eine Bit-Feld-Komponente (die keinen Deklarator haben muß, und die daher namenlos bleiben kann) besitzt den Typ int, unsigned int oder signed int und wird als ganzzahliges Objekt mit der angegebenen Anzahl Bits interpretiert; ob ein int-Feld vorzeichenbehaftet ist, ist implementierungsabhängig. Benachbarte Bit-Felder in Strukturen werden in implementierungsabhängigen Speicherstücken in einer imple- implementierungsabhängigen Richtung angelegt. Paßt ein Bit-Feld, das einem anderen Bit- Feld folgt, nicht in ein teilweise gefülltes Speicherstück, kann es auf zwei Stücke verteilt werden, oder der Rest des angefangenen Stücks kann ausgelassen werden {padding). Ein unbenanntes Bit-Feld mit Breite 0 erzwingt dies; damit beginnt das nächste Feld an der Kante des nächsten Speicherstücks.
A.8 Vereinbarun 209 Der ANSI-Standard macht Bit-Felder sogar noch mehr implementierungsabhängig, als das in der Ersten Ausgabe geschah. Es empfiehlt sich, die Sprachregeln zur Speicherung von Bit-Feldern ohne jede Einschränkung als implementierungsabhängig zu betrachten. Strukturen mit Bit-Feldern können als portable Methode verwendet werden, mit der man versucht, den Speicherbedarf einer Struktur zu verringern (mit dem wahrscheinli- wahrscheinlichen Kostenfaktor einer Vergrößerung des Programmtexts und der Ausführungszeit, die zum Zugriff auf die Bit-Felder benötigt werden), oder als nicht-portable Methode, um eine Speicheranordnung zu beschreiben, die auf der Bit-Ebene bekannt ist. Im zweiten Fall muß man die Regeln der lokalen Implementierung verstehen. Die Adressen der Komponenten einer Struktur werden in der Reihenfolge ihrer Vereinbarungen größer. Abgesehen von Bit-Feldern, wird eine Strukturkomponente auf eine Adresse ausgerichtet, die von ihrem T^p abhängt; eine Struktur kann deshälFjinbe- nannte Löcher jsnthalten. Wenn ein Zeiger auf eine Struktur in den Typ eines Zeigers auf ihre erste Komponente umgewandelt wird, verweist das Resultat auf die erste Kom- Komponente. Eine Union kann als Struktur angesehen werden, bei der alle Alternativen bei der relativen Adresse 0 beginnen, und die groß genug ist, um jede Alternative einzeln aufzu- aufzunehmen. Höchstens eine Alternative auf einmal kann zu jedem Zeitpunkt in einer Union gespeichert werden. Wenn ein Zeiger auf eine Union in den Typ eines Zeigers auf eine Alternative umgewandelt wird, verweist das Resultat auf diese Alternative. Ein einfaches Beispiel einer Strukturvereinbarung ist struct tnode { char tword[20]; int count; struct tnode *left; struct tnode 'right; >; Diese Struktur enthält einen Vektor mit 20 Zeichen, einen int-Wert und zwei Zeiger auf gleichartige Strukturen. Wenn diese Vereinbarung getroffen worden ist, vereinbart struct tnode s, *sp; s als derartige Struktur und sp als Zeiger auf eine derartige Struktur. Mit diesen Verein- Vereinbarungen verweist der Ausdruck sp->count auf die count-Komponente der Struktur, auf die sp zeigt; s.left bezeichnet den linken Unterbaum-Zeiger der Struktur s; und s.right->t«ord[0] verweist auf das erste Zeichen der Komponente tword im rechten Unterbaum von s. Allgemein darf auf eine Alternative einer Union nicht zugegriffen werden, wenn der Wert der Union nicht mit Hilfe dieser Alternative zugewiesen wurde. Eine besonde- besondere Garantie vereinfacht allerdings die Benutzung von Unionen: wenn eine Union mehre- mehrere JJtrukturen enthält, die sich eine gemeinsame Anfangsfolge teilen, und wenn die Union zur Zeit eine dieser Strukturen enthält, darf auf die gemeinsame Anfangsfolge von jeder der enthaltenen Strukturen zugegriffen werden. Folgendes Fragment ist beispielsweise legal:
210 / '-Sprachbeschreibung union { struct { int type; > n; struct { int type; int intnode; > ni; struct { int type; float floatnode; > nf; > u; U.nf.type « FLOAT; u.nf. floatnode = 3.U; if (u.n.type «« FLOAT) ... sin(u.nf.floatnode) ... AJJ.4 Aufzählungen Aufzählungen (enumerations) sind eindeutige Typen mit Werten aus einer Menge von benannten Konstanten, die Aufzählungskonstanten (enumerators) genannt werden. Aufzahlungen werden ähnlich formuliert wie Strukturen und Unionen. enum-specifier: enum identifier^ { enumerator-list} enum identifier enumerator-list: enumerator enumerator-list , enumerator enumerator identifier identifier - constant-expression Die Namen in einer enumerator-list werden als Konstantenvon^TypJnt vereinbart, und sie können überall auftretenTwöKonstanten verlangt sind. Gibt es keine Äufzählungs- konstanten mit =, dann beginnen die Werte der entsprechenden Konstanten bei 0 und sie werden in Schritten von 1 in der Reihenfolge der Vereinbarung von links nach rechts fortgezählt. Eine Aufzählungskonstante mit = definiert für den zugehörigen Namen den angegebenen Wert; nachfolgende Namen werden von dort ab mit aufsteigenden Werten vereinbart. Die Namen von Aufzählungskonstanten im gleichen Gültigkeitsbereich müssen alle voneinander und von Variablennamen verschieden sein, aber die Werte müssen nicht ver- verschieden sein. DieJEtoUe des Namens im enum-specifier ist analog zum Struktur-Etikett in einem struct-specifier; er bezeichnet eine bestimmte Aufzählung. Die Regeln für enum- specifiers mit und ohne Etikett und Liste sind die gleichen wie für Strukturen oder Unionen, nur gibt es keine unvollständigen Aufzählungen; in einem enum-specifier ohne Liste muß sich das Etikett auf einen Aufzählungstyp im Gültigkeitsbereich mit einer Li- Liste beziehen.
A.8 Verebbart n 211 Aufzählungen sind neu seit der (englischen) Ersten Ausgabe dieses Buches, aber sie sind schon seit einigen Jahren Teil der Sprache. AJ&S Deklaratoren Deklaratoren haben folgende Syntax: declarator: pointer^ direct-declarator direct-declarator: identifier ( declarator ) direct-declarator [ constant-expression^ ] direct-declarator ( parameter-type-list ) direct-declarator ( identifier-listopl ) pointer: * type-qualifier-listopl * type-quaUfier-listopl pointer type-qualifier-list: type-qualifier type-qualifier-list type-qualifier Die Struktur von Deklaratoren erinnert an Inhaltsoperationen, Funktionen und Vektor- Vektorausdrücke; sie werden in gleicher Weise zusammengefaßt. AJi.6 Die Bedeutung von Deklaratoren Eine Liste von Deklaratoren erscheint nach einer Folge von Angaben zu Typ und Speicherklasse. Jeder Deklarator vereinbart einen eindeutigen zentralen Namen, näm- nämlich den aus der ersten Alternative der Regel für direct-declarator. Die Angaben zur Spei- Speicherklasse beziehen sich direkt auf diesen Namen, aber der Typ hängt von der Form des Deklarators ab. Ein Deklarator gilt als Zusicherung, daß ein Objekt vom angegebenen Typ resultiert, wenn der Name aus dem Deklarator in einem Ausdruck auftritt, der die gleiche Form hat wie der Deklarator. Berücksichtigt man nur die Typangaben in einem declaration-specifier (§ A.8.2) und einen einzigen Deklarator, so hat eine Vereinbarung die Form „T D", wobei T ein Typ und D ein Deklarator ist. Der Typ, den der Name durch die verschiedenen Formen des Deklarators erlangt, wird mit dieser Notation induktiv beschrieben. Ist in einer Vereinbarung T D der Deklarator D nur ein einfacher Name, so ist der Typ des Namens T. Hat der Deklarator D in einer Vereinbarung T D die Form ( Dl ) dann ist der Typ des Namens in Dl der gleiche wie in D. Die Klammern ändern den Typ nicht, aber sie können den Vorrang bei komplexen Deklaratoren ändern.
212 A_ prachbeschreibung AJi.6.1 Deklaratoren für Zeiger Hat in einer Vereinbarung T D der Deklarator D die Form * type-qualifier-listgp, Dl und hat der Name in einer Vereinbarung T Dl den Typ ,Jype-modifier T", dann hat der Name von D den Typ ,jype-modifier type-qualifier-list Zeiger auf T". Folgen Attribute auf *, so beziehen sie sich auf den Zeiger selbst und nicht auf das Objekt, auf das der Zeiger zeigt. Als Beispiel betrachten wir die Deklaration int *»p[]; Hier spielt ap[ ] die Rolle von Dl; durch eine Vereinbarung „int ap[ ]" hätte ap den Typ „Vektor von int" (siehe unten), die type-qualifier-list ist leer und der type-modifier ist „Vektor von". Folglich gibt die tatsächliche Deklaration ap den Typ „Vektor von Zeigern auf int". Als weitere Beispiele vereinbaren int i, *pi, *const cpi ■ &i; const int ci = 3, *pci; eine int-Variable i und einen int-Zeiger pi. Der Wert des konstanten Zeigers cpi darf nicht geändert werden. Der Zeiger zeigt immer auf die gleiche Adresse; der Wert, auf den er zeigt, darf jedoch geändert werden. Die int-Variable ci ist konstant und darf nicht geändert werden (sie darf allerdings so wie hier initialisiert werden), pci hat den Typ „Zeiger auf const int" und pci selbst kann geändert werden und auf eine andere Adresse zeigen, aber der Wert, auf den pci zeigt, kann nicht durch eine Zuweisung über pci geän- geändert werden. AJJ.62 Deklaratoren für Vektoren Hat in einer Vereinbarung T D der Deklarator D folgende Form Dl [constant-expression„p,] und hat der Name in einer Vereinbarung T Dl den Typ ,Jtype-modifier T", dann hat der Name von D den Typ ,jtype-modifier Vektor von T". Ist eine constant-expression vorhan- vorhanden, muß sie einen Integer-Typ und einen positiven Wert besitzen. Fehlt die constant- expression , die die Vektorlänge angibt, so hat der Vektor einen unvollständigen Typ. Ein Vektor darf aus einem arithmetischen Typ, aus einem Zeiger, einer Struktur oder Union gebildet werden, sowie aus einem anderen Vektor (um einen mehrdimensio- mehrdimensionalen Vektor zu erzeugen). Jeder Typ, aus dem ein Vektor gebildet wird, muß vollstän- vollständig sein; er darf weder Vektor noch Struktur mit unvollständigem Typ sein. Daraus folgt, daß bei einem mehrdimensionalen Vektor nur die erste Dimensionierung fehlen darf. Der Typ eines Objekts mit unvollständigem Vektortyp wird durch eine andere, vollständi- vollständige Vereinbarung für das Objekt vervollständigt (§A.10.2), oder durch Initialisierung (§A.8.7). Zum Beispiel vereinbart float fa[17l, *afp[17]; einen Vektor mit float-Elementen und einen Vektor mit Zeigern auf float-Werte. Weiter vereinbart
A.8 Vereinbarun 213 static int x3d[3] [5] [7] ; einen statischen, dreidimensionalen Vektor von int-Werten mit Rang 3x5x7. Im Detail betrachtet, ist x3d ein Vektor mit drei Elementen; jedes Element ist ein Vektor aus fünf Vektoren; jeder der letzteren Vektoren hat sieben int-Elemente. Jeder der Ausdrücke x3d, x3d[i], x3d[i]|j], x3d[i]fj][k] kann vernünftigerweise in einem Ausdruck vorkom- vorkommen. Die ersten drei haben den Typ „Vektor", der letzte hat den Typ int. Exakter ist x3d[i] fj] ein Vektor mit 7 int-Elementen und x3d[i] ist ein Vektor mit 5 Vektoren von je 7 int-Elementen. Indizierte Vektorverweise sind so definiert, daß E1[E2] identisch mit »(E1+E2) ist. Trotz ihres asymmetrischen Aussehens ist die Indexoperation kommutativ. Wegen der Typumwandlungsregeln für + und Vektoren (§§A.6.6, A.7.1, A.7.7) bezieht sich E1[E2] auf das E2-te Element in El; dabei muß El ein Vektor und E2 ein Integer-Wert sein. In dem Beispiel ist x3d[i] fj] [k] äquivalent zu *(x3d[i] fj] + k). Der erste Teilaus- Teilausdruck x3d[i]fj] wird nach § A.7.1 in den Typ „Zeiger auf Vektor von int" umgewandelt; nach § A.7.7 gehört zur Addition eine Multiplikation mit der Größe eines int-Werts. Aus den Regeln folgt, daß Vektoren zeilenweise gespeichert werden (der letzte Index ändert sich am schnellsten) und daß die erste Dimensionierung in der Deklaration hilft, den Speicherplatzbedarf eines Vektors zu bestimmen, daß sie aber sonst an der Indexberech- Indexberechnung nicht beteiligt ist. AJi.63 Deklaratoren für Funktionen Hat in einer Funktionsvereinbarung neuen Stils T D der Deklarator D folgende Form Dl {.parameter-type-list') und hat der Name in einer Vereinbarung T Dl den Typ ,Jtype-modifier T", dann hat der Name von D den Typ ,-type-modifier Funktion mit Argumenten parameter-type-list und Resultattyp T". Die Parameter haben folgende Syntax: par am eter-type-list: parameter-list parameter-list , ... parameter-list: par am eter-declaration parameter-list , parameter-declaration parameter-declaration: declaration-specifiers declarator declaration-specifiers abstract-dectaratoropt Im neuen Stil der Vereinbarung legt die Parameterliste die Typen der Parameter fest. Als Sonderfall hat der Deklarator für eine Funktion im neuen Stil ohne Parameter als parameter-type-list nur das Wort void. Endet die parameter-type-list mit ", dann darf die Funktion mehr Argumente akzeptieren als Parameter explizit beschrieben sind; siehe §A.7.3.2.
214 : ,-Sprachbeschreibung Die Typen von Parametern, die Vektoren oder Funktionen sind, werden nach den Regeln für Parameterumwandlung in Zeiger abgeändert, siehe §A.10.1. Als einzige Speicherklassenangabe ist in einer Parameterdeklaration register erlaubt, und diese An- Angabe wird ignoriert, wenn der Funktionsdeklarator nicht eine Funktionsdefinition einlei- einleitet. Wenn die Deklamatoren bei Parameterdeklarationen Namen enthalten und wenn der Funktionsdeklarator nicht eine Funktionsdefinition einleitet, endet der Gültigkeitsbereich der Parameternamen sofort. Abstrakte Deklaratoren, in denen keine Namen verwendet werden, werden in § A.8.8 besprochen. Hat in einer Funktionsvereinbarung alten Stils T D der Deklarator D folgende Form Dl(identifier-listopl') und hat der Name in einer Vereinbarung T Dl den Typ ,Jype-modifier T", dann hat der Name von D den Typ ,Jype-modifier Funktion mit unbekannten Argumenten und Resul- Resultattyp T'. Falls Parameter angegeben sind, haben sie folgende Form: identifier-list: identifier identifier-list , identifier Im Deklarator alten Stils darf die identifier-list nur angegeben werden, wenn der Deklara- Deklarator eine Funktionsdefinition einleitet (§ A.10.1). Die Vereinbarung liefert keine Informa- Information über die Typen der Parameter. Beispielsweise vereinbart int f<), *fpi(), C*pfOO; eine Funktion f mit Resultattyp int, eine Funktion fpi, die einen Zeiger auf int liefert, und einen Zeiger pfl auf eine Funktion, die int als Resultat liefert. Die Vereinbarungen verwenden den alten Stil, in keiner sind die Parametertypen angegeben. In der Deklaration neuen Stils int strcpy(char *dest, const char *source), rand(void); ist strcpy eine Funktion mit int-Resultat und zwei Argumenten, das erste Argument ist ein Zeiger auf char, das zweite ein Zeiger auf konstante Zeichen. Die Parameternamen sind effektiv Kommentare. Die zweite Funktion rand akzeptiert keine Argumente und liefert int. Funktionsdeklaratoren mit Parameterprototypen sind bei weitem die wichtigste Sprachänderung, die der ANSI-Standard einführt. Sie haben einen Vorteil gegenüber den Deklaratoren „alten Stils" der Ersten Ausgabe, denn sie erlauben Fehlererkennung und Typumwandlung von Argumenten bei Funktionsaufrufen, aber das hat seinen Preis: Auf- Aufruhr und Verwirrung bei ihrer Einführung und die Notwendigkeit, beide Formen zu un- unterstützen. Aus Kompatibilitätsgründen wurden gewisse syntaktische Unschönheiten in Kauf genommen, nämlich void als explizite Markierung der Funktionen neuen Stils ohne Parameter. Die Schreibweise mit ,„..." für Funktionen mit variabler Argumentliste ist ebenfalls neu. Zusammen mit den Makros aus der Standard-Definitionsdatei <stdarg.h> formalisiert sie einen Mechanismus, der in der Ersten Ausgabe offiziell verboten war, der aber inoffi- inoffiziell vergeben wurde. Die Schreibweisen wurden von C++ übernommen.
A.8 Vereinbaru a 215 KA.l Initialisierung Wenn ein Objekt definiert wird, kann sein init-dedarator einen Anfangswert für den Namen festlegen, der gerade vereinbart wird. Vor der Initialisierung steht =, und sie ist entweder ein Ausdruck oder eine Liste von Initialisierungen, die mit geschweiften Klammern umgeben ist. Die Liste darf mit einem Komma enden, als Hilfe zur gefälligen Formatierung. initializer: assignm ent-expression {initializer-list } (initializer-list , ) initializer-list: initializer initializer-list , initializer Alle Ausdrücke in der Initialisierung eines statischen Objekts oder Vektors müssen konstante Ausdrücke sein, wie in §A.7.19 beschrieben. Die Ausdrücke in der Initialisie- Initialisierung eines au to- oder register-Objekts oder eines Vektors müssen ebenfalls konstante Ausdrücke sein, wenn die Initialisierung eine mit geschweiften Klammern umgebene Li- Liste ist. Ist die Initialisierung eines automatischen Objekts jedoch ein einagerj^usdruck, muß er nicht konstant sein, sondern muß nur einen Typ besitzen, der an das Objekt zuge- zugewiesen werden kann. Die Erste Ausgabe erlaubte keine Initialisierung von automatischen Strukturen, Unionen oder Vektoren. Der ANSI-Standard erlaubt es, aber nur mit konstanten Konstruktionen, es sei denn, die Initialisierung kann als einfacher Ausdruck angegeben werden. Ein statisches Objekt, das nicht explizit initialisiert ist, wird so initialisiert, als ob die Konstante 0 an das Objekt (oder seine Teile) zugewiesen wird. Der Anfangswert ei- eines automatischen Objekts, das nicht explizit initialisiert wurde, ist Undefiniert. Ein Zeiger oder ein Objekt mit arithmetischem Typ wird durch einen einzigen Ausdruck initialisiert, der auch in geschweiften Klammern stehen kann. Der Ausdruck wird an das Objekt zugewiesen. Die Initialisierung für eine Struktur ist entweder ein Ausdruck mit dem gleichen Typ oder eine mit geschweiften Klammern umgebene Liste von Initialisierungen für die Komponenten der Reihe nach. Unbenannte Bit-Feld-Komponenten werden ignoriert und nicht initialisiert. Enthält die Liste weniger Initialisierungen, als die Struktur Kom- Komponenten hat, werden die restlichen Komponenten mit 0 initialisiert. Mehr Initialisierun- Initialisierungen als Komponenten dürfen nicht angegeben werden. Die Initialisierung für einen Vektor ist eine mit geschweiften Klammern umgebene Liste von Initialisierungen für die Elemente. Ist die Größe des Vektors nicht bekannt, legt die Anzahl der Initialisierungen die Größe des Vektors fest, und der Typ des Vektors wird vervollständigt. Liegt die Größe des Vektors fest, dürfen nicht mehr Initialisierun- Initialisierungen als Elemente angegeben sein; sind es weniger, werden die restlichen Elemente mit 0 initialisiert. Als Sonderfall darf ein Zeichenvektor mit einer konstanten Zeichenkette initiali- initialisiert werden; die Zeichen der Zeichenkette initialisieren die Elemente des Vektors der
216 A 'iprachbeschreibung Reihe nach. Ebenso darf eine erweiterte Zeichenkette (§A.2.6) einen Vektor vom Typ wchar t initialisieren. Ist die Größe des Vektors nicht bekannt, bestimmt die Anzahl der Zeichen in der Zeichenkette, unter Berücksichtigung des abschließenden NUL-Zeichens, die Größe des Vektors; liegt die Größe des Vektors fest, dürfen ohne das abschließende NUL-Zeichen höchstens so viele Zeichen in der konstanten Zeichenkette sein, wie der Vektor Elemente hat. Die Initialisierung für eine Union ist entweder ein einfacher Ausdruck mit dem gleichen Typ, oder eine mit geschweiften Klammern umgebene Initialisierung für die er- erste Alternative der Union. Die Erste Ausgabe erlaubte keine Initialisierung von Unionen. Die Erste-Alternative- Regel ist unbeholfen, aber sie kann nur schwer ohne neue Syntax verallgemeinert wer- werden. Mit dieser ANSI-Regel kann man Unionen wenigstens auf primitive Art initialisie- initialisieren; außerdem macht sie die Semantik von statischen Unionen klar, die nicht explizit in- initialisiert werden. Ein Aggregat ist eine Struktur oder ein Vektor. Wenn ein Aggregat Bestandteile hat, die ebenfalls eben Aggregat-Typ besitzen, gelten die Initialisierungsregeln rekursiv. In der Initialisierung können geschweifte Klammern_folgejidejmaßjen^^ den: Ist ein Bestandteil eines Aggregats selbst ein Aggregat und beginnt seine Initialisie- Initialisierung mit einer linken geschweiften Klammer, dann initialisiert die anschließende, mit Komma getrennte Liste von Initialisierungen die Bestandteile des inneren Aggregats; da- dabei dürfen nicht mehr Initialisierungen als Bestandteile vorkommen. Beginnt jedoch die Initialisierung des inneren Aggregats nicht mit einer Unken geschweiften Klammer, dann werden nur genügend Initialisierungen für die Bestandteile des inneren Aggregats aus der Liste entnommen; etwa verbleibende Initialisierungen sind für den nächsten Bestand- Bestandteil des äußeren Aggregats übrig. Zum Beispiel definiert und initialisiert int x[] = { 1, 3, 5 >; x als eindimensionalen Vektor mit drei Elementen, da keine Größe angegeben war und da drei Initialisierungen vorhanden sind. float y[4] [3] = { < 1. 3, 5 >. < 2. «. 6 >, < 3, 5, 7 >, >; ist eine vollständig geklammerte Initialisierung: 1, 3 und 5 initialisieren die erste Zeile des Vektors y[0], nämlich die Elemente y[0][0], y[0][l] und y[0][2]. Analog initialisie- initialisieren die nächsten beiden Zeilen y[l] und y[2]. Die Initialisierung hört vorzeitig auf, des- deshalb werden die Elemente von y[3] mit 0 initialisiert. Exakt der gleiche Effekt hätte mit float y[4] [3] = { 1, 3, 5, 2, 4, 6, 3, 5, 7 >; erzielt werden können. Die Initialisierung für y beginnt mit einer Unken geschweiften Klammer, nicht aber die für y[0]; deshalb werden drei Elemente der Liste benutzt. Ana- Analog werden die nächsten drei der Reihe nach für y[l] und dann für y[2] verwendet.
A.8 Vereinbarunge 217 float y[4][3] = < { 1 ), < 2 ), ( 3 }, { 4 } >; initialisiert die erste Spalte von y (als Matrix interpretiert) und setzt den Rest auf 0. Schließlich zeigt char msg[] = "Syntax error on line Xs\n"; einen Zeichenvektor, dessen Elemente mit einer konstanten Zeichenkette initialisiert werden; zur Größe gehört das abschließende NUL-Zeichen dazu. AiJ.8 Typnamen Manchmal (bei einer expliziten Typumwandlung mit Hilfe eines cart-Operators, bei der Deklaration von Parametertypen in Funktionsdeklaratoren und als Argument von sizeof) wird der Name eines Datentyps benötigt. Dies kann durch einen type-name ge- geschehen, syntaktisch jlurchjlie Vereinbarung^eines ObjektsjnU dem gewünschten Typ, in der der Name des Objekts fehlt. type-name: specifier-qualifier-tist abstract-declaratoropl abstract-declarator: pointer pointer^ direct-abstract-declarator direct-abstract-declarator: ( abstract-declarator ) direct-abstract-declaratoropt [ constant-expression^ ] direct-abstract-declarator op ( parameter-type-listgp, ) Man kann eindeutig den Punkt im abstract-declarator feststellen, wo ein Name stehen würde, wenn die Konstruktion ein Deklarator in einer Vereinbarung wäre. Der type- name bezeichnet dann den Typ, den der hypothetische Name hätte. Die Angaben int int • int *[3] int (•)[] int •() int (*[])(void) bezeichnen der Reihe nach die Typen „Integer", „Zeiger auf Integer", „Vektor mit 3 Zei- Zeigern auf Integer", „Zeiger auf Vektor mit unbestimmter Anzahl von Integern", „Funkti- „Funktion mit unbekannten Parametern, die Zeiger auf Integer liefert" und „Vektor mit unbe- unbestimmter Größe von Zeigern auf Funktionen ohne Parameter, die ein Integer-Resultat liefern". typedef Vereinbarungen mit der Speicherklasse typedef vereinbaren keine Objekte, son- sondern Namen für Typen. Ein solcher Name wird als typedef-name bezeichnet. typedef-nam e: identifier
218 >Sprachbeschreibung Eine typedef-Vereinbarung gibt jedem Namen in ihren Deklaratoren wie üblich einen Typ (siehe § A.8.6). Anschließend ist jeder derartige typedef-name syntaktisch äquivalent zu einem reservierten Wort als Typangabe für den zugehörigen Typ. Zum Beispiel sind nach typedef long Blockno, »Blockptr; typedef struct { double r, theta; } Complex; die Konstruktionen Blockno b; extern Blockptr bp; Complex z, *zp; legale Vereinbarungen. Der Typ von b ist long, die Variable bp ist ein „Zeiger auf long" und z hat die angegebene Struktur; zp ist ein Zeiger auf eine solche Struktur. typedef führt keine neuen Typen ein, sondern nur Synonyme für Typen, die auch anders angegeben werden könnten. In dem Beispiel hat b den gleichen Typ wie jedes an- andere long-Objekt. Ein typedef-name darf in einem inneren Gültigkeitsbereich neu vereinbart werden, aber die Liste der Typangaben darf nicht leer sein. Zum Beispiel vereinbart extern Blockno; Blockno nicht neu, wohl aber extern int Blockno; AJi.10 Äquivalenz von Typen Zwei type-specifier-Listen sind äquivalent, wenn sie die gleiche Menge von Typan- Typangaben enthalten, wobei berücksichtigt wird, daß manche Angaben von anderen impliziert werden (zum Beispiel Impliziert long allein schon long int). Strukturen, Unionen und Aufzählungen mit verschiedenen Etiketten sind verschieden und ohne Etikett definiert eine Union, Struktur oder Aufzählung einen eindeutigen Typ. Zwei Typen sind gleich, wenn ihre abstrakten Deklaratoren (§A.8.8) bis auf die Äquivalenz der type-specifier-Listen gleich sind; dabei werden typedef-Typen expandiert und die Namen von Funktionsparametern entfernt. Vektorgrößen und die Parameterty- Parametertypen von Funktionen sind signifikant. A.9 Anweisungen Falls nicht anders beschrieben, werden Anweisungen sequentiell nacheinander aus- ausgeführt. Anweisungen werden für ihre Effekte ausgeführt, sie haben keine Werte. Es gibt verschiedene Gruppen: Statement: labeled-statement expression-statem ent compound-statement selection-statement iteration-statement jump-statement
A.9 Anweisungen 219 A.9.1 Marken an Anweisungen Vor Anweisungen können Marken stehen: labeled-statement: identifier : Statement case constant-expression : Statement default : Statement Eine Marke, die aus einem Namen besteht, vereinbart den Namen. Eine derartige Mar- ke kann nur als Ziel von göto vefwenäet werden. Der GMtlgkeitslKreich^esJNamens ist die aktuelle Funktion. Da Marken ihren eigenen Namensraum besitzen, können sie nicht mit anderen Namen kollidieren und sie können nicht erneut vereinbart werden. Siehe IA.11.1. case- und default-Marken werden zusammen mit der switch-Anweisung (§ A.9.4) verwendet. Der konstante Ausdruck bei case muß einen Integer-Typ besitzen. Von sich aus ändern Marken den Ablauf eines Programms nicht. A.92 Ausdruck als Anweisung Die Berechnung eines Ausdrucks ist die am häufigsten verwendete Anweisung; sie hat folgende Form: expression-statement: expression^ ; Solche Anweisungen sind normalerweise Zuweisungen oder Funktionsaufrufe. Alle Ne- Nebenwirkungen des Ausdrucks werden abgeschlossen, bevor die nächste Anweisung aus- ausgeführt wird. Fehlt der Ausdruck, wird die Konstruktion als leere Anweisung (null Statement) bezeichnet; sie wird oft für eine Schleife oder zum Anbringen einer Marke be- benutzt. A.93 Block Damit mehrere Anweisungen verwendet werden können, wo eine einzelne Anwei- Anweisung erwartet wird, gibt es die zusammengesetzte Anweisung, die auch als Block bezeich- bezeichnet wird. Der Rumpf einer Funktionsdefinition ist ein Block. compound-statement: { declaration-list„p, statement-listopl} declaration-list: declaration declaration-list declaration statement-list: statement statement-list statement Wenn ein Name in der declaration-list bereits außerhalb des Blocks vereinbart war, wird die äußere Vereinbarung innerhalb des Blocks ausgesetzt (siehe §A.11.1) und nach dem Block wiederhergestellt. Ein Name darf einmal im gleichen Block vereinbart werden. Diese Regem beziehen sich auf Namen im gleichen Namensraum (§A.ll); Namen in verschiedenen Namensräumen werden als verschieden behandelt.
220 D-Sprachbeschreibung Die Initialisierung von automatischen Objekten erfolgt jedesmal, wenn der Block sequentiell von außen her erreicht wird, und geschieht in der Reihenfolge der Deklarato- ren.^Wird ein Sprung in einen Block ausgeführt, finden diese Initialisierungen nicht statt. Für static-Objekte erfolgen Initialisierungen nur einmal, bevor die Ausführung des Pro- Programms beginnt. A.9.4 Auswahlanweisungen Auswahlanweisungen wählen einen von mehreren Programmabläufen. selection-statement: If ( expression ) Statement if ( expression ) statement else statement switch ( expression ) statement Bei beiden Formen der if-Anweisung wird der Ausdruck, der einen arithmetischen Typ oder Zeigertyp besitzen muß, mit allen Nebenwirkungen berechnet. Ist das Resultat verschieden von 0, wird die erste abhängige Anweisung ausgeführt. In der zweiten Form wird die zweite abhängige Anweisung ausgeführt, wenn der Ausdruck 0 ist. Die Mehr- deutigkeit bei eise wird dadurch entschieden, daß jün eise jeweils dem zuletzt aufgetrete- neji else-losen if auf der gleichen Verschachtelungsebene von Blöcken zugeordnet wird. Die switch-Anweisung setzt den Programmablauf mit einer von mehreren Anwei- Anweisungen in Abhängigkeit vom Wert eines Ausdrucks ab, der einen Integer-Typ besitzen muß. Typischerweise ist die von switch kontrollierte Anweisung ein Block. Jede Anwei- Anweisung innerhalb der abhängigen Anweisung kann mit einer oder mehreren case-Marken (§A.9.1) markiert sein. Für den kontrollierenden Ausdruck findet Integer-Erweiterung statt (§A.6.1), und die case-Konstanten werden in den erweiterten Typ umgewandelt. Nach der Typumwandlung dürfen keine zwei case-Konstanten, die zum gleichen switch gehören, den gleichen Wert besitzen. Zu einem switch darf auch höchstens eine default-Marke gehören. switch-Anweisungen dürfen verschachtelt werden; eine case- oder default-Marke gehört zum kleinsten switch, der sie enthält. Zur Ausführung der switch-Anweisung wird der Ausdruck mit allen Nebenwirkun- Nebenwirkungen bewertet und mit allen case-Konstanten verglichen. Gibt es eine case-Konstante, die denselben Wert hat wie der Ausdruck, so wird der Programmablauf mit der Anweisung bei der case-Marke fortgesetzt. Gibt es keine passende case-Konstante, geht der Pro- Programmablauf mit der Anweisung bei default weiter, falls eine default-Marke vorhanden ist. Gibt es weder eine passende case-Konstante noch eine default-Marke, so wird keine der von switch abhängigen Anweisungen ausgeführt. In der Ersten Ausgabe mußten der Kontrollausdruck bei switch, wie auch die case- Konstanten, Werte mit dem Typ int besitzen. A.9.5 Wiederholungsanweisungen Wiederholungsanweisungen formulieren Schleifen. iteration-statement: while ( expression ) Statement do Statement while ( expression ) ; for ( expressionopl ; expressionopl ; expressionopl ) statement
A.9 Anweisung? 221 Bei den while- und do-Anweisungen wird die abhängige Anweisung so lange wie- wiederholt ausgeführt, wie der Wert des Ausdrucks verschieden von 0 ist; der Ausdruck muß einen arithmetischen Typ oder Zeigertyp besitzen. Bei while erfolgt der Test, mit allen Nebenwirkungen des Ausdrucks, vor jeder Ausführung der abhängigen Anweisung; bei do erfolgt der Test nach jeder Wiederholung. Bei der for-Anweisung wird der erste Ausdruck einmal bewertet und dient deshalb zur Initialisierung der Schleife. Der Typ des Ausdrucks unterliegt keiner Einschränkung. Der zweite Ausdruck muß eben arithmetischen Typ oder Zeigertyp besitzen; er wird vor jeder Wiederholung berechnet, und wenn er den Wert 0 besitzt, wird for beendet. Der dritte Ausdruck wird nach jeder Wiederholung bewertet und gibt deshalb eine Reinitiali- sierung für die Schleife an. Der Typ des Ausdrucks unterliegt keiner Einschränkung. Nebenwirkungen von jedem Ausdruck werden unmittelbar nach seiner Berechnung abge- abgeschlossen. Wenn die abhängige Anweisung continue nicht enthält, ist eine Anweisung for ( expression i ; expression 2 ; expression 3 ) Statement äquivalent zu expression \ ; while ( expression! ) { statement expression 3 ; > Jeder der drei Ausdrücke darf fehlen. Fehlt der mittlere Ausdruck, ist der impli- implizierte Test äquivalent zum Test einer von 0 verschiedenen Konstanten. A.9.6 Sprunganweisungen Durch Sprunganweisungen wechselt der Programmablauf unbedingt. jump-statem ent: goto identifier ; continue ; break ; return expression^ ; Bei der goto-Anweisung muß der Name eine Marke (§A.9.1) in der aktuellen Funktion sein. Der Programmablauf wird bei der markierten Anweisung fortgesetzt. Eine continue-Anweisung darf nur innerhalb einer Wiederholungsanweisung auf- auftreten. Durch diese Anweisung wird der Programmablauf am Wiederholungspunkt der kleinsten umgebenden derartigen Anweisung fortgesetzt. Exakter formuliert, bei jeder der folgenden Anweisungen while (...) { do < for (...) < contin: ; contin: ; contin: ; > > while (...); > ist eine continue-Anweisung, die nicht noch in einer kleineren Wiederholungsanweisung enthalten ist, äquivalent zu goto contin. Eine break-Anweisung darf nur in einer Wiederholungsanweisung oder in einer switch-Anweisung auftreten und beendet die Ausführung der kleinsten umgebenden der-
222 A Sprachbeschreibung artigen Anweisung-, der Programmablauf geht mit der Anweisung weiter, die der beende- beendeten Anweisung folgt. Eine Funktion kehrt mit der return-Anweisung zu ihrem Aufrufer zurück. Wenn nach return ein Ausdruck folgt, wird dieser Wert dem Aufrufer der Funktion geliefert. Der Ausdruck wird wie bei einer Zuweisung in den Resultattyp der Funktion umgewan- umgewandelt, in der er auftritt. Erreicht der Programmablauf das Ende einer Funktion, ist das äquivalent zur Aus- Ausführung von return ohne einen Ausdruck. In jedem Fall ist dann der Resultatwert Unde- Undefiniert. A.10 Externe Vereinbarungen Die Eingabe für den C-Übersetzer wird als Übersetzungseinheit bezeichnet; sie be- besteht aus einer Folge von externen Vereinbarungen, die entweder Vereinbarungen oder Funktionsdefinitionen sind. translation-unit: external-declaration translation-unit external-declaration external-declaration: function-definition declaration Der Gültigkeitsbereich von externen Vereinbarungen reicht bis zum Ende der Übersetzungseinheit, in der sie auftreten, ebenso wie der Effekt von Vereinbarungen in einem Block bis zum Ende des Blocks reicht. Die Syntax von externen Vereinbarungen ist dieselbe wie bei allen Vereinbarungen, abgesehen davon, daß nur auf dieser Ebene der Code von Funktionen angegeben werden kann. A.10.1 Funktionsdefinitionen Funktionsdefinitionen haben folgende Form: function-definition: declaration-specifiers„p, declarator declaration-list^ compound-statement Bei den declaration-specifiers sind als Speicherklassen nur extern oder static erlaubt; der Unterschied wird in § A.11.2 erklärt. Eine Funktion kann als Resultat einen arithmetischen Typ, eine Struktur, eine Union, einen Zeiger oder void liefern, nicht aber eine Funktion oder einen Vektor. Der Deklarator in einer Funktionsvereinbarung muß explizit ausdrücken, daß der vereinbarte Name einen Funktionstyp besitzt, das heißt, er muß eine der folgenden Formen enthalten (siehe §A.8.6.3) direct-declarator ( parameter-type-list ) direct-declarator ( identifier-list^ ) dabei ist der direct-declarator ein Name oder ein Name in Klammern. Er darf insbeson- insbesondere nicht mit Hilfe von typedef seinen Funktionsstatus erlangen. In der ersten Form ist die Definition eine Funktion im neuen Stil, und ihre Para- Parameter werden zusammen mit ihren Typen in der parameter-type-list deklariert; nach dem
A. 10 Externe Ve barungen 223 Funktionsdeklarator darf keine declaration-list folgen. Falls die parameter-type-list nicht nur aus void besteht und damit zeigt, daß die Funktion keine Parameter akzeptiert, muß jeder Deklarator in der parameter-type-list einen Namen enthalten. Endet die parameter- type-tist mit »,...", dann darf die Funktion mit mehr Argumenten als Parametern aufgeru- aufgerufen werden; der Makro va arg und die in der Definitionsdatei < stdarg.h > vereinbarte und im Anhang B beschriebene Technik muß verwendet werden, um auf die zusätzlichen Argumente zuzugreifen. Funktionen mit einer variablen Anzahl von Argumenten müs- müssen wenigstens einen benannten Parameter besitzen. In der zweiten Form erfolgt die Definition im alten Stil; die identifier-list benennt die Parameter und die declaration-list gibt ihnen Typen. Wenn für einen Parameter keine Deklaration angegeben ist, erhält er den Typ int. Die declaration-list darf nur Parameter vereinbaren, die in der identifier-list angegeben sind, Initialisierung ist nicht erlaubt, und als einzige Speicherklasse kann register angegeben werden. Bei beiden Arten von Funktionsdefinitionen wird so verfahren, als ob die Parame- Parameter unmittelbar am Anfang des Blocks deklariert wurden, der den Rumpf der Funktion bildet; folglich können die gleichen Namen in diesem Block nicht nochmals vereinbart werden (sie können natürlich, wie andere Namen, in inneren Blöcken neu vereinbart werden). Wird ein Parameter mit dem Typ „Vektor von Typ" deklariert, so wird die De- Deklaration in „Zeiger auf Typ" abgeändert; analog, wird ein Parameter mit dem Typ „Funktion mit Resultat Typ" deklariert, so wird die Deklaration in „Zeiger auf Funktion mit Resultat Typ" abgeändert. Beim Aufruf einer Funktion werden die Argumente falls nötig umgewandelt und an die Parameter zugewiesen; siehe § A.7.3.2. Der neue Stil für Funktionsdefinitionen ist neu im ANSI-Standard. Es gibt auch eine klei- kleine Änderung in den Details zur Erweiterung: in der Ersten Ausgabe war festgelegt, daß die Deklarationen von float-Parametern in double abgeändert wurden. Der Unterschied macht sich bemerkbar, wenn in einer Funktion ein Zeiget auf einen Parameter erzeugt wird. Ein komplettes Beispiel einer Funktionsdefinition im neuen Stil ist int maxd'nt s, int b, int c) { int ra; m = (a > b) ? a : b; return (m > c) ? m : c; > int ist hier der declaration-specifier, max(int a, int b, int c) ist der Funktionsdeklarator und { ... } ist der Block mit dem Code der Funktion. Die entsprechende Definition im alten Stil wäre int max(a, b, c) int a, b, c; Jetzt ist int max(a, b, c) der Deklarator und int a, b, c; ist die declaration-list für die Para- Parameter.
224 / '-Sprachbeschreibung A.10.2 Externe Vereinbarungen Externe Vereinbarungen legen die Charakteristika von Objekten, Funktionen und anderen Namen fest. Der Begriff „extern" bezieht sich auf ihre Position außerhalb von Funktionen und ist nicht direkt mit dem reservierten Wort extern verbunden; die Spei- Speicherklasse für ein extern vereinbartes Objekt kann leer bleiben, oder sie kann als extern oder static angegeben werden. In der gleichen Übersetzungseinheit dürfen mehrere externe Vereinbarungen für den gleichen Namen existieren, wenn sie nach Typ und Bindung übereinstimmen und wenn es höchstens eine Definition für den Namen gibt. Ob zwei Vereinbarungen für ein Objekt oder eine Funktion im Typ übereinstim- übereinstimmen, wird nach den Regeln in § A.8.10 entschieden. Darüber hinaus, wenn sich die Ver- Vereinbarungen unterscheiden, weil ein Typ ein unvollständiger Struktur-, Union- oder Auf- Aufzählungstyp ist (§ A.8.3), und der andere ist der entsprechende vollständige Typ mit dem gleichen Etikett, werden die Typen als übereinstimmend angesehen. Außerdem, wenn ein Typ ein unvollständiger Vektortyp ist (§ A.8.6.2), und der andere ist ein vollständiger Vektortyp, gelten die Typen, wenn sie sonst gleich sind, als übereinstimmend. Ist schließ- schließlich ein Typ eine Funktion alten Stils, und der andere eine ansonsten identische Funktion neuen Stils mit Parameterdeklarationen, gelten die Typen als übereinstimmend. Enthält die erste externe Vereinbarung für eine Funktion oder ein Objekt die An- Angabe static, hat der Name interne Bindung (internal linkage); andernfalls hat er externe Bindung (external linkage). Bindung wird in § A.11.2 besprochen. Eine externe Vereinbarung für ein Objekt ist eine Definition, wenn sie eine Initiali- Initialisierung enthält. Eine externe Vereinbarung für ein Objekt ohne eine Initialisierung und ohne die Angabe extern gilt als vorläufige Definition (tentative definition). Wenn eine De- Definition für ein Objekt in einer Übersetzungseinheit erscheint, werden vorläufige Defini- Definitionen nur als redundante Deklarationen behandelt. Wenn in der Übersetzungseinheit keine Definition für das Objekt erscheint, werden alle vorläufigen Definitionen zu einer einzigen Definition mit der Initialisierung 0. Jedes Objekt muß exakt eine Definition besitzen. Bei Objekten mit interner Bin- Bindung gilt diese Regel separat für jede Übersetzungseinheit, denn Objekte mit interner Bindung existieren eindeutig für jede Übersetzungseinheit. Bei Objekten mit externer Bindung gilt das für das ganze Programm. Die Eine-Definition-Regel wird zwar in der Ersten Ausgabe etwas anders formuliert, aber sie ist effektiv identisch mit der hier angegebenen Regel. Manche Implementierun- Implementierungen sind weniger streng und erweitern die Idee der vorläufigen Definition. In der alter- alternativen Formulierung, die bei UNIX Systemen Üblich ist und vom Standard als gebräuchli- gebräuchliche Erweiterung anerkannt wird, werden alle vorläufigen Definitionen für ein Objekt mit externer Bindung, in allen Übersetzungseinheit zusammen, gemeinsam und nicht für jede Übersetzungseinheit separat betrachtet. Existiert eine Definition irgendwo im Pro- Programm, werden die vorläufigen Definitionen einfach zu Deklarationen, erscheint aber keine Definition, werden alle vorläufigen Definitionen zusammen zu einer Definition mit Initialisierung 0.
A.ll Gültigkeit,' eich und Bindung 225 A.11 Gültigkeitsbereich und Bindung Ein Programm muß nicht insgesamt auf einmal übersetzt werden; das Quellpro- Quellprogramm kann in mehreren Dateien aufbewahrt werden, die Übersetzungseinheiten enthal- enthalten, und früher übersetzte Routinen können aus Bibliotheken geladen werden. Die Funktionen eines Programms können durch Aufrufe sowie durch die Manipulation von externen Daten korrespondieren. Zwei Gültigkeitsbereiche müssen folglich unterschieden werden: einerseits der Gültigkeitsbereich im Text (lexical scope) für einen Namen, das ist der Abschnitt eines Programms, in dem die Charakteristika eines Namens verstanden werden; andrerseits der externe Gültigkeitsbereich für Objekte und Funktionen mit externer Bindung, der die Verbindungen zwischen Namen in separat bearbeiteten Übersetzungseinheiten bestimmt. A.11.1 Gültigkeitsbereich im Text Namen gehören zu mehreren Namensräumen, die nicht miteinander kollidieren; der gleiche Name darf für verschiedene Zwecke verwendet werden, sogar im gleichen Gültigkeitsbereich, wenn die Verwendung in verschiedenen Namensräumen erfolgt. Die Klassen sind: Objekte,JFunktionen, typedef-Namen und Aufzählungskonstanten; Mar- Marken; Etiketten vonJJtrukturen, Unionen und Aufzählungen; und Komponenten jeder ein- einzelnen Struktur ^ Diese Regeln weichen in mehreren Punkten von denen ab, die in der Ersten Ausgabe be- beschrieben wurden. Marken hatten früher keinen eigenen Namensraum; Etiketten von Strukturen und Unionen hatten jeweils ihren eigenen Namensraum, und bei manchen Implementierungen auch die Etiketten von Aufzählungen; daß verschiedene Arten von Etiketten in einem Namensraum zusammengefaßt werden, ist eine neue Einschränkung. Die wichtigste Abweichung von der Ersten Ausgabe ist, daß jede Struktur und Union einen eigenen Namensraum für ihre Bestandteile erzeugt, so daß der gleiche Name in mehreren verschiedenen Strukturen auftreten darf. Diese Regel war schon seit mehre- mehreren Jahren allgemein in Gebrauch. Der Gültigkeitsbereich im Text beginnt für den Namen eines Objekts oder einer Funktion in einer externen Vereinbarung am Ende des Deklarators und reicht bis zum Ende der Übersetzungseinheit, in der er erscheint. Der Gültigkeitsbereich eines Para- Parameters in einer Funktionsdefinition beginnt am Anfang des Funktionsblocks und reicht über die ganze Funktion; der Gültigkeitsbereich eines Parameters in einer Funktionsde- Funktionsdeklaration endet am Ende des Deklarators. Der Gültigkeitsbereich eines Namens, der am Anfang eines Blocks vereinbart wird, beginnt am Ende seines Deklarators und reicht bis zum Ende des Blocks. Der Gültigkeitsbereich einer Marke ist die ganze Funktion, in der sie erscheint. Der Gültigkeitsbereich des Etiketts einer Struktur, einer Union oder einer Aufzählung, oder einer Aufzählungskonstanten beginnt, wenn der Name in einer Typan- Typangabe erscheint, und reicht bis zum Ende der Übersetzungseinheit (bei externen Verein- Vereinbarungen) oder bis zum Ende des Blocks (bei Vereinbarungen in einer Funktion). Wenn ein Name explizit am Anfang eines Blocks vereinbart wird, auch am Anfang des Funktionsrumpfs, wird jede Vereinbarung des Namens außerhalb des Blocks bis zum Ende des Blocks ausgesetzt.
226 A^ Sprachbeschreibung A.1U Bindung Innerhalb einer Übersetzungseinheit beziehen sich alle Vereinbarungen des glei- gleichen Objekts oder Funktionsnamens mit interner Bindung auf das Gleiche, und das Ob- Objekt oder die Funktion ist in der Übersetzungseinheit eindeutig. Alle Vereinbarungen des gleichen Objekts oder Funktionsnamens mit externer Bindung beziehen sich auf das Gleiche, und das Objekt oder die Funktion wird im ganzen Programm gemeinsam be- benutzt. Wie in § A.10.2 besprochen, gibt die erste externe Vereinbarung eines Namens dem Namen interne Bindung, wenn static verwendet wird, und sonst externe Bindung. Wenn eine Vereinbarung für den Namen eines Objekts innerhalb eines Blocks extern nicht be- beinhaltet, dann besitzt der Name keine Bindung und existiert eindeutig in der Funktion. Enthält eine Funktionsvereinbarung keine Angabe zur Speicherklasse, wird so verfahren, als ob extern angegeben wurde. Wenn extern angegeben ist und wenn eine externe Ver- Vereinbarung für den Namen im Gültigkeitsbereich um den Block aktiv ist, dann hat der Na- Name die gleiche Bindung wie die externe Vereinbarung, und er bezieht sich auf das gleiche Objekt oder die gleiche Funktion; ist aber keine externe Vereinbarung sichtbar, so hat der Name externe Bindung. A.12 Der Preprozessor Der Preprozessor ersetzt Makros, sorgt für bedingte Übersetzung (conditional compilation) und kann Dateien mit Namen einfügen. Der Preprozessor wird in Zeilen angesprochen, die mit # beginnen; vor # kann noch Zwischenraum stehen. Die Syntax dieser Zeilen ist unabhängig vom Rest der Sprache; sie können an beliebigen Stellen vor- vorkommen; ihr Effekt hält bis zum Ende der Übersetzungseinheit an, unabhängig von den anderen Überlegungen zu Gültigkeitsbereichen. Zeilengrenzen sind signifikant; jede Zeile wird einzeln analysiert (Zeilen können aber verbunden werden, siehe §A.12.2). Für den Preprozessor ist ein Symbol (token) jedes Symbol der Sprache oder auch eine Zeichenfolge, die einen Dateinamen definiert, wie bei der #include-Anweisung (§ A.12.4); zusätzlich ist jedes Zeichen, das nicht anders definiert ist, ein Symbol. Inner- Innerhalb von Preprozessor-Zeilen ist jedoch der Effekt von Zwischenraum, mit Ausnahme von Leerzeichen und (horizontalen) Tabulatorzeichen, Undefiniert. Der Preprozessor arbeitet in verschiedenen Phasen logisch nacheinander; in einer Implementierung können die Phasen auch zusammengefaßt sein. 1. Zuerst werden die in § A.12.1 beschriebenen Drei-Zeichen-Folgen durch ihre äquiva- äquivalenten Zeichen ersetzt. Wenn die Betriebssystem-Umgebung dies verlangt, werden Zeilentrenner zwischen die Zeilen der Quelle eingefügt. 2. Wenn ein Gegenschrägstrich \ gefolgt von einem Zeilentrenner auftritt, werden bei- beide entfernt; damit werden Zeilen verbunden (§ A.12.2). 3. Das Programm wird in Symbole zerlegt, die durch Zwischenraumzeichen getrennt sind; Kommentare werden durch einzelne Leerzeichen ersetzt. Dann werden die Preprozessor-Anweisungen befolgt und Makros (§§ A.12.3-A.12.10) expandiert. 4. Ersatzdarstellungen in Zeichenkonstanten und konstanten Zeichenketten (§§ A.2.5.2, A.2.6) werden durch die äquivalenten Zeichen ersetzt; anschließend werden benach- benachbarte konstante Zeichenketten aneinandergehängt.
A.12 DerPreprc sor 227 5. Das Resultat wird übersetzt und mit anderen Programmen und Bibliotheken gebun- gebunden, indem die nötigen Programme und Daten gesammelt sowie externe Punktionen und Objektverweise mit ihren Definitionen verknüpft werden. A.L2.1 Drei-Zeichen-Folgen Der Zeichensatz von C-Quellprogrammen ist im 7-Bit-ASCII enthalten, aber er ist eine Obermenge vom ISO 646-1983 Invariant Code Set. Damit Programme im einge- eingeschränkten Zeichensatz dargestellt werden können, werden die folgenden Drei-Zeichen- Folgen (trigraph sequences) jeweils durch das entsprechende einzelne Zeichen ersetzt. Dieser Ersatz erfolgt vor jeder anderen Bearbeitung. ??• * ??( t ??< { 11/ \ 11) ] ??> > ??' A ??! | 11- Anderer derartiger Ersatz findet nicht statt. Drei-Zeichen-Folgen sind neu im ANSI-Standard. A.12.2 Verbinden von Zeilen Zeilen, die mit dem Gegenschrägstrich \ enden, werden zusammengefaßt, indem der Gegenschrägstrich und der nachfolgende Zeilentrenner entfernt werden. Dies ge- geschieht, bevor in Symbole zerlegt wird. A.123 Makrodeflnition und Expansion Eine Kontrollzeile der Form # define identifier token-sequence veranlaßt, daß der Preprozessor anschließend den angegebenen Namen durch die ange- angegebene Folge von Symbolen ersetzt; Zwischenraum vor und nach der Symbolfolge wird entfernt. Eine zweite #define-Anweisung für den gleichen Namen gilt als Fehler, es sei denn, die zweite Symbolfolge ist identisch zur ersten, wobei alle trennenden Zwischen- Zwischenräume als äquivalent angesehen werden. Eine Zeile der Form # define identifier^ identifier-list ) token-sequence definiert einen Makro mit Parametern, die durch die identifier-list festgelegt werden; in der Definition muß die Unke Klammer dem Makronamen unmittelbar folgen. Wie bei der ersten Form wird Zwischenraum vor und nach token-sequence entfernt, und der Ma- Makro darf nur neu definiert werden, wenn Anzahl und Namen der Parameter sowie die Symbolfolge identisch sind. Eine Kontrollzeile der Form # undef identifier löscht die Definition des Namens für den Preprozessor. Es ist kein Fehler, wenn #undef auf einen unbekannten Namen angewendet wird. Wenn ein Makro mit der zweiten Art von Definition eingeführt wird, besteht ein Makroaufruf aus dem Makronamen, dem Zwischenraum folgen kann, danach einer lin-
228 / Sprachbeschreibung ken Klammer, einer Folge von Symbolen, die durch Komma getrennt sind, und einer rechten Klammer. Die Argumente des Makroaufrufs sind die Symbolfolgen, die durch Komma getrennt sind; Kommas innerhalb von verschachtelten Klammern, Zeichenkon- Zeichenkonstanten oder konstanten Zeichenketten gelten nicht als Argumenttrenner. Bei der Zu- sammenstellung des Makroaufrufs findet keine Makro-Expansion in den Argumenten statt. Die Anzahl der Argumente beim Aufruf muß gleich der Anzahl der Parameter in der Makrodefinition sein. Wenn die Argumente isoliert sind, wird Zwischenraum vor und nach jedem Argument entfernt. Dann wird die Symbolfolge jedes Arguments über- überall dort an Stelle des entsprechenden Parameternamens in der Symbolfolge aus der Ma- Makrodefinition eingesetzt, wo der Parametername außerhalb von Zeichenkonstanten und konstanten Zeichenketten auftritt. Wenn_dem Parameter in der Ersaty-Symholfolge kein ^vorausgeht, und wenn ihm ## weder vorausgeht noch folgt, werden die Argument- Symbolfolgen auf Makrqaufrufe untersucht, und_bei_Bedarf^^wd^^^dklJLSJBHÜKelbar bevor eingesetzt wird. Zwei besondere Operatoren beeinflussen die Ersetzung. Erstens, wenn einem Pa- Parameter in der Ersatz-Symbolfolge # unmittelbar vorausgeht, wird das zugehörige Argu- Argument mit Doppelanführungszeichen umgeben, und dann werden # und der Parameterna- Parametername insgesamt durch das Zeichenketten-Argument ersetzt. Ein Gegenschrägstrich \ wird vor jedem Doppelanführungszeichen und jedem Gegenschrägstrich \ eingefügt, die als Begrenzung oder innerhalb einer konstanten Zeichenkette oder Zeichenkonstante im Ar- Argument auftreten. Zweitens, wenn die token-sequence jeder der beiden Formen einer Makrodefinition das Symbol ## enthält, wird unmittelbar nach Ersatz der Parameter jedes ## gelöscht, und zwar zusammen mit Zwischenraum auf beiden Seiten, so daß die benadibji£tenjSvm- bole verkettet werden und ein neues Symbol bilden. Der Effekt ist Undefiniert, wenn un- ungültige Symbole produziert werden oder wenn das Resultat von der Reihenfolge der Be- Bearbeitung mehrerer ## abhängt. ## darf außerdem weder am Anfang noch am Ende der Ersatz-Symbolfolge auftreten. Bei beiden Arten von Makros wird die Ersatz-Symbolfolge wiederholt auf weitere definierte Namen abgesucht. Wenn allerdings bei einer Expansion ein bestimmter Name einmal ersetzt wurde, wird er nicht mehr ersetzt, wenn er bei der Suche erneut auftaucht; er bleibt dann unverändert. Auch wenn die endgültige Expansion eines Makros mit # beginnt, gut siejnicht als Preprozessor-Anweisung. Der ANSI-Standard definiert die Details der Makro-Expansion exakter, als das in der Er- Ersten Ausgabe geschah. Die wichtigste Änderung sind die neuen Operatoren # und ##, die Umwandlung in Zeichenketten und Verkettung zulassen. Manche der neuen Regeln, insbesondere diejenigen im Zusammenhang mit Verkettung, sind bizarr. (Siehe das nachfolgende Beispiel.) Mit Makros können zum Beispiel wesentliche Konstanten definiert werden: «define TABSIZE 100 int tabtetTABSIZE];
A.12 DerPrei essor 229 Die Definition «define ABSDIFFO, b) ((a)>(b) ? (a)-(b) : (b)-(a)) definiert einen Makro, der den absoluten Wert der Differenz seiner Argumente liefert. Anders als bei einer Funktion, die dasselbe tun soll, können die Argumente und der Re- Resultatwert beliebige arithmetische "typen besitzen oder sogar Zeiger sein. Außerdem werden die Argumente, die auch Nebenwirkungen verursachen können, zweimal bewer- bewertet, einmal für die Bedingung und einmal, um den Resultatwert zu berechnen. Mit der Definition «define tempf i le(dir) «dir "As" liefert der Makroaufruf tempfile(/usr/tmp) "/usr/tnp" "/Xs" und das Resultat wird anschließend zu einer einzigen konstanten Zeichenkette zusam- zusammengefügt. Nach «define cat<x, y) x ## y liefert der Aufruf cat(var,123) den Wert varl23. Der Aufruf cat(cat(l^K)) ist jedoch Undefiniert: die Angabe von ## verhindert die Expansion der Argumente des äußeren Aufrufs. Der Aufruf liefert also die Symbolfolge cat ( 1 , 2 K und K (die Verkettung des letzten Symbols vom ersten Argument mit dem ersten Symbol des zweiten Arguments) ist kein legales Symbol. Wenn man eine weitere Makrodefiniti- Makrodefinition einführt «define xcat(x,y) cat(x.y) funktioniert es besser; xcat(xcat(l, 2), 3) liefert wirklich 123, denn die Expansion von xcat selbst enthält ## nicht. Ebenso produziert ABSDIFF(ABSDIFF(a,b),c) das erwartete, voll expandierte Re- Resultat. A.12.4 Einfügen von Dateien Eine Kontrollzeile der Form # include <fdename> wird durch den Inhalt der angegebenen Datei ersetzt. Als Zeichen im Namen filename dürfen weder > noch ein Zeilentrenner auftreten, und der Effekt ist Undefiniert, wenn filename ", ', \ oder /• enthält. Nach der angegebenen Datei wird an einer Folge von Stellen gesucht, die von der Implementierung abhängen. Analog wird bei einer Kontrollzeile der Form # include "filename" zuerst im Zusammenhang mit der ursprünglichen Quelldatei gesucht (absichtlich imple- implementierungsabhängig formuliert), und wenn diese Suche erfolglos^ bleibt, dann^ sojwie in der ersten Form. Der Effekt von ', \ oder /• im Dateinamen bleibt Undefiniert, aber > ist erlaubt.
230 A I jrachbeschreibung Schließlich wird eine Anweisung der Form # include token-sequence die keiner der vorhergehenden Formen entspricht, so interpretiert, daß zunächst die token-sequence wie normaler Text expandiert wird. Eine der beiden Formen mit <... > oder "..." muß entstehen, und sie wird dann so bearbeitet wie vorher beschrieben. A.12.5 Bedingte Übersetzung Teile eines Programms können bedingt übersetzt werden, wie im folgenden Syn- Syntax-Schema beschrieben: preprocessor-conditional: if-line text elif-parts else-part^ //end if if-line: // if constant-expression # if def identifier # ifndef identifier elif-parts: elif-line text elif-partsopl elif-line: // el if constant-expression else-part: else-line text else-line: # else Jede der Anweisungen (if-line, elif-line, else-line und #endif) erscheint allein auf einer Zeile. Die konstanten Ausdrücke in #if- und nachfolgenden #elif-Zeilen werden der Reihe nach bewertet, bis ein Ausdruck mit einem von Null verschiedenen Wert gefunden wird; der Text, der einer Zeile mit Wert 0 folgt, wird übergangen. Der Text im Anschluß an die erfolgreiche Anweisungszeile wird normal verarbeitet, text bezeichnet hier beliebi- beliebiges Material, inklusive Preprozessor-Zeilen, das nicht Teil der #if-Struktur ist; dieser Teil kann auch leer sein. Wenn eine erfolgreiche #if- oder #elif-Zeile gefunden und der zugehörige Text verarbeitet wurde, werden nachfolgende #elif- und #else-Zeilen zusam- zusammen mit ihrem Text übergangen. Sind alle Ausdrücke null und gibt es #else, wird der Text nach #else normal verarbeitet. Text, der von inaktiven Zweigen der #if-Struktur kontrolliert wird, wird ignoriert, abgesehen davon, daß die Verschachtelung der #if- Strukturen kontrolliert wird. Im konstante^ Ausdruck beiJfitjind^eUf findetjiormale Makro-Expansion statt. Außerdem werden Ausdrücke der Form defined identifier oder defined ( identifier ) ersetzt, bevor nach Makros gesucht wird,,und zwar duTÖiJLL, wenn der Name im Prepro- zessor definiert ist, und durch 0L, falls mcht._WejMjiach Makro-Expansion noch Namen
A. 12 Der Pr( zessor 231 verbleiben, werden sie durch PL ersetzt. Schließlich wird für jede Integer-Konstante L als Suffix angenommen, damit die Arithmetik im Bereich long oder unsigned long statt- stattfindet. Der resultierende konstante Ausdruck (§ A.7.19) ist eingeschränkt: er muß ganz- ganzzahlig sein und darf sizeof, Umwandlungsoperationen und Aufzählungskonstanten nicht enthalten. Die Kontrollzeilen # if def identifier # ifndef identifier sind jeweils zu // if defined identifier # if t defined identifier äquivalent. #elif ist neu seit der Ersten Ausgabe, war aber in manchen Preprozessoren vorhanden. Der Preprozessor-Operator defined ist ebenfalls neu. A.12.6 Zeilenkontrolle Als Hilfe für andere Preprozessoren, die C-Programme generieren, sorgen Zeilen folgender Form # line constant "filename" # line constant dafür, daß der Übersetzer für Fehlermeldungen annimmt, daß die dezimale Integer-Kon- Integer-Konstante die Zeilennummer der nächsten Quellzeile ist und daß filename den Namen der aktuellen Eingabedatei festlegt. Fehlt die Angabe eines Dateinamens, bleibt der vorher gespeicherte Name unverändert. Makros auf der Zeile werden expandiert, bevor die Zeile interpretiert wird. A.12.7 Fehlermeldungen Eine Preprozessor-Zeile der Form // error token-sequenceopt veranlaßt den Prozessor, eine Fehlermeldung auszugeben, die token-sequence enthält. A.12.8 pragma Eine Kontrollzeile der Form # pragma token-sequence^ veranlaßt den Prozessor, eine implementierungsabhängige Aktion durchzuführen. Eine unbekannte pragma-Anweisung wird ignoriert. A.12.9 Leere Anweisung Eine Preprozessor-Zeile der Form # hat keinen Effekt.
232 A C-Sprachbeschreibung A.12.10 Vordefinierte Namen Einige Namen sind vordefiniert und liefern besondere Information, wenn sie ex- expandiert werden. Wie auch der Preprozessor-Operator defined dürfen sie weder gelöscht noch umdefiniert werden. line Eine dezimale Konstante, die die Nummer der aktuellen Quellzeile enthält. file Eine konstante Zeichenkette, die den Namen der Datei enthält, die gerade übersetzt wird. date Eine konstante Zeichenkette, die das Datum der Übersetzung enthält, in der Form "Mmm dd yyyy". time Eine konstante Zeichenkette, die die Uhrzeit der Übersetzung enthält, in der Form "hh:itm:ss". STDC Die Konstante 1. Dieser Name soll mit Wert 1 nur in Implementierungen definiert werden, die dem Standard genügen. #error und #pragma sind neu im ANSI-Standard; die vordefinierten Preprozessor-Ma- kros sind neu, aber einige von ihnen waren in manchen Implementierungen schon vor- vorhanden. A.13 Grammatik Hier folgt nochmals die Grammatik, die schon in den vorhergehenden Abschnitten dieses Anhangs gezeigt wurde. Diese Grammatik hat exakt den gleichen Inhalt, aber ei- eine andere Reihenfolge. Die Grammatik hat die bislang Undefinierten Eingabesymbole integer-constant, character-constant, floating-constant, identifier, string und enumeration-constant; Worte in Schreibmaschinenschrift sowie Symbole aus Sonderzeichen sind Eingabe- Eingabesymbole, die unverändert angegeben werden. Die Grammatik kann mechanisch in eine Eingabe transformiert werden, die ein automatischer Parser-Generator akzeptiert. Dazu müssen die Alternativen in den Regeln syntaktisch korrekt markiert werden. Außerdem müssen die Regeln mit „eins von" expandiert und (je nach den Vorschriften des Parser- Generators) die Regeln mit opt mit dem und ohne das optionale Symbol dupliziert wer- werden. Entfernt man dann noch die Regel typedef-name: identifier und vereinbart typedef- name als Eingabesymbol, so akzeptiert jie£J'arser-Generator Yacc diese Grammatik. Die Grammatik hat einen einzigen Konflikt, der von der if-else^Mehrdeutigkeit herrührt. translation-unit: external-declaration translation-unit external-declaration external-declaration: function-definition declaration function-definition: declaration-specifiersopl declarator declaration-listopt compound-statement declaration: declaration-specifiers init-declarator-list^, ;
A.13 Grammatik 233 declaration-list: declaration declaration-list declaration declaration-specifiers: storage-class-specifier declaration-specifiers^ type-specifierdeclaration-specifiersopl type-qualifier declaration-specifiers^, storage-class-specifier: eins von auto register static extern typedef type-specifier: eins von void char short int long float double signed unsigned struct-or-union-specifier enum-specifier typedef-name type-qualifier: eins von const volatile struct-or-union-specifier: struct-or-union identifier^ ( struct-declaration-list ) struct-or-union identifier struct-or-union: eins von struct union struct-declaration-list: struct-declaration struct-declaration-list struct-declaration init-declarator-list: init-declarator init-declarator-list , init-declarator init-declarator: declarator declarator - initializer struct-declaration: specifier-qualifier-list struct-declarator-list ; specifier-qualifier-list: type-specifier specifier-qualifier-listop, type-qualifier specifier-qualifier-listop, struct-declarator-list: struct-declarator struct-declarator-list , struct-declarator struct-declarator: declarator declarator^ : constant-expression enum-specifier: enum identifier^ ( enumerator-list ) enum identifier
234 / Sprachbeschreibung enumerator-list: enumerator enumerator-list , enumerator enumerator: identifier identifier ■> constant-expression declarator: pointer^ direct-declarator direct-declarator identifier ( declarator ) direct-declarator [ constant-expressionopl ] direct-declarator ( parameter-type-list ) direct-declarator ( identifier-listopl ) pointer: * type-qualifier-listop, * type-qualifier-listopl pointer type-qualifier-list: type-qualifier type-qualifier-list type-qualifier parameter-type-list: parameter-list parameter-list , ... parameter-list: parameter-declaration parameter-list , parameter-declaration parameter-declaration: declaration-specifiers declarator declaration-specifiers abstract-declarator^ identifier-list: identifier identifier-list , identifier initializer: assignment-expression ( initializer-list ) ( initializer-list , ) initializer-list: initializer initializer-list , initializer type-name: specifier-qualifier-list abstract-declarator^
A.13 Grammai abstract-declarator: pointer pointer^ direct-abstract-declarator direct-abstract-declarator: ( abstract-declarator ) direct-abstract-declaratoropl [ constant-expression^ ] direct-abstract-declaratoropt ( parameter-type-list^ ) typedef-name: identifier statement: labeled-statement expression-statement compound-statement selection-statement iteration-statement jump-statement labeled-statement: identifier : statement case constant-expression : statement default : statement expression-statement: expression^ ; compound-statement: ( declaration-listoplstatement-listopl) statement-list: statement statement-list statement selection-statement: if ( expression ) statement if ( expression ) statement else statement switch ( expression ) statement iteration-statement: while ( expression ) statement do statement while ( expression ) ; for ( expression^ ; expression^ ; expression^ ) statement jump-statement: goto identifier ; continue ; break ; return expression^ ; expression: assignment-expression expression , assignment-expression
236 / "'-Sprachbeschreibung assignment-expression: conditional-expression unary-expression assignment-operator assignment-expression assignment-operator: eins von . - *- /- x- +- — «- »- &- *- |- conditional-expression: logical-OR-expression logical-OR-expression 1 expression : conditional-expression constant-expression: conditional-expression logical-OR-expression: logical-A ND-expression logical-OR-expression \ \ logical-AND-expression logical-AND-expression: inclusive-OR-expression logical-AND-expression && inclusive-OR-expression inclusive-OR-expression: exclusive-OR-expression inclusive-OR-expression \ exclusive-OR-expression exclusive-OR-expression: AND-expression exclusive-OR-expression A AND-expression AND-expression: equality-expression AND-expression & equality-expression equality-expression: relational-expression equality-expression ■— relational-expression equality-expression ! ■• relational-expression relational-expression: shift-expression relational-expression < shift-expression relational-expression > shift-expression relational-expression <— shift-expression relational-expression >— shift-expression shift-expression: additive-expression shift-expression « additive-expression shift-expression » additive-expression additive-expression: multiplicative-expression additive-expression + multiplicative-expression additive-expression - multiplicative-expression
A.13 Grammat' multiplicative-expression: cast-expression multiplicative-expression * cast-expression multiplicative-expression / cast-expression multiplicative-expression X cast-expression cast-expression: unary-expression ( type-name ) cast-expression unary-expression: postfix-expression ++ unary-expression — unary-expression unary-operator cast-expression sizeof unary-expression sizeof ( type-name ) unary-operator eins von & * + - ~ ! postfix-expression: primary-expression postfix-expression [ expression ] postfix-expression ( argument-expression-listopl ) postfix-expression . identifier postfix-expression -> identifier postfix-expression ++ postfix-expression — primary-expression: identifier constant string ( expression ) argument-expression-list: assignment-expression argument-expression-list , assignment-expression constant: integer-constant character-constant floating-constant enumeration-constant
238 A_f )rachbeschreibung Die folgende Grammatik für den Preprozessor faßt die Struktur der Kontrollzeilen zusammen, aber sie eignet sich nicht für eine mechanisierte Erkennung. Sie enthält das Symbol text, das sich auf gewöhnlichen Programmtext, unbedingt zu bearbeitende Pre- prozessor-Kontrollzeilen oder vollständige bedingte Preprozessor-Konstruktionen be- bezieht. control-line: it define identifier token-sequence it define identifier^ identifier identifier ) token-sequence it undef identifier it include <filename> # include "filename" it include token-sequence it line constant "filename" it 1 ine constant it error token-sequence^ it pragma token-sequence ^ it preprocessor-conditional preprocessor-conditional: if-line text elif-parts else-part^, it end if if-line: it if constant-expression it ifdef identifier it ifndef identifier elif-parts: elif-line text elif-parts^ elif-line: it el if constant-expression else-part: else-line text else-line: it else
239 B Die Standard-Bibliothek Dieser Anhang ist eine kurze Beschreibung der Bibliothek, die im ANSI-Standard definiert wird. Die Standard-Bibliothek ist kein Teil der Programmiersprache C selbst, aber eine Umgebung, die Standard-C realisiert, wird auch die Funktionen, Typen und Makros dieser Bibliothek zur Verfügung stellen. Wir haben ein paar Funktionen ausge- ausgelassen, die nur begrenzt nützlich oder leicht aus anderen aufgebaut werden können; wir haben Multi-Byte-Zeichen weggelassen, und wir besprechen nationale Gesichtspunkte nicht, das heißt, Eigenschaften, die von der lokalen Sprache, Nationalität oderTCultur ab- abhängen. Die Funktionen, Typen und Makros der Standard-Bibliothek sind in Standard-De- Standard-Definitionsdateien deklariert: <assert.h> <ftoat.h> <math.h> <stdarg.h> <stdlib.h> <ctype.h> <timits.h> <setjnp.h> <stddef.h> <string.h> <errno.h> <locate.h> <signal.h> <stdio.h> <time.h> Auf eine Definitionsdatei header wird mit «include <header> zugegriffen. Definitionsdateien können in beliebiger Reihenfolge und beliebig oft mit #include eingefügt werden. Eine Definitionsdatei muß außerhalb von allen externen Vereinbarungen eingefügt werden, und zwar bevor irgendetwas benutzt wird, das in der Definitionsdatei vereinbart wird. Eine Definitionsdatei muß keine Quelldatei sein. Für die Standard-Bibliothek sind externe Namen reserviert, die mit einem Unter- Unterstrich „_" beginnen sowie alle anderen Namen, die mit einem Unterstrich und einem Großbuchstaben oder einem weiteren Unterstrich beginnen. B.1 Ein-und Ausgabe: <stdio.h> Die Ein- und Ausgabefunktionen, Typen und Makros, die in <stdio.h > vereinbart sind, machen nahezu ein Drittel der Bibliothek aus. Ein Datenstrom (stream) ist Quelle oder Ziel von Daten und wird mit einer Platte oder einem anderen Peripheriegerät verknüpft. Die Bibliothek unterstützt zwei Arten vor^Datenströmen^för JText^und binärejnformation, die_allerdings bei manchen Syste- men_und insbesondere bei UNIX identisch sind. Ein Textstrom ist eine Folge von Zeilen; jede Zeile enthalt null oder mehr Zeichen und ist mit '\n' abgeschlossen. Eine Umge- Umgebung muß möglicherweise zwischen einem Textstrom und einer anderen Repräsentierung umwandeln (also zum Beispiel '\n* als Wagenrücklauf und Zeilenvorschub abbilden). Ein Binarstrom ist eine Folge unbearbeiteter Bytes zur Aufzeichnung interner Daten. Wird ein Binarstrom geschrieben und auf dem gleichen System wieder eingelesen, so ent- entsteht die gleiche Information. Ein Strom wird durch Eröffnen (open) mit einer Datei oder einem Gerät verbun- verbunden; die Verbindung wird durch Abschließen (dose) wieder aufgehoben. Eröffnet man eine Datei, so erhält man einen Zeiger auf ein Objekt vom Typ FILE, wo alle Information hinterlegt ist, die zur Kontrolle des Stroms nötig ist. Wenn die Bedeutung eindeutig ist, werden wir die Begriffe FILE-Zeiger und Datenstrom gleichberechtigt verwenden.
240 B_^ Standard-Bibliothek Wenn die Ausführung eines Programms beginnt, sind die drei Ströme stdin, stdout und stderr bereits eröffnet. B.l.l Dateioperationen Die folgenden Funktionen beschäftigen sich mit Dateioperationen. Der Typ size_t ist der vorzeichenlose, ganzzahlige Resultattyp des sizeof-Operators. FILE *fopen(const char *filename, const char *mode) fopen eröffnet die angegebene Datei und liefert einen Datenstrom oder NULL bei Mißerfolg. Zu den erlaubten Werten von mode gehören "r" Textdatei zum Lesen eröffnen "w" Textdatei zum Schreiben erzeugen; gegebenenfalls alten Inhalt wegwerfen "a" anfügen; Textdatei zum Schreiben am Dateiende eröffnen oder erzeugen "r+" Textdatei zum Ändern eröffnen (Lesen und Schreiben) "»+" Textdatei zum Ändern erzeugen; gegebenenfalls alten Inhalt wegwerfen »a+" anfügen; Textdatei zum Ändern eröffnen oder erzeugen, Schreiben am Ende Ändern bedeutet, daß die gleiche Datei gelesen und geschrieben werden darf; fflush oder eine Funktion zum Positionieren in Dateien muß zwischen einer Lese- und einer Schreib- Schreiboperation oder umgekehrt aufgerufen werden. Enthält mode nach dem ersten Zeichen noch b, also etwa "rb" oder V+b", dann wird auf eine binäre Datei zugegriffen. Dateina- Dateinamen sind auf FILENAMEMAX Zeichen begrenzt. Höchstens FOPENMAX Dateien kön- können gleichzeitig offen sein. FILE *freopen(const char *filename, const char *mode, FILE *stream) freopen eröffnet die Datei für den angegebenen Zugriff mode und verknüpft stream damit. Das Resultat ist stream oder NULL bei einem Fehler. Mit freopen ändert man normalerweise die Dateien, die mit stdin, stdout oder stderr verknüpftjind. int fflush(FILE *stream) Bei einem Ausgabestrom sorgt fflush dafür, daß gepufferte, aber noch nicht ge- geschriebene Daten geschrieben werden; bei einem Eingabestrom ist der Effekt Undefi- Undefiniert. Die Funktion liefert EOF bei einem Schreibfehler und sonst Null. fflush(NULL) bezieht sich auf alle offenen Dateien. int fclose(FILE *stream) fclose schreibt noch nicht geschriebene Daten für stream, wirft noch nicht gelese- gelesene, gepufferte Eingaben weg, gibt automatisch angelegte Puffer frei und schließt den Da- Datenstrom. Die Funktion liefert EOF bei Fehlern und sonst Null. int remove(const char «filename) remove entfernt die angegebene Datei, so daß ein anschließender Versuch, sie zu eröffnen, fehlschlagen wird. Die Funktion liefert bei Fehlern einen von Null verschiede- verschiedenen Wert. int rename(const char *oldname, const char *newname) rename ändert den Namen einer Datei und liefert nicht Null, wenn der Versuch fehlschlägt.
B.l Ein- und Aus e: <stdio.h> 241 FILE *tmpfile(void) tmpflle erzeugt eine temporäre Datei mit Zugriff "wb+", die automatisch gelöscht wird, wenn der Zugriff abgeschlossen wird, oder wenn das Programm normal zu Ende geht, tmpfile liefert einen Datenstrom oder NULL, wenn die Datei nicht erzeugt werden konnte. char *tmpnam(char s[L_tmpnam]) tmpnam(NULL) erzeugt eine Zeichenkette, die nicht der Name einer existenten Datei ist, und liefert einen Zeiger auf einen internen Vektor im statischen Speicherbe- Speicherbereich, tmpnam(s) speichert die Zeichenkette in s und liefert auch s als Resultat; in s müssen wenigstens L_tmpnam Zeichen abgelegt werden können, tmpnam erzeugt bei jedem Aufruf einen anderen Namen; man kann höchstens von TMP_MAX verschiedenen Namen während der Ausführung des Programms ausgehen. Zu beachten ist, daß tmpnam einen Namen und keine Datei erzeugt. int setvbuf(FILE *stream, char *buf, int mode, size_t size) setvbuf kontrolliert die Pufferung bei einem Datenstrom; die Funktion muß aufge- aufgerufen werden, bevor gelesen oder geschrieben wird, und vor allen anderen Operationen. Hat mode den Wert IOFBF, so wird vollständig gepuffert, IOLBF sorgt für zeilenweise Pufferung bei Textdateien und _IONBF verhindert Puffern. Wenn buf nicht NULL ist, wird buf als Puffer verwendet; andernfalls wird ein Puffer angelegt, size legt die Puffer- Puffergröße fest. Bei einem Fehler liefert setvbuf nicht Null. void setbuf(FILE *stream, char *buf) Wenn buf den Wert NULL hat, wird der Datenstrom nicht gepuffert. Andernfalls ist setbuf äquivalent zu (void) setvbuf (stream, buf, IOFBF, BUFSIZ). B.U Formatierte Ausgabe Die printf-Funktionen ermöglichen Ausgabe-Umwandlungen unter Formatkon- Formatkontrolle. int fprintf(FILE *stream, const char *fonnat, ...) fprintf wandelt Ausgaben um und schreibt sie in stream unter Kontrolle von format. Der Resultatwert ist die Anzahl der geschriebenen Zeichen; er ist negativ, wenn ein Fehler passiert ist. Die Format-Zeichenkette enthält zwei Arten von Objekten: gewöhnliche Zeichen, die in die Ausgabe kopiert werden, und Umwandlungsangaben, die jeweils die Umwand- Umwandlung und Ausgabe des nächstfolgenden Arguments von fprintf veranlassen. Jede Um- Umwandlungsangabe beginnt mit dem Zeichen % und endet mit einem Umwandlungszei- Umwandlungszeichen. Zwischen % und dem Umwandlungszeichen kann, der Reihe nach, folgendes ange- angegeben werden: • Steuerzeichen (flags) (in beliebiger Reihenfolge), die die Umwandlung modifizieren: - veranlaßt Ausrichtung des umgewandelten Arguments in seinem Feld nach links. + bestimmt, daß die Zahl immer mit Vorzeichen ausgegeben wird.
Leerzeichen wenn das erste Zeichen kein Vorzeichen ist, wird ein Leerzeichen vorange- vorangestellt. 0 legt bei numerischen Umwandlungen fest, daß bis zur Feldbreite mit führen- führenden Nullen aufgefüllt wird. # verlangt eine alternative Form der Ausgabe. Bei o ist die erste Ziffer eine Null. Bei x oder X werden Ox oder OX einem von Null verschiedenen Resultat vorangestellt. Bei e, E, f, g und G enthält die Ausgabe immer einen Dezimal- Dezimalpunkt; bei g und G werden Nullen am Schluß nicht unterdrückt. • eine Zahl, die eine minimale Feldbreite festlegt. Das umgewandelte Argument wird in einem Feld ausgegeben, das mindestens so breit ist und bei Bedarf auch breiter. Hat das umgewandelte Argument weniger Zeichen als die Feldbreite verlangt, wird links (oder rechts, wenn Ausrichtung nach links verlangt wurde) auf die Feldbreite aufgefüllt. Normalerweise wird mit Leerzeichen aufgefüllt, aber auch mit Nullen, wenn das entsprechende Steuerzeichen angegeben wurde. • Ein Punkt, der die Feldbreite von der Genauigkeit {precision) trennt. • Eine Zahl, die Genauigkeit, die die maxmalej\nzahl^on Zeichen festlegt, die von ei- einer Zeichenkette ausgegeben werden, oder die Anzahl Ziffern, die nach dem Dezi- Dezimalpunkt bei e, E oder f Umwandlungen ausgegeben werden, oder die Anzahl signifi- signifikanter Ziffern bei g oder G Umwandlung oder die minimale Anzahl von Ziffern, die bei einem ganzzahligen Wert ausgegeben werden sollen (führende Nullen werden dann bis zur gewünschten Breite hinzugefügt). • Ein Buchstabe als Längenangabe: h, 1 oder L. „h" bedeutet, daß das zugehörige Ar- gument als short oder unsigned short ausgegeben wird; „1" bedeutet, daß das Argu- Argument long oder unsigned long ist; „L" bedeutet, daß das Argument long double ist. Als Feldbreite oder Genauigkeit kann jeweils * angegeben werden; dann wird der Wert durch Umwandlung von dem nächsten oder den zwei nächsten Argumenten festgelegt, die den Typ int besitzen müssen. Die Umwandlungszeichen und ihre Bedeutung erklärt Tabelle B-l. Wenn das Zei- Zeichen nach % kein Umwandlungszeichen ist, ist der Verlauf Undefiniert. int printf(const char *format, ...) printfC) ist äquivalent zu fprintf(stdout,...). int sprintf(char *s, const char *format, ...) sprintf funktioniert wie printf, nur wird die Ausgabe in den Zeichenvektor s ge- geschrieben und mit '\0' abgeschlossen, s muß groß genug für das Resultat sein. Im Re- Resultatwert wird '\0' nicht mitgezählt. vprintf(const char *format, va_list arg) vfprintf(FILE *stream, const char *format, va_list arg) vsprintf(char *s, const char *format, va_list arg) Die Funktionen vprintf, vfprintf und vsprintf sind äquivalent zu den entsprechen- entsprechenden printf-Funktionen, jedoch wird die variable Argumentliste durch arg ersetzt. Dieser Wert wird mit dem Makro va_start und vielleicht mit Aufrufen von va_arg initialisiert. Siehe dazu die Beschreibung von < stdarg.li > im Abschnitt B.7.
B.l Ein-und Au- e: <stdio.h> 243 Zeichen Tabelle B-l. printf Umwandlungen Argument; Umwandlung in d, i o x,X u c f e, E 9,G int; dezimal mit Vorzeichen. int; oktal ohne Vorzeichen (ohne führende Null). int; hexadezimal ohne Vorzeichen (ohne führendes Ox oder OX), mit abcdef bei Ox oder ABCDEF bei OX. int; dezimal ohne Vorzeichen int; einzelnes Zeichen, nach Umwandlung in unsigned char. char •; aus der Zeichenkette werden Zeichen ausgegeben bis vor '\0' oder so viele Zeichen, wie die Genauigkeit verlangt. double; dezimal als \-\nmm.ddd, wobei die Genauigkeit die Anzahl der d festlegt. Voreinstellung ist 6; bei 0 entfällt der Dezimalpunkt. double; dezimal als [-]m.dddddde±xx oder [-yn.ddddddE±xx, wobei die Genauigkeit die Anzahl der d festlegt. Voreinstellung ist 6; bei 0 entfällt der Dezimalpunkt. double; %e oder %E wird verwendet, wenn der Exponent kleiner als -4 oder nicht kleiner als die Genauigkeit ist; sonst wird %f benutzt. Null und Dezimalpunkt am Schluß werden nicht ausgegeben. void *; als Zeiger (Darstellung hängt von Implementierung ab). int *; die Anzahl der bisher von diesem Aufruf von printf ausgegebenen Zeichen wird im Argument abgelegt. Ein Argument wird nicht umgewandelt. kein Argument wird umgewandelt; ein % wird ausgegeben. B.13 Formatierte Eingabe Die scanf-Funktionen behandeln Eingabe-Umwandlungen unter Formatkontrolle, int fscanf(FILE *stream, const char *format, ...) fscanf liest von stream unter Kontrolle von format und legt umgewandelte Werte mit Hilfe von nachfolgenden Argumenten ab, die alle Zeiger sein müssen. Die Funktion wird beendet, wenn format abgearbeitet ist. fscanf liefert EOF, wenn vor der ersten Um- Umwandlung das Dateiende erreicht wird oder ein Fehler passiert; andernfalls liefert die Funktion die Anzahl der umgewandelten und abgelegten Eingaben. Die Format-Zeichenkette enthält normalerweise Umwandlungsangaben, die zur Interpretation der Eingabe verwendet werden. Die Format-Zeichenkette kann folgendes enthalten: • Leerzeichen oder Tabulatorzeichen, die ignoriert werden. • Gewöhnliche Zeichen (nicht aber %), die dem nächsten Zeichen nach Zwischenraum im Eingabestrom entsprechen müssen. • Umwandlungsangaben, bestehend aus %; einem optionalen Zeichen *, das die Zuwei- Zuweisung an ein Argument verhindert; einer optionalen Zahl, die die maximale Feldbreite festlegt; einem optionalen Buchstaben h, 1 oder L, der die Länge des Ziels beschreibt; und einem Umwandlungszeichen.
244 * ">ie Standard-Bibliothek Eine Umwandlungsangabe bestimmt die Umwandlung des nächsten Eingabefelds. Normalerweise wird das Resultat in der Variablen abgelegt, auf die das zugehörige Argu- Argument zeigt. Wenn jedoch * die Zuweisung verhindern soll, wie bei %*s, dann wird das Eingabefeld einfach übergangen und eine Zuweisung findet nicht statt. Ein Eingabefeld ist als Folge von Zeichen definiert, die keine Zwischenraumzeichen sind; es reicht entwe- entweder bis zum nächsten Zwischenraumzeichen, oder bis eine explizit angegebene Feldbreite erreicht ist. Daraus folgte daß scanf über Zeilengrenzen hinweg Jiest, um seine Eingabe zu finden, denn Zeilentrenner sind Zwischenraumzeichen. (Zwischenraumzeichen sind Leerzeichen, Tabulatorzeichen \t, Zeilentrenner \n, Wagenrücklauf \r, Vertikal-Tabula- tor \v und Seitenyorschub \f). Das Umwandlungszeichen gibt die Interpretation des Eingabefelds an. Das zu- zugehörige Argument muß ein Zeiger sein. Die erlaubten Umwandlungszeichen zeigt Ta- Tabelle B-2. Den Umwandlungszeichen d, 1, n, o, u und x kann h vorausgehen, wenn das Argu- Argument ein Zeiger auf short statt int ist, oder der Buchstabe 1, wenn das Argument ein Zei- Zeiger auf long ist. Vor den Umwandlungszeichen e, f und g kann der Buchstabe 1 stehen, wenn ein Zeiger auf double und nicht auf float in der Argumentliste steht, und L, wenn es sich um einen Zeiger auf long double handelt. int scanf(const char *format, ...) scanf(...) ist äquivalent zu fscanf(stdin,.„). int sscanf(char *s, const char *format, ...) sscanf(s,...) ist äquivalent zu scanf(...), mit dem Unterschied, daß die Eingabezei- Eingabezeichen aus der Zeichenkette s stammen. B.1.4 Ein- und Ausgabe von Zeichen int fgetc(FILE *stream) fgetc liefert das nächste Zeichen aus stream als unsigned char (umgewandelt in int) oder EOF bei Dateiende oder bei einem Fehler. char *fgets(char *s, int n, FILE *stream) fgets liest höchstens die nächsten n-1 Zeichen in s ein und hört vorher auf, wenn ein Zeilentrenner gefunden wird. Der Zeilentrenner wird im Vektor abgelegt. Der Vek- Vektor wird mit '\0' abgeschlossen, fgets liefert s oder NULL bei Dateiende oder bei einem Fehler. int fputc(int c, FILE *stream) fputc schreibt das Zeichen c (umgewandelt in unsigned char) in stream. Die Funk- Funktion liefert das ausgegebene Zeichen oder EOF bei Fehler. int fputs(const char *s, FILE *stream) fputs schreibt die Zeichenkette s (die '\n' nicht zu enthalten braucht) in stream. Die Funktion liefert einen nicht-negativen Wert oder EOF bei einem Fehler.
B.l Ein- und A abe: <stdio.h> 245 Zeichen Tabelle B-2. scanf Umwandlungen Eingabedaten; Argumenttyp e,f,g C...] tA..J dezimal, ganzzahlig; int •. ganzzahlig; int *. Der Wert kann oktal (mit 0 am Anfang) oder hexadezimal (mit Ox oder OX am Anfang) angegeben sein. oktal ganzzahlig (mit oder ohne 0 am Anfang); int *. dezimal ohne Vorzeichen; unsigned int *. hexadezimal ganzzahlig (mit oder ohne Ox oder OX am Anfang); int *. ein oder mehrere Zeichen; char •. Die nachfolgenden Eingabezeichen werden im angegebenen Vektor abgelegt, bis die Feldbreite erreicht ist; Voreinstellung is 1. *\0' wird nicht angefügt. In diesem Fall wird Zwischenraum nicht überlesen; das nächste Zeichen nach Zwischenraum liest man mit %ls. Folge von Nicht-Zwischenraum-Zeichen (ohne Anführungszeichen); char •, der auf einen Vektor zeigen muß, der die Zeichenkette und das abschließende '\0' aufnehmen kann, das dazukommt. Gleitpunkt; float*. Das Eingabeformat erlaubt für float ein optionales Vorzeichen, eine Ziffernfolge, die auch einen Dezunalpunkt enthalten kann, und einen optionalen Exponenten, bestehend aus E oder e und einer ganzen Zahl, optional mit Vorzeichen. Zeiger, wie ihn printf("%p") ausgibt; void •. legt im Argument die Anzahl Zeichen ab, die bei diesem Aufruf bisher gelesen wurden; int *. Vom Eingabestrom wird nicht gelesen, die Zählung der Umwandlungen bleibt unbeeinflußt. erkennt die längste nicht-leere Zeichenkette aus den Eingabezeichen in der angegebenen Klasse; char •. Dazu kommt '\0\ Die Klasse []...] enthält auch ]. erkennt die längste nicht-leere Zeichenkette aus den Eingabezeichen nicht in der angegebenen Klasse; char •. Dazu kommt '\0\ Die Klasse [*]...] enthält auch]. erkennt %; eine Zuweisung fmdet nicht statt. int getc(FILE *stream) getc ist äquivalent zu fgetc, kann aber ein Makro sein und dann das Argument für stream mehr als einmal bewerten. int getchar(void) getchar ist äquivalent zu getc(stdin). char *gets(char *s) gets liest die nächste Zeile von stdin in den Vektor s und ersetzt dabei den ab- schließenden Zeilentrenner durch '\0\ Die Funktion liefert s oder NULL bei Dateiende oder bei einem Fehler.
246 B_ Standard-Bibliothek int putc(int c, FILE *stream) putc ist äquivalent zu fputc, kann aber ein Makro sein und dann das Argument für stream mehr als einmal bewerten. int putchar(int c) putchar(c) ist äquivalent zu putc(c,stdout). int puts(const char *s) puts schreibt die Zeichenkette s und einen Zeilentrenner in stdout. Die Funktion liefert EOF, wenn ein Fehler passiert, andernfalls einen nicht-negativen Wert. int ungetc(int c, FILE *stream) ungetc stellt c (umgewandelt in unsigned char) in stream zurück, von wo das Zei- Zeichen beim nächsten Lesevorgang wieder geholt wird. Man kann sich nur darauf verlas- verlassen, daß pro Datenstrom ein Zeichen zurückgestellt werden kann. EOF darf nicht zurückgestellt werden, ungetc liefert das zurückgestellte Zeichen oder EOF bei einem Fehler. B.1.5 Direkte Ein-und Ausgabe size_t fread(void *ptr, size_t size, size_t nobj, FILE *stream) fread liest aus stream in den Vektor ptr höchstens nobj Objekte der Größe size ein. fread liefert die Anzahl der eingelesenen Objekte; das kann weniger als die gefor- geforderte Zahl sein. Der Zustand des Datenstroms muß mit feof und Terror untersucht wer- werden. size_t fwrite(const void *ptr, size_t size, size_t nobj, FILE *stream) fwrite schreibt nobj Objekte der Größe size aus dem Vektor ptr in stream. Die Funktion liefert die Anzahl der ausgegebenen Objekte; bei Fehler ist das weniger als nobj. B.1.6 Positionieren in Dateien int £seek(FILE *stream, long offset, int origin) fseek setzt die Dateiposition für stream; eine nachfolgende Lese- oder Schreibope- Schreiboperation wird auf Daten von der neuen Position ab zugreifen. Für eine binäre Datei wird die Position auf offset Zeichen relativ zu origin eingestellt; dabei können die Werte SEEKSET (Dateianfang) SEEKCUR (aktuelle Position) oder SEEKEND (Dateiende) angegeben werden. Für einen Textstrom muß offset Null sein oder ein Wert, der von ftell stammt (dafür muß dann origin den Wert SEEKSET erhalten), fseek liefert einen von Null verschiedenen Wert bei Fehler. long ftell(FILE *stream) ftell liefert die aktuelle Dateiposition für stream oder -1L bei Fehler.
B.2 Tests für Z enklassen: <ctype.h> 247 void rewind(FILE *stream) rewind(fp) ist äquivalent zu rseek(fp,OL,SEEK_SET); clearerr(rp). int fgetpos(FILE *stream, fpos_t *ptr) fgetpos speichert die aktuelle Position für stream bei *ptr. Der Wert kann später mit fsetpos verwendet werden. Der Datentyp fpos_t eignet sich zum Speichern von sol- solchen Werten. Bei Fehler liefert fgetpos einen von Null verschiedenen Wert. int fsetpos(FILE *stream, const fpos_t *ptr) fsetpos positioniert stream auf die Position, die von fgetpos in *ptr abgelegt wurde. Bei Fehler liefert fsetpos einen von Null verschiedenen Wert. B.1.7 Fehlerbehandlung Viele der Bibliotheksfunktionen notieren Zustandsangaben, wenn ein Dateiende oder ein Fehler gefunden wird. Diese Angaben können explizit gesetzt und getestet wer- werden. Außerdem kann der Integer-Ausdruck errno (der in <errno.h> deklariert ist) eine Fehlernummer enthalten, die mehr Information über den zuletzt aufgetretenen Fehler liefert. void clearerr(FILE *stream) clearerr löscht die Dateiende- und Fehlernotizen für stream, int feof(FILE *stream) feof liefert einen von Null verschiedenen Wert, wenn für stream ein Dateiende no- notiert ist. int ferror(FILE *stream) fen-or liefert einen von Null verschiedenen Wert, wenn für stream ein Fehler no- notiert ist. void perror(const char *s) perror(s) gibt s und eine von der Implementierung definierte Fehlermeldung aus, die sich auf die Fehlernummer in errno bezieht. Die Ausgabe erfolgt im Stil von fprintf(stderr, "Xs: Xs\n", s, "Fehlermeldung") Siehe strerror im Abschnitt B.3. B.2 Tests für Zeichenklassen: <ctype.h> Die Definitionsdatei <ctype.h> vereinbart Funktionen zum Testen von Zeichen. Jede Funktion hat ein int-Argument, dessen Wert entweder EOF ist oder als unsigned char dargestellt werden kann, und der Resultatwert hat den Typ int. Die Funktionen lie- liefern einen von Null verschiedenen Wert (wahr), wenn das Argument c die beschriebene Bedingung erfüllt; andernfalls liefern sie Null. isalnum(c) isalpha(c) oder isdigit(c) ist wahr isalpha(c) isupper(c) oder islower(c) ist wahr iscntrl(c) Steuerzeichen isdigit(c) dezimale Ziffer
248 F :e Standard-Bibliothek isgraph(c) sichtbares Zeichen, kein Leerzeichen istouer(c) Kleinbuchstabe (aber kein Umlaut oder ß) i spr i nt(c) sichtbares Zeichen, auch Leerzeichen ispunct(c) sichtbares Zeichen, mit Ausnahme von Leerzeichen, Buchstabe oder Ziffer isspace(c) Leerzeichen, Seitenvorschub (\f), Zeilentrenner (\n), Wagenrücklauf (\r), Tabulatorzeichen (\t), Vertikal- Tabulator (\v) isupper(c) Großbuchstabe (aber kein Umlaut) isxdigit(c) hexadezimale Ziffer Im 7-Bit Asai-Zeichensatz sind die sichtbaren Zeichen 0x20 ('') bis 0x7E ('"'); die Steu- Steuerzeichen sind 0 (NUL) bis OxlF (US) sowie 0x7F (DEL). Zusätzlich gibt es zwei Funktionen zur Umwandlung zwischen Groß- und Klein- Kleinbuchstaben: wandelt c in einen Kleinbuchstaben um wandelt c in einen Großbuchstaben um int tolower(int c) int toupperO'nt c) Wenn c ein Großbuchstabe ist, liefert tolower(c) den entsprechenden Kleinbuchstaben; andernfalls ist das Resultat c. Wenn c ein Kleinbuchstabe ist, liefert toupper(c) den ent- entsprechenden Großbuchstaben; andernfalls ist das Resultat c. B.3 Funktionen für Zeichenketten: < string.h > In der Defmitionsdatei < string.h > werden zwei Gruppen von Funktionen für Zei- Zeichenketten vereinbart. Die erste Gruppe hat Namen, die mit str beginnen; die Namen der zweiten Gruppe beginnen mit mem. Sieht man von memmove ab, ist der Effekt der Funktionen Undefiniert, wenn zwischen überlappenden Objekten kopiert wird. In der folgenden Tabelle sind die Variablen s und t vom Typ char •; die Parameter es und et haben den Typ const char •; der Parameter n hat den Typ size_t; und c ist ein int-Wert, der in char umgewandelt wird. Die Vergleichsfunktionen behandeln ihre Argu- Argumente als unsigned char Vektoren. char *strcpy(s,ct) Zeichenkette et in Vektor s kopieren, inklusive '\0'; liefert s. char *strncpy(s,ct,n) höchstens n Zeichen aus et in s kopieren; liefert s. Mit '\0' auffüllen, wenn et weniger als n Zeichen hat. Zeichenkette et hinten an die Zeichenkette s anfügen; liefert s. höchstens n Zeichen von et hinten an die Zeichenkette s anfügen und s mit '\0' abschließen; liefert s. Zeichenketten es und et vergleichen; liefert <0 wenn cs<ct, 0 wenn cs==ct, oder >0 wenn cs>ct. höchstens n Zeichen von es mit der Zeichenkette et vergleichen; liefert <0 wenn cs<ct, 0 wenn cs==ct, oder >0 wenn cs>ct. liefert Zeiger auf das erste c in es oder NULL, falls nicht vorhanden. char *strcat(s,ct) char *strncat(s,ct,n) int strcmp(cs,ct) int strncmp(cs,ct,n) char *strchr(cs,c)
B.4 Mathemat e Funktionen: <math.h> 249 char *»trrchr(cg,c) »izet strspn(cs,ct) »izet strcspn(cs,ct) char *strpbrk(cs,ct) char *strstr(cs,ct) sizet strten(cs) char *strerror(n) char *strtok(g,ct) liefert Zeiger auf das letzte c in es, oder NULL, falls nicht vorhanden. liefert Anzahl der Zeichen am Anfang von es, die sämtlich in et vorkommen. liefert Anzahl der Zeichen am Anfang von es, die sämtlich nicht in et vorkommen. liefert Zeiger auf die Position in es, an der irgendein Zeichen aus et erstmals vorkommt, oder NULL, falls keines vorkommt. liefert Zeiger auf erste Kopie von et in es oder NULL, falls nicht vorhanden. liefert Länge von es (ohne '\0'). liefert Zeiger auf Zeichenkette, die in der Implementierung für Fehler n definiert ist. strtok durchsucht s nach Zeichenfolgen, die durch Zeichen aus et begrenzt sind; siehe unten. Eine Folge von Aufrufen von strtok(s.ct) zerlegt s in Zeichenfolgen, die jeweils durch ein Zeichen aus et begrenzt sind. Beim ersten von einer Reihe von Aufrufen ist s nicht NULL. Dieser Aufruf findet die erste Zeichenfolge in s, die nicht aus Zeichen in et besteht; die Folge wird dadurch abgeschlossen, daß das nächste Zeichen in s mit '\0' überschrieben wird; der Aufruf liefert dann einen Zeiger auf die Zeichenfolge. Jeder weitere Aufruf der Reihe ist kenntlich dadurch, daß NULL für s übergeben wird; er liefert die nächste derartige Zeichenfolge, wobei unmittelbar nach dem Ende der vorhergehen- vorhergehenden mit der Suche begonnen wird, strtok liefert NULL, wenn keine weitere Zeichenfolge gefunden wird. Die Zeichenkette et kann bei jedem Aufruf verschieden sein. Die mem... Funktionen sind zur Manipulation von Objekten als Zeichenvektoren gedacht; sie sollen eine Schnittstelle zu effizienten Routinen sein. In der folgenden Ta- Tabelle haben s und t den Typ void •; die Argumente es und et sind vom "Typ const void *; das Argument n hat den Typ size t; und c ist ein int-Wert, der in unsigned char umge- umgewandelt wird. kopiert n Zeichen von et nach s; liefert s. wie memepy, funktioniert aber auch, wenn die Objekte überlappen. vergleicht die ersten n Zeichen von es mit et; Resultat analog zu stremp. liefert Zeiger auf das erste c in es oder NULL, wenn das Zeichen in den ersten n Zeichen nicht vorkommt. setzt die ersten n Zeichen von s auf den Wert c, liefert s. void *memcpy(s,ct,n) void *memn)ove(s,ct,n) int memcmp(cs,ct,n) void *memchr(cs,c,n) void *memset(s,c,n) B.4 Mathematische Funktionen: <math.h> Die Definitionsdatei <math.h> vereinbart mathematische Funktionen und Ma- Makros. Die Makros EDOM und ERANGE (die man in <errno.h> findet), sind von Null verschiedene ganzzahlige Konstanten, mit denen Fehler im Argument- und Resultatbe-
250 F %ie Standard-Bibliothek reich der Funktionen angezeigt werden; HUGE_VAL ist ein positiver double-Wert. Ein Argumentfehler (domain error) liegt vor, wenn ein Argument nicht in dem Bereich liegt, für den eine Funktion definiert ist. Bei einem Argumentfehler erhält errno den Wert EDOM; der Resultatwert hängt von der Implementierung ab. Ein Resultatfehler (range error) liegt vor, wenn das Resultat der Funktion nicht als double dargestellt werden kann. Ist das Resultat absolut zu groß, also bei overflow, liefert die Funktion HUGE_VAL mit dem korrekten Vorzeichen und errno erhält den Wert ERANGE. Ist das Resultat zu nahe bei Null, also bei underflow, liefert die Funktion null; je nach Implementation kann errno den Wert ERANGE erhalten. In der folgenden Tabelle sind x und y vom Typ double, das Argument n ist ein int- Wert, und alle Funktionen liefern double. Winkel werden bei trigonometrischen Funktio- Funktionen in Radian angegeben. sin(x) Sinus vonjc cos(x) Kosinus vonjc tan(x) Tangens vonjc asin(x) arcsin(jc) im Bereich [-*/2, n/2],x e [-1, 1]. acos(x) arccos(jc) im Bereich [0, n],x 6 [-1, 1]. atan(x) arctan(jc) im Bereich [-tf/2, n/2]. atan2(y,x) arctan(y /x) im Bereich [-5T, n]. sinh(x) Sinus Hyperbolicus vonx cosh(x) Cosinus Hyperbolicus vonjc t anh < x) Tangens Hyperbolicus von x exp( x) Exponentialfunktion ex log(x) natürlicher Logarithmus ln(jc),x >0. logiO(x) Logarithmus zur Basis 10 logio(jt),Jt>0. pow(x,y) xy. Ein Argumentfehler liegt vor bei x = 0 und y<0, oder bei x<0 und y ist nicht ganzzahlig, sqrt(x) ceil(x) floor(x) fabs(x) ldexp(x,n) frexp(x, int *exp) modf(x, double *ip) fmod(x,y) kleinster ganzzahliger Wert, der nicht kleiner als x ist, als double. größter ganzzahliger Wert, der nicht größer als x ist, als double. absoluter Wert | x \ x-2" zerlegt x in eine normalisierte Mantisse im Bereich [1/2, 1), die als Resultat geliefert wird, und eine Potenz von 2, die in *exp abgelegt wird. Ist x null, sind beide Teile des Resultats null. zerlegt x in einen ganzzahligen Teil und einen Rest, die beide das gleiche Vorzeichen wie x besitzen. Der ganzzahlige Teil wird bei •ip abgelegt, der Rest ist das Resultat. Gleitpunktrest von x/y, mit dem gleichen Vorzeichen wie x. Wenn y null ist, hängt das Resultat von der Implementierung ab.
B.5 Hilfsfunk: sn: <stdlib.h> 251 B.5 Hilfsfunktionen: <stdlib.h> Die Definitionsdatei < stdlib.h > vereinbart Funktionen zur Umwandlung von Zah- Zahlen, für Speicherverwaltung und ähnliche Aufgaben. double atof(const char *s) atof wandelt s in double um; die Funktion ist äquivalent zu strtod(s, (char«) NULL) int atoi(const char *s) atol wandelt s in int um; die Funktion ist äquivalent zu (int)strtol(s, (char**) NULL, 10) long atol(const char *s) atol wandelt s in long um; die Funktion ist äquivalent zu strtoKs, (char**) NULL, 10) double strtod(const char *s, char **endp) strtod wandelt den Anfang der Zeichenkette s in double um, dabei wird Zwischen- Zwischenraum am Anfang ignoriert. Die Funktion speichert einen Zeiger auf einen nicht umge- umgewandelten Rest der Zeichenkette bei *endp, falls endp nicht NULL ist. Falls das Ergebnis zu groß ist, (also bei overflow), wird als Resultat HUGE_VAL mit dem korrekten Vorzei- Vorzeichen geliefert; liegt das Ergebnis zu dicht bei Null (also bei underflow), wird Null gelie- geliefert. In beiden Fällen erhält errno den Wert ERANGE. long strtol(const char *s, char **endp, int base) strtol wandelt den Anfang der Zeichenkette s in long um, dabei wird Zwischen- Zwischenraum am Anfang ignoriert. Die Funktion speichert einen Zeiger auf einen nicht umge- umgewandelten Rest der Zeichenkette bei *endp, falls endp nicht NULL ist. Hat base einen Wert zwischen 2 und 36, erfolgt die Umwandlung unter der Annahme, daß die Eingabe in dieser Basis repräsentiert ist. Hat base den Wert Null, wird als Basis 8,10 oder 16 ver- verwendet; eine führende Null bedeutet dabei oktal und Ox oder 0X zeigen eine hexadezima- hexadezimale Zahl an. In jedem Fall stehen Buchstaben für die Ziffern von 10 bis base-1; bei Basis 16 darf Ox oder OX am Anfang stehen. Wenn das Resultat zu groß werden würde, wird je nach Vorzeichen LONGMAX oder LONGMIN geliefert und errno erhält den Wert ERANGE. unsigned long strtoul(const char *s, char **endp, int base) strtoul funktioniert wie strtol, nur ist der Resultattyp unsigned long und der Feh- Fehlerwert ist ULONGMAX. int rand(void) rand liefert eine ganzzahlige Pseudo-Zufallszahl im Bereich von 0 bis RAND_MAX; dieser Wert ist mindestens 32767. void srand(unsigned int seed) srand benutzt seed als Ausgangswert für eine neue Folge von Pseudo-Zufallszah- len. Der erste Ausgangswert ist 1.
252 BJ_ Standard-Bibliothek void *calloc(size_t nobj, size_t size) calloc liefert einen Zeiger auf einen Speicherbereich für einen Vektor von nobj Objekten, jedes mit der Größe size, oder NULL, wenn die Anforderung nicht erfüllt wer- werden kann. Der Bereich wird mit Null-Bytes initialisiert. void *malloc(size_t size) malloc liefert einen Zeiger auf einen Speicherbereich für ein Objekt der Größe size oder NULL, wenn die Anforderung nicht erfüllt werden kann. Der Bereich ist nicht initialisiert. void *realloc(void *p, size_t size) realloc ändert die Größe des Objekts, auf das p zeigt, in size ab. Bis zur kleineren der alten und neuen Größe bleibt der Inhalt unverändert. Wird der Bereich für das Ob- Objekt größer, so ist der zusätzliche Bereich uninitialisiert. realloc liefert einen Zeiger auf den neuen Bereich oder NULL, wenn die Anforderung nicht erfüllt werden kann; in die- diesem Fall ist *p unverändert. void free(void *p) free gibt den Bereich frei, auf den p zeigt; die Funktion hat keinen Effekt, wenn p den Wert NULL hat. p muß auf einen Bereich zeigen, der zuvor mit calloc, malloc oder realloc angelegt wurde. void abort(void) abort sorgt für eine anormale Beendigung des Programms im Stil von raise(SIGABRT). void exit(int status) exit beendet das Programm normal. atexit-Funktionen werden in umgekehrter Reihenfolge ihrer Hinterlegung aufgerufen, die Puffer offener Dateien werden geschrie- geschrieben, offene Ströme werden abgeschlossen, und die Kontrolle geht an die Umgebung des Programms zurück. Wie status an die Umgebung des Programms geliefert wird, hängt von der Implementierung ab, aber Null gilt als erfolgreiches Ende. Die Werte EXTTSUCCESS und EXrr_FAILURE können ebenfalls angegeben werden. int atexit(void (*fcn)(void)) atexit hinterlegt die Funktion fcn, damit sie aufgerufen wird, wenn das Programm normal endet, und liefert einen von Null verschiedenen Wert, wenn die Funktion nicht hinterlegt werden kann. int system(const char *s) system liefert die Zeichenkette s an die Umgebung zur Ausführung. Hat s den Wert NULL, so liefert system einen von Null verschiedenen Wert, wenn es einen Kom- Kommandoprozessor gibt. Wenn s von NULL verschieden ist, dann ist der Resultatwert im- implementierungsabhängig. char *getenv(const char *name) getenv liefert die zu name gehörende Zeichenkette aus der Umgebung oder NULL, wenn keine Zeichenkette existiert. Die Details hängen von der Implementierung ab.
B.6 Fehlersuc) <assert.h> 253 void *bsearch(const void *key, const void *base, size_t n, size_t size, int (*cmp)(const void *keyval, const void *datum)) bsearch durchsucht base[O]...base[n-l] nach einem Eintrag, der gleich *key ist. Die Funktion cmp muß einen negativen Wert liefern, wenn ihr erstes Argument (der Suchschlüssel) kleiner als ihr zweites Argument (ein Tabelleneintrag) ist, Null, wenn bei- beide gleich sind, und sonst einen positiven Wert. Die Elemente des Vektors base müssen aufsteigend sortiert sein, bsearch liefert einen Zeiger auf das gefundene Element oder NULL, wenn keines existiert. void qsort(void *base, size_t n, size_t size, int (*cmp)(const void *, const void *)) qsort sortiert einen Vektor base[O]...base[n-l] von Objekten der Größe size in aufsteigender Reihenfolge. Für die Vergleichsfunktion cmp gilt das gleiche wie bei bsearch. int abs(int n) abs liefert den absoluten Wert seines int Arguments, long labs(long n) labs liefert den absoluten Wert seines long Arguments. div_t div(int num. int denom) div berechnet Quotient und Rest von num/denom. Die Resultate werden in den int Komponenten quot und rem einer Struktur vom Typ div_t abgelegt. ldiv_t ldiv(long num, long denom) div berechnet Quotient und Rest von num/denom. Die Resultate werden in den long Komponenten quot und rem einer Struktur vom Typ ldiv_t abgelegt. B.6 Fehlersuche: <assert.h> Mit dem assert-Makro fügt man Testpunkte zu Programmen hinzu: void assert(int expression") Hat expression den Wert Null wenn assert (expression ) ausgeführt wird, dann gibt der assert-Makro auf stderr etwa folgende Meldung aus: Assertion failed: expression, f Me filename, tine nnn Anschließend wird die Ausführung durch Aufruf von abort abgebrochen. Der Dateina- Dateiname der Programmquelle sowie die Zeilennummer stammen von den Preprozessor-Ma- kros __FILE__ und __LINE__. Wenn beim Einfügen von <assert.h> ein Makroname NDEBUG definiert ist, wird der assert-Makro ignoriert. B.7 Variable Argumentlisten: <stdarg.h> Die Definitionsdatei < stdarg.h > bietet die Möglichkeit, eine Liste von Funktions- Funktionsargumenten abzuarbeiten, deren Länge und Datentypen nicht bekannt sind.
254 ,3ie Standard-Bibliothek Angenommen, lastarg ist der letzte benannte Parameter einer Funktion f, die mit einer variablen Anzahl von Argumenten aufgerufen wird. Dann vereinbart man in f eine Variable ap vom Datentyp vajist, die der Reihe nach auf jedes Argument zeigen wird: valist ap; ap muß einmal mit dem Makro va start initialisiert werden, bevor auf die unbenannten Argumente zugegriffen wird: va_start(va_ligt ap, lastarg); Daran anschließend liefert jede Ausführung des Makros va_arg einen Wert, der den Da- Datentyp und den Wert des nächsten unbenannten Arguments besitzt. Außerdem ändert der Makro den Wert von ap so, daß der nächste Aufruf von va_arg das nächste Argu- Argument liefert: type va_arB(va_list ap, type); Der Makro void va_end(va_list ap); muß einmal aufgerufen werden, nachdem die Argumente bearbeitet wurden, aber vor Schluß von f. B.8 Globale Sprünge: <setjmp.h> Mit den Vereinbarungen in <setjmp.h> kann man die normale Folge von Funkti- Funktionsaufruf und -ende umgehen. Typischerweise erlaubt das einen direkten Rücksprung von einem tief verschachtelten Funktionsaufruf. int setjmp(jmp_buf env) Der Makro setjmp speichert Zustandsinformation in env zur Auswertung durch longjmp. Der Resultatwert ist Null, wenn setjmp direkt aufgerufen wird, und von Null verschieden bei einem späteren Aufruf von longjmp. Der Makro setjmp darf nur in be- bestimmten Kontexten aufgerufen werden, im wesentlichen innerhalb der Bedingung bei if, switch und Schleifen, und nur innerhalb von einfachen Vergleichen. if (setjmp(env) « 0) /* hierher beim direkten Aufruf */ eise /* hierher durch Aufruf von longjmp */ void longjmp(jmp_buf env, int val) longjmp stellt den Zustand wieder her, der beim letzten Aufruf von setjmp gespei- gespeichert wurde. Dazu wird die Information in env verwendet. Die Ausführung geht so wei- weiter, als ob die Funktion se^jmp gerade ausgeführt wurde und den von Null verschiedenen Resultatwert val geliefert hat. Die Funktion, von der aus se{jmp aufgerufen wurde, darf noch nicht beendet worden sein. Die erreichbaren Objekte haben dann die Werte, die sie beim Aufruf von longjmp hatten. In der Funktion, von der setjmp aufgerufen wurde, sind jedoch die Werte der automatischen Variablen Undefiniert, die ohne das Attribut volatile definiert wurden und die seit dem Aufruf von sejjmp verändert wurden. B.9 Signale: <signal.h> Die Definitionsdatei <signal.h> stellt Hilfsmittel zur Behandlung von Ausnahme- Ausnahmebedingungen zur Verfügung, die während der Ausführung eines Programms eintreten
B.10 Fimktione j Datum und Uhrzeit: <time.h> 255 können, wie zum Beispiel ein „Interrupt-Signal" von einer externen Quelle oder ein Feh- Fehler während der Ausführung. void (*signal(int sig, void (*handler)(int)))(int) signal legt fest, wie nachfolgende Signale behandelt werden. Wird SIG_DFL als handler angegeben, so wird nach einer implementierungsabhängigen Voreinstellung ver- verfahren; wird SIG_IGN angegeben, so wird das Signal ignoriert; andernfalls wird die Funk- Funktion aufgerufen, auf die handler verweist. Die Funktion erhält als Argument den Signal- Signaltyp. Unter anderem gelten folgende Signale sigabrt anomaler Abbruch, zum Beispiel durch abort sigfpe Arithmetikfehler, zum Beispiel Division durch Null oder Overflow sigill illegaler Funktionstext, zum Beispiel illegaler Maschinenbefehl s i gi NT Dialogsignal, zum Beispiel interrupt sigsegv illegaler Speicherzugriff, zum Beispiel außerhalb von Speichergrenzen sigterm Aufforderung an das Programm, sich zu beenden signal liefert den vorhergehenden Wert von handler für das angegebene Signal, oder SIG_ERR, wenn ein Fehler passiert. Trifft später ein Signal sig ein, wird für dieses Signal wieder die Behandlung nach Voreinstellung festgelegt; anschließend wird die vereinbarte Funktion in der Form ('handier) (sig) aufgerufen. Wenn dieser Funktionsaufruf zu Ende geht, wird die Aus- Ausführung an dem Punkt fortgesetzt, an dem das Signal eintraf. Der anfängliche Zustand der Signale ist implementierungsabhängig, int raise(int sig) reise sendet das Signal sig an das Programm und liefert einen von Null verschiede- verschiedenen Wert bei Fehler. B.10 Funktionen für Datum und Uhrzeit: < time.h > Die Definitionsdatei < time.h > vereinbart Typen und Funktionen zum Umgang mit Datum und Uhrzeit. Manche Funktionen verarbeiten die Ortszeit, die von der Kalender- Kalenderzeit zum Beispiel wegen einer Zeitzone abweicht, clock t und time_t sind arithmetische Typen, die Zeiten repräsentieren, und struct tm enthält die Komponenten einer Kalen- Kalenderzeit: int tro_sec; Sekunden nach der vollen Minute @, 61)* int tmjnin; Minuten nach der vollen Stunde @,59) int tm_hour; Stunden seit Mitternacht @, 23) int tmjnday; Tage im Monat A, 31) int tmjnon; Monate seit Januar @,11) int tm_year; Jahre seit 1900 int tro_wday; Tage seit Sonntag @, 6) int tro_yday; Tage seit dem 1. Januar @,365) int tm_isdst; Kennzeichen für Sommerzeit tm isdst ist positiv, wenn Sommerzeit gilt, Null, wenn Sommerzeit nicht gilt, und negativ, wenn die Information nicht zur Verfügung steht.
clock_t clock(void) clock liefert die Rechnerkern-Zeit, die das Programm seit Beginn seiner Aus- Ausführung verbraucht hat, oder -1, wenn diese Information nicht zur Verfügung steht. clock()/CLOCKS_PER_SEC ist eine Zeit in Sekunden. time_t time(time_t *tp) time liefert die aktuelle Kalenderzeit oder -1, wenn diese nicht zur Verfügung steht. Wenn tp von NULL verschieden ist, wird der Resultatwert auch bei *tp abgelegt. double difftime(time_t time2, time_t timel) dlfftime liefert time2- timel ausgedrückt in Sekunden. time_t mktime(struct tm *tp) mktime wandelt die Ortszeit in der Struktur *tp in Kalenderzeit um, die so darge- dargestellt wird wie bei time. Die Komponenten erhalten Werte in den angegebenen Berei- Bereichen, mktime liefert die Kalenderzeit oder -1, wenn sie nicht dargestellt werden kann. Die folgenden vier Funktionen liefern Zeiger auf statische Objekte, die von ande- anderen Aufrufen überschrieben werden können. char *asctime(const struct tm *tp) asctime konstruiert aus der Zeit in der Struktur *tp eine Zeichenkette der Form Sun Jan 3 15:K:13 1988\n\0 char *ctime(const time_t *tp) ctime verwandelt die Kalenderzeit *tp in Ortszeit; dies ist äquivalent zu asctime(localtime(tp)) struct tm *gmtime(const time_t *tp) gmtime verwandelt die Kalenderzeit *tp in Coordinated Universal Time (UTC). Die Funktion liefert NULL, wenn UTC nicht zur Verfügung steht. Der Name gmtime hat hi- historische Bedeutung. struct tm *localtime(const time_t *tp) localtime verwandelt die Kalenderzeit *tp in Ortszeit. size_t str£time(char *s, size_t smax, const char *fmt, const struct tm *tp) strftime formatiert Datum und Zeit aus *tp in s unter Kontrolle von fmt, analog zu einem printf-Format. Gewöhnliche Zeichen (insbesondere auch das abschließende '\0') werden nach s kopiert. Jedes %c wird so wie unten beschrieben ersetzt, wobei Werte verwendet werden, die der lokalen Umgebung entsprechen. Höchstens smax Zeichen werden in s abgelegt, strftime liefert die Anzahl der resultierenden Zeichen, mit Aus- Ausnahme von '\0\ Wenn mehr als smax Zeichen erzeugt wurden, liefert strftime den Wert Null. Xa abgekürzter Name des Wochentags. XA voller Name des Wochentags. Xb abgekürzter Name des Monats. X8 voller Name des Monats.
B.ll Grenzwerte Xc Xd XH XI Xj Xm XH Xp XS xu Xw XU Xx XX Xy XV xz XX e Implementierung: <limits.h> und < float.h> 257 lokale Darstellung von Datum und Zeit. Tag im Monat@1-31). Stunde @0-23). Stunde @1-12). Tag im Jahr @01-366). Monat @1-12). Minute @0-59). lokales Äquivalent von AM oder PM. Sekunde @0-61). Woche im Jahr (Sonntag ist erster Wochentag) @0-53). Wochentag @-6, Sonntag ist 0). Woche im Jahr (Montag ist erster Wochentag) @0-53). lokale Darstellung des Datums. lokale Darstellung der Zeit. Jahr ohne Jahrhundert @0-99). Jahr mit Jahrhundert. Name der Zeitzone, falls existent. B.11 Grenzwerte einer Implementierung: <limits.h> und <float.h> Die Definitionsdatei <limits.h> definiert Konstanten für den Wertumfang der ganzzahligen Typen. Die nachfolgenden Werte sind zugelassene minimale Größen; größere Werte können in einer Implementierung benutzt sein. CHAR_BIT CHAR MAX 8 Bits in einem char UCHAR_MAX oder schar_max maximaler Wert für char CHAR_min 0 oder scharmin minimaler Wert für char intmax +32767 maximaler Wert für int int_Min -32767 minimaler Wert für int LONG_HAX +2147483647 maximaler Wert für long LONG_MIN -2147483647 minimaler Wert für long SCHAR_MAX +127 maximaler Wert für signed char schar_min -127 minimaler Wert für signed char shrt_max +32767 maximaler Wert für short SHRT_MIN -32767 minimaler Wert für short UCHar_max 2SS maximaler Wert für unsigned char UINT_HAX 6SS3S maximaler Wert für unsigned int Ulong_HAX 4294967295 maximaler Wert für unsigned long USHRTMAX 6SS3S maximaler Wert für unsigned short Die Namen in der nächsten Tabelle, einer Teilmenge von <floath>, sind Konstan- Konstanten, die sich auf Gleitpunktarithmetik beziehen. Wenn ein Wert angegeben ist, handelt es sich um ein Minimum für die entsprechende Größe. Jede Implementierung definiert entsprechende Werte.
258 B Dl andard-Bibliothek flt_radix 2 Basis der Exponentendarstellung, zum Beispiel 2,16 fltjkxjnds Art der Rundung bei Gleitpunktaddition fltdig 6 Genauigkeit in Dezimalziffern (für float) flt_epsilon 1E-5 kleinster Wert* für den 1.0 + x 11.0 gilt flt_mant_dig Länge der Mantisse in Basis-Ziffern flt_max 1E+37 maximaler Gleitpunktwert flt_max_exp maximales n, für das FLT_RADIX"-1 darstellbar ist flt_min 1E-37 minimaler, normalisierter Gleitpunktwert FLT_min_EXP minimales n, für das 10" normalisiert werden kann dbldig 10 Genauigkeit in Dezimalziffern (für double) dbl_epsiion 1E-9 kleinster Wert* für den 1.0 + x 11.0 gilt dbl_mant_d i g Länge der Mantisse in Basis-Ziffern dbl_hax 1E+37 maximaler Gleitpunktwert dbl_max_exp maximales n, für das FLT_RADDC"-1 darstellbar ist dbl_hin 1E-37 minimaler, normalisierter double Gleitpunktwert dbl_min_exp minimales n, für das 10" normalisiert werden kann
259 c Änderungen in Kürze Seit die Erste Ausgabe dieses Buches erschien, hat sich die Definition der Sprache C geändert. Die meisten Änderungen erweiterten nur die ursprüngliche Sprache und sie waren sorgfältig so entworfen, daß sie sich mit dem existierenden Sprachgebrauch vertru- vertrugen. Manche reparierten Mehrdeutigkeiten in der ursprünglichen Beschreibung und ei- einige sind auch Modifikationen, die den existierenden Sprachgebrauch abändern. Viele der neuen Eigenschaften wurden in den Dokumenten angekündigt, die mit den Überset- Übersetzern von AT&T geliefert wurden, und sie wurden später von anderen Herstellern von C- Übersetzern übernommen. In neuerer Zeit hat die ANSI-Kommission, die einen Stan- Standard für C entwickelt, die meisten Änderungen übernommen, und sie hat auch andere wesentliche Modifikationen eingeführt. Ihr Bericht wurde teilweise schon von einigen kommerziellen Übersetzern berücksichtigt, noch bevor der C-Standard formal erschien. In diesem Anhang werden die Unterschiede kurz beschrieben zwischen der Spra- Sprache, die in der Ersten Ausgabe definiert wurde, und der, die sicherlich der endgültige Standard definiert. Der Anhang behandelt nur die Sprache selbst, nicht ihre Umgebung und Bibliothek; letztere sind zwar ein wichtiger Bestandteil des Standards, aber man kann sie mit wenig vergleichen, denn die Erste Ausgabe unternahm keinen Versuch, eine Umgebung oder eine Bibliothek vorzuschreiben. • Der Preprozessor wurde im Standard sorgfältiger definiert als in der Ersten Ausgabe, und er wurde erweitert: er beruht explizit auf Eingabesymbolen (tokens); es gibt neue Operatoren zur Verkettung von Symbolen (##) und zur Erzeugung von Zei- Zeichenketten (#); es gibt neue Preprozessor-Anweisungen wie #elif und #pragma; die wiederholte Definition eines Makros mit der gleichen Folge von Symbolen ist aus- ausdrücklich erlaubt; Parameter werden innerhalb von Zeichenketten nicht mehr ersetzt. Zeilen können jetzt überall mit \ aneinandergehängt werden, nicht nur in Definitio- Definitionen von Zeichenketten und Makros. Siehe § A.12. • Alle internen Namen sind jetzt wenigstens auf 31 Zeichen signifikant; minimal werden für extern sichtbaren Namen immer noch nur sechs signifikante Buchstaben (entwe- (entweder alle groß oder alle klein) gefordert. (Viele Implementierungen erlauben mehr.) • Drei-Zeichen-Folgen (trigraphs), die mit ?? beginnen, erlauben die Darstellung von Zeichen, die in manchen Zeichensätzen fehlen. Für #\*[]{ }|" sindErsatzdarstel- lungen definiert, siehe § A.12.1. Man beachte, daß durch die Drei-Zeichen-Folgen die Bedeutung von Zeichenketten verändert werden kann, die ?? enthalten. • Neue reservierte Worte (void, const, volatile, signed, enum) wurden eingeführt. Das totgeborene Wort entry ist nicht mehr reserviert. • Neue Ersatzdarstellungen wurden definiert, die in Zeichenkonstanten und konstanten Zeichenketten verwendet werden. Wenn nach \ ein Zeichen folgt, das nicht Teil einer erlaubten Ersatzdarstellung ist, ist der Effekt Undefiniert. Siehe § A.2.5.2. • Die allen liebste triviale Änderung: 8 und 9 sind keine oktalen Ziffern.
260 " Änderungen in Kürze • Der Standard führt eine größere Anzahl von Suffixen ein, mit denen der Typ von Kon- Konstanten explizit festgelegt wird: U oder L für ganzzahlige Werte, F oder L für Gleit- Gleitpunktwerte. Auch die Regeln zur Bestimmung des Typs von Konstanten ohne solche Suffixe sind verfeinert worden (§ A.2S). • Aufeinanderfolgende konstante Zeichenketten werden aneinandergehängt. • Es gibt eine Schreibweise für konstante Zeichenketten und Zeichenkonstanten mit ei- einem erweiterten Zeichensatz; siehe § A.2.6. • Man kann explizit vereinbaren, ob Zeichen, wie auch andere Typen, ein Vorzeichen haben oder nicht. Dazu dienen die Schlüsselworte signed oder unsigned. Die Um- Umschreibung long float als Synonym für double existiert nicht mehr, aber long double kann verwendet werden, um Gleitpunktwerte mit zusätzlicher Genauigkeit zu verein- vereinbaren. • Der Typ unsigned char war schon seit einiger Zeit verfügbar. Der Standard führt das Wort signed ein, um die Existenz eines Vorzeichens bei char und anderen Integer- Objekten explizit auszudrücken. • Den Datentyp void gab es in den meisten Implementierungen schon seit einigen Jah- Jahren. Der Standard fuhrt void * als allgemeinen Zeigertyp ein; diese Rolle spielte früher char *. Gleichzeitig wurden explizite Regeln festgelegt, die sich gegen ein Mi- Mischen von Zeigern und Integer-Werten richten oder Mischen von Zeigern, die auf ver- verschiedene Typen zeigen, ohne daß explizite Umwandlungsoperationen verwendet wer- werden. • Der Standard schreibt minimale Bereichsgrenzen für die arithmetischen Typen expli- explizit vor, und fordert Definitionsdateien (<limlts.h> und <float.h>), die die Charakte- ristika jeder speziellen Implementierung angeben. • Seit der Ersten Ausgabe wurden Aufzählungen neu eingeführt.* • Von C++ übernimmt der Standard die Idee eines Attributs für Typen (type qualifier), zum Beispiel const (§ A.8.2). • Zeichenketten können nicht mehr modifiziert werden und können deshalb im schreib- schreibgeschützten Speicher angelegt werden. • Die „üblichen arithmetischen Umwandlungen" wurden geändert, im wesentlichen von „bei ganzzahligen Werten gewinnt immer unsigned; bei Gleitpunkt wird immer double verwendet" in „wandle in den kleinsten Typ um, der genügend Fassungsver- Fassungsvermögen hat." Siehe § A.6.5. • Die alten Zuweisungsoperatoren wie = + sind wirklich hinüber. Zuweisungsoperato- Zuweisungsoperatoren sind jetzt auch einzelne Symbole; in der Ersten Ausgabe waren sie aus Symbol- Symbolpaaren zusammengesetzt, die durch Zwischenraum getrennt sein konnten. • Der Freibrief für einen Übersetzer, mathematisch assoziative Operatoren auch für Computer als assoziativ behandeln zu dürfen, gilt nicht mehr. • Ein unärer Operator + wurde aus Symmetriegründen zu - eingeführt. Sie wurden in der ersten deutschen Ausgabe bereits berücksichtigt. A.d.D.
C Änderungen in ¥ x 261 • Ein Zeiger auf eine Funktion kann zum Aufruf einer Funktion verwendet werden, oh- ohne daß der Inhaltsoperator • explizit angegeben werden muß. Siehe § A.7.3.2. • Strukturen können zugewiesen, als Argumente an Funktionen übergeben und als Re- Resultat von Funktionen geliefert werden. • Der Adreß-Operator & darf auf Vektoren angewendet werden und liefert dann einen Zeiger auf den Vektor. • In der Ersten Ausgabe lieferte der Operator sizeof ein Resultat vom Typ int; später sind viele Implementierungen zu unsigned übergegangen. Der Standard macht den Resultattyp explizit implementierungsabhängig, verlangt aber, daß dieser Typ, nämlich size t, in einer Standard-Definitionsdatei ( < stddef.h > ) vereinbart wird. Eine ähnli- ähnliche Änderung erfolgt für den Typ der Differenz zweier Zeiger (ptrdifTt). Siehe §A.7.4.8und§A.7.7. • Der Adreß-Operator & darf nicht auf ein Objekt angewendet werden, das als register vereinbart wurde, auch dann nicht, wenn die Implementierung das Objekt gar nicht in einem Register ablegt. • Der Resultattyp bei den Shift-Operatoren << und » ist der Typ des linken Operan- Operanden; der rechte Operand kann das Resultat nicht erweitern. Siehe § A.7.8. • Der Standard erlaubt, daß ein Zeigerwert direkt hinter das Ende eines Vektors er- erzeugt wird, und erlaubt dafür Arithmetik und Vergleiche; siehe § A.7.7. • Der Standard führt (angelehnt an C++) die Idee der Deklaration eines Funktions- Funktionsprototyps ein, der die Typen der Parameter enthält. Dabei werden auch Funktionen mit variablen Argumentlisten explizit erkannt, und es gibt eine offizielle Technik, sie zu verarbeiten. Siehe §§ A.7.3.2, A.8.6.3 und B.7. Mit Einschränkungen ist der alte Stil noch erlaubt. • Leere Deklarationen, die keine Deklaratoren enthalten und nicht wenigstens eine Struktur, eine Union oder Aufzählung vereinbaren, werden im Standard verboten. Andrerseits vereinbart eine Deklaration, die nur den Namen einer Struktur oder Union enthält, diesen Namen auch dann neu, wenn der Name in einem äußeren Gül- Gültigkeitsbereich schon vereinbart war. • Externe Datendeklarationen ohne Typ oder Attribut (die also nur aus Deklaratoren bestehen) sind verboten. • Manche Implementierungen, wenn konfrontiert mit einer extern Deklaration in einem inneren Block, haben diese Deklaration in den Rest der Datei exportiert. Der Stan- Standard besteht darauf, daß der Gültigkeitsbereich einer solchen Deklaration nur der Block selbst ist. • Parameternamen haben als Gültigkeitsbereich den Funktionsblock, damit Vereinba- Vereinbarungen im äußersten Block der Funktion die Parameter nicht mehr verbergen können. • Die Namensräume sind leicht abgeändert. Der Standard faßt alle Etiketten (tags) in einem einzigen Namensraum zusammen und führt außerdem einen eigenen Namens- Namensraum für Marken ein; siehe § A.ll.l. Außerdem werden Komponentennamen mit der Struktur oder Union verknüpft, in der sie definiert sind. (Dies war schon seit einiger Zeit gebräuchlich.)
262 ' Änderungen in Kürze • Eine Union darf initialisiert werden; die Initialisierung bezieht sich auf die erste Al- Alternative. • Auch im automatischen Speicherbereich dürfen Strukturen, Unionen und Vektoren initialisiert werden, allerdings mit Einschränkungen. • Zeichenvektoren, die explizit dimensioniert sind, dürfen mit einer konstanten Zei- Zeichenkette initialisiert werden, die genau so viele Zeichen hat (\0 wird dann still- stillschweigend unterdrückt). • Der Auswahlausdruck und die case-Marken bei einer switch-Anweisung können einen beliebigen Integer-T^p besitzen.
263 I Negation 42,198 I »ungleich 16,41,201 " Doppelanführungszeichen 7,19,38,186 # Zeichenkette, Preprozessor 88,228 ## verketten, Preprozessor 88,228 #define 14,87,227 #elif,#else 88,230 #endif 88 #error 231 #if 88,130,230 #ifdef 89,231 #ifndef 89,231 «include 32,86,146,229 #line 231 «pragma 231 #undef 88,165, 227 X Rest nach Division 40f, 199 XI d bei prlntf, Umwandlung 18 & Adreßoperator ix, 91,197 & UND, Bit-Operator 48,201 tA UND 21, 41, 48, 202 ■ Anführungszeichen 19, 37f, 185 () Klammern 194f * Inhaltsoperator ix, 91,197 * Multiplikation 40,199 ♦ Addition 40,199 ♦ unäres Plus 197 ♦♦ Inkrement 18,46,102f, 196f ♦» Zuweisung 49 , Komma-Operator 62, 203 - Subtraktion 40,199f - unäres Minus 198 - Dekrement 18,46,102f, 196f -> Strukturverweis 127,194 . Strukturauswahl 124,194 ... Deklaration 149,196 / Division 10, 40f, 199 ; 9,15,18, 55f < kleiner 41,200 « Knks-shift, Bit-Operator 48,200 Sachverzeichnis <= kleiner-gleich 41, 200 = Zuweisung 16,41,203 == gleich 19, 41, 201 > größer 41, 200 >= größer-gleich 41, 200 » rechts-s/ti/f, Bit-Operator 48,200 ?: bedingter Ausdruck 50f, 202 [] Klammern -» Vektor 22,194 \ Gegenschrägstrich 8, 38 A exklusiv-ODER, Bit-Operator 48, 201 _ Unterstrich, Buchstabe 35,184, 239 O Klammern 7,10, 55, 82 | inklusiv-ODER, Bit-Operator 48, 201 11 ODER 21, 41, 48, 202 - Eins-Komplement, Bit-Operator 48,198 0... oktale Konstante 37,184 Ox... hexadezimale Konstante 37,184 \0 Nullzeichen 29, 38,185 \a Klingelzeichen 38,185 Abbruch, Programm 156f abgeleitete "typen 1, 9,189 abhängig —»Portabilität abort Bibliotheksfunktion 252 abs Bibliotheksfunktion 253 Abschluß, Anweisung 9, 55 Abschneiden bei Division 10,40f, 199 Abschneiden von Gleitpunkt 44,190 abstrakter Deklarator 217 acos Bibliotheksfunktion 250 Addition + 40,199 addpoint Funktion 126 add tree Funktion 136 Adreßoperator & ix, 91,197 Adresse einer Variablen 27, 91,197 und register 205 afree Funktion 99 Aktualparameter ix, 25 alert —* Klingelzeichen alloc Funktion 98
264 Sachverzeichnis alphabetische Sortierung 114 alter Stil, Funktion 26, 32, 72,195 Alternative ix, 142, 207 American National Standards Institute, ANSI v, ix, 2,183 anfügen an Datei 154,168, 240 Anführungszeichen ' 19, 37f, 185 Anweisung 218ff Abschluß 9, 55 Ausdruck 55f, 219 Auswahl 220 Block 55, 82, 219, 222f break 59, 63f, 221 continue 64, 221 do-while 62f, 221 for 13, 18, 59, 221 goto 64f, 221 if-else 19,21,55,220 leer 18, 219 Marke 65,219 return 25, 29, 69, 72, 222 sequentielle Ausführung 218 Sprung 221 switch 58, 74f,220 while 10,59,221 Wiederholung 220f zusammengesetzt 55, 82, 219, 222f Zuweisung, geschachtelt 17, 20, 50 a.out 6, 70 Äquivalenz, Typen 218 Äquivalenzvergleich, Operatoren 41, 201 Argument ix, 25,195 Anzahl, arge 110 «define 87 Erweiterung 45,195 Funktion 25,195 Kommandozeile HOff Makro 87 Teilvektor 97 Umwandlung 45 Vektor, argv 110,157 Vektorname 27, 97,108 Zeiger 97 Argumentliste variabel 149, 167,196, 213, 223, 253f void 32, 72, 213, 223 Arithmetik Operatoren 40 Typen 188 übliche Umwandlung 42,190f Zeiger 92,96ff, 113,132f, 199f Zeiger, illegale 99f, 133,199f Zeiger, Skalierung 100,191 array —* Vektor ASCII Zeichensatz 19, 37, 43, 227, 248 asetime Bibliotheksfunktion 256 as in Bibliotheksfunktion 250 asm reserviertes Wort 184 <assert.h> 253 Assoziativität von Operatoren 51 f, 193 atan, atan2 Bibliotheksfunktionen 161, 250 atexit Bibliotheksfunktion 252 atof Bibliotheksfunktion 251 Funktion 70f atoi Bibliotheksfunktion 251 Funktion 42, 61, 72 atol Bibliotheksfunktion 251 Attribut eines Typs ix, 202, 206 const 40, 189, 206 volatile 189,206 Aufruf einer Funktion 195 Aufzählungstyp, enum 39,188f, 210 Etikett 210 Konstante 39, 88,184, 186, 210 Ausdruck 193ff Anweisung 55 f, 219 bedingt,?: 50 f, 202 geklammert 194 konstant 38, 58,88, 204 logischer Wert 43 Reihenfolge der Bewertung 52,193 Wert eines Vergleichs 42f Zuweisung 17, 20, 50, 203 Ausführung, sequentiell 218 Ausgabe 146,155,163 Bildschirm 15,146,157,163 Umlenkung 146 Ausnahmen 193, 255 Ausrichtung 133,137,141,161,178, 192 Bit-Feld 144, 208f mit union 178 auto Speicherklasse 205
Sachverzeichnis 265 automatische Variable 30,73,187 Gültigkeitsbereich 78, 225 Initialisierung 30, 40, 83, 215 B B 1 \b backspace 7,38,185 backslash —»Gegenschrägstrich \ 8, 38 basic type —* elementarer Datentyp Baum, binär 134 BCPL 1 bedingte Übersetzung 88, 230 bedingter Ausdruck, ?: 50 f, 202 Beispiel —» Funktionsbeispiel, Programm Bewertungsreihenfolge 21, 48, 52f, 62, 75, 87, 92,193 Bibliotheksfunktion 7, 67, 78 abort 252 abs 253 acos 250 asctime 256 asin 250 atan, atan2 250 atexit 252 atof, atoi, atol 251 bsearch 253 calloc 161,252 ceil 250 clearerr 247 clock 256 cos 161,250 cosh 250 ctime 256 difftime 256 div 253 exit 157,252 exp 161, 250 fabs 161,250 fclose 156, 240 feof, ferror 158, 247 fflush 240 fgetc 244 fgetpos 247 fgets 158,244 floor 250 fmod 250 Bibliotheksfunktion «— fopen 154,240 fprintf 155, 241f fputc 244 fputs 158,244 fread 246 free 161,252 f reopen 156,240 frexp 250 fscanf 155,243 fseek 246 fsetpos 247 ftell 246 furite 246 getc 155,245 getchar 15, 145, 155, 245 getenv 252 gets 158,245 gmtime 256 i sal nun 131,160,247 isalpha 131,160,247 iscntrl 247 isdigit 160,247 isgraph 248 is lower 160,248 isprint 248 ispunct 248 isspece 131, 160,248 i supper 160,248 isxdigit 248 labs 253 Idexp 250 Idiv 253 localtime 256 'log, Iog10 161,250 longjmp 254 ma Hoc 137,160,252 memchr, memcmp, memcpy, memmove, memset 249 mktime 256 modf 250 perror 247 pow 24, 161, 250 printf 7, 10, 18, 147f, 242 putc 155,246 putchar 15, 146, 155, 246
266 Sachverzeichnis Bibliotheksfunktion ♦— puts 158, 246 qsort 253 raise 255 rand 161, 251 realloc 252 remove 240 rename 240 rewind 247 scanf 94, 150ff, 244 setbuf, setvbuf 241 setjmp 254 signal 255 sin 161,250 sinh 250 sprintf 149,242 sqrt 161,250 srand 162, 251 sscanf 244 strcat, strchr, strcmp, strcpy 159, 248 strcspn 249 strerror 249 strftime 256 strlen 159, 249 strncat, strncmp, strncpy 159, 248 strpbrk 249 strrchr 159,249 strspn 249 strstr 249 strtod 251 strtok 249 strtol, strtoul 251 system 160, 252 tan 250 tanh 250 time 256 tropf He 241 tmpnam 241 to lower 146, 160, 248 toupper 160,248 ungetc 160,246 vfprintf, vprintf, vsprintf 167, 242 Bildschirmausgabe 15,146,157,163 binär, Baum 134 binär, Strom 154, 239f Bindung 187, 225f extern 72,184,187, 205,226 intern 187, 226 binsearch Funktion 57, 129,132 Bit-Feld ix, 144,207ff Bit-Operator 48,143,198, 201 bit count Funktion 50 Block 55, 82, 219, 222f Initialisierung 83, 220 Blockstruktur 55, 82f, 219 break Anweisung 59, 63f, 221 bsearch Bibliotheksfunktion 253 Bücherei —»Bibliothek bufsiz 241 C-Programm übersetzen 6, 24 call by reference 26 call by value —* Wertübergabe calloc Bibliotheksfunktion 161,252 canonrect Funktion 126 carriage return —* Wagenrücklauf case Marke 58, 219 cast —* Typumwandlung, Umwandlungsoperation cat Programm 153,156 f cc Kommando 6, 70 ceil Bibliotheksfunktion 250 char Typ 9,36,188, 206 signed 43,188 unsigned 36, 43,165,188 clearerr Bibliotheksfunktion 247 CLK_TCK 256 clock Bibliotheksfunktion 256 ciock_t Typname 255 close Systemaufruf 167 closedir Funktion 176 Compiler —»übersetzen const Attribut ix, 40, 189, 206 continue Anweisung 64, 221 copy Funktion 29, 31 cos Bibliotheksfunktion 161, 250 cosh Bibliotheksfunktion 250 creat Systemaufruf 165f et i me Bibliotheksfunktion 256 <ctype.h> 43,146,160, 247
Sachverzeichnis 267 D Datei aneinanderhängen, Programm 153 anfügen 154,168,240 einfügen 86,229 eröffnen 154,163,166 erzeugen 154,163 Kennung .h 32 kopieren, Programm 16f, 164,166f übersetzen, mehrere 70 Zugriff 153,163,170f, 240 Zugriffsart 154,170f, 240 Zugriffsschutz 166 Dateisystem, UNIX 163,172 Datum umrechnen 107 day_of_year Funktion 107 del Funktion 119 del Programm 119f default Marke 58,219 defensive Programmierung 57, 59 «define 14,87,227 mehrzeilig 87 mit Argumenten 87 und enuro 39, 143 defined Preprozessor-Operator 88, 230 Definition —» Vereinbarung ix, 9 externe Variable 32,224 Funktion 24,69, 222 Makro 227 Speicherplatz 205 und Deklaration 32, 79,204 vorläufig 224 Definitionsdatei ix, 32, 80 <assert.h> 253 <ctype.h> 43,146,160, 247 dir.h 175 <errno.h> 247 <fcntl.h> 166 <float.h> 36,258 <limits.h> 36,257 <locale.h> 239 <math.h> 44, 161, 249f <setjmp.h> 254 <signal.h> 254f stat.h 173 <stdarg.h> 149,167, 253f Definitionsdatei «— <stddef .h> 100, 130,198, 239 <stdio.h> 6,16, 86f, 99, 145f, 239 <stdlib.h> 70, 137,251 <string.h> 39,102f, 159, 248 syscalls.h 165 <time.h> 255 types.h 173,175 Deklaration —» Vereinbarung ix, 9 ... 149,1% und Definition 32, 79, 204 Deklarator 211 ff abstrakter 217 für Funktion 213 für Vektor 212 Dekrement — 18,46,103,196f dereferencing —»Inhaltsoperator Diagnose-Ausgabe 155,163 di ff time Bibliotheksfunktion 256 DIR Struktur 173 dirdcl Funktion 119 Di rent Struktur 173 dir.h Definitionsdatei 175 dirwalk Funktion 175 div Bibliotheksfunktion 253 Division / 10, 40 f, 199 abschneiden 10, 40 f, 199 ganzzahlig, Integer 10, 40f Rest X 40f, 199 div_t, ldiv_t Typnamen 253 do-uhi le Anweisung 62f, 221 domain —* Argumentbereich 249 f Doppelanführungszeichen " 7,19,38,186 double Typ 9,18, 36,188, 206 Konstante 37,186 Umwandlung in float 44,190 Drei-Zeichen-Folge 227 druckbares Zeichen 248 E E Schreibweise 37, 72,186 EBCDIC Zeichensatz 43 echo Programm 111 EOOM 249f Effizienz 50, 81, 86,136,179 einfügen, Datei 86, 229
268 Sachverzeichnis Eingabe 145,155, 163 Fehler 158, 247 gepuffert 164 Tastatur, Terminal 15,145,163 Umlenkung .146,155, 164 ungepuffert 164 zurückstellen 77 Zeichen 15,145 Eingabesymbol 183,227 Verkettung 88, 228 einrücken, Programmtext 10,18, 23, 56 Einschränkung bei Ausrichtung 133,137, 141,161,178,192 Eins-Komplement, Bit-Operator - 48,198 Element eines Vektors ix, 22 elementare Typen 9, 36,188 #elif, «eise 88,230 else-if 23, 56f #endif 88 endlose Schleife, for(;;) 60, 87 enum Aufzählungstyp 39, 210 und#define 39, 143 enumeration —> Aufzählung enumerator —»Aufzählungskonstante environment —► Umgebung EOF 16,145, 240 ERANGE 249f eröffnen, Datei 154, 163,166 errno 247, 250 <errno.h> 247 #error 231 error Funktion 167 Ersatzdarstellung 7, 19, 37 f, 185, 227 hexadezimal \x 37,185 Tabelle 38, 185 erweiterte konstante Zeichenkette 186 erweiterte Zeichenkonstante 185 Erweiterung bei Argument 45, 195 Erweiterung bei Integer 44,189f erzeugen, Datei 154, 163 erzeugen, Zeiger 193 Etikett ix Aufzählung 210 Struktur 123, 207 Union 207 . exit Bibliotheksfunktion 157,252 EXIT_FAILURE, EXIT_SUCCESS 252 exklusiv-ODER, Bit-Operator A 48, 201 exp Bibliotheksfunktion 161, 250 Expansion, Makro 228 explizite Typumwandlung 45,198 Exponentenschreibweise 37, 72,186 exponenzieren 24, 250 expression —* Ausdruck 193ff extern Speicherklasse 30, 32, 79, 205 externe Bindung 72,184,187, 205, 226 externe Variable 30, 72,187 Definition 32, 224 Gültigkeitsbereich 78, 225 Initialisierung 40, 79, 83, 215 static 81 Vereinbarung 30, 222, 224 externer Name, Länge 35,184 \f Seitenvorschub 38,185 fabs Bibliotheksfunktion 161, 250 fclose Bibliotheksfunktion 156, 240 <fcntl.h> 166 fehlende Speicherklasse 205 fehlender Typ 206 Fehler bei Eingabe/Ausgabe 158, 247 feof Bibliotheksfunktion 158, 247 Makro 169 ferror Bibliotheksfunktion 158, 247 Makro 169 ff lush Bibliotheksfunktion 240 fgetc Bibliotheksfunktion 244 fgetpos Bibliotheksfunktion 247 fgets Bibliotheksfunktion 158, 244 Funktion 158 file ~* Datei FILE-Zeiger 154,169,239 File-Deskriptor 163 FILE Makro, Preprozessor 253 filecopy Funktion 156 FILENAHE_HAX 240 jfillbuf Funktion 171 float Typ 9, 36,188, 206 Konstante 37,186 Umwandlung in double 44, 190 <float.h> 36,258
Sachverzeichnis 269 floor Bibliotheksfunktion 250 f mod Bibliotheksfunktion 250 fopen Bibliotheksfunktion 154,240 Funktion 170 fopem_max 240 for Anweisung 13,18, 59, 221 endlose Schleife for(;;) 60,87 und while 14, 59f Formalparameter ix, 25 Format, Programm 10,18,23,40,133,183 Formatelement, printf Tabelle 13 formfeed —* Seitenvorschub Forth 73 Fortran 19,23f, 26,30,60,72 fortran reserviertes Wort 184 f pos_t Typname 247 fprintf Bibliotheksfunktion 155,241f f putc Bibliotheksfunktion 244 fputs Bibliotheksfunktion 158, 244 Funktion 159 frand Funktion 162 freed Bibliotheksfunktion 246 free Bibliotheksfunktion 161, 252 Funktion 180 f reopen Bibliotheksfunktion 156, 240 frexp Bibliotheksfunktion 250 fscanf Bibliotheksfunktion 155, 243 f seek Bibliotheksfunktion 246 fsetpos Bibliotheksfunktion 247 fsize Funktion 174 fsize Programm 174 f stat Systemaufruf 176 ftell Bibliotheksfunktion 246 function designator —► Funktionsaufruf, Funktionsbe Zeichner Funktion alter Stil 26,32,72,195 Argument 25,195 Aufruf 195 Definition 24, 69, 222 Deklarator 213 implizite Vereinbarung 26, 72,195 Länge des Namens 35,184 leere 69 mathematische 161 neuer Stil 195 Funktion «— Prototyp 25f, 29, 45, 71,115f, 195 static vereinbart 81 Typ nach Voreinstellung 29,195 Umwandlung 193 Vereinbarung 213 f Verweis 195 Zeichen testen 160, 247 Zeichenketten 159 Zeiger 114,116,141,195 Funktionsbeispiele addpoint 126 addtree 136 afree 99 attoc 98 atof 70f atoi 42,61,72 binsearch 57, 129, 132 bitcount 50 canonrect 126 closedir 176 copy 29, 31 day_of_year 107 del 119 dirdcl 119 dirualk 175 error 167 fgets 158 filecopy 156 _f illbof 171 fopen 170 fputs 159 frand 162 free 180 fsize 174 get 168 getbits 49 getch 77 get int 94 get line 28,31,69,159 getop 76 gettoken 120 getword 131 hash 139 install 140 itoa 63
270 Sachverzeichnis Funktionsbeispiele «— lookup 139 lower 43 main 6 makepoint 125 malloc 178f minprintf 150 monthday 107 monthname 109 morecore 180 numcmp 116 opendir 176 pop 76 power 24, 27 printd 84f ptinrect 126 push 76 qsort 85, 106, 115 rand 45 readdir 176 readlines 105 reverse 62 shellsort 61 squeeze 46 srand 46 strcat 47 strcmp 103 strcpy 102 strdup 137 strindex 69 strlen 39,96, 100 swap 85,93,106,116 talloc 137, 141 treeprint 136 trim 64 ungetch 77 writelinas 105f fwrite Bibliotheksfunktion 246 Gegenschrägstrich \ 8,38 ganze Zahl —»Integer generischer Zeiger 91,100,115,192f, 260 gepufferte Eingabe 164f geschweifte Klammern 7,10, 55, 82 get Funktion 168 getbi ts Funktion 49 getc Bibliotheksfunktion 155, 245 Makro 169 getch Funktion 77 getchar Bibliotheksfunktion 15,145,155, 245 Funktion 165 getenv Bibliotheksfunktion 252 get int Funktion 94 get line Funktion 28, 31, 69,159 getop Funktion 76 getrennte Übersetzung 67, 78, 225 gets Bibliotheksfunktion 158, 245 get token Funktion 120 getword Funktion 131 gleich « 19, 41, 201 Gleitpunkttypen 189 abschneiden 44,190 Konstante 12, 37,186 Umwandlung in Integer 44,190 Zufallszahlen 162 gmtima Bibliotheksfunktion 256 goto Anweisung 64 f, 221 Grenzbedingung 19, 64 Größe von Struktur 133,198 Größe von Zahlen 9,18, 36, 257 größer > 41, 200 größer-gleich >* 41, 200 Gültigkeitsbereich 78,187, 225 automatische Variable 78, 225 externe Variable 78, 225 lexikalisch 225 Marke 65, 219, 225 H .h Kennung bei Dateiname 32 hash Funktion 138f Header-Datei —► Definitionsdatei hexadezimal Ersatzdarstellung \x 37,185 Konstante Ox... 37,184 Hoare, C. A. R. 85 HUGEJ/AL 250 I identifier —* Name
Sachverzeichnis 271 #if 88,130,230 if-else Anweisung 19, 21, 55, 220 Mehrdeutigkeit 55f, 220, 232 #ifdef 89,231 «Mfndef 89,231 illegale Zeigerarithmetik 99f, 133,199f implementierungsabhängig —»Portabilität Implizite Vereinbarung einer Funktion 26, 72,195 »include 32,86,146,229 include file —► Definitionsdatei Index für Vektor 22,95,194, 213 negativ 97 und Zeiger 95f, 213 indirection —* Inhaltsoperator information hiding 67f, 74, 76 Inhaltsoperator * ix, 91,197 Initialisierung 40, 83, 215, 224 auto Variable 30, 40,83,215 Block 83, 220 externe Variable 40,79,83,215 Form 83f, 204 konstante Zeichenkette 84, 215 static 40,83,215 Struktur 124, 215 Union 216 Vektor 84,109,215 Vektor von Strukturen 128 Voreinstellung 84, 215 Zeiger 99,132 zweidimensionaler Vektor 108, 216 inklusiv-ODER, Bit-Operator | 48, 201 inkonsistente Typenvereinbarung 71 Inkrement ♦♦ 18,46,103,196f Inode 172 install Funktion 140 int Typ 9, 36, 206 Integer-Typen 189 Division 10, 40f Erweiterung 44,189f Konstante 12, 37,184 Umwandlung in Gleitpunkt 12,190 Umwandlung in Zeichen 44 Umwandlung in Zeiger 192,199f interne Bindung 187,226 interne static Variable 81 interner Name, Länge 35,184 JOFBF, JOLBF, JONBF 241 isalnum Bibliotheksfunktion 131,160, 247 isalpha Bibliotheksfunktion 131,160, 247 iscntrl Bibliotheksfunktion 247 isdigit Bibliotheksfunktion 160, 247 isgraph Bibliotheksfunktion 248 is lower Bibliotheksfunktion 160,248 ISO Zeichensatz 227 isprint Bibliotheksfunktion 248 ispunct Bibliotheksfunktion 248 isspace Bibliotheksfunktion 131,160, 248 i supper Bibliotheksfunktion 160, 248 isxdigit Bibliotheksfunktion 248 i toa Funktion 63 K Katalog ausgeben, Programm 172 Kernighan, B. W. x Klammern eckige -» Vektor 22,194 geschweifte 7,10, 55, 82 runde 194f kleiner < 41, 200 kleiner-gleich « 41, 200 Kleinschreibung, Programm 146 Klingelzeichen \a 38,185 Komma als Operator 62, 203 Kommandozeilen-Argumente 110 ff Kommentar 9,183, 227 Komponente einer Struktur ix, 123, 208 Konstante ix, 37,184 Aufzählung enum 39, 88,184, 186, 210 Ausdruck 38, 58,88, 204 double 37,186 erweitert, Zeichen 185 erweitert, Zeichenkette 186 float 37,186 ganzzahlig 12, 37,184 Gleitpunkt 12, 37,186 hexadezimal Ox... 37,184 hexadezimal, Zeichen 37,185 long 37, 184 long double 37,186 Länge im Preprozessor 35 oktal 0... 37,184
272 Sachverzeichnis Konstante «— oktal, Zeichen 37 Suffix 37,185 Typ 37,185 unsigned 37, 184 unsigned long 37, 185 Zeichen 19, 37,185 Zeichenkette 7,19, 29,38,97,101, 186 Zeichenkette, Initialisierung 84, 215 Zeichenkette, Länge 29,38,101 kopieren, Programm 16f, 164,166f L-Wert 189 labs Bibliotheksfunktion 253 Länge konstante Zeichenkette 29,38,101 Name 35,184 symbolische Konstante 35 längste Zeile, Programm 28, 31 Idexp Bibliotheksfunktion 250 Idiv Bibliotheksfunktion 253 ldiv_t, div_t Typnamen 253 leer Anweisung 18, 219 Funktion 69 Zeichenkette 38 Lesbarkeit, Programm 10, 50, 63,84,141 lexikalische Konventionen 183 lexikalischer Gültigkeitsbereich 225 <limits.h> 36,257 «line 231 line Makro, Preprozessor 253 linkage —* Bindung links-shift, Bit-Operator 48, 200 Literal —»Konstante <iocaie.h> 239 I oca Itima Bibliotheksfunktion 256 log, logiO Bibliotheksfunktionen 161,250 logische Negation ! 42,198 logischer Ausdruck, Wert 43 logisches ODER 11 21, 41, 48, 202 logisches UND && 21, 41, 48, 202 lokale Erwägungen 239 long Typ 9,18, 36,188, 206 Konstante 37,184f long double Typ 36, 188 Konstante 37,186 longjmp Bibliotheksfunktion 254 LONGJUX, L0NGJMN 251 lookup Funktion 139 lower Funktion 43 Is Kommando 172 I seek Systemaufruf 168 M magische Zahlen 14 mein Funktion 6 return 25, 157 mekepoint Funktion 125 Makro 227f feof, ferror, getc, putc 169 mit Argumenten 87 Preprozessor 86, 226 malloc Bibliotheksfunktion 137,160, 252 Funktion 178f Marke 65,219 case, default 58,219 Gültigkeitsbereich 65,219, 225 maschinenabhängig —»Portabilität <math.h> 44, 161, 249f mathematische Funktionen 161, 250 Mehrdeutigkeit, if-else 55 f, 220, 232 mehrdimensionaler Vektor 107, 213 mehrere Dateien übersetzen 70 mehrfache Zuweisung 20 mehrzelliges #define 87 member —»Alternative, Komponente memchr, memcmp, memcpy, memmove, memset Bibliotheksfunktionen 249 Methode des rekursiven Abstiegs 118f minprintf Funktion 150 Minus, unär - 198 mktime Bibliotheksfunktion 256 modf Bibliotheksfunktion 250 Modularisierung 24, 27f, 32, 67, 73 f, 104 monthday Funktion 107 month_name Funktion 109 morecore Funktion 180 MS-DOS 163,172 multi-dimensionaler Vektor 107, 213 Multiplikation • 40,199 Muster suchen, Programm 67f, 112f
Sachverzeichnis 273 N \n Zeilentrenner 7,15,19,37f, 185, 239 Name 184 Länge 35,184 Strukturkomponente 123,208 verbergen 83 vordefiniert, Preprozessor 232 Namensraum 225 nationale Zeichen 43,146 Nebenwirkung 52f, 87,193,1% Negation ! 42,198 negative Vektorindizes 97 neuer Stil, Funktion 195 newline —* Zeilentrenner null 99 Null, Test weglassen 55,102 Nullzeichen \0 29,38,185 Nullzeiger 99,191 numcmp Funktion 116 numerisch sortieren 114 o Objekt 187,189 ODER 11 21, 41, 48, 202 ODER, Bit-Operatoren A | 48, 201 oktale Konstante 37,184 open Systemaufruf 166 opendi r Funktion 176 Operationen bei union 142f Operationen bei Zeigern 100 Operator 52 Addition ♦ 40,199 Adresse & 91,197 Äquivalenzvergleich 41, 201 Arithmetik 40 Assoziativität 51 f, 193 Dekrement — 18,46,103,196f Division / 10, 40f, 199 gleich == 19, 41, 201 größer > 41, 200 größer-gleich >= 41, 200 Inhalt • ix, 91 Inkrement +♦ 18,46,103,196f kleiner < 41, 200 kleiner-gleich <= 41, 200 Komma 62, 203 Operator «— Minus, unär - 198 Multiplikation • 40,199 Negation • 42,198 ODER 11 21,41,48,202 Plus, unär ♦ 197 Preprozessor 88, 228, 230 Rest nach Division X 40f, 199 sizeof 88,100,130,198, 246 Strukturauswahl . 124,194 Strukturverweis -> 127,194 Subtraktion - 40,199f Tabelle 52 UND && 21,41,48,202 ungleich ! = 16,41,201 Vergleich 16, 41, 200 Vorrang 17, 41, 52, 92,127,193 Zuweisung« 16,41,49,203 Zuweisung ♦= 49 Operator, Bit-Manipulation 48,198,200f Eins-Komplement - 48,198 ODER * | 48, 201 shift « » 48, 200 UND & 48, 201 0_R0ONLY, 0_R0UR, 0_URONLY 166 overflow 41,193, 250, 255 Parameter ix, 25, 82, 97,195 Parameterliste, void 213 Pascal 6,19, 23, 26, 30, 55,60,63, 72, 82, 123,141 perror Bibliotheksfunktion 247 Pipe 145,164 Plus, unär ♦ 197 Polnische Schreibweise, Taschenrechner 73 pop Funktion 76 Portabilität 3, 37, 43, 48,141,144,147,177 Position, geschweifte Klammern 10 Postfix ++ und — 46,102 PostScript 73 pou Bibliotheksfunktion 24,161, 250 power Funktion 24, 27 Präfix ♦+ und - 46,103 #pragma 231
274 Sachverzeichnis precision —»Genauigkeit Preprozessor 86ff, 226ff _FILE , _LINE_ 253 Namen, vordefiniert 232 Operatoren 88,228, 230 Primärausdruck 194 printd Funktion 84 f printf Bibliotheksfunktion 7,10,18,147f, 242 Beispiele, Tabelle 148 Umwandlungen, Tabelle 13,148, 243 Programm Abbruch 156f cat 153,156f Datei kopieren 16f, 164,166f Dateien aneinanderhängen 153 del 119f echo 111 Format 10,18, 23, 40,133,183 fsize 174 Katalog ausgeben 172 Kleinschreibung 146 längste Zeile 28, 31 Lesbarkeit 10, 50, 63, 84, 141 Muster suchen 67f, 112f reservierte Worte zählen 129 sortieren 105,115 Tabellensuche 138 Taschenrechner 71, 73ff, 152 Temperaturumwandlung 8,12f, 15 übersetzen 6, 24 undcl 121 Wörter zählen 20,134 Zeichen zählen 17 Zeilen zählen 19 Zwischenraum zählen 22, 58 Propagierung, Vorzeichen 43f, 170, 185 Prototyp einer Funktion 25f, 29, 45, 71, 115f, 195 ptinrect Funktion 126 ptrdifft Typname 100, 141, 200 push Funktion 76 putc Bibliotheksfunktion 155, 246 Makro 169 putchar Bibliotheksfunktion 15,146,155, 246 puts Bibliotheksfunktion 158, 246 Q qualifier —»Attribut qsort Bibliotheksfunktion 253 Funktion 85, 106,115 Quicksort 85,106 R \r Wagenrücklauf 38,145,185, 239 raise Bibliotheksfunktion 255 rand Bibliotheksfunktion 161, 251 Funktion 45 RAN0_MAX 251 range —* Resultatbereich 249f read Systemaufruf 164 readdi r Funktion 176 read lines Funktion 105 real loc Bibliotheksfunktion 252 TtchX&shifi, Bit-Operator » 48, 200 recursive-descent parser 118 f reference —* Adreßoperator register Speicherklasse 81f, 205 Adresse 205 Reihenfolge Bewertung 21,48, 52f, 62, 75, 87, 92, 193 Übersetzung 226 f Vektor im Speicher 108,213 Rekursion 84,135f, 175,1%, 274 rekursive Struktur 135, 208 rekursiver Abstieg 118f remove Bibliotheksfunktion 240 rename Bibliotheksfunktion 240 reservierte Worte 35,184 Tabelle 184 zählen, Programm 129 Rest nach Division X 40 f, 199 return Anweisung 25, 29, 69, 72, 222 in main 25, 157 Typumwandlung 72, 222 reverse Funktion 62 rewind Bibliotheksfunktion 247 Richards, M. 1 Ritchie, D. M. vii sbrk Systemaufruf 180
Sachverzeichnis 275 scanf Bibliotheksfunktion 94,150 ff, 244 Umwandlungen, Tabelle 152,245 Zuweisung unterdrücken 151, 243f Schaltjahr, Berechnung 40,107 scope -»Gültigkeitsbereich 78,187, 225 SEEK_CUR, SEEK_EM0, SEEK_SET 246 Seiteneffekt -»Nebenwirkung 52f, 87,193, 1% Seitenvorschub \f 38,185 selbst-verweisende Struktur 135,208 Semikolon 9,15,18,55f sequentiell, Anweisungen 218 setbuf, setvtxif Bibliotheksfunktionen 241 set jmp Bibliotheksfunktion 254 <setjmp.h> 254 Shell, D. L. 61 shellsort Funktion 61 shift, Bit-Operatoren « » 48,200 short Typ 9, 36,188, 206 SIG_0FL, SIG_ERR, SIGJGN 255 sign extension —»Vorzeichen-Propagierung signal Bibliotheksfunktion 255 <signal.h> 254f signed —* vorzeichenbehaftet signed char 43,188 signed Typ 36,206 sin Bibliotheksfunktion 161,250 sinh Bibliotheksfunktion 250 sizeof Operator 88,100,130,198, 246 size_t Typname 100,130,141,198,240 Skalierung bei Zeigerarithmetik 100,191 sortieren, Programm 104f, 114f Speicher reservieren 205 Speicherklasse 187,205 auto, automatisch 30,187,205 extern 30, 32, 79, 205 keine Angabe 205 register 81 f, 205 static 30,81,187,205 Speicherverwaltung 137,177ff sprintf Bibliotheksfunktion 149,242 Sprunganweisung 221 sqrt Bibliotheksfunktion 161,250 squeeze Funktion 46 srand Bibliotheksfunktion 162, 251 Funktion 46 sscanf Bibliotheksfunktion 244 Standard-Ausgabe 146,155,163 Standard-Definitionsdateien, Tabelle 239 Standard-Eingabe 145,155,163 standard error —* Diagnose-Ausgabe stat Struktur, Systemaufruf 173 stat.h Definitionsdatei 173 static Speicherklasse 30, 81,187,205 Initialisierung 40, 83, 215 <stdarg.h> 149,167, 253f <stddef .h> 100,130,198, 239 stderr 155, 157, 240 stdin 155,240 <stdio.h> 6,16, 86f, 99,145f, 239 Inhalt 169 <stdlib.h> 70,137,251 stdout 155,240 Steuerzeichen 248 strcat Bibliotheksfunktion 159, 248 Funktion 47 strchr Bibliotheksfunktion 159, 248 strcmp Bibliotheksfunktion 159, 248 Funktion 103 strcpy Bibliotheksfunktion 159, 248 Funktion 102 strcspn Bibliotheksfunktion 249 strdup Funktion 137 stream —* Datenstrom, Strom strerror Bibliotheksfunktion 249 strftime Bibliotheksfunktion 256 strindex Funktion 69 String —»Zeichenkette <string.h> 39, 102f, 159, 248 strlen Bibliotheksfunktion 159,249 Funktion 39, 96,100 strncat Bibliotheksfunktion 159, 248 strncmp Bibliotheksfunktion 159, 248 strncpy Bibliotheksfunktion 159,248 Strom, binär 154, 239f Strom, Text 15,145, 239 strpbrk Bibliotheksfunktion 249 strrchr Bibliotheksfunktion 159, 249 strspn Bibliotheksfunktion 249 strstr Bibliotheksfunktion 249 strtod Bibliotheksfunktion 251
276 Sachverzeichnis strtok Bibliotheksfunktion 249 strtol, strtoul Bibliotheksfunktion 251 struct Struktur ix, 123, 207f Auswahloperator . 124,194 DIR, Di rent 173 Etikett ix, 123, 207 geschachtelt 124 Größe 133,198 Initialisierung 124, 215 Komponente ix, 123, 208 rekursiv 135, 208 selbst-verweisend 135, 208 stat 173 Vektor 127f Vereinbarung 123,207 Verweisoperator -> 127,194 Zeiger 131 Subtraktion - 40,199f Zeiger 100,133,191 Suffix bei Konstante 37,185 swap Funktion 85, 93,106,116 sui tch Anweisung 58, 74f, 220 symbolische Konstante, Länge 35 Syntax Funktionsaufruf 195 Schreibweise 187 Strukturverweis 1% Variablenname 35,184 Syntaxbaum 118 syscalls.h Definitionsdatei 165 system Bibliotheksfunktion 160, 252 Systemaufruf 163 close 167 creat 165f fstat 176 Iseek 168 open 166 read 164 sbrk 180 stat 173 unlink 167 write 164 \t Tabulatorzeichen 7, 11, 38,185 Tabellen Ersatzdarstellungen 38,185 Operatoren 52 printf Beispiele 148 printf Umwandlungen 13, 148, 243 reservierte Worte 184 scanf Umwandlungen 152, 245 Standard-Definitionsdateien 239 Tabellensuche, Programm 138 tog-»Etikett talioc Funktion 137,141 tan, tanh Bibliotheksfunktionen 250 Taschenrechner, Programm 71, 73 ff, 152 Tastatur-Eingabe 15,145,163 Teilvektor als Argument 97 Temperaturumwandlung, Programm 8, 12f, 15 Terminal, Eingabe und Ausgabe 15 Textstrom 15,145, 239 Textzeilen sortieren 104,114 Thompson, K. L. 1 time Bibliotheksfunktion 256 <time.h> 255 time_t Typname 255 tmpf i le Bibliotheksfunktion 241 THP_HAX 241 tmpnam Bibliotheksfunktion 241 token —* Eingabesymbol to lower Bibliotheksfunktion 146,160, 248 toupper Bibliotheksfunktion 160, 248 treeprint Funktion 136 trigraph —► Drei-Zeichen-Folge trim Funktion 64 Typ 206, 213 abgeleitet 1,9,189 äquivalent 218 arithmetisch 188 Attribut ix, 202, 206 Aufzählung enum 188 f char 9, 36,188, 206 double 9,18, 36,188, 206 elementar 9, 36,188 float 9,36,188,206 ganzzahlig 189 Gleitpunkt 189
Sachverzeichnis 277 Typ«- inkonsistente Vereinbarung 71 int 9, 36, 206 Integer 189 keine Angabe 206 Konstante 37,185 long 9,18, 36,188, 206 long double 36, 188 short 9, 36, 188, 206 signed 36,206 Umwandlungsregeln 42, 44,190 Umwandlung explizit 45 Umwandlung bei return 72, 222 unsigned 36,50,188,206 unsigned char 36, 165 unvollständig 207f Vereinbarung 213 void 29,189,192, 206 Zeichenkette 194 type qualifier -»Typattribut typedef Vereinbarung 140f, 205, 217f types.h Definitionsdatei 173,175 Typname 217 clock_t 255 div_t, ldiv_t 253 FILE 154 fpost 247 ptrdiff_t 100,141,200 size_t 100,130,141,198,240 time_t 255 uchar_t 185 u übersetzen, C Programm ix, 6, 24 bedingt 88, 230 getrennt 67, 78, 225 mehrere Dateien 70 Phasen 183, 226 Reihenfolge 226f Übersetzungseinheit 183,222,225 übliche arithmetische Umwandlung 42, 190f ULONG_MAX 251 umgekehrte Pohlische Schreibweise 73 Umlaut 43,146 Umlenkung, Ein-/Ausgabe 146,155,164 Umwandlung 189 ff Argument 45 arithmetisch 42, 190f Datum 107 double-float 44, 190 float-double 44, 190 Funktion 193 Gleitpunkt-Integer 44,190 Integer-Gleitpunkt 12,190 Integer-Zeichen 44 Integer-Zeiger 192,199 f Regehi 42, 44,190 return 72, 222 Tabelle für printf 13,148, 243 Tabelle für scanf 152,245 Vektorname 96,193 Zeichen-Integer 22, 42,189f Zeiger 137,191, 199f Zeiger-Integer 191,199f Zuweisung 44, 203 Umwandlungsoperation 45,137, 161, 191, 198, 217 unäres Minus - 198 unäres Plus + 197 UND && 21,41,48,202 UND, Bit-Operator & 48, 201 undcl Programm 121 #undef 88, 165, 227 underflow 41, 250, 255 ungepufferte Eingabe, getchar 164f ungetc Bibliotheksfunktion 160, 246 ungetch Funktion 77 ungleich, != 16,41,201 union, Union, —»Struktur Ausrichtung 178 Etikett 207 Initialisierung 216 Operationen 142f Vereinbarung 141 f, 207 UNIX Dateisystem 163,172 unlink Systemaufruf 167 unsigned —* vorzeichenlos unsigned Typ 36, 50,188, 206 Konstante 37,184 unsigned char Typ 36, 43, 165, 188 unsigned long Konstante 37, 185 unspezifischer Zeiger -* void •
278 Sachverzeichnis Unterstrich _ 35,184, 239 unvollständiger Typ 207f \v Vertikal-Tabulator 38,185 vaarg, vaend, va_list, va_start 149, 167, 242,254 Variable 187 Adresse 27,91,197 intern, static 81 Variable, automatisch 30, 73,187 Gültigkeitsbereich 78,225 Initialisierung 30, 40, 83, 215 Variable, extern 30, 32, 72,187, 222,224 Gültigkeitsbereich 78, 225 Initialisierung 40, 79, 83, 215 static 81 variable Argumentliste 149,167,1%, 213, 223,253f Variablenname, Syntax 35,184 Länge 184 Variante —» Union Vektor ix, 22,107, 212 Argument argv 110,157 Deklarator 212 Index 22, 95,194, 213 multi-dimensional 107, 213 Reihenfolge im Speicher 108, 213 und Zeiger 95ff, 101,109f Verweis 194 zweidimensional 107 f, 216 Vektor, Initialisierung 84,109, 215 Struktur 128 zweidimensional 108, 216 Vektor von Strukturen 127f Zeichen 19, 27,101 Zeigern 104 Vektorgröße, Voreinstellung 84,109,129 Vektorname als Argument 27, 97, 108 Vektorname, Umwandlung 96,193 Vereinbarung ix, 9, 32, 39, 79, 204ff —> Definition, Deklaration Bit-Feld 144, 207 extern 222, 224 externe Variable 30, 222 Funktion 213f Vereinbarung «— Funktion, implizit 26, 72,195 inkonsistente Typen 71 Speicherklasse 205 Struktur 123,207 Typ 213 typedef 140f, 205, 217f Union 141 f, 207 Vektor 22,107, 212 Zeiger 92, 97, 212 Vereinigung —» uni on Vergleich, Zeiger 99,133,180, 200f Vergleichsausdruck, Wert 42f Vergleichsoperatoren 16, 41, 200 verketten, Eingabesymbol 88,228 verketten, Zeichenkette 38,88,186 Vertikal-Tabulator \v 38,185 Verweisoperator —»Inhaltsoperator verzweigte Entscheidung 23, 57 vfprintf Bibliotheksfunktion 167, 242 void Typ 29,189,192, 206 void Argumentliste 32, 72, 213, 223 void • Zeiger 91,100,115,192f, 260 volatile ix, 189,206 vordefinierte Preprozessor-Namen 232 Voreinstellung Funktionstyp 29, 195 Initialisierung 84, 215 Vektorgröße 84,109,129 vorläufige Definition 224 Vorrang von Operatoren 17, 41, 52, 92, 127, 193 Vorzeichen —» unärer Operator Vorzeichen-Propagierung 43f, 170,185 vprintf, vfprintf, vsprintf Bibliotheksfunktionen 167, 242 w Wagenrücklauf \r 38,145,185, 239 uchar_t Typname 185 Wert, logischer Ausdruck 43 Wert, Vergleich 42 f Wertebereich verlassen 41 —♦ overflow, underflow Wertübergabe 26,93,195 while Anweisung 10,59,221
Sachverzeichnis 279 wide character constant —»erweiterte Zeichenkonstante Wiederholungsanweisungen 220f wissenschaftliche Schreibweise 37,72 Wörter zählen, Programm 20,134 Worte, reservierte 35,184 zählen, Programm 129 write Systemaufruf 164 uritelines Funktion lOSf \x hexadezimale Ersatzdarstellung 37,185 Zahlengröße 9,18,36, 257 Zeichen \ Gegenschrägstrich 8,38 \0 Null 29,38,185 \a Klingel 38,185 \b backspace 7,38,185 druckbar 248 Eingabe/Ausgabe 15,145 \f Seitenvorschub 38,185 Konstante 19, 37,185 Konstante, erweitert 185 Konstante, oktal 37 \n Zeilentrenner 7,15,19, 37f, 185, 239 nationale 43,146 \r Wagenrücklauf 38,145,185, 239 Steuerzeichen 248 \t Tabulator 7,11,38,185 Testfunktionen 160,247 Umwandlung in Integer 22,42,189f W Vertikal-Tabulator 38,185 Vektor 19, 27,101 Zwischenraum 151,160, 244, 248 Zeichenkette Funktionen 159 in Initialisierung 84, 215 Konstante 7,19,29,38,97,101,186 Konstante, erweitert 186 Länge 29, 38,101 leer 38 Typ 194 verketten 38,88,186 Zeichensatz 227 ASCII 19, 37, 43,227, 248 EBCDIC 43 ISO 227 Zeiger 92, 97, 212 Argument 97 Arithmetik 92,96ff, 113,132f, 199f Arithmetik, illegal 99f, 133,199f Arithmetik, Skalierung 100,191 auf Funktion 114,116,141,195 auf Struktur 131 erlaubte Operationen 100 erzeugen 193 file 154,169,239 Initialisierung 99,132 Nullzeiger 99,191 Subtraktion 100,133,191 Umwandlung 137,191,199f und Vektor 95ff, 101,109f, 213 unspezifisch, void • 91,100,115,192 f, 260 Vergleich 99,133,180, 200f Zeigervektor 104 Zeilen zählen, Programm 19 Zeilen zusammenfügen 227 Zeilentrenner 145,183,227 \n 7,15,19, 37f, 185,239 Zufallszahlen 161 f, 251 Gleitpunkt 162 Zugriff auf Datei 153,163,170f, 240 Zugriffsart 154,170 f, 240 Zugriffsschutz 166 zurückstellen, Eingabe 77 zusammengesetzte Anweisung 55, 82, 219, 222f Zuweisung 16f, 41, 49, 203 Ausdruck 17, 20, 50, 203 mehrfach 20 Umwandlung 44, 203 unterdrücken bei scanf 151,243f zweidimensionaler Vektor 107 f, 216 Initialisierung 108, 216 Zwischenraum 151,160,183, 244, 248 zählen, Programm 22, 58
Häufig verwendete Zeichentabellen
Tabelle 2-1. Vorrang und Assoziativität der Operatoren Dperatoren () n -> . ! - ++ -_ + _ • j (typt) sjzeOf * / X + - « >> < <= > >= == ! = & 1 && II ?: = += -= *= /= x= &= *■ |= «= »= Assoziativität von links her von rechts her von links her von links her von links her von links her von links her von links her von links her von links her von links her von links her von rechts her von rechts her von links her Unär haben +, - und • mehr Vorrang als binär. Zeichen Tabelle B-l. printf Umwandlungen Argument; Umwandlung in d, i int; dezimal mit Vorzeichen. o int; oktal ohne Vorzeichen (ohne führende Null), x, x int; hexadezimal ohne Vorzeichen (ohne führendes Ox oder OX), mit abcdef bei Ox oder ABCDEF bei OX. u int; dezimal ohne Vorzeichen c int; einzelnes Zeichen, nach Umwandlung in unsigned char. s char •; aus der Zeichenkette werden Zeichen ausgegeben bis vor '\0' oder so viele Zeichen, wie die Genauigkeit verlangt, f double; dezimal als [-]mmm.ddd, wobei die Genauigkeit die Anzahl der d festlegt. Voreinstellung ist 6; bei 0 entfällt der Dezimalpunkt. e, E double; dezimal als [-]m.dddddde±xx oder [-]m.ddddddE±xx, wobei die Genauigkeit die Anzahl der d festlegt. Voreinstellung ist 6; bei 0 entfällt der Dezimalpunkt, a, g double; %e oder %E wird verwendet, wenn der Exponent kleiner als -4 oder nicht kleiner als die Genauigkeit ist; sonst wird %f benutzt. Null und Dezimalpunkt am Schluß werden nicht ausgegeben, p void •; als Zeiger (Darstellung hängt von Implementierung ab), n int •; die Anzahl der bisher von diesem Aufruf von printf ausgegebenen Zeichen wird im Argument abgelegt. Ein Argument wird nicht umgewandelt. kein Argument wird umgewandelt; ein % wird ausgegeben.
Zeichen Tabelle B-2. scanf Umwandlungen Eingabedaten; Argumenttyp e,f,9 t...] dezimal, ganzzahlig; int •. ganzzahlig; int •. Der Wert kann oktal (mit 0 am Anfang) oder hexadezimal (mit Ox oder OX am Anfang) angegeben sein. oktal ganzzahlig (mit oder ohne 0 am Anfang); int *. dezimal ohne Vorzeichen; unsigned int *. hexadezimal ganzzahlig (mit oder ohne Ox oder OX am Anfang); int *. ein oder mehrere Zeichen; char •. Die nachfolgenden Eingabezeichen werden im angegebenen Vektor abgelegt, bis die Feldbreite erreicht ist; Voreinstellung is 1. '\0' wird nicht angefügt. In diesem Fall wird Zwischenraum nicht überlesen; das nächste Zeichen nach Zwischenraum liest man mit %ls. Folge von Nicht-Zwischenraum-Zeichen (ohne Anführungszeichen); char •, der auf einen Vektor zeigen muß, der die Zeichenkette und das abschließende '\0' aufnehmen kann, das dazukommt. Gleitpunkt; float*. Das Eingabeformat erlaubt für float ein optionales Vorzeichen, eine Ziffernfolge, die auch einen Dezimalpunkt enthalten kann, und einen optionalen Exponenten, bestehend aus E oder e und einer ganzen Zahl, optional mit Vorzeichen. Zeiger, wie ihn printf("%p") ausgibt; void •. legt im Argument die Anzahl Zeichen ab, die bei diesem Aufruf bisher gelesen wurden; int *. Vom Eingabestrom wird nicht gelesen, die Zählung der Umwandlungen bleibt unbeeinflußt. erkennt die längste nicht-leere Zeichenkette aus den Eingabezeichen in der angegebenen Klasse; char •. Dazu kommt *\0'. Die Klasse []...] enthält auch ]. erkennt die längste nicht-leere Zeichenkette aus den Eingabezeichen nicht in der angegebenen Klasse; char •. Dazu kommt *\0'. Die Klasse [*]...] enthält auch]. erkennt %; eine Zuweisung findet nicht statt.