/
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.