Usenet Replayer



iso-8859-1


Path:  news2.ip-mobilphone.net ! NNTPLoader.ip-mobilphone.net ! news.netfront.net ! news.unit0.net ! news.szaf.org ! fu-berlin.de ! uni-berlin.de ! individual.net ! not-for-mail
From:  Martin Woelfel <dclcfaq@zwillingsskorpion.de>
Newsgroups:  de.comp.lang.c
Subject:  de.comp.lang.c FAQ
Supersedes:  <dclc_FAQ_171_Part_1@dclc-faq.de>
Followup-To:  de.comp.lang.c
Date:  1 Apr 2014 01:24:55 GMT
Lines:  4570
Message-ID:  <dclc_FAQ_172_Part_1@dclc-faq.de>
Reply-To:  dclcfaq@zwillingsskorpion.de
Mime-Version:  1.0
Content-Type:  text/plain; charset=ISO-8859-1
Content-Transfer-Encoding:  8bit
X-Trace:  individual.net YILqbFkB+CwdnhMqZcHQdANrl0oYXbQF7DMFY2Mw8IT9B/MMwN
Summary:  This article is part 1 of the Frequently Asked Questions' list with answers for the newsgroup de.comp.lang.c, which deals with the C programming language. Like the newsgroup, this article is in German.
Cancel-Lock:  sha1:r6CXnaDNSdcnsV0vMQ4KwE6gPCM=
Cancel-Key:  sha1:18T8ol+IbfFUz90JDorDNAJfcn8=
Xref:  news2.ip-mobilphone.net de.comp.lang.c:52643


Archive-Name: C-faq/german/part1
Posting-Frequency: monthly
Last-modified: 2004-03-14
URL: http://home.pages.de/~c-faq/ (Who's computer is this?)


Das folgende ist die FAQ für de.comp.lang.c. Die meisten Kapitel sind
bisher eine Übersetzung der alten comp.lang.c FAQ von Steve Summit, das
Copyright für die englische Version liegt bei Steve.

Einige kurze Bemerkungen anstelle einer formellen Einleitung:

* Die ursprüngliche Idee, eine FAQ zu erstellen bzw. die FAQ von Steve
zu übersetzen ist schon älter (genauer gesagt: sie kam Mitte 95 auf),
einige Leute haben ziemlich viel Arbeit in das Projekt gesteckt, bevor
es wieder eingeschlafen ist.

* An der Übersetzung/Neuerstellung dieser FAQ haben sich beteiligt:

Ullrich von Bassewitz (uz@musoftware.de)
Kai Baumbach (kai.baumbach@faktori.de)
Stefan Baumgart (stefan.baumgart@mega.jena.thur.de)
Stefan Bodewig (stefan.bodewig@megabit.net)
Rolf Czedzak (roc@viking.ruhr.com)
Robert Figura (template@bigben.dssd.sub.org)
Oliver Pathofer (op@ewok.ruhr.de)
Jochen Schoof (joscho@bigfoot.de)
Silvio Schurig (zryp0104@baracke.rus.uni-stuttgart.de)
Kurt Watzka (kurt@stat.uni-muenchen.de)
Thomas Wolf (thomas.wolf@di.epfl.ch)

* Die Übersetzung ist nicht exakt, es wurde mehr Wert auf Lesbarkeit als
auf eine wörtliche Übersetzung gelegt.

* Die "References" Zeilen wurden nicht übersetzt bzw. so gelassen, wie
sie von den anderen Leuten zugeschickt wurden, weil diese sowieso in
Referenzen auf deutsche Titel umgewandelt, oder zumindest um deutsche
Querverweise ergänzt werden sollten.

* Die einzelnen Übersetzer haben sich z.T. die Freiheit genommen,
manche Dinge zu kommentieren. Ich fasse das als ersten Schritt zu
einer Erweiterung gegenüber der englischen FAQ auf, trotzdem sind die
Anmerkungen bisher noch als solche markiert. Das wird wahrscheinlich
nicht immer so bleiben.

* Die FAQ berücksichtigt gegenwärtig noch nicht den inzwischen
aktuellen Standard ISO 9899:1999 (kurz als C99 bezeichnet).

Aenderung zur letzten Version:
* neue Mailadresse
* 11.12

============================================================================

Bestimmte Punkte tauchen wieder und wieder in dieser Newsgroup auf. Es sind
gute Fragen, und die Antworten sind nicht immer offensichtlich, aber jedes
erneute Auftauchen einer solchen Frage verursacht unnötigen Traffic und
natürlich Zeit, die für die Beantwortung der Fragen und für die Korrektur
von falschen Antworten draufgeht.

Dieser Artikel, der monatlich gepostet wird, versucht, die gängigen Fragen
knapp aber endgültig zu beantworten, um eine Diskussion über die wirklich
interessanten Themen zu ermöglichen, ohne jedesmal auf bestimmte Grundlagen
zurückzukommen.

Kein einzelner Artikel kann ein ausführliches Tutorial oder ein Reference
Manual für die Sprache ersetzen. Jeder, der sich soweit für die Sprache C
interessiert, dass er diese Newsgroup liest sollte auch genügend Interesse
aufbringen, ein oder mehrere gute Bücher zum Thema zu lesen. Einige Bücher
über C und einige Compiler-Handbücher sind leider nicht ausreichend,
einige wenige verbreiten sogar Gerüchte, mit denen dieser Artikel
aufräumen will. Die Bibliographie listet einige Bücher auf, in die es sich
lohnt, hineinzuschauen. Viele der hier besprochenen Fragen und Antworten
enthalten Verweise auf Bücher, die der interessierte Leser für
weitergehende Informationen konsultieren kann (aber Vorsicht: der ANSI und
der ISO Standard unterscheiden sich bei der Nummerierung der Kapitel,
siehe Frage 5.1).

Diese Version der FAQ wird bis auf weiteres am Anfang jedes Monats nach
de.comp.lang.c gepostet.

Außerdem ist diese Version unter der URL

http://www.dclc-faq.de/ (Who's computer is this?)
bzw.
http://home.pages.de/~c-faq/ (Who's computer is this?)

verfügbar. Neben der reinen Textversion steht zur Zeit auch noch eine
HTML-Version zur Verfügung, die einige zusätzliche Komfortmerkmale bietet.

Die Fragen, die hier beantwortet werden, sind in folgende Abschnitte
aufgeteilt:


0. de.comp.lang.c
1. Null-Zeiger
2. Arrays und Zeiger
3. Speicherbelegung (dynamischer Speicher)
4. Ausdrücke
5. ANSI C
6. Der C Präprozessor
7. Variable Argumentlisten
8. Boolesche Ausdrücke und Variablen
9. Structs, Enums und Unions
10. Deklarationen
11. Stdio
12. Library Unterprogramme
13. Lint
14. Programmierstil
15. Gleitkomma-Probleme
16. Systemabhängiges
17. Verschiedenes



---------------------------------------------------------------------------

Abschnitt 0: de.comp.lang.c
===========================

0.1: Was ist de.comp.lang.c?

A: de.comp.lang.c ist eine der vielen tausend Newsgruppen des Usenet.
Ihr Sinn und Zweck ergibt sich unmittelbar aus ihrem Namen:

de - man spricht deutsch
comp - computer-orientiert
lang - Programmiersprache
c - C (gemäß K&R1, ISO 9899:1990 oder 9899:1999)

Alle Themen, die auf diese kurze Beschreibung passen, können und
sollen in de.comp.lang.c diskutiert werden. Allgemeine Regeln für
das Verhalten in Newsgruppen findet man an vielen Stellen. Einige
spezielle Regeln werden in diesem Abschnitt erläutert.

References: Netiquette

0.2: Was wird genau in de.comp.lang.c diskutiert?

A: Die Beschreibung zum Gruppennamen lautet "C (K&R, ANSI)". Damit ist
bereits alles gesagt. Diskussionsthema sind alle Aspekte bisheriger
(K&R C), aktueller (ANSI bzw. ISO C 90 und 99) und zukünftiger
verbindlicher Standards für die Programmiersprache C.

Hierzu gehören insbesondere Fragen zur Konformität bestimmter
Funktionen oder Techniken, Ratschläge zur portierbaren Lösung
bestimmter Aufgaben, Diskussionen über wünschenswerte Erweiterungen
sowie natürlich Kritik und Anregungen für die FAQ der Newsgruppe.

References: Abschnitt 5

0.3: Welche Themen gehören nicht nach de.comp.lang.c?

A: Themen wie "Verkaufe Grafikkarte mit 2MB" gehören naturgemäß nicht
in eine Newsgruppe, die sich um die Programmierung in C dreht.
Leider ist die Entscheidung aber nicht in allen Fällen so einfach.
Die Tatsache, dass ein Programm in C geschrieben wird, bedeutet
nicht, dass automatisch alle das Programm betreffenden Fragen in
dieser Newsgruppe richtig plaziert sind. Es ist wichtig zu
verstehen, dass nicht alles, was sich mit dem heimischen C-Compiler
übersetzen läßt, standard-konformes C ist. Die Sprache C wird durch
internationale Standards und nicht durch die Implementierung eines
mehr oder weniger bedeutenden Compiler-Herstellers festgelegt.

Für Fragen der Art "Wie mache ich XY mit Compiler ABC?" ist diese
Gruppe also nicht der richtige Platz. Vielmehr sollte man hierfür
Gruppen wählen, die sich dem jeweiligen Compiler oder zumindest
der benutzten Rechnerplattform widmen.

Es sei noch darauf hingewiesen, dass C++ eine eigenständige
Programmiersprache ist, die sich zwar von C ableitet, auf Grund der
völlig veränderten Konzepte aber nicht in einer C-orientierten
Newsgruppe diskutiert werden sollte. Die korrekte Newsgruppe für
Fragen zum C++-Standard ist de.comp.lang.iso-c++.

0.4: Welche Fragen führen traditionell zu heftigen Reaktionen?

A: Alle Fragen, die sich mit hochgradig systemspezifischen Problemen
befassen. Eine (immer unvollständig bleibende) Auswahl:

- Wie löscht man in C den Bildschirm?
- Wie programmiere ich eine Maussteuerung?
- Wie öffne ich in Windows eine File-Selection-Box?
- Was stimmt mit meinem Terminal-Programm nicht?

Allen diesen Fragen ist gemeinsam, dass sie sich mit Programmen oder
Peripherie beschäftigen, die im C-Standard nicht vorgesehen sind.
Daher bietet ANSI-C hierfür keine standardisierten Lösungen an. Oft
verwenden sogar verschiedene Compiler für denselben Rechner und
dasselbe Betriebssystem unterschiedliche Techniken. Die Chance, in
de.comp.lang.c jemand zu finden, der bei diesen Problemen helfen
kann, ist daher sehr gering. In der Regel wird ein großer Teil der
Leser, den dieses Problem nicht interessiert, verärgert reagieren.

0.5: Wozu soll diese Newsgruppe gut sein, wenn ich nur zum Standard,
nicht aber zu meinem tatsächlichen Compiler Fragen stellen darf?

A: Das eine schließt das andere nicht aus. Viele Probleme, die mit
einem beliebigen Compiler auftreten, lassen sich durchaus auf ein
Problem in ANSI-C reduzieren. Wesentlich ist, zu erkennen, welche
Fähigkeiten des eigenen Compilers über den ANSI-Standard
hinausgehen. Als Faustregel kann dienen, dass alle Probleme bei der
Programmentwicklung, die mit Ein- oder Ausgabegeräten (Drucker,
Maus, Schnittstellen usw.) sowie mit grafischen Benutzeroberflächen
(MS-Windows, X11 usw.) zu tun haben, systemspezifisch sind und in
eine andere Newsgruppe gehören. Was hiernach übrig bleibt ist mit
hoher Sicherheit in de.comp.lang.c richtig plaziert.

0.6: Wozu soll eigentlich diese Beschränkung auf K&R C bzw. ANSI C als
"reine Lehre" gut sein?

A: Sie soll einem Fragesteller die größtmögliche Chance auf hilfreiche
Antworten geben. Die Newsgruppe wurde geschaffen, um Fragen zu
diesem Thema zu beantworten und deshalb schreiben hier Leute, die
zu diesem Thema etwas zu sagen haben. Da sie viele verschiedene
Compiler auf vielen verschiedenen Rechnern verwenden, sinkt die
Anzahl derjenigen, die potentiell vernünftige Antworten geben
können, bei systemspezifischen Fragen rapide. Es kann deshalb nicht
im Sinne des Fragestellers sein, Fragen zu stellen, die "off-topic"
sind.

Die gelegentlich geäußerte Ansicht, eine Newsgruppe müsse mit den
Anforderungen ihrer Nutzergemeinde Schritt halten ist nur bedingt
richtig. Wer unbedingt über einen speziellen Compiler diskutieren
will, kann eine eigene Gruppe dafür initiieren und sollte keine
bestehenden Gruppen mißbrauchen.

0.7: Wo sollen denn Fragen zu einem bestimmten Compiler gepostet werden,
wenn nicht in de.comp.lang.c?

A: Idealerweise in eine compiler-spezifische Newsgruppe. Da es nicht
für alle Compiler eigene Newsgruppen gibt, wird man oft andere Wege
gehen müssen. Da die meisten Fragen eher rechner- oder
betriebssystem-spezifisch sind, können entsprechende Newsgruppen
gut geeignet sein. Erfahrungsgemäß landen sehr viele Postings in
de.comp.lang.c, die eigentlich in eine der folgenden Gruppen
gehören:

de.comp.gnu
de.comp.os.unix.programming
de.comp.os.ms-windows.programmer
de.comp.os.msdos
de.comp.os.os2.programmer
de.comp.sys.amiga.tech

Außerdem gibt es natürlich auch entsprechende internationale
Newsgruppen, die sich diesen und ähnlichen Themen widmen. Aber auch
hier gilt die Regel: Erst informieren, dann fragen.

0.8: Meine Frage dreht sich aber wirklich nur um ANSI-C. Darf ich sie
jetzt posten?

A: Bevor man eine Frage postet, sollte man in der FAQ nachschauen, ob
die Frage dort auftaucht. Am besten liest man gleich den gesamten
zugehörigen Abschnitt durch, denn oft ist ein Sachverhalt über
mehrere Fragen verteilt. Ist die Frage in der FAQ nicht zu finden,
so kann man sie (endlich) in die Newsgruppe posten.

0.9: Wie sollte eine Frage in de.comp.lang.c aussehen?

A: Kurz, präzise und freundlich.

Mit der Länge einer Frage sinkt die Anzahl derjenigen, die sie
lesen. Man sollte auch nicht um den heißen Brei herumreden,
sondern gleich zur Sache kommen. Hierzu gehört insbesondere auch
ein aussagekräftiger Titel für das Posting. Der Titel "Frage zu C"
ist in dieser Newsgruppe wenig sinnvoll. Besser wäre zum Beispiel
"Funktion als Parameter - wie?".

Schließlich sollte man sich bemühen, die Frage in einem Stil
abzufassen, der potentielle Helfer nicht gleich vergrault.

0.10: Ich habe hier ein Programm das nicht läuft. Soll ich es zusammen
mit der Problembeschreibung posten?

A: Ja - aber bitte nicht gleich alles. Bei Problemen in C-Programmen
sollte der kritische Abschnitt möglichst eng gefaßt werden. Das
kleinste Stück ANSI-konformen und compilierbaren Quellcodes, bei
dem der Fehler noch nachvollziehbar ist, sollte gepostet werden.

0.11: Ich habe selten Zeit de.comp.lang.c zu lesen. Kann ich bei einer
Frage um Antwort per Mail bitten?

A: Das wird in aller Regel als äußerst schlechter Stil aufgefaßt
werden. Die öffentliche Beantwortung von Fragen soll nämlich nicht
nur dem Fragesteller, sondern auch anderen Lesern der Newsgruppe,
die möglicherweise ähnliche Probleme haben, helfen. Wer ein Problem
für wichtig genug hält, um es in die Newsgruppe zu posten, sollte
auch die Newsgruppe für genügend wichtig halten, um sie zu lesen.

0.12: Jemand hat eine in der FAQ enthaltene Frage in de.comp.lang.c
gestellt. Wie soll man darauf reagieren?

A: Man sollte den Fragesteller auf die Existenz der FAQ und die
Tatsache, dass sie die von ihm gesuchte Lösung enthält, hinweisen.
Dabei sollte man sich um einen verbindlichen Ton bemühen und den
Fragesteller nicht gleich wüst beschimpfen.

0.13: Ich kenne die Antwort auf eine gerade gestellte Frage. Soll ich sie
gleich posten?

A: Zunächst sollte man sich vergewissern, dass nicht bereits eine
Antwort auf die Frage vorliegt. Danach ist sicherzustellen, dass die
eigene Antwort tatsächlich korrekt ist. Vor dem Posten von
Quellcode sollte beispielsweise überprüft werden, ob er tatsächlich
das Gewünschte leistet. Natürlich kann es vorkommen, dass man
falsche Antworten postet. Man sollte aber soviel Sorgfalt auf die
Überprüfung eigener Antworten verwenden, dass dieses Risiko so
gering wie möglich ist.

0.14: Ich habe nur wissen wollen, wie ich in C den Bildschirm lösche.
Jetzt habe ich einige unfreundliche Mails erhalten, in denen es
heißt, diese Frage sei in de.comp.lang.c irrelevant. Was für
Leute schreiben sowas?

A: Vermutlich überwiegend treue Anhänger von de.comp.lang.c, die sich
über das mehr und mehr die Oberhand gewinnende Rauschen in der
Newsgruppe ärgern. Es ist nunmal eine elementare Regel im Usenet,
sich vor dem Stellen von Fragen zu informieren, ob diese in der
entsprechenden Newsgruppe "on-topic" sind. Wer diese Regel
mißachtet, setzt durch dieses oft als rüde empfundene Benehmen die
Ursache für in etwas rauhem Ton gehaltene Antworten.

Andreas Burmester hat einmal eine Newsgruppe treffend mit einem
Seminar verglichen, in dem Interessierte ein bestimmtes Thema
diskutieren. Leider kommt alle paar Minuten jemand herein, den
keiner kennt und der auf der Stelle eine unpassende Frage
beantwortet haben will. Wenn man ihn dann in etwas schärferem Ton
zurechtweist, tauchen noch weitere Gestalten vom Gang auf, die sich
darüber beschweren, dass die Seminarteilnehmer arrogant sind.

Man sollte also vernünftig vorgebrachte Kritik ernst nehmen. Das
heißt aber nicht, dass man sich nach einem Fehler alles gefallen
lassen muß. Etwaige Diskussionen sollten aber per Mail abgewickelt
werden und nicht in de.comp.lang.c.

0.15: In letzter Zeit sieht man häufig die Zeichenfolge "[HOT]" vor
Antworten. Was bedeutet das?

A: Es bedeutet "Hinweis auf Off-Topic" und soll deutlich machen,
dass der Fragesteller in dieser Antwort darauf aufmerksam
gemacht wird, dass seine Frage in der Gruppe Off-Topic (d.h.
nicht themengerecht) ist. Wer sich mit diesen Hinweisen - und
den oft folgenden Diskussionen - nicht befassen möchte, kann
diese Postings ignorieren.

---------------------------------------------------------------------------


Abschnitt 1: Null-Zeiger
========================

1.1: Was ist denn nun eigentlich dieser verflixte Null-Zeiger?

A: Die Sprachdefinition legt fest, dass es für jeden Zeiger-Typ einen
bestimmten Wert gibt, der von allen anderen Zeigerwerten
verschieden ist und der nicht die Adresse irgendeines Objektes
oder irgendeiner Funktion enthält: der "Null-Zeiger" eben. Das
heißt also: der Adress-Operator & liefert niemals einen
Null-Zeiger - ebensowenig wie ein erfolgreicher Aufruf von
malloc(). malloc() liefert ja bei Mißerfolg einen Null-Zeiger
zurück - womit wir beim typischen Anwendungsfall für Null-Zeiger
wären: Wir haben damit einen "besonderen" Zeiger-Wert, der eine
besondere Aussage trifft - normalerweise "kein Speicher
beschafft" oder "ich zeige noch auf nichts".

Es besteht ein großer Unterschied im Konzept des Null-Zeigers und
dem des nicht initialisierten Zeigers: für den Null-Zeiger ist
garantiert, dass er nirgendwohin zeigt, ein nicht initialisierter
Zeiger hingegen kann überallhin zeigen (im schlimmsten Fall
sogar an eine Stelle, die dem eigenen Programm gehört, wo der
Fehler also nicht sofort auffällt. Außerdem kann auf einen
nicht initialisierten (ungültigen) Zeiger nicht getestet werden).
Vgl. auch Frage 3.1, 3.13 und 17.1.

Wie oben erwähnt, gibt es für jeden Zeiger-Typ einen eigenen
Null-Zeiger mit einer möglicherweise unterschiedlichen internen
Repräsentation. Allerdings ist garantiert, dass man zwei
Null-Zeiger beliebig zwischen verschiedenen Zeiger-Typen
umwandeln kann, vergleicht man sie dann, muß das Ergebnis wieder
ihre Gleichheit sein. Während dem Programmierer deshalb der
interne Wert eines Null-Zeigers gleichgültig sein kann, muß die
Umgebung stets wissen, welche Art von Null-Zeiger benötigt wird,
damit er wenn nötig einen Unterschied machen kann (s. unten).

Siehe: K&R 1, 5.4; K&R 2, 5.4; H&S 5.3; ANSI 3.2.2.3; Rationale
3.2.2.3; ISO 6.2.2.3; P&B S. 49, 105.

1.2: Wie erzeuge ich einen Null-Zeiger in meinen Programmen?

A: Laut Sprachdefinition wird ein integraler konstanter Ausdruck
mit dem Wert 0 zu einem Null-Zeiger, wenn er einem Zeiger
zugewiesen oder auf Gleichheit mit einem Zeiger verglichen wird
(Äquivalenzvergleich). Die Umgebung (in aller Regel wohl die
Übersetzungsumgebung) muß in einem solchen Fall feststellen, dass
ein Null-Zeiger benötigt wird und einen Wert für den
entsprechenden Typ eintragen. Deshalb sind die folgenden
Code-Fragmente einwandfrei:

char *z = 0;
if (z != 0)

Hingegen ist bei einem Funktionsargument nicht notwendig ein
Zeiger-Kontext feststellbar. Die Umgebung kann also u.U. nicht
feststellen, dass der Programmierer mit einer einfachen 0 einen
Null-Zeiger meint. Der Unix Systemaufruf "execl" erwartet eine
Liste variabler Länge von Zeigern auf char, die mit einem
Null-Zeiger abgeschlossen werden. Um im Umfeld eines
Funktionsaufrufes einen Null-Zeiger zu erzeugen, ist
normalerweise eine ausdrückliche Typumwandlung nötig - erst
dadurch wird 0 in einen Zeiger-Kontext gestellt:

execl ("/bin/sh", "sh", "-c", "ls", (char *) 0);

Ließe man die Typumwandlung nach (char *) weg, wüßte die Umgebung
nicht, dass ein Zeiger übergeben werden soll und würde sich in
diesem Fall für eine 0 als ganze Zahl entscheiden. (Beachte, dass
eine ganze Reihe von Handbüchern bei diesem Beispiel einen Fehler
machen.)

Liegt ein Funktions-Prototyp vor, werden die Argumente - wie bei
einer Zuweisung - an Hand der zugehörigen Parameter im Protoyp
umgewandelt. Bei variablen Argumentlisten funktioniert dies
natürlich nur bis zum Ende der explizit festgelegten Parameter
- alles was danach kommt wird nach den Regeln für die
Typerweiterung behandelt, d.h. eine ausdrückliche Typumwandlung
wird erforderlich. Es ist bestimmt kein Fehler, wenn man
Null-Zeiger-Konstanten als Funktionsargumente immer einer
expliziten Typumwandlung unterzieht. Wenn es zur Gewohnheit wird,
vergißt man es nicht so leicht und ist dann auch bei Funktionen
mit variabler Argument-Anzahl und den immer noch zulässigen
Deklarationen im alten Stil auf der sicheren Seite.

Zusammenfassung:

Die "nackte" Konstante 0 ausdrückliche Typumwandlung
ist zulässig bei: erforderlich bei:
_______________________________________________________________

der Initialisierung eines einem Funktionsaufruf ohne
Zeigers Prototyp im selben
(char *z = 0;) Gültigkeitsbereich

der Zuweisung an einen einem variablen Argument eines
Zeiger Funktionsaufrufes mit variabler
(z = 0;) Argumenten-Anzahl

einem Äquivalenzvergleich
(if (z != 0), if (z == 0))

einem Funktionsaufruf mit
fester Argumenten-Anzahl und
einem Prototyp im selben
Gültigkeitsbereich

Siehe: K&R 1, A.7.7, A.14, K&R 2, A.7.10, A.7.17; H&S 4.6.3;
ANSI 3.2.2.3; ISO 6.2.2.3

1.3: Was ist NULL und wie sieht sein #define aus?

A: Viele Programmierer sind der Meinung, dass es kein besonders guter
Stil ist, unbenannte Konstanten überall im Programm herumfahren zu
lassen: besser man nutzt die Fähigkeit des Präprozessors,
symbolische Konstanten auszutauschen (zu "erweitern"), bevor das
Programm übersetzt und gebunden wird. NULL ist nun ein solches
Makro, dessen #define in <stddef.h> oder <stdio.h> zu finden ist.
Die Beschreibung der Standardbibliothek legt fest, dass dieses Makro
zu einer Null-Zeiger-Konstanten erweitert wird, die von der
Implementierung definiert ist.

Die Sprachbeschreibung definiert den Begriff Null-Zeiger-Konstante
als konstanten integralen Ausdruck mit dem Wert 0, oder einen
entsprechenden Ausdruck, dessen Typ nach (void *) umgewandelt
wurde. Damit ergeben sich die folgenden möglichen #defines:

#define NULL 0
#define NULL 0L
#define NULL (void *) 0

Andere konstante integrale Ausdrücke mit dem Wert 0 sind natürlich
ebenfalls möglich, sinvoll sind sie jedoch nicht.

Nun kann man also - wenn man 0 als Integer und 0 im Zeigerkontext
unterscheiden will - NULL verwenden, wenn ein Null-Zeiger benötigt
wird (vgl. 1.2). Das bleibt aber (wie viele Aspekte des Themas
"Makro") eine reine Stilfrage: die Makroerweiterung erfolgt bereits
in einer frühen Phase der Übersetzung: zu dem Zeitpunkt, an dem ein
maschinenabhängiger Wert für einen Null-Zeiger eingetragen muß, ist
nur noch 0, 0L oder (void *) 0 zu sehen.

Die Definition von NULL als (void *) 0 hat hauptsächlich den
Vorteil, dass sie u.U. der Übersetzungsumgebung Arbeit abnimmt. Der
Programmierer sollte sich hüten, auf die in Frage 1.2 erwähnten
Typumwandlungen zu verzichten - schließlich ist auch

#define NULL 0

strikt konform - und dann fehlt die Typumwandlung im
Funktionsaufruf.

Ebensowenig sollte NULL irgendwo anders als im Zeiger-Kontext
verwendet werden: ist es als (void *) definiert, wird seine
anderweitige Verwendung zum Risiko:

int a = 3;
if (a > NULL)

könnte dann fehlschlagen.

Siehe: K&R 1, 5.4; K&R 2 5.4; H&S 13.1, ANSI 4.1.5, 3.2.2.3;
Rationale 4.1.5; ISO 7.1.6, 6.2.2.3; P&B S. 49.

1.4 Wie sollte das #define für NULL auf einer Maschine aussehen,
auf der Null-Zeiger intern nicht mit einem Bitmuster aus lauter
Nullen dargestellt werden?

A: ANSI beschreibt die Ausführungsumgebung als einen "abstrakten
Automaten", über dessen interne Arbeitsweise nichts ausgesagt
wird, und der sich nach außen so zu verhalten hat, als ob er jede
Anforderung des Standards genau erfüllt - überspitzt formuliert
könnte dies auch eine Sekretärin sein, die den Code liest und auf
die schriftliche Eingabe:

puts ("Hello World!");

mit der Ausgabe der entsprechenden Zeile auf ihrer Schreibmaschine
reagiert.

Deshalb muß sich ein Programmierer über die interne Repräsentation
eines Null-Zeigers auch keine Gedanken machen: die Umgebung nimmt
sich dieser Frage so an, dass es aus der Sicht des Programmes nicht
zu ermitteln ist, welcher Wert wirklich vorliegt.

Ein #define gehört jedoch zum Code des Programmes und muß, sollen
damit strikt konforme Programme erzeugt werden können, selbst
strikt konform sein. Die Umgebung - der solche engen Grenzen
nicht auferlegt sind - muß dann dafür sorgen, dass aus NULL oder 0
im Quelltext im ausführbaren Programm-Image im Zeiger-Kontext ein
Null-Zeiger wird.

Deshalb bleibt es bei den in Frage 1.3 genannten #defines.

Siehe: ISO 4, 5, 5.1.1.2, 5.1.2.3

1.5: Wenn NULL etwa so definiert wäre:

#define NULL ((char *)0)

würde das nicht das Problem mit den fehlenden Typumwandlungen bei
Funktionsaufrufen lösen?

A: Nein. Das war schon in "Vor-ANSI-Zeiten" problematisch, und ist
durch die im Standard alternativ vorgesehene Definition als

#define NULL (void *) 0

ganz überflüssig geworden. Eigentlich bringt aber auch diese
Definition nicht viel, außer vielleicht, dass sie hilft, Fehler
wie:

char a[]="Hallo Welt!";
a[5] = NULL;

(wo eigentlich ASCII-NUL/'\0' gemeint war) zu entdecken. Hat der
eine Compiler eine solche Typumwandlung eingebaut, so muß es beim
nächsten noch lange nicht so sein, denn eine einfache 0 ist ja
weiterhin eine gültige Null-Zeiger-Konstante. Darauf zu vertrauen,
dass NULL als (void *) 0 definiert ist, erzeugt also unportabele
Programme.

Abhilfe würde hier im besten Falle ein eigener Header "meinnull.h"
bieten, der etwa so aussehen könnte

#ifdef NULL
#undef NULL
#endif
#define NULL (void *) 0

bieten.

Siehe: Rationale 4.1.5.

1.6: Ich verwende das Präprozessor-Makro

#define nullzeiger(typ) (typ *)0

um korrekt typisierte Null-Zeiger zu erhalten.

A: Schön, aber es bringt nichts. Die Umgebung muß auf jeden Fall
selbst für die korrekte Umwandlung einer Null-Zeiger-Konstanten in
einen Null-Zeiger sorgen (vgl. Frage 1.2).

Dieses #define vermittelt dem Leser lediglich den Eindruck, dass
der Verfasser nicht so recht weiß, wie das mit den Null-Zeigern
eigentlich funktioniert. Es erfordert deutlich mehr Tippaufwand
und ist zusätzlich potentiell fehlerträchtig: wird beispielsweise
der Typ eines Zeigers nachträglich geändert, muß das auch an jeder
Stelle, an der dieser Zeiger gegen den Null-Zeiger getestet wird,
geschehen (vgl. auch Frage 8.1).

1.7: Ist die Abkürzung "if(z)" als Test auf Nicht-Null-Zeiger zulässig?
Was, wenn die interne Repräsentation für Null-Zeiger nicht 0 ist?

A: Laut Sprachdefinition wird die erste Unteranweisung bei if() genau
dann ausgeführt, wenn der Kontrollausdruck ungleich 0 ist.

Also kann man für

if (Ausdruck)

wobei von Ausdruck nur gefordert wird, dass sein Typ skalar ist
(skalare Typen sind ganze Zahlen, Gleitkommazahlen und Zeiger),
ohne Probleme:

if (Ausdruck != 0)

schreiben.

Genau das macht auch die Umgebung: sie überprüft, ob Ausdruck den
Wert 0 hat oder nicht - egal ob man das ausdrücklich hinschreibt.
"!= 0" ist hier also nur eine Formulierung, die dem menschlichen
Leser klarer machen soll, was gemeint ist, und deshalb von manchen
bevorzugt wird.

Wenn wir nun den einfachen Zeigerausdruck "z" für "Ausdruck"
einsetzen (und das dürfen wir, denn ein Zeiger ist ein skalarer
Typ) stellen wir fest, dass

if (z)

äquivalent zu

if (z != 0)

ist. Damit steht eine 0 in einem Äquivalenzvergleich mit einem
Zeiger und muß von der Umgebung regelgerecht in den passenden
Null-Zeiger umgewandelt werden.

Die interne Repräsentation des Null-Zeigers spielt hier wiederum
absolut keine Rolle.

Noch deutlicher wird das beim logischen Negations-Operator !. Die
Sprachdefinition legt fest, dass !Ausdruck äquivalent zu
(Ausdruck==0) ist.

if (!z)

ist also exakt dasselbe wie

if (z == 0).

Vgl. auch Frage 8.2


Siehe: K&R 2, A.7.4.7; H&S 5.3, ANSI 3.3.3.3, 3.3.9, 3.3.13,
3.3.14, 3.3.15, 3.6.4.1, 3.6.5; ISO 6.1.2.5, 6.3.3.3.

1.8: Wenn NULL und 0 dasselbe sind, was soll ich denn dann nun
verwenden?

A: Das ist hauptsächlich eine stilistische Frage und deshalb schwer
zu beantworten. Viele Programmierer sind der Meinung, man solle
grundsätzlich keine namenlosen Konstanten (auch keine 0 in anderem
Zusammenhang) an allen möglichen Stellen im Programm stehen haben,
andere gehen nicht so weit, finden es aber gut, zu unterscheiden,
wann sie einen Zeiger meinen und wann nicht. Eine weitere Gruppe
ist der Meinung, dass es ein völliger Unsinn war, 0 hinter einem
#define zu verstecken (weil es die Leute nur verwirrt), und
schreiben konsequent 0 "ohne allen Schnickschnack".

Eine einfache Grundregel lautet:

- NULL kann _immer_ durch 0 ersetzt werden.
- 0 kann _nicht_ immer durch NULL ersetzt werden.

Letzteres liegt daran, dass die Sprachbeschreibung auch (void *) 0
als #define für NULL zuläßt. In diesem Fall ist natürlich ein:

int i = NULL; das dann zu:
int i = (void *) 0;

erweitert wird, unzulässig.

Ein weiterer häufiger Fehler ist der Einsatz von NULL, wenn
eigentlich das ASCII-Zeichen NUL gemeint ist. Wenn sich dessen
Verwendung absolut nicht vermeiden läßt, sollte man es selbst
definieren:

#define NUL '\0'

(Dies ist aber in der Regel keine gute Idee, denn für das
Null-Zeichen '\0' ist vom Standard garantiert, das es vorhanden ist
und dass alle seine Bits auf 0 gesetzt sind. Das Zugrundeliegen
eines ASCII-Zeichensatzes ist jedoch definitiv nicht garantiert.
Wer NUL definiert signalisiert also nur, dass er das nicht weiß.)

Siehe: K&R 2, 5.2, ISO 5.2.1.

1.9: Ist es nicht besser, NULL statt 0 zu verwenden, falls sich der
Wert von NULL einmal ändert - beispielsweise auf Maschinen mit
Null-Zeigern ungleich 0?

A: Nein. Man verwendet symbolische Konstanten zwar oft an Stellen, an
denen sich der zugrundeliegende Wert ändern könnte. Das ist aber
bei NULL _nicht_ der Fall.

Um es nochmals zu wiederholen: die Sprachbeschreibung garantiert,
dass die Konstante 0 unter den genannten Bedingungen (Frage 1.2) in
einen Null-Zeiger umgewandelt wird.

Dadurch wird die Verwendung von NULL zur reinen Stilfrage.

1.10: Jetzt bin ich aber etwas durcheinander: bei NULL ist der Wert
garantiert 0, beim Null-Zeiger aber nicht?

A: Das liegt daran, dass viele Leute NULL sofort mit dem Begriff
Null-Zeiger gleichsetzen. Wenn man "Null" aber sprachlich nicht
exakt verwendet, kann damit einiges gemeint sein:

1. Das Konzept des Null-Zeigers. Das abstakte sprachliche Konzept
des Null-Zeigers wurde in Frage 1.1 diskutiert. Es wird durch
die folgenden zwei Konzepte umgesetzt:

2. Die interne Repräsentation des Null-Zeigers zur Laufzeit auf
einer bestimmten Maschine. Dies muß nicht den Wert 0 haben und
kann für verschiedene Zeigertypen unterschiedlich sein; es kann
sogar etwas sein, das mit dem Zeigerkonzept von C gar nichts zu
tun hat. Das sollte aber nur für Leute von belang sein, deren
Aufgabe es ist, einen Compiler zu bauen. "Normalsterbliche"
C-Programmierer sehen diese Werte nie; sie verwenden:

3. Die für Quelltexte verbindliche Syntax - also einfach den
Buchstaben "0" oder die Zeichenkette "(void *) 0". Diese werden
oft durch ein Präprozessor-Makro verborgen:

4. NULL. Für dieses sind folgende #defines möglich:

#define NULL 0
#define NULL 0L
#define NULL (void *) 0

Dann gibt es noch zwei Anwendungen von Null, die vom Thema nur
ablenken und nichts damit zu tun haben:

5. Das ASCII Nullzeichen NUL. Dies ist die ASCII-Entsprechung des
C-Nullzeichens '\0', bei dem garantiert alle Bits auf 0 gesetzt
sind und das deshalb auch den Wert 0 hat. Ähnlichkeiten zum
Null-Zeiger sind nicht beabsichtigt und rein zufällig.

6. Die "Null-Zeichenkette" als Synonym für die leere Zeichenkette
(""). Sie enthält ein einzelnes Nullzeichen, erzeugt aber
keinen Null-Zeiger. Das Fragment

char *zk = "";
if (zk == 0) puts ("Null-Zeiger");
if (zk[0] == 0) puts ("Nullzeichenkette");

sollte den Unterschied verdeutlichen.

Dieser Artikel verwendet den Begriff "Null-Zeiger" in der Bedeutung
1, das Schriftzeichen "0" für 3. und "NULL" in Großbuchstaben in
der in 4. definierten Bedeutung.

1.11: Warum ist die Unsicherheit in Bezug auf Null-Zeiger eigentlich so
groß? Warum werden diese Fragen so oft gestellt?

A: Nun, wer in C programmiert, weiß in der Regel viel über die
Maschinen, auf denen er arbeitet, oder ist dabei es zu lernen
- meist mehr, als eigentlich nötig wäre.

Dazu kommt, dass viele Leute grundsätzlich unterschiedliche
Bereiche in der Sprachdefinition nicht sauber auseinanderhalten:
Zum einen Programm und Implementierung, zum anderen aber
Übersetzungsumgebung und Ausführungsumgebung

Programme können beispielsweise strikt konform sein - das
bedeutet allerdings auch, dass die verwendeten Header strikt
konform sein müssen - denn die sind Teil des Programmes -,
was wiederum eine Beschränkung auf die im Standard spezifizierten
Sprachelemente bedeutet.

Eine Implementierung kann nur konform sein - nicht strikt
konform. Sie darf - ohne dieses Attribut zu verlieren
- Erweiterungen einführen, die allerdings das Verhalten eines
strikt konformen Programmes nicht beeinflussen dürfen.

Die Übersetzungsumgebung erzeugt aus den Quelltexten eines
Programmes ein "program image" das durch die Ausführungsumgebung
ausgeführt werden kann. Dabei könnnen Null-Zeigerkonstanten in
einer der letzten Phasen zu Null-Zeigern werden.

Es wäre es theoretisch denkbar, dass auf einer fiktiven Maschine
die tatsächliche Umwandlung einer Null-Zeigerkonstante in einen
Null-Zeiger erst zur Laufzeit (in der Ausführungsumgebung)
stattfinden kann - wenn etwa die Architektur keine festen Werte
für diesen Zweck kennt.

Die Verwendung eines Präprozessor-Makros erweckt oft den Eindruck,
der dahinter verborgene Wert könne bei Bedarf geändert werden. Das
ist im Fall des Makros NULL nicht so. NULL muß immer eine
Null-Zeigerkonstante sein - die Implementierung darf sich nur
noch eine der im Standard festgelegten Varianten aussuchen. Täte
sie dies nicht und würde stattdessen

#define NULL (void *) 1234567890

definieren, verlöre sie den Status einer konformen Implementierung,
weil sie das Verhalten eines strikt konformen Programmes damit
ändern könnte.

Ein großer Teil der Fragen beruht jedoch einfach darauf, dass die
unterschiedliche Semantik des Begriffes "Null" (wie in 1.10
aufgeführt) übersehen wird.

1.12: Ich begreife es immer noch nicht: wie soll ich eigentlich mit
diesen Null-Zeigern umgehen?

A: Dafür gibt es zwei ganz einfache Regeln:

1. Wenn im Quelltext ein Null-Zeiger benötigt wird, verwendet man
die Null-Zeiger-Konstanten 0 oder NULL.

2. Wenn "0" oder "NULL" Argument eines Funktionsaufrufes ist,
wendet man die von der Funktion erwartete Typumwandlung an.

Der Rest der Diskussion dreht sich um Mißverständnisse, die
interne Repräsentation des Null-Zeigers (die für die Sprache an
sich vollkommen belanglos ist) oder um die ANSI-C Erweiterung
(void *) 0.

Wenn man die Fragen 1.1, 1.2, 1.3 verstanden hat - und über
1.8 und 1.11 nachgedacht - sollte es eigentlich ganz gut gehen.

1.13: Bei all dem Durcheinander, das mit dem Begriff Null-Zeiger
einhergeht - wäre es nicht wirklich einfacher zu verlangen, dass
sie intern durch 0 dargestellt werden?

A: Damit würde man Implementierungen unnötig einschränken. Der
Zugriff auf eine bestimmte Adresse kann auf einer bestimmten
Maschine eine Ausnahmebedingung erzeugen, die durchaus
beabsichtigt sein kann, um ungültige Zugriffe abzufangen.

Die Festlegung auf Nullbits würde in diesem Fall einen eigentlich
beabsichtigten Zweck des Null-Zeigers ausschalten - und das ohne
jeglichen Nutzen für den Programmierer.

Es gibt nichts, was dadurch zu gewinnen wäre.

1.14: Nun mal im Ernst: gibt es überhaupt irgendwelche Maschinen, die
Null-Zeiger ungleich 0 oder unterschiedliche Darstellungen für
Zeiger unterschiedlichen Typs verwenden?

A: Die Prime 50 Serie verwendete für den Null-Zeiger Segment 07777,
Offset 0 - mindestens für PL/I. Spätere Modelle setzten für
C-Null-Zeiger Segment 0, Offset 0 ein. Dies machte allerdings
neue Anweisungen - etwa TCNP (Test C Null Pointer) - notwendig,
natürlich als Zugeständnis an bereits bestehenden, miserabelen
Quellcode, der von falschen Annahmen ausgegangen war.
Ältere Prime-Computer mit Wort-Adressierung waren dafür bekannt,
dass die (char *)-Zeiger größer als die (int *)-Zeiger waren.

Die Eclipse MV-Serie von Data General unterstützt 3 Zeigerformate:
Wort, Byte und Bit. Davon werden 2 von C verwendet: Byte-Zeiger
für char * und void *, Wort-Zeiger für den ganzen Rest.

Manche Honeywell-Bull Mainframes verwenden die Bitfolge 06000 für
die interne Darstellung der Null-Zeiger.

Die CDC CDC-Cyber 180 Serie hat 48-Bit-Zeiger, die aus Ring,
Segment und Offset bestehen. Die meisten Benutzer (in Ring 11)
haben interne Null-Zeiger mit dem Wert 0xB00000000000.

Die Symbolics Lisp Maschine, bei der Speicheradressen eine
Kennung des gespeicherten Wertes besitzen ("tagged architecture"),
besitzt nicht einmal konventionelle numerische Zeiger, dort
wird ein Paar <NIL, 0> (im Prinzip ein nicht existierendes
<Objekt, Offset> Handle) als C Null-Zeiger verwendet.

Je nach verwendetem "Speichermodell" verwenden 80*86 Prozessoren
16-Bit-Daten- und 32-Bit-Funktionszeiger - oder umgekehrt.

Die alte HP 3000 Serie verwendet verschiedene Adressierungsmodi
für Byte- und Wort-Adressen. Deshalb haben char- und void-Zeiger
intern eine andere Darstellung als andere Zeiger auf die selbe
Adresse.

1.15: Was bedeutet die Fehlermeldung: "null pointer assignment" und
wie kann ich die Ursache isolieren?

A: Diese Meldung gibt es nur unter MS-DOS (siehe deshalb Abschnitt
16).

Microsoft- und Borland-Compiler verwenden zur Ermittlung eines
illegalen Zugriffes über einen Null-Zeiger eine heuristische
Methode: beim Programmstart wird der Inhalt der Speicherstelle
Null abgespeichert und als Teil des Exitcodes wird der so
gewonnene Wert nochmals überprüft - hat er sich geändert, ist
mit großer Wahrscheinlichkeit über einen Null-Zeiger darauf
zugegriffen worden.

Als Abhilfe bietet sich an, mit dem Debugger einen Breakpoint auf
die Adresse Null zu setzen, oder ähnlich zu verfahren wie der
Compiler. Letzteres sollte man aber besser unterlassen, denn
Debug-Code, der die Speicherbelegung ändert, erschwert in diesem
Fall nur die Fehlersuche.

Siehe: van der Linden (D), S. 63.

---------------------------------------------------------------------------


Abschnitt 2: Arrays und Zeiger
==============================

2.1: Ich hatte die Definition char a[6] in einer Quelltextdatei und in
einer anderen habe ich extern char *a deklariert. Warum hat das
nicht funktioniert?

A: Die Deklaration extern char *a passt einfach nicht zu der
eigentlichen Definition. Der Typ "Zeiger auf Typ T" ist nicht das
gleiche wie der Typ "Array aus Typ T". In diesem Fall sollte
extern char a[] verwendet werden.

Literatur: CT&P Sec. 3.3 pp. 33-4, Sec. 4.5 pp. 64-5.

2.2: Aber ich habe gehört dass char a[] das gleiche wie char *a ist.

A: Überhaupt nicht. (Diese Aussage hat etwas mit den formalen
Parametern einer Funktion zu tun. Vgl. Frage 2.4.) Arrays sind
keine Zeiger. Die Feldvereinbarung "char a[6]" fordert, dass Platz
für sechs Zeichen bereitgestellt wird, der unter dem Namen "a"
bekannt ist. Das bedeutet, dass es einen Ort mit dem Namen "a"
gibt, an dem sechs Zeichen gespeichert sein können. Die
Zeigervereinbarung "char *p" dagegen fordert Platz für einen
Zeiger an. Der Zeiger trägt den Namen "p" und er kann auf jedes
Zeichen (oder jedes zusammenhängende Array von Zeichen) irgendwo
im Speicher zeigen.

Wie so häufig ist ein Bild tausend Worte wert. Die Anweisungen

char a[] = "hello";
char *p = "world";

würden zu Datenstrukturen führen, die auf folgende Weise
dargestellt werden können:

+---+---+---+---+---+---+
a: | h | e | l | l | o |\0 |
+---+---+---+---+---+---+

+-----+ +---+---+---+---+---+---+
p: | *======> | w | o | r | l | d |\0 |
+-----+ +---+---+---+---+---+---+

Es ist wichtig zu begreifen, dass ein Bezug wie x[3] zu
unterschiedlichem Maschinencode führt, je nach dem, ob x
ein Array oder ein Zeiger ist. Wenn man den obigen
Quelltext heranzieht, wird ein Compiler für den Ausdruck
a[3] Maschinencode ausgeben, der an der Speicherposition "a"
beginnt, von dort drei Schritte weitergeht und das Zeichen an
der so gefundene Speicherposition liest. Wenn der Compiler
auf den Ausdruck p[3] trifft, erzeugt er Maschinencode der an
der Speicherposition "p" beginnt, den Zeiger holt der dort
liegt, zu diesem Zeiger 3 dazuzählt und zum Schluß das
Zeichen holt, auf das dieser Zeiger zeigt. In dem obigen
Beispiel sind zufällig sowohl a[3] als auch p[3] das Zeichen
'l', aber der Compiler kommt auf verschieden Wegen zu diesem
Zeichen. (Siehe auch 17.19 und 17.20)

2.3: Was ist dann mit der "Äquivalenz von Zeigern und Arrays" in C
gemeint?

A: Ein großer Teil der Verwirrung, die Zeiger in C umgibt, kann
auf ein falsches Verständnis dieser Aussage zurückgeführt
werden. Wenn gesagt wird, dass Arrays und Zeiger "äquivalent"
sind, bedeutet das nicht, dass sie identisch oder austauschbar
seien.

"Äquivalenz" bezieht sich auf die folgende wichtige Definition:

Ein Lvalue [vgl. Frage 2.5] vom Typ Array aus T, der in
einem Ausdruck verwendet wird, verfällt (mit drei
Ausnahmen) zu einem Zeiger auf sein erstes Element.
Der Typ des Zeigers, der sich so ergibt, ist Zeiger auf T.

(Die Ausnahmen hiervon sind ein Array, das als Operand des sizeof
oder des & Operators auftritt, oder das eine buchstäbliche
Zeichenkette [Anm: d.h. eine Zeichenkette in Anführungszeichen]
ist, die verwendet wird, um ein Array von Zeichen zu
initialisieren.)

Als Folge dieser Definition gibt es keinen offensichtlichen
Unterschied im Verhalten des "Array Element Zugriffs"-Operators,
wenn er auf Arrays und Zeiger angewendet wird. In einem
Ausdruck der Form a[i] verfällt der Verweis auf das Array a
nach der obigen Regel zu einem Zeiger und der Elementzugriff
erfolgt dann wie bei einer Zeigervariablen in dem Ausdruck p[i]
(obwohl der tatsächliche Speicherzugriff verschieden ist, wie
in Frage 2.2. erklärt wird). In beiden Fällen ist der
Ausdruck x[i], wobei x entweder ein Array oder ein Zeiger ist),
definitionsgemäß identisch mit *((x)+(i)).

Literatur: K&R I Sec. 5.3 pp. 93-6; K&R II Sec. 5.3 p. 99; H&S
Sec. 5.4.1 p. 93; ANSI Sec. 3.2.2.1, Sec. 3.3.2.1, Sec. 3.3.6

2.4: Warum sind dann Array- und Zeigerdeklarartionen als formale
Parameter einer Funktion austauschbar?

Weil Arrays sofort zu Zeigern zerfallen, wird ein Array nie
wirklich an eine Funktion übergeben. Der Bequemlichkeit halber
werden alle Parameterdeklarationen, die wie ein Array "aussehen",
z.B. also

f(a)
char a[];

vom Compiler behandelt als wären sie Zeiger, weil es ja Zeiger
sind, die an die Funktion übergeben werden:

f(a)
char *a;

Diese Umwandlung gilt nur für die formalen Parameter einer
Funktion, nirgendwo sonst. Wer diese Umwandlung als störend
empfindet, sollte sie vermeiden; Viele Menschen sind zu dem
Schluß gekommen, dass die Verwirrung, die die Umwandlung
hervorruft, den kleinen Vorteil zunichte macht , dass die
Deklaration wie der Aufruf "aussieht".

Literatur: K&R I Sec. 5.3 p. 95, Sec. A10.1 p. 205; K&R II
Sec. 5.3 p. 100, Sec. A8.6.3 p. 218, Sec. A10.1 p. 226; H&S
Sec. 5.4.3 p. 96; ANSI Sec. 3.5.4.3, Sec. 3.7.1, CT&P Sec. 3.3
pp. 33-4.

2.5: Wie kann ein Array ein Lvalue sein, wenn man ihm nichts zuweisen
kann?

A: Der ANSI Standard definiert einen "veränderbaren Lvalue", und das
ist ein Array nicht.

Literatur: ANSI Sec. 3.2.2.1 p. 37.

2.6: Warum liefert sizeof nicht die wirkliche Größe eines Arrays das
ein Parameter einer Funktion ist?

A: Der sizeof-Operator liefert die Größe des tatsächlich an die
Funktion übergebenen Parameters, der ein Zeiger ist. (vgl. Frage
2.4)

2.7: Jemand hat mir erklärt, dass Arrays in Wirklichkeit nur konstante
Zeiger sind.

A: Das ist eine zu grobe Vereinfachung. Der Bezeichner eines Arrays
ist "konstant" in dem Sinn, das man ihm nichts zuweisen kann, aber
ein Array ist _kein_ Zeiger, wie aus den Ausführungen und Bildern
in Frage 2.2 klar werden sollte.

2.8: Vom praktischen Standpunkt betrachtet, was ist der Unterschied
zwischen Arrays und Zeigern?

A: Arrays belegen automatisch Speicher, aber sie können nicht an
einen anderen Ort im Speicher verschoben oder in ihrer Größe
verändert werden. Zeigern muß ausdrücklich ein Wert zugewiesen
werden, damit sie auf belegten Speicher zeigen (etwa über malloc),
aber ihnen kann später nach Belieben ein anderer Wert zugewiesen
werden (so dass sie auf andere Objekte zeigen), und sie können
nicht nur auf den Anfang eines Speicherblocks zeigen.

Wegen der sogenannten Äquivalenz von Arrays und Zeigern (vgl.
Frage 2.3) scheinen Arrays und Zeiger oft austauschbar zu sein.
Insbesondere wird ein Zeiger auf einen Speicherblock, der mit
malloc belegt wurde, oft wie ein richtiges Array behandelt (Er
kann auch genauso mit [] angesprochen werden. Vgl. Frage 2.14;
vgl. auch Frage 17.20)

2.9: Ich bin auf "scherzhaften" Quelltext gestoßen, in dem der Ausdruck
5["abcdef"] vorkam. Wie kann so etwas in C erlaubt sein?

A: Man möchte es kaum glauben, aber die Indizierung von Arrays ist in
C kommutativ. Diese merkwürdige Tatsache ergibt sich logisch aus
der Zeigerdefinition der Indizierung von Arrays, nämlich dass a[e]
mit *((a) + (e)) identisch ist, und zwar für jeden Ausdruck a und
e, solange einer der Ausdrücke vom einem Zeigertyp und der andere
von einem Ganzzahltyp ist. Diese unerwartete Kommutativität wird
in Lehrbüchern über C oft so dargestellt, als ob sie etwas sei,
worauf man stolz sein könnte, aber es gibt wohl keine praktische
Anwendung außerhalb des Obfuscated C Contest (vgl. Frage 17.13)

Literatur: ANSI Rationale Absch. 3.3.2.1 S. 41.

2.10: Mein Compiler beschwert sich, wenn ich ein zweidimensionales Array
an eine Funktion übergebe, die einen Zeiger auf einen Zeiger
erwartet.

A: Die Regel, nach der ein Array zu einem Zeiger zerfällt wird nicht
rekursiv angewendet. Ein Array von Arrays (d.h. ein
zweidimensionales Array in C) zerfällt zu einem Zeiger auf ein
Array, nicht zu einem Zeiger auf einen Zeiger. Zeiger auf Arrays
können verwirrend sein, und müssen vorsichtig behandelt werden.
(Die Verwirrung wird durch die Existenz von Compilern gesteigert,
die fälschlicherweise Zuweisungen von mehrdimensionalen Arrays zu
mehrfachen Zeigern akzeptieren. Zu diesen Compilern gehören auch
pcc und von pcc abgeleitete lints) Wenn ein zweidimensionales Feld
an eine Funktion übergeben wird:

int array[NROWS][NCOLUMNS];
f(array);

sollte die Funktion entweder so

f(int a[][NCOLUMNS]) {...}

oder so

f(int (*ap)[NCOLUMNS]) {...} /* ap ist ein Zeiger auf ein
Array der Länge NCOLUMNS */

definiert sein. Für die erste Definition nimmt der Compiler die
übliche automatische Umsetzung von "Array von Arrays" auf "Zeiger
auf Arrays" vor. In der zweiten Form ist die Definition explizit.
Da die aufgerufene Funktion keinen Speicher für das Array
bereitstellen muß, muß sie nicht die gesamte Größe des Arrays
kennen, und deshalb kann die Anzahl der "Zeilen", NROWS,
wegfallen. Die "Gestalt" des Arrays ist aber immer noch wichtig,
und deshalb muß die "Spalten"dimension NCOLUMNS (und, bei einem
drei- oder mehrdimensionalen Array, jede weitere außer der ersten
Dimension) angegeben werden.

Wenn eine Funktion schon so vereinbart ist, dass sie einen Zeiger
auf einen Zeiger erwartet, ist es wahrscheinlich nicht richtig,
ihr ein zweidimensionales Array zu übergeben [Anm. d. Übers.: Mit
"wahrscheinlich" meint Steve sicher nicht, dass er es nicht so
genau weiß, sondern nur, dass es Implementationen geben kann, die
Arrays falsch implementieren, so dass ein Array von Arrays eben
doch zu einem Zeiger auf Zeiger zerfällt.]

Literatur: K&R I Absch. 5.10 S. 110; K&R II Absch. 5.9 S. 113.

2.11: Wie schreibe ich Funktionen, die zweidimensionale Arrays als
Argumente annehmen, wenn die "Breite" zum Zeitpunkt der
Übersetzung unbekannt ist?

A: Das ist nicht einfach. Eine Möglichkeit ist, einen Zeiger auf das
Element [0][0] und die beiden Dimensionen zu übergeben, und den
Zugriff auf die Elemente "von Hand" zu simulieren.

f2(aryp, nrows, ncolumns)
int *aryp;
int nrows, ncolumns;
{ ... ary[i][j] ist hier aryp[i * ncolumns + j] ... }

Diese Funktion kann mit dem Array aus Frage 2.10 als

f2(&array[0][0], NROWS, NCOLUMNS);

aufgerufen werden. Dazu ist zu bemerken, dass ein Programm, das
mehrdimensionale Arrayzugriffe auf diese Weise "von Hand"
realisiert, nicht "strictly conforming" im Sinne des ANSI C
Standards ist; das Verhalten von (&array[0][0])[x] ist fuer
x >= NCOLUMNS nicht definiert.
[Anm. d. Übers.: Steve schreibt hier, m.E. nicht ganz korrekt,
dass das Verhalten von (&array[0][0])[x] undefiniert sei. In
Wirklichkeit ist eher das Verhalten des ganzen Programms
undefiniert, wenn Addressarithmetik über die Grenzen eines Objekts
hinausgeht].

gcc erlaubt die Vereinbarung von lokalen Arrays mit Größenangaben,
die durch Funktionsargumente spezifiziert werden, aber das ist
eine Erweiterung und nicht Standard. [Anm. d. Übers.: Und hat
außerdem nichts mit dieser Frage zu tun.]

Vgl. auch Frage 2.15.

2.12: Wie vereinbare ich einen Zeiger auf ein Array?

A: Normalerweise gar nicht. Wenn jemand lässig von einem Zeiger auf
ein Array spricht, meint er meistens einen Zeiger auf dessen
erstes Element.

Anstatt zu überlegen, wie einen Zeiger auf ein Array vereinbart
wird, sollte die Verwendung eines Zeigers auf eines der Elemente
des Array erwogen werden. Arrays aus Typ T zerfallen zu Zeigern
auf den Typ T (vgl. Frage 2.3), und das ist sehr nützlich;
Zugriffe über den Array-Zugriffsoperator [] oder über
Zeigerarithmetik erlauben einen Zugriff auf die einzelnen Elemente
des Arrays. Echte Zeiger auf Arrays führen zu einem Zugriff auf
das "nächste" Array, wenn auf sie Zeigerarithmetik oder der
Array-Zugriffsoperator angewendet wird, und sie sind, wenn
überhaupt, im allgemeinen nur sinnvoll, wenn mit Arrays von Arrays
gearbeitet wird (Vgl. auch Frage 2.10 weiter oben).

Wenn wirklich ein Zeiger auf ein ganzes Array gebraucht wird, ist
die korrekte Syntax etwas wie "int (*ap)[n];", wobei N die Größe
des Arrays ist (vgl. auch Frage 10.4). Wenn die Größe des Arrays
unbekannt ist, kann N auch weggelassen werden, aber der sich dann
ergebende Typ, "Zeiger auf ein Array unbekannter Größe", ist
nutzlos.

[Anm. d. Übers.: Der Typ ist insofern nicht völlig nutzlos, als
solche Zeiger zueinander zuweisungskopatibel sind. Manche
Programmierrichtlinien sehen ein solches Konstrukt vor, um zu
dokumentieren, dass es sich nicht um einen Zeiger auf eine
Variable, sonder auf mehrere, im Speicher aufeinanderfolgende
Variablen handelt. Solche Programmierrichtlinien stammen nach
meiner Erfahrung von Projektleitern, denen das Zeigerkonzept von C
"unheimlich" ist.]

2.13: Nachdem Bezeichner von Arrays zu Zeigern zerfallen, was ist bei

int array[NROWS][NCOLUMNS];

der Unterschied zwischen array und &array?

A: In ANSI/ISO Standard C liefert &array einen Zeiger vom Typ "Zeiger
auf Array aus T" auf das ganze Array (vgl. auch Frage 2.12). In
prä-ANSI C führte das & in &array im allgemeinen zu einer Warnung
und wurde dann ignoriert. Bei allen C-Compilern liefert ein
einfacher Bezeichner eines Arrays einen Zeiger vom Typ "Zeiger auf
T" auf das erste Element des Arrays. Arrays aus T zerfallen zu
Zeigern auf T (vgl. auch Frage 2.3.)

2.14: Wie kann ich dynamisch ein mehrdimensionales Array allozieren?

A: Es ist meistens die beste Lösung, ein Array von Zeigern zu
allozieren und dann jeden Zeiger mit einer dynamisch allozierten
"Zeile" zu besetzen. Hier ein Beispiel für zwei Dimensionen:

int **array1 = malloc(nrows * sizeof(*array1));
for(i = 0; i < nrows; i++)
array1[i] = malloc(ncolumns * sizeof(*array1[0]));

(In "ernstgemeintem" Quelltext wäre malloc() natürlich richtig
vereinbart und alle Rückgabewerte würden geprüft.)

Der Inhalt des Arrays kann mit ein wenig expliziter Zeigerarithmetik
in einem Speicherblock zusammengehalten werden, was die spätere
Anpassung der Größe von Zeilen aber schwierig macht:

int **array2 = malloc(nrows * sizeof(*array2));
array2[0] = malloc(nrows * ncolumns * sizeof(*array2[0]));
for(i = 1; i < nrows; i++)
array2[i] = array2[0] + i * ncolumns;

In beiden Fällen kann auf die Elemente des dynamisch allozierten
Arrays mit normal aussehenden Ausdrücken zugegriffen werden:
array[i][j].

Wenn die zweifache Verzeigerung, die durch die oben gezeigten
Methoden bedingt wird, aus irgendeinem Grund nicht annehmbar ist,
kann ein zweidimensionales Array auch mit einem einzigen,
dynamisch allozierten eindimensionalen Array simuliert werden.

int *array3 = malloc(nrows * ncolumns * sizeof(*array3));

Allerdings muß dann die Berechnung der Indices von Hand
durchgeführt werden, also z.B. zum Zugriff auf das j-te Element
der i-ten Zeile mit array[i * ncolumns + j]. (Die Berechnung kann
hinter einem Präprozessor-Makro verborgen werden, aber der Aufruf
erfolgt dann mit Klammern und Kommata, was etwas anders aussieht
als der gewöhnliche Zugriff auf ein mehrdimensionales Array.)

Schließlich können auch Zeiger auf Arrays verwendet werden:

int (*array4)[NCOLUMNS] =
(int (*)[NCOLUMNS])malloc(nrows * sizeof(*array4));

aber die Syntax sieht dann sehr abschreckend aus, und alle
Dimensionen bis auf die erste müssen zum Zeitpunkt der
Übersetzung bekannt sein.

Bei allen diesen Methoden ist es natürlich wichtig, die Arrays
auch wieder freizugeben, (was möglicherweise mehrere Schritte
erfordert, vgl. Frage 3.9.) wenn sie nicht mehr benötigt werden,
und es ist nicht sicher, dass solche dynamisch allozierten Arrays
mit konventionellen, statisch allozierten austauschbar sind (vgl.
Frage 2.15, und auch Frage 2.10)

2.15: Wie können sowohl statisch als auch dynamisch allozierte Arrays
an die gleiche Funktion übergeben werden?

A: Es gibt keine perfekte Lösung. Mit den Deklarationen

int array[NROWS][NCOLUMNS];
int **array1;
int **array2;
int *array3;
int (*array4)[NCOLUMNS];

und Besetzungen wie in den Quelltextausschnitten in Frage 2.10
und 2.14, und den folgenden Funktionen:

f1(int a[][NCOLUMNS], int m, int n);
f2(int *aryp, int nrows, int ncolumns);
f3(int **pp, int m, int n);

(vgl. Frage 2.10 und 2.11) sollten die folgenden Aufrufe wie
erwartet funktionieren:

f1(array, NROWS, NCOLUMNS);
f1(array4, nrows, NCOLUMNS);
f2(&array[0][0], NROWS, NCOLUMNS);
f2(*array2, nrows, ncolumns);
f2(array3, nrows, ncolumns);
f2(*array4, nrows, NCOLUMNS);
f3(array1, nrows, ncolumns);
f3(array2, nrows, ncolumns);

Die folgenden beiden Aufrufe werden möglicherweise funktionieren,
aber sie enthalten zweifelhafte Typanpassungen und sind davon
abhängig, dass die dynamische Spaltenzahl ncolumns mit der
statischen Spaltenzahl NCOLUMNS übereinstimmt:

f1((int (*)[NCOLUMNS])(*array2), nrows, ncolumns);
f1((int (*)[NCOLUMNS])array3, nrows, ncolumns);

Es soll noch einmal betont werden, das der Aufruf von f2() mit
dem Argument &array[0][0] nicht strictly conforming ist; vgl.
Frage 2.11.

Zu verstehen warum alle oben angeführten Aufrufe funktionieren,
und warum sie in der Form erfolgen in der sie erfolgen und warum
die nicht aufgeführten Kombinationen nicht funktionieren würden,
bedeutet ein _sehr_ gutes Verständnis von Arrays und Zeigern (und
einigen anderen Gebieten) der Sprache C zu haben.

2.16: Es gibt einen praktischen Trick: wenn man

int realarray[10];
int *array = &realarray[-1];

schreibt, kann array wie ein Array mit der Basis 1 verwendet
werden.

A: Obwohl dieser Trick attraktiv ist (und in älteren Ausgaben der
Numerical Recipes in C verwendet wurde), entspricht er nicht der
Definition von C. Zeigerarithmetik ist nur innerhalb eines
allozierten Blocks und des "abschließenden" Elements direkt hinter
diesem allozierten Block definiert; anderfalls ist das Verhalten
nicht definiert, _selbst dann wenn der Zeiger nie dereferenziert
wird_. Der oben gezeigte Quelltext könnte zu einem Fehler führen,
wenn beim subtrahieren des Offsets eine ungültige Addresse erzeugt
würde (vieleicht weil eine Adresse über den Anfang eines
Speichersegments hinausgehen würde).

Literatur: ANSI Sec. 3.3.6 S. 48, Rationale Sec. 3.2.2.3 S. 38;
K&R II Sec. 5.3 S. 100, Sec. 5.4 S. 102f, Sec. A7.7 S. 205f.

2.17: Ich habe einen Zeiger an eine Funktion übergeben, die ihn
beschreibt:

...
int *ip;
f(ip);
...

void f(ip)
int *ip;
{
static int dummy = 5;
ip = &dummy;
}

Der Zeiger in der aufrufenden Funktion hat sich aber nicht
verändert.

A: Hat die Funktion versucht, den Zeiger selbst zu beschreiben, oder
nur das, worauf er zeigt? In C werden Parameter "by value"
übergeben. Die aufgerufene Funktion hat nur die an sie übergebene
Kopie verändert. Es ist erforderlich, einen Zeiger auf den Zeiger
an die Funktion zu übergeben (d.h. die Funktion nimmt dann einen
Zeiger auf einen Zeiger als Argument) oder den neuen Zeigerwert
als Funktionsergebnis zurückzugeben.

2.18: Ich habe einen Zeiger von Typ char * der auf einige ints zeigt,
und ich möchte diese verarbeiten. Warum funktioniert

((int *)p)++;

nicht?

A: Der "cast" in C bedeutet nicht, dass "diese Bits einen anderen Typ
haben und entsprechend behandelt werden sollen". Es handelt sich
vielmehr um einen Umwandlungsoperator, und dieser liefert
definitionsgemäß einen Wert ("rvalue"), an den nichts zugewiesen
werden kann. Er kann auch nicht mit ++ inkrementiert werden. Zwar
ist dies mit einzelnen Compilern möglich, aber nicht vom Standard
gedeckt. Stattdessen sollte der Quelltext ausdrücken, was gemeint
ist:

p = (char *)((int *)p + 1);

Oder einfach

p += sizeof(int);

Literatur: ANSI Sec. 3.3.4, Rationale Sec. 3.3.2.4 S. 43.

2.19: Kann ein Zeiger vom Typ void ** verwendet werden, um einen Zeiger
von beliebigem Typ per Referenz an eine Funktion zu übergeben?

A: Nicht portabel. Es gibt in C keinen allgemeinen Zeiger-auf-Zeiger
Typ. void * als Zeiger auf einen beliebigen Typ funktioniert nur,
weil bei Zuweisungen an einen oder von einem void * automatisch
Umwandlungen vorgenommen werden. Diese Umwandlungen können nicht
vorgenommen werden (weil der richtige Zeigertyp nicht bekannt
ist), wenn versucht wird, einen void ** zu dereferenzieren, die
nicht auf einen void * zeigt.

---------------------------------------------------------------------------

Abschnitt 3: Speicherbelegung (dynamischer Speicher)
====================================================

3.1: Warum funktioniert dieser Schnippsel nicht?

char *ergebnis;
printf("Gib etwas ein:\n");
gets(ergebnis);
printf("Du hast \"%s\" eingegeben.\n", ergebnis);

A: Die Zeiger-Variable "ergebnis" soll beim Aufruf von gets() auf
einen Speicherbereich zeigen, in den gets() die eingelesenen Daten
schreiben kann. Beim Aufruf von gets() zeigt "ergebnis" aber nicht
auf einen gültigen Speicherbereich. (Da lokale Variablen nicht
initialisiert werden und normalerweise zufällige Werte enthalten,
ist es noch nicht einmal sicher, dass "ergebnis" ein NULL-Zeiger
ist. Siehe hierzu 17.1)

Das obige Programm läßt sich leicht korrigieren, indem man ein
lokales Feld statt eines Zeigers benutzt und es dem Compiler
überläßt, sich um die Speicherbelegung zu kümmern. Etwa:

#include <stdio.h>
#include <string.h>

char ergebnis[100], *p;
printf("Gib etwas ein:\n");
fgets(ergebnis, sizeof(ergebnis), stdin);
if((p = strchr(ergebnis, '\n')) != NULL)
*p = '\0';
printf("Du hast \"%s\" eingegeben.\n", ergebnis);

In diesem Beispiel wurde außerdem fgets() statt gets() benutzt
(siehe hierzu 11.6). Dadurch wird es möglich, die Größe des Feldes
anzugeben, damit das Ende des Feldes nicht überschrieben wird, wenn
der Benutzer eine Zeile eingibt, die zu lang ist.
(Unglücklicherweise - für dieses Beispiel - löscht fgets() im
Gegensatz zu gets() das abschließende '\n' nicht.) Eine weitere
Möglichkeit, das Programm zu korrigieren, besteht darin malloc() zu
verwenden, um den Puffer für die Eingabe zu reservieren.

3.2: strcat funktioniert einfach nicht. Ich benutze

char *s1 = "Hallo, ";
char *s2 = "Welt!";
char *s3 = strcat(s1, s2);

und bekomme sehr seltsame Ergebnisse.

A: Das Problem besteht wieder darin, dass der Speicher für das
verkettete Ergebnis nicht belegt wurde. C kennt keinen automatisch
verwalteten String-Typ. C Compiler belegen nur dann Speicher für
Objekte, wenn diese ausdrücklich im Quelltext enthalten sind (im
Falle von Strings trifft dies auf String-Literale und
character-Felder zu). Der Programmierer muß (ausdrücklich) dafür
sorgen, dass zur Laufzeit für die Ergebnisse von Operationen wie
String-Verkettungen Speicher bereitgestellt wird. Hierzu benutzt er
normalerweise Felder oder malloc(). (Siehe hierzu auch 17.20)

strcat() übernimmt keine Speicherbelegung; der zweite String wird
an den ersten angehängt (der erste String bleibt dabei in dem
Speicherbereich, in dem er vorher war). Eine einfache Lösung des
Problems besteht somit darin, den erste String als Feld zu
definieren, das groß genug ist, um auch den verketteten String zu
enthalten:

char s1[20] = "Hallo, ";

Da strcat() den Wert seines ersten Argumentes zurückgibt (in diesem
Fall s1), ist s3 überflüssig.

Siehe hierzu: CT&P Abschn. 3.2 Seite 32.

3.3: Aber "man strcat" sagt, strcat nimmt zwei Argumente vom Typ
char *. Wie soll ich wissen, dass ich mich um die
Speicherbelegung kümmern muß?

A: Hierzu gilt die Daumenregel: Wenn Zeiger benutzt werden, muß man
sich _immer_ Gedanken über die Speicherbelegung machen. Wenn der
Compiler den Speicher nicht automatisch reserviert, mußt malloc()
benutzt werden. Wenn die Dokumentation einer Bibliotheksfunktion
nicht ausdrücklich von Speicherbelegung spricht, bleibt diese
Aufgabe normalerweise dem Programmierer überlassen.

Der "Synopsis" Abschnitt am Anfang einer (UNIX-artigen)
Manual-Seite kann manchmal irreführend sein. Die Beispiele zeigen
eher, wie die Funktion definiert wurde, nicht, wie sie aufgerufen
wird. Insbesondere erwarten viele Funktionen, die Zeiger-Argumente
benutzen, dass der Zeiger auf ein bereits existierendes Objekt
(struct oder ein Feld) zeigt (siehe hierzu 2.3 und 2.4); typische
Beispiele hierfür sind time() und stat().

3.4: Meine Funktion soll einen String zurückgeben, aber ihr Rückgabewert
enthält nur Datenmüll.

A: Die Funktion gibt einen Zeiger auf einen Speicherbereich zurück; es
muß sichergestellt sein, dass dieser Speicher auch korrekt
reserviert wurde. Der zurückgegebene Zeiger sollte auf einen
statischen Puffer, einen beim Aufruf der Funktion als Argument
übergebenen Puffer oder mittels malloc() reservierten Speicher
zeigen. Er darf _nie_ auf ein lokales (auto) Feld zeigen, da dieses
nicht mehr existiert, wenn die Funktion verlassen wird. In anderen
Worten, Deine Funktion sollte niemals so aussehen:

char *f()
{
char buf[10];
/* ... */
return buf;
}

Eine Möglichkeit (die nicht perfekt ist, sie funktioniert
beispielsweise nicht, wenn f() rekursiv aufgerufen wird) besteht
darin, den Puffer als statisches (static) Feld zu deklarieren:

static char buf[10];

Siehe auch 17.5.

3.5: Warum benutzen manche Programme ausdrückliche Typkonversionen, um
den Rückgabewert von malloc() in eine Zeiger des Typs zu
verwandeln, für den Speicher alloziert wurde?

A: Bevor der ANSI/ISO Standard für C den Zeiger-Typ void * einführte,
brauchte man solche Konversionen, damit der Compiler keine
Warnungen über inkompatible Zeiger-Typen produzierte. (Mit ANSI/ISO
C sind die Konversionen nicht mehr nötig.)

[Hinweis Uz:]
Eine explizite Konversion kann auch dann sinnvoll sein, wenn das
Modul mit moeglichst wenig Änderungen in einem C++ Projekt benutzt
oder später als C++ Code übersetzt werden soll. C++ konvertiert
void* Zeiger _nicht_ automatisch in typisierte Zeiger, d.h. ein
Aufruf von malloc() ohne Cast des Rückgabewertes in einen
typisierten Zeiger erzeugt bei der Übersetzung mit einem C++
Compiler einen Fehler. Wie wichtig dieser Grund ist muß im
Einzelfall entschieden werden, es gibt auch Argumente, die gegen
eine explizite Konvertierung sprechen.
[Ende Hinweis Uz]

3.6: Mein Programm stürzt - scheinbar innerhalb eines Aufrufs von
malloc() ab, aber ich kann den Fehler nicht finden.

A: Es ist leider zu einfach, die internen Daten, die malloc()
verwendet, um den Speicher zu verwalten, zu zerstören. Die Ursache
dafür läßt sich oft nur durch langwieriges und mühsames Suchen
finden. Ein besonders häufig auftretender Fehler besteht darin, dass
man in einen durch malloc() gewonnenen Bereich mehr Daten schreiben
will, als dieser fassen kann (beispielsweise wenn der Platz für
einen String mittels malloc(strlen(s)) statt malloc(strlen(s)+1)
alloziert wurde). Andere übliche Fehler sind, einen Zeiger auf
bereits freigegebenen Speicher zu benutzen, einen Speicherbereich
zweimal freizugeben, einen Zeiger auf einen Bereich, der nicht mit
malloc() belegt wurde, als Argument an free() zu geben oder ein
realloc für einen NULL-Zeiger zu verwenden (siehe 3.13).

Es existiert eine Reihe von Programmpaketen, die helfen, derartige
Probleme mit malloc zu entdecken; populär ist "dbmalloc" von Conor
P. Cahill oder "leak", das in volume 27 des comp.sources.unix
archives; JMalloc.c und JMalloc.h in Fidonet C_ECHO Snippets (oder
archie fragen; siehe frage 17.12); darüberhinaus MEMDEBUG von
ftp.crpht.lu in pub/sources/memdebug. Siehe auch 17.12.

3.7: Wenn dynamisch allozierter Speicher freigegeben wurde, kann man ihn
nicht mehr benutzen. Oder doch?

A: Nein. Einige frühe Dokumentationen für malloc() behaupten zwar, dass
freigegebene Speicherbereiche ihren Inhalt nicht ändern, aber
dieses Verhalten war nie portabel und wird vom ANSI Standard nicht
gefordert.

Nur wenige Programmierer werden freigegebenen Speicher absichtlich
benutzen, aber es ist sehr einfach, dies unbeabsichtigt zu tun. Das
folgende Beispiel zeigt, wie man eine verkettete Liste (richtig)
freigibt:

struct liste *listp, *nextp;
for(listp = anfang; listp != NULL; listp = nextp) {
nextp = listp->next;
free((char *)listp);
}

Auf den ersten Blick scheint es natürlicher, die
Iterationsvorschrift in der for-Schleife in der Form listp =
listp->next zu schreiben. In diesem Fall würde aber auf listp
zugegriffen, obwohl der zugehörige Speicher bereits freigegeben
wurde.

Siehe hierzu: ANSI Rationale Abschnitt 4.10.3.2 Seite 102;
CT&P Abschnitt 7.10 Seite 95.

3.8: Ich reserviere Speicher für structs, die Zeiger auf weitere
dynamisch belegte Objekte enthalten. Müssen die anderen Objekte,
auf die Zeiger meiner Struktur zeigen, freigegeben werden, bevor
ich die Struktur freigeben kann?

A: Ja. Im allgemeinen muß man immer dafür sorgen, dass jeder Zeiger,
der von malloc() zurückgegeben wurde, genau einmal (wenn der
Speicher überhaupt freigegeben wird) an free() als Argument
übergeben wird.

3.9: Muß ich allen dynamisch belegten Speicher am Ende des Programms
wieder freigeben?

A: Ein richtiges Betriebssystem organisiert den Speicher nach dem Ende
eines Programmes neu, dann muß der Speicher nicht freigegeben
werden. Von einigen Personal-Computern ist allerdings bekannt, dass
es ihnen nicht immer gelingt, den kompletten Speicher wieder
verfügbar zu machen, in diesem Fall sollte sich der Programmierer
darum kümmern. Der ANSI/ISO Standard erklärt hierzu, es sei eine
Frage der Implementationsqualität.

Siehe hierzu: ANSI Abschnitt 4.10.3.2

3.10: Ich habe ein Programm geschrieben, das große Speicherbereiche
dynamisch belegt und dann wieder freigibt. Wenn ich mir (mit ps)
anschaue, wieviel Speicher mein Programm benötigt, so ändert der
sich allerdings nicht.

A: Die meisten Implementationen von malloc/free geben freigegebenen
Speicher nicht an das Betriebssystem (sofern es eines gibt) zurück,
sondern benutzen diesen Speicher für spätere Aufrufe von malloc()
im selben Prozeß.

3.11: Woher weiß free(), wie viele Bytes es freigeben soll?

A: Das malloc/free Paket merkt sich die Größe jedes Speicherblocks,
der belegt wurde. Es ist also nicht nötig, die Größe des Blocks an
free() weiterzugeben.

3.12: Gibt es dann einen Weg, vom malloc-Paket zu erfahren, wie groß
ein belegter Block ist?

A: Keinen portablen.

3.13: Darf ich einen NULL-Zeiger als erstes Argument an realloc()
übergeben? Wozu soll das gut sein?

A: ANSI C erlaubt dies (und das dazu verwandte realloc(..., 0), das
den Speicher freigibt), aber einige frühe Implementationen
unterstützen es nicht, es ist also nicht völlig portabel. Einen
NULL-Zeiger beim ersten Aufruf von realloc() zu verwenden, kann
einen Algorithmus, der ohne Initialisierung fortschreitend Speicher
belegt, vereinfachen.

Siehe hierzu: ANSI Abschnitt 4.10.3.4

3.14: Worin besteht der Unterschied zwischen calloc() und malloc()? Füllt
calloc() auch Zeiger- und Gleitkomma-Felder mit Null-Werten?
Brauche ich ein cfree() oder kann free() auch mit calloc()
reservierte Speicherbereiche freigeben?

A: calloc(m, n) ist eigentlich äquivalent zu

p = malloc(m * n);
memset(p, 0, m * n);

calloc() füllt den Speicher mit binären Nullen (alle Bits sind
Null). Gleitkomma- und Zeiger-Nullen (siehe Abschnitt 1 dieser FAQ)
können aber ganz anders repräsentiert werden (und werden es oft
auch), weshalb man sich für solche Felder nicht auf die Nullen
verlassen darf. free() kann (und sollte) benutzt werden, um den mit
calloc() allozierten Speicher wieder freizugeben.

Siehe hierzu: ANSI Abschnitte 4.10.3 bis 4.10.3.2

3.14: Was ist alloca() und warum soll man es nicht benutzen?

A: alloca() belegt Speicher, der automatisch freigegeben wird, wenn
die Funktion, in der alloca() benutzt wurde, beendet wird.
Speicher, der mit alloca() alloziert wurde, ist also nur im Kontext
dieser Funktion korrekt belegt.

alloca() läßt sich nicht portabel implementieren, auf Maschinen
ohne Stack ist dies recht schwierig. Sehr problematisch wird es,
wenn man einen Zeiger auf mittels alloca() belegten Speicher an
eine Funktion direkt weitergibt, etwa fgets(alloca(100), 100,
stdin) (die natürlich erscheinende Implementation auf einer
Maschine mit Stack funktioniert hier nicht).

Aus diesen Gründen kann alloca() nicht in Programmen verwendet
werden, die auf viele Plattformen portiert werden sollen,
unabhängig davon, wie nützlich die Funktion sein mag.

Siehe hierzu: ANSI Rationale Abschnitt 4.10.3 Seite 102.

---------------------------------------------------------------------------

Abschnitt 4: Ausdrücke
======================

4.1: Warum funktioniert dieser Schnippsel

a[i] = i++;

nicht?

A: Der Teilausdruck i++ besitzt einen Seiteneffekt - er ändert den
Wert von i. Wird der Wert von i nun an anderer Stelle in diesem
Ausdruck benutzt, dann führt dies zu undefiniertem Verhalten. (In
K&R wird vorgeschlagen, dass das Verhalten für diesen Ausdruck nicht
festgelegt sein soll. Der ANSI/ISO Standard geht allerdings noch
einen Schritt weiter und erklärt: solche Ausdrücke führen zu
undefiniertem Verhalten - siehe 5.23)

Siehe hierzu: ANSI Anschnitt 3.3 Seite 39.

4.2: Bei meinem Compiler liefert

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

49. Sollte das Ergebnis nicht 56 lauten, egal in welcher
Reihenfolge die Terme ausgewertet werden?

A: Die Postinkrement und -dekrement Operatoren ++ und -- führen ihren
Seiteneffekt erst aus, nachdem sie den vorherigen Wert ausgegeben
haben. Das "nachdem" in dieser Aussage wird dabei aber oft
mißverstanden: Der Seiteneffekt muß sich _nicht_ sofort auswirken,
nachdem der ursprüngliche Wert der Variablen ausgegeben wurde; er
kann sich erst auswirken, nachdem andere Teilausdrücke bearbeitet
wurden. Das einzige, auf das man sich verlassen kann, ist, dass alle
Seiteneffekte berücksichtigt wurden, nachdem der Ausdruck
"vollständig" berechnet wurde (vor dem nächsten "sequence point" in
ANSI C Terminologie). Im obigen Beispiel kann der Compiler zuerst
die beiden ursprünglichen Werte miteinander multiplizieren und
danach alle Seiteneffekte berücksichtigen.

Von jeher war das Verhalten von Programmen, die vielfältige,
zweideutige Seiteneffekte in Ausdrücken benutzen undefiniert
("vielfältige, zweideutige Seiteneffekte" steht hier für eine
Kombination von ++, --, =, +=, usw. innerhalb eines Ausdrucks, bei
dem ein Objekt mehrfach verändert oder verändert und benutzt wird.
Dies ist nur eine sehr grobe Definition. Für die Definition von
"undefiniert" siehe 5.23). Am besten versuchst man erst gar nicht
herauszufinden, wie der Compiler solche Ausdrücke auswertet (im
Gegensatz zu einigen schlecht gestellten Aufgaben in vielen
C-Büchern); wie K&R in seiner Weisheit sagt: ``wenn Du nicht weißt,
_wie_ dies auf verschiedenen Maschinen gemacht wird, kann Dich
diese Unschuld vor Fehlern bewahren.''

Siehe hierzu: K&R I Abschnitt 2.12 Seite 50;
K&R II Abschnitt 2.12 Seite 54;
ANSI Abschnitt 3.3 Seite 39;
CT&P Abschnitt 3.7 Seite 47;
PCS Abschnitt 9.5 Seiten 120-121;
(vergiß H&S Abschnitt 7.12 Seiten 190-191, das ist überholt.)

4.3: Ich habe ein wenig mit

int i = 2;
i = i++;

experimentiert. Bei manchen Compilern hatte i danach den Wert 2,
bei anderen 3 und bei einigen 4. Ich verstehe ja, dass das Verhalten
undefiniert ist, aber wie kann dabei 4 herauskommen?

A: Wenn ein Verhalten undefiniert ist, kann _alles_ passieren [Anm.
Uz: von einem korrekten Verhalten bis zur Auslösung des dritten
Weltkriegs - um Thomas König aus dem Gedächtnis zu zitieren]. Siehe
5.23.

4.4: Kann ich die Reihenfolge, in der Teilausdrücke ausgewertet werden,
nicht durch Klammern festlegen? Selbst ohne Klammern sollte die
Reihenfolge doch durch die Rangfolge der Operatoren vorgeschrieben
sein.

A: Die Hierarchie der Operatoren und Klammern legen nur zum Teil fest,
in welcher Reihenfolge die Teilausdrücke ausgewertet werden. Im
Beispiel

f() + g() * h()

ist nur sicher, dass die Multiplikation vor der Addition ausgeführt
wird. Welche der drei Funktionen zuerst aufgerufen wird, ist
allerdings undefiniert.

Wenn die Reihenfolge, in der Teilausdrücke ausgewertet werden,
wichtig ist, führt an temporären Variablen kein Weg vorbei.

4.5: Und wie steht es mit den &&-, ||- und Komma-Operatoren? Ich sehe
häufiger Ausdrücke der Form

if((c = getchar()) == EOF || c == '\n') ...

A: Für diese Operatoren (sowie für den ?:-Operator) gilt tatsächlich
eine Ausnahme; Jeder einzelne dieser Operatoren stellt einen
"sequence point" dar (d.h. der Ausdruck wird von links nach rechts
ausgewertet). Dies sollte in jedem C-Buch klargestellt werden.

Siehe hierzu: K&R I Abschn. 2.6 S. 38, Abschn. A7.11-12 S. 190-191;
K&R II Abschn. 2.6 S. 41, Abschn. A7.14-15 S. 207-208;
ANSI Abschn. 3.3.13 S. 52, 3.3.14 S. 52, 3.3.15 S. 53, 3.3.17 S.55;
CT&P Abschnitt 3.7 Seiten 46-47

4.6: Wenn ich den Wert eines Ausdrucks nicht brauche, sollte ich i++
oder ++i verwenden, um den Wert von i zu erhöhen?

A: Die beiden Formen unterscheiden sich nur im Wert, den sie
weitergeben. Wenn es nur um den Seiteneffekt geht, sind sie völlig
äquivalent. (Weder i++ noch ++i bedeuten dasselbe wie i+1. Wenn Du
i um 1 erhöhen willst, benutze i=i+1 oder i++ oder ++i, keine
Kombination dieser Möglichkeiten.)

4.7: Warum funktioniert

int a = 1000, b = 1000;
long int c = a * b;

nicht?

A: Nach den C Regeln für die integrale Erweiterung wird die
Multiplikation mit int-Arithmetik ausgeführt. Das Resultat kann
dabei zu groß werden, um von einem int gehalten zu werden, und
deshalb abgeschnitten werden, bevor es an die long-int-Variable auf
der linken Seite übergeben wird. Eine explizite Typkonversion auf
der rechten Seite sorgt dafür, dass long-int-Arithmetik benutzt
wird:

long int c = (long int)a * b;

(long int)(a * b) führt im übrigen nicht zum gewünschten Resultat.

Ein ähnliches Problem stellt sich, wenn zwei integrale Objekte in
einer Division benutzt werden und das Ergebnis einer
Gleitkomma-Variablen zugewiesen wird (ohne ausdrückliche
Typkonversion wird die Ganzzahldivision ausgeführt).

---------------------------------------------------------------------------


Abschnitt 5: ANSI/ISO C
=======================

5.1: Was ist der "ANSI-C Standard?"

A: 1983 rief das American National Standards Institute (ANSI) ein
Komitee namens X3J11 ins Leben mit der Aufgabe, die Sprache C zu
standardisieren. Nach langem, mühsamen Ringen um Definitionen und
mehreren öffentlichen Überarbeitungen wurde die Arbeit dieses
Komitees am 14. Dezember 1989 schliesslich als ANSI Standard
X3.159-1989 verabschiedet und im Frühjahr 1990 veröffentlicht.
Mehrheitlich wurde dabei einfach schon bestehende Gepflogenheiten
standardisiert, mit einigen Anleihen bei C++ (z.B. Prototypen)
sowie einem international angepassten Zeichensatz (eingeschlossen
die umstrittenen Trigraphs). Der Standard definiert auch die
C-Bibliotheken.

Der ursprüngliche ANSI Standard beinhaltete auch eine sogenannte
"Rationale", also eine Erklärung, warum gewisse Entscheide so und
nicht anders getroffen wurden. In dieser "Rationale" werden auch
gewisse Feinheiten der Sprache C angesprochen die z.T. auch hier
behandelt werden. Da diese "Rationale" nicht zum offiziellen ANSI
Standard gehörte ("[it was] included for information only"), ist
sie auch im ISO Standard nicht dabei.

Im Jahr 1990 wurde dieser Standard auch als Internationaler
Standard akzeptiert (ISO/IEC 9899:1990), jedoch sind die Kapitel
anders numeriert: ISO Kapitel 5 bis 7 sind Kapitel 2 bis 4 im ANSI
Standard.

[Anmerkung TW:]
Wie jeder ISO Standard wird auch der C Standard ständig
überarbeitet. Bis heute gibt es zwei sogenannte "Technical
Corrigenda", welche Unklarheiten im Standard korrigieren.
Auch gibt es mittlerweile ein "Normative Addendum 1" (ca. 50
Seiten dick), welches den Standard erweitert (im wesentlichen
um I/O von "wide chars" und "multibyte chars"). Zudem findet
gerade eine Generalüberholung des Standards statt, die wohl
grundlegende Änderungen mit sich bringen wird (Stichwort "C9X").
[Ende Anmerkung TW]

5.2: Wo kann ich eine Kopie des Standards bekommen?

Der ISO Standard wird durch ISO in Genf vertrieben:

ISO Distribution
Case Postale 56
CH-1211 Geneve 20
---------
Suisse

(Da ISO unter anderem vom Verkauf von Standards lebt, gibt es
keine Online-Version des Standards.)

In Herbert Schildts Buch "The Annotated C Standard" ist fast der
ganze Text von ISO/IEC 9899:1990 enthalten (Osborne/McGraw-Hill,
ISBN 0-07-881952-0).

Die "Rationale" ist via FTP von ftp.uu.net verfügbar: Verzeichnis
doc/standards/ansi/X3.159-1989. Sie ist auch in gedruckter Form
erhältlich von Silicon Press, ISBN 0-929306-07-4.

5.3: Hat jemand ein Programm, das C-Programme im alten Stil nach ANSI-C
übersetzt und dabei automatisch Prototypen generiert?

A: Die zwei Programme "protoize" und "unprotoize" konvertieren
zwischen den beiden Arten von Funktionsdefinitionen und
-deklarationen. (Sie können aber keine vollständige Übersetzung
zwischen "klassischem" C und ANSI-C vornehmen.) Diese Programme
sind Teil des gcc-Systems, man schaue im Verzeichnis pub/gnu
auf prep.ai.mit.edu (18.71.0.38) oder jedem anderen FSF-Archiv
nach.

Das Programm "unproto" (/pub/unix/unproto5.shar.Z auf
ftp.win.tue.nl) ist ein Filter, der zwischen Präprozessor und
dem Rest des Compilers aufgerufen wird und dabei die meisten
ANSI-C-Konstrukte in traditionelles C übersetzt.

Im GNU "Ghostscript" gibt es ebenfalls ein kleines Programm
namens ansi2knr.

Zu guter Letzt ist es eigentlich nicht nötig, einen Haufen alten
Quelltext nach ANSI-C zu übersetzen: die traditionelle Syntax für
Funktionen ist auch in ANSI-C noch gültig.

Siehe auch 5.8 und 5.9.

5.4: Ich will einen String generieren, der den Wert einer symbolischen
Konstante enthält; dazu verwende ich den '#'-Operator des ANSI-C
Präprozessors. Mein String enthält jedoch den Namen des Makros
statt seines Wertes.

A: Da beim Operanden des '#'-Operators keine Makro-Ersetzung gemacht
wird, ist ein zweistufiges Vorgehen nötig, wenn der String aus der
Ersetzung des Makros gebildet werden soll:

#define Str(x) #x
#define Xstr(x) Str(x)
#define OP plus

char *opname = Xstr (OP);

Hier wird 'opname' auf "plus" gesetzt, nicht auf "OP".

Ein ähnlicher Zwischenschritt ist auch beim '##'-Operator nötig,
wenn die Ersetzungen von Makros (statt ihrer Namen) zusammengesetzt
werden sollen.

References: ANSI Sec. 3.8.3.2, Sec. 3.8.3.5 example; ISO
Sec. 6.8.3.2, Sec. 6.8.3.5.

5.5: Warum kann ich keine "const"-Werte in Initialisierungen oder als
Array-Dimensionen verwenden? Z.B.

const int n = 5;
int a[n];

A: Der Typ-Qualifier "const" bedeutet eigentlich "read-only"; ein so
qualifiziertes Objekt ist ein Laufzeit-Objekt, das nur gelesen
werden kann. Der Wert eines solchen Objektes ist somit *kein*
konstanter Ausdruck im eigentlichen Sinne. Wenn eine Konstante
gebraucht wird, kann ein #define verwendet werden.

References: ANSI Sec. 3.4; ISO Sec. 6.4; H&S Secs. 7.11.2,7.11.3
pp. 226-7.

5.6: Was ist der Unterschied zwischen "const char *p" und
"char * const p"?

A: "char const *p" (oder eben auch "const char *p") deklariert einen
Zeiger auf einen read-only Character, "char * const p" dagegen
deklariert einen konstanten Zeiger auf einen (variablen) Character
(d.h. der Zeiger kann nicht verändert werden, wohl aber das, worauf
er zeigt).

References: ANSI Sec. 3.5.4.1 examples; ISO Sec. 6.5.4.1;
Rationale Sec. 3.5.4.1; H&S Sec. 4.4.4 p. 81.

5.7: Warum kann ich keinen "char **" an ein Funktion übergeben, die
einen "const char **" erwartet?

A: Zwar kann man stets zeiger-auf-T (für alle Typen T) verwenden wo
zeiger-auf-const-T erwartet wird. Diese Ausnahmeregel gilt aber
nicht rekursiv!

Für Zuweisungen zwischen Zeigern, deren Qualifier sich auf einer
anderen als der ersten Stufe unterscheiden, muss eine explizite
Typkonvertierung (z.B. in diesem Fall "(const char **)") verwendet
werden.

References: ANSI Sec. 3.1.2.6, Sec. 3.3.16.1, Sec. 3.5.3; ISO
Sec. 6.1.2.6, Sec. 6.3.16.1, Sec. 6.5.3; H&S Sec. 7.9.1
pp. 221-222.

5.8: Mein ANSI Compiler reklamiert darüber:

extern int func (float);

int func (x)
float x;
{...

A: Hier wurde die Deklaration im ANSI-Stil mit einer Definition im
alten Stil vermengt. Das ist zwar meistens in Ordnung (siehe 5.3),
nicht aber in diesem Fall.

C nach alten Stil "erweitert" gewisse Argumente bei der Übergabe
von Parameten: float wird zu double, und char und short int werden
zu int. Innerhalb der Funktion wird dann eine Rückumwandlung auf
den "kleineren" Typen vorgenommen. (ANSI C führt diese Erweiterung
ebenfalls durch, wenn kein Prototyp sichtbar ist oder in variabel
langen Argumentlisten, jedoch gibt es keine automatische
Rück-konvertierung.)

Das Problem kann gelöst werden, indem entweder eine Definition nach
ANSI verwendet wird:

int func (float x)
{...

oder der Prototyp der Definition nach altem Stil angepasst wird:

extern int func (double);

(In diesem Fall wäre es aber wohl sauberer, auch die Definition so
zu ändern, dass sie auch double verwendet, sofern nicht die Adresse
des Parameters genommen wird.)

Eine andere Alternative ist natürlich, "kleine" Argumenttypen
(char, short int und float) ganz zu vermeiden.

References: K&R1 Sec. A7.1 p. 186; K&R2 Sec. A7.3.2 p. 202; ANSI
Sec. 3.3.2.2, Sec. 3.5.4.3; ISO Sec. 6.3.2.2, Sec. 6.5.4.3;
Rationale Sec. 3.3.2.2, Sec. 3.5.4.3; H&S Sec. 9.2 pp. 265-7,
Sec. 9.4 pp. 272-3.

5.9: Kann man Funktionsdeklarationen und -definitionen nach altem und
neuem Stil mischen? Ist der alte Stil noch erlaubt?

A: Der alte Stil ist noch erlaubt, und eine Mischung ist möglich.
Dabei sollte man aber äusserst umsichtig sein, da es sonst zu
unerwünschten Effekte kommen kann (siehe 5.3). Zu beachten ist
auch, dass der alte Stil im ISO-Standard als "überholt"
klassifiziert ist und somit in Zukunft vielleicht nicht mehr
unterstützt werden wird.

References: ANSI Sec. 3.7.1, Sec. 3.9.5; ISO Sec. 6.7.1,
Sec. 6.9.5; H&S Sec. 9.2.2 pp. 265-7, Sec. 9.2.5 pp. 269-70.

5.10: Weshalb erhalte ich bei

extern f (struct x {int s;} *p);

eine Warnung "struct x introduced in prototype scope" oder so
ähnlich?

A: Das ist eine Anomalie der Scope-Regeln von C. Eine struct, die
zum ersten Mal in einem Prototypen vorkommt (ohne vorher schon
deklariert zu sein) kann zu keiner anderen struct kompatibel sein,
da ihr Scope am Ende des Prototyps endet.

Das Problem kann behoben werden, indem die unvollständige
Deklaration

struct x;

auf Datei-Ebene vor dem Prototypen gemacht wird. Damit können alle
folgenden Deklarationen, die struct x verwenden, sich auf den
selben Typen beziehen.

References: ANSI Sec. 3.1.2.1, Sec. 3.1.2.6, Sec. 3.5.2.3; ISO
Sec. 6.1.2.1, Sec. 6.1.2.6, Sec. 6.5.2.3.

5.11: Mein Compiler gibt komische Fehlermeldungen in Zeilen, die durch
eine #ifdef-Direktive ausgeschlossen sind!

A: In ANSI-C muss auch Text, der durch eine #ifdef-Direktive
ausgeschaltet ist, aus gültigen Präprozessor-Tokens bestehen.
Das bedeutet unter anderem, dass keine Zeilenumbrüche innerhalb
von Anführungszeichen (Strings) vorkommen dürfen, und dass
Apostrophen und Anführungszeichen immer paarweise auftreten müssen.
Deshalb sollten Kommentare und Pseudo-Code immer durch /* und */
geklammert werden, nicht mittels #ifdef. Siehe aber auch 17.14.

[Anmerkung TW:]
Der folgende Ausschnitt z.B. wird eine Fehlermeldung auslösen:

#if 0
Dies ist Hans' Lösung...
#endif
[Ende Anmerkung TW]

References: ANSI Sec. 2.1.1.2, Sec. 3.1; ISO Sec. 5.1.1.2,
Sec. 6.1; H&S Sec. 3.2 p. 40.

5.12: Kann ich main() als void deklarieren, um diese störenden Warnungen
"main returns no value" zu umgehen? (Ich rufe exit() auf, also
gibt es gar kein Return von main.)

A: Nein. main() muss mit Rückgabewert int deklariert sein, und hat
entweder keine oder aber genau zwei Argumente (und diese müssen
dann die richtigen Typen haben). Falls exit() aufgerufen wird, so
muss vielleicht eine unnütze 'return' - Anweisung eingefügt werden.

Eine Funktion als void zu deklarieren vermeidet nicht nur gewisse
Warnungen, es kann auch in ganz anderem Code für Aufruf oder Return
resultieren, meist nicht kompatibel mit dem, was der Aufrufer (im
Falle von main() ist das der Startup-Code) erwartet.

References: ANSI Sec. 2.1.2.2.1, Sec. F.5.1; ISO Sec. 5.1.2.2.1,
Sec. G.5.1; H&S Sec. 20.1 p. 416; CT&P Sec. 3.10 pp. 50-51.

5.13: Ist 'exit(status)' wirklich das Gleiche wie die Rückgabe eines
Wertes von 'main'?

A: Jein. Der Standard definiert sie als äquivalent. Einige ältere,
nicht dem Standard entsprechende Implementationen können mit der
einen oder anderen Form Probleme haben. Und natürlich sind die
beiden Formen nicht das Selbe, wenn 'main' rekursiv aufgerufen
wird.

References: K&R2 Sec. 7.6 pp. 163-4; ANSI Sec. 2.1.2.2.3; ISO
Sec. 5.1.2.2.3.

5.14: Warum garantiert der Standard nicht mehr als 6 signifikante Zeichen
(Gross- und Kleinschreibung ignoriert!) für externe Bezeichner?

A: Das Problem sind ältere Linker, über die weder der Standard noch
die Compiler-Entwickler irgendeine Kontrolle haben. Die Begrenzung
ist nur auf 6 *signifikante* Zeichen, d.h. der volle Bezeichner
kann sehr wohl länger sein.

Diese Konzession gegenüber restriktiven Linkern musste einfach
gemacht werden, auch wenn viele damit nicht einverstanden waren.
(Die Rationale erwähnt, diese Entscheidung sei "most painful"
gewesen.) Falls Sie nicht einverstanden sind oder glauben, eine
Methode entwickelt zu haben, diese Beschränkung zu umgehen, lesen
Sie Abschnitt 3.1.2 in der X3.159 Rationale (siehe 5.1), wo
verschiedene Möglichkeiten vorgeschlagen und verworfen werden.

References: ANSI Sec. 3.1.2, Sec. 3.9.1; ISO Sec. 6.1.2,
Sec. 6.9.1; Rationale Sec. 3.1.2; H&S Sec. 2.5 pp. 22-3.

5.15: Was ist der Unterschied zwischen 'memmove()' und 'memcpy()'?

A: Der Standard garantiert, dass 'memmove()' auch dann korrekt
funktioniert, wenn sich die beiden Speicherbereiche überlappen.
Für 'memcpy()' gibt es keine solche Garantie, es kann deshalb
etwas effizienter implementiert werden. Im Zweifelsfalle sollte
'memmove()' verwendet werden.

References: K&R2 Sec. B3 p. 250; ANSI Sec. 4.11.2.1,
Sec. 4.11.2.2; ISO Sec. 7.11.2.1, Sec. 7.11.2.2; Rationale
Sec. 4.11.2; H&S Sec. 14.3 pp. 341-2; PCS Sec. 11 pp. 165-166.

5.16: Mein Compiler weigert sich, auch nur die allersimpelsten winzigen
Progrämmchen zu übersetzen.

A: Vielleicht ist es ein alter Compiler, der noch kein ANSI-C
versteht: keine Prototypen von Funktionen und solche Dinge.

Siehe auch 5.17 und 17.2.

5.17: Wieso werden manche Funktionen aus der ANSI/ISO-Standard-Bibliothek
als "undefiniert" angezeigt, obwohl ich einen ANSI-kompatiblen
Compiler habe?

A: Es ist sehr wohl möglich, zwar einen ANSI-kompatiblen Compiler zu
haben, nicht aber eine ANSI-kompatible Bibliothek (und ebensolche
Headerfiles). Das kommt insbesondere mit gcc häufig vor. Siehe
auch 5.16 und 17.2.

5.18: Wieso akzeptiert mein Compiler, der angeblich ANSI-konform ist,
diesen Code nicht? Ich weiss, dass der Code selbst ANSI-konform
ist, denn gcc akzeptiert ihn.

A: Viele Compiler implementieren ein paar nicht standardgemässe
Erweiterungen, gcc mehr als viele andere. Wird im Code wirklich
keine solche Erweiterung benutzt? Generell ist es keine gute
Idee, mit Compilern zu experimentieren, um die Charakteristiken
der Sprache zu ergründen - der Standard erlaubt vielleicht
Unterschiede, oder der Compiler hat Fehler. Siehe auch 4.4.

[Anmerkung TW:]
Übrigens kann auch gcc Fehler beinhalten - gcc ist keine
Referenz-Implementierung!
[Ende Anmerkung TW]

5.19: Warum ist mit 'void *'-Zeigern keine Zeiger-Arithmetik möglich?

A: Weil der Compiler die Grösse des Objektes, auf das gezeigt wird,
nicht kennt. Erst nach einer Umwandlung auf 'char *' bzw. auf den
Zeiger-Typ, der wirklich manipuliert werden soll, ist Arithmetik
mit dem Zeiger möglich. (Siehe jedoch auch 2.18.)

References: ANSI Sec. 3.1.2.5, Sec. 3.3.6; ISO Sec. 6.1.2.5,
Sec. 6.3.6; H&S Sec. 7.6.2 p. 204.

5.20: Ist char a[3] = "abc"; erlaubt? Was bedeutet das?

A: Das ist in ANSI-C erlaubt, allerdings nur selten nützlich. Es
wird ein Array mit 3 Elementen deklariert, das dann mit den drei
Zeichen 'a', 'b' und 'c' initialisiert wird, ohne das sonst
übliche '\0'-Zeichen am Ende! Das Array enthält also keinen String
und kann somit *nicht* mit 'strcpy', 'printf %s' etc. verwendet
werden.

[Anmerkung TW:]
Nebenbei bemerkt sagt der Standard nichts darüber aus, was mit
den Elementen 4 .. 9 in folgender Deklaration zu geschehen hat:

char a[10] = "abc";

Das in 5.1 erwähnte "Technical Corrigendum 2" präzisiert, dass
die Elemente 4 bis 9 ausgenullt werden müssen (egal, ob 'a'
static, extern oder automatic ist).
[Ende Anmerkung TW]

References: ANSI Sec. 3.5.7; ISO Sec. 6.5.7; H&S Sec. 4.6.4 p.
98.

5.21: Was sind #pragmas und wozu sind sie gut?

A: Die #pragma-Direktive stellt eine wohldefinierte Schnittstelle zur
Verfügung, die der Compiler für alle Arten von selbstdefinierten,
implementations-spezifischen Kontrollen und Erweiterungen verwenden
kann, z.B. Optimierungen, "Packen" von structs, Unterdrückung von
Warnungen, etc.

References: ANSI Sec. 3.8.6; ISO Sec. 6.8.6; H&S Sec. 3.7 p. 61.

5.22: Was bedeutet "#pragma once"? Das kommt in einigen Headerfiles vor.

A: Manche Compiler stellen dieses Pragma zur Verfügung, um Headerfiles
idempotent zu machen. "#pragma once" ist mehr oder weniger das
Gleiche wie der #ifndef-Trick in 6.4.

5.23: Anscheinend nehmen einige Leute die Unterschiede zwischen
"undefined" (undefiniertem), "unspecified" (nicht spezifiziertem)
und "implementation-defined" (durch den Compiler definiertem)
Verhalten ziemlich ernst. Was ist der Unterschied?

A: Kurz gesagt: "implementation-defined" bedeutet, dass der Compiler
eine Möglichkeit auswählen muss, und diese auch dokumentiert sein
muss. "Unspecified" heißt, der Compiler sollte eine Möglichkeit
wählen, die aber nicht dokumentiert sein braucht. "Undefined"
schliesslich bedeutet, dass irgendetwas passieren kann. In keinem
dieser Fälle legt der Standard irgendwelche Richtlinien fest, in
den zwei ersten Fällen wird manchmal eine Auswahl möglicher
Verhaltensweisen vorgeschlagen, wovon der Compiler eventuell eine
zu wählen hat.

Wenn ein Programm portabel sein soll, können diese Unterscheidungen
getrost vergessen werden: Code, der von obigen Verhaltensweisen
abhängt, ist nicht portabel.

References: ANSI Sec. 1.6; ISO Sec. 3.10, Sec. 3.16, Sec. 3.17;
Rationale Sec. 1.6.

---------------------------------------------------------------------------

Abschnitt 6: Der C Präprozessor
===============================

6.1: Wie schreibe ich ein Makro, um zwei Werte zu vertauschen?

A: Darauf gibt es keine wirklich gute Antwort. Falls beides
Integer-Werte sind, kann eventuell der bekannte Trick mit mehreren
XORs verwendet werden. Das funktioniert aber nicht für Gleitkomma-
oder Zeiger-Typen. Schlimmer noch, es funktioniert auch nicht,
wenn die beiden Werte die gleiche Variable sind (und die
"offensichtliche" hyper-komprimierte Implementation für integrale
Typen a^=b^=a^=b ist illegal weil sie mehrfache Nebeneffekte hat,
siehe Fragen 4.1 und 4.2). Falls das Makro mit x-beliebigen Typen
funktionieren soll (was ja meistens gewünscht ist), kann es auch
keine Hilfsvariable verwenden, denn der Typ dieser Variable ist
unbekannt und ANSI C kennt keinen 'typeof' operator.

Die beste Lösung ist wohl, den Makro-Ansatz aufzugeben, es sei
denn, man ist willens, den Typ der Parameter als dritten Parameter
mitzugeben.

6.2: Ich habe alten Sourcen, in denen mittels

#define Paste(a, b) a/**/b

Identifier zusammengebastelt werden, aber das funktioniert nicht
mehr.

A: Manche frühe C-Präprozessoren entfernten Kommentare komplett,
deshalb konnte obiges Konstrukt verwendet werden, neue Tokens zu
generieren. In ANSI/ISO C (und auch schon in K&R 1) ist aber
festgelegt, dass Kommentare durch "Whitespace" ersetzt werden.
Da jedoch eine offensichtliche Notwendigkeit existiert, neue
Tokens aus anderen zusammenzusetzen, wurde in ANSI/ISO C der
"Token-Pasting"-Operator ## eingeführt. Dieser kann wie folgt
verwendet werden:

#define Paste(a, b) a ## b

Siehe auch Frage 5.4.

References: ANSI Sec. 3.8.3.3; ISO Sec. 6.8.3.3; Rationale
Sec. 3.8.3.3; H&S Sec. 3.3.9 p. 52.

6.3: Wie wird ein Makro mit mehreren Statements am besten geschrieben?

A: Üblicherweise ist es das Ziel, das Makro so zu schreiben, dass es
wie eine einzelne Anweisung, die aus einem Funktionsaufruf
besteht, verwendet werden kann. Dies bedeutet, das Makro selbst
darf kein schliessendes Semikolon am Ende haben - dieses wird beim
"Aufruf" gesetzt. Somit kann nicht einfach ein Block (d.h.
Anweisungen, die in '{' und '}' eingeklammert sind) verwendet
werden, denn sonst würde der Compiler Syntaxfehler melden wenn das
Makro als if-Zweig einer if-Anweisung mit else-Teil verwendet
wird. Die althergebrachte Lösung ist also

#define MACRO(arg1, arg2) do { \
/* Deklarationen */ \
stmt1; \
stmt2; \
/* ... */ \
} while (0) /* Ohne ';' am Schluss! */

Wenn beim Aufruf dann das Semikolon gesetzt wird, ergibt sich aus
dieser Ersetzung eine vollständige do-while-Anweisung. Falls im
if-Zweig einer if-Anweisung mit else-Teil kein Strichpunkt gesetzt
wird, so ist das Resultat auch korrekt. (Ein optimierender
Compiler wird "toten" Code, der vom "while (0)" herrührt,
entfernen, Programme wie 'lint' werden möglicherweise Warnungen
ausgeben.) Falls alle Anweisungen, die in das Makro sollen,
einfache Ausdrücke sind (ohne Deklarationen oder Schleifen), gibt
es noch eine zweite Möglichkeit: man schreibt eine einzigen,
geklammerten Ausdruck mit dem Komma-Operator. Auf diese Art kann
sogar ein Wert "zurückgegeben" werden.

References: H&S Sec. 3.3.2 p. 45; CT&P Sec. 6.3 pp. 82-3.

6.4: Darf eine Headerdatei weitere Headerdateien einbinden?

A: Das ist natürlich erlaubt. Die eigentliche Frage ist wohl, ob es
guter Programmierstil ist. Wie bei allen Stilfragen, gibt es auch
hier ungefähr so viele Meinungen wie Programmierer. Viele
Leute glauben, verschachtelte #includes sollten besser vermieden
werden: der weitherum anerkannte "Indian Hill Style Guide" (siehe
Frage 14.3) rät davon ab; es kann zu Fehlern auf Grund mehrfacher
Definition von Objekten kommen wenn eine Datei mehrmals
eingebunden wird, und die Wartung von Makefiles von Hand wird
erschwert. Andererseits ist es mit verschachtelten #includes
möglich, Headerfiles modular zu verwenden (d.h. jedes Headerfile
bindet ein was es benötigt, anstatt sich darauf zu verlassen, dass
der Benutzer des Files die benötigten anderen Headerfiles zuerst
einbindet). Definitionen können mit grep (oder entsprechenden
Programmen) einfach gefunden werden, ohne zu wissen, in welcher
Datei sie nun stehen. Headerfiles können mit dem einfachen
Trick

#ifndef HFILENAME_USED
#define HFILENAME_USED
... Inhalt des Headerfiles ...
#endif

"idempotent" gemacht werden, wonach ein mehrfaches Einbinden kein
Problem mehr darstellt (es sollte für jedes Headerfile ein anderer
Makroname verwendet wird, z.B. "H" gefolgt vom Dateinamen gefolgt
von "_USED"). Programme zur automatischen Erzeugung von Makefiles
haben keine Probleme mit verschachtelten Headerfiles. Siehe auch
Abschnitt 14.

[Anmerkung TW:]
Der "Indian Hill Style Guide" ist in dieser Hinsicht wohl
veraltet. Gerade unter dem Gesichtspunkt des "Information Hiding"
ist der zweite Ansatz klar besser geeignet, eine Applikation zu
strukturieren.
[Ende Anmerkung TW]

References: Rationale Sec. 4.1.2.

6.5: Funktioniert der sizeof-Operator in Präprozessor-Direktiven?

A: Nein. Präprozessing findet in einer Phase der Übersetzung statt,
zu der noch keine Typinformation verfügbar ist. Statt 'sizeof'
können die in <limits.h> vordefinierten Konstanten verwendet
werden. Noch besser ist es natürlich, das Programm so zu
schreiben, dass es unabhängig von der Grösse bestimmter Typen ist.

References: ANSI Sec. 2.1.1.2, Sec. 3.8.1 footnote 83; ISO
Sec. 5.1.1.2, Sec. 6.8.1; H&S Sec. 7.11.1 p. 225.

6.6: Wie kann ich in einer #if Direktive herausfinden, ob eine Maschine
big- oder little-endian ist?

A: Das ist nicht möglich, da der Präprozessor alle Arithmetik als
long integer ausführt und keine Adressen kennt. Wird diese
Information wirklich gebraucht? Meistens ist es besser, Code zu
schreiben, der von so etwas unabhängig ist.

References: ANSI Sec. 3.8.1; ISO Sec. 6.8.1; H&S Sec. 7.11.1
p. 225.

6.7: Ich möchte (dies oder das, meist kompliziert) mit dem Präprozessor
umwandeln, kann aber nicht herausfinden, wie's geht.

A: Der C-Präprozessor ist kein Allround-Werkzeug (es ist nicht einmal
garantiert, dass er überhaupt ein separates Programm ist.) Statt
den Präprozessor zu "vergewaltigen" ist es vielleicht einfacher,
ein Programm zu schreiben, das genau das tut, was erwünscht ist.
Mit make kann ein solches Programm problemlos in den
Entwicklungszyklus eingebaut werden.

Wenn ein Präprozessor für etwas anderes als C Quellen gesucht
wird, dann lohnt sich vielleicht ein Blick auf andere Pakete (wie
z.B. m4).

6.8: Ich muß Code warten, der für meinen Geschmack viel zu viele
#ifdefs enthält. Wie kann ich diese Sourcen mit dem Präprozessor
bearbeiten, so dass nur eine Variante übrig bleibt, ohne dabei auch
alle #includes und #defines zu ersetzen?

A: Die Programme "unifdef", "rmifdef" oder "scpp" tun genau das
(siehe 17.2).

6.9: Wie kann ich eine Liste aller vordefinierten Makro-Bezeichner
kriegen?

A: Obwohl dies oft benötigt wird, gibt es dafür keine standardisierte
Lösung. Wenn die Dokumentation zum Compiler hier nicht
weiterhilft, dann gibt es noch die (umständliche) Möglichkeit mit
einem Utility wie "strings" das Compiler-Executable zu
durchsuchen. Achtung: Viele vordefinierte Macros auf älteren
Systemen (z.B. "unix") entsprechen nicht mehr neueren Standards
(weil sie den für Benutzerprogramme definierten Namensraum
verwenden) und sind deshalb in zukünftigen Versionen des Compilers
womöglich nicht mehr verfügbar.

6.10: Wie kann ich ein Makro mit einer beliebigen Anzahl von Argumenten
schreiben?

A: Ein beliebter Trick ist es, ein Makro mit nur einem Argument zu
schreiben. Dieses wird dann in Klammern die variable Argumentliste
beinhalten:

#define DEBUG(args) (printf ("DEBUG: "), printf args)

...
if (n != 0) DEBUG (("n is %d\n", n));

Der offensichtliche Nachteil dabei ist, dass der Benutzer eines
solchen Makros immer daran denken muß, die doppelte Klammerung
anzugeben. Andere Möglichkeiten sind die Verwendung von
unterschiedlichen Macros mit ähnlichen Namen (DEBUG1, DEBUG2,
etc.) die dann eine unterschiedliche Anzahl von Argumenten nehmen,
oder Spielereien mit dem Komma:

#define DEBUG(args) (printf ("DEBUG: "), printf (args))
#define _ ,

...
DEBUG ("i = %d" _ i);

Es ist meistens besser, für solche Zwecke eine Funktion zu
verwenden, denn dort gibt es einen wohldefinierten Mechanismus,
beliebig viele Argument zu übergeben.

---------------------------------------------------------------------------

Abschnitt 7: Variable Argumentlisten
====================================

7.1: Wie kann ich eine Funktion schreiben, die eine variable Anzahl von
Argumenten übergeben bekommt?

A: Unter Benutzung der Macros in <stdarg.h> (oder notfalls unter
Benutzung des älteren Files <varargs.h>).

Hier ist als Beispiel eine Funktion, die eine beliebige Anzahl von
Strings in einem neu belegten Speicherbereich aneinanderhängt:

#include <stdlib.h> /* malloc, NULL, size_t */
#include <stdarg.h> /* va_ Zeugs */
#include <string.h> /* strcat und Konsorten */

char *vstrcat(char *first, ...)
{
size_t len = 0;
char *retbuf;
va_list argp;
char *p;

if(first == NULL)
return NULL;

len = strlen(first);

va_start(argp, first);

while((p = va_arg(argp, char *)) != NULL)
len += strlen(p);

va_end(argp);

retbuf = malloc(len + 1); /* +1 for abschliessende 0 */

if(retbuf == NULL)
return NULL; /* Fehler */

(void)strcpy(retbuf, first);

va_start(argp, first);

while((p = va_arg(argp, char *)) != NULL)
(void)strcat(retbuf, p);

va_end(argp);

return retbuf;
}

Benutzt wird die Funktion z.B. so:

char *str = vstrcat("Hello, ", "world!", (char *)NULL);

Achtung: Der Cast für das letzte Argument ist notwendig (siehe
Frage 1.2). Außerdem muß die aufrufende Funktion den von vstrcat
belegten Speicher wieder freigegen!

Für einen Vor-ANSI Compiler sollte keine Prototypendefinition
erfolgen ("char *vstrcat(first) char *first; {"), anstatt von
<stdlib.h> sollte <stdio.h> eingebunden werden, malloc sollte
"zu Fuß" als "extern char* malloc();" deklariert werden, und statt
size_t ist int zu verwenden. Eventuell müssen auch die (void)
Casts entfernt und varargs.h anstatt von stdargs.h verwendet
werden. Einige Hinweise dazu werden in der Antwort auf die nächste
Frage gegeben.

Bei Funktionen mit variablen Argumentlisten stellt ein Prototyp
keine Informationen über die variablen Argumente bereit, der
Compiler wendet deshalb für diese Argumente die "default
promotions" an (siehe Frage 5.8), aus dem Grund müssen Null-Zeiger
Argumente auch explizit gecastet werden (Frage 1.2).

References: K&R II Sec. 7.3 p. 155, Sec. B7 p. 254; H&S
Sec. 13.4 pp. 286-9; ANSI Secs. 4.8 through 4.8.1.3 .

7.2: Wie kann ich eine Funktion schreiben, die einen Format-String und
eine variable Anzahl von Argumenten nimmt (ähnlich wie printf) und
die diese Argumente an printf weitergibt?

A: Das ist möglich unter Verwendung von vprintf, vfprintf oder
vsprintf.

Hier ist eine "error" Funktion, die eine Fehlermeldung ausgibt,
wobei dem "error" vorangestellt und die Zeile mit einem Newline
abgeschlossen wird:

#include <stdio.h>
#include <stdarg.h>

void
error(char *fmt, ...)
{
va_list argp;
fprintf(stderr, "error: ");
va_start(argp, fmt);
vfprintf(stderr, fmt, argp);
va_end(argp);
fprintf(stderr, "\n");
}

Wenn das ältere <varargs.h> Headerfile verwendet werden muß, dann
muß der Header der Funktion wie folgt geändert werden:

void error(va_alist)
va_dcl
{
char *fmt;

Der Aufruf von va_start sieht dann so aus:

va_start(argp);

Und zwischen den Aufrufen von va_start und vfprintf muß noch die
folgende Zeile eingefügt werden:

fmt = va_arg(argp, char *);

(Achtung: Nach va_dcl darf kein Semikolon stehen!)

References: K&R II Sec. 8.3 p. 174, Sec. B1.2 p. 245; H&S
Sec. 17.12 p. 337; ANSI Secs. 4.9.6.7, 4.9.6.8, 4.9.6.9 .

7.3: Wie kann ich zur Laufzeit feststellen, mit wievielen Argumenten
eine Funktion aufgerufen wurde?

A: Diese Information ist auf portablem Wege nicht erhältlich. Einige
ältere Systeme verfügen über eine (nicht standardkonforme) nargs()
Funktion, aber deren Nützlichkeit war schon immer fraglich, da sie
die Anzahl der übergebenen Worte zurückgegeben hat, und nicht die
Anzahl der Argumente (structs und Gleitkommawerte benötigen bei
der Übergabe als Parameter üblicherweise mehr als ein Wort).

Jede Funktion, der eine variable Anzahl von Argumenten übergeben
werden kann, muß in der Lage sein, die Anzahl der Argumente selber
zu ermitteln. Funktionen wie printf entnehmen diese Information
den Formatangaben (%d, %i usw.), die sich im Formatstring
befinden, der als erstes Argument übergeben wird (das ist auch der
Grund, warum man Laufzeitfehler bekommt, wenn der Formatstring und
die wirklich übergebenen Parameter nicht übereinstimmen). Eine
andere gebräuchliche Technik (die vor allem dann nützlich ist,
wenn alle übergebenen Argumente vom selben Typ sind) ist das
Einfügen eines speziellen Ende-Symbols am Schluß der Liste (oft
wird 0 oder -1 verwendet oder ein passend gecasteter Null-Zeiger).
Beispiele für diese Techniken werden in den Antworten zu Fragen
1.2 und 7.1 gezeigt.

7.4: Ich kann den va_arg Macro nicht dazu bringen, ein Argument vom Typ
"Zeiger auf eine Funktion" korrekt zu verwenden.

A: Der Makro va_arg verwendet üblicherweise diverse Typ-Umwandlungen
um seine Arbeit durchzuführen. Diese Typ-Umwandlungen
funktionieren teilweise nicht korrekt, wenn der übergebene
Datentyp relativ komplex ist (wie ein Zeiger auf eine Funktion).
Abhilfe ist möglich durch Verwendung eines typedefs für den
Funktionszeiger.

References: ANSI Sec. 4.8.1.2 p. 124.

7.5: Wie kann ich eine Funktion schreiben, die eine variable Anzahl von
Argumenten nimmt und diese Argumente an eine andere Funktion
weitergeben (die auch eine variable Anzahl von Argumenten nimmt).

A: Dafür gibt es keine allgemeine Lösung. Die zweite Funktion muß
einen va_list Zeiger als Argument nehmen, wie z.B. vfprintf. Wenn
die Argumente an die zweite Funktion als echte Argumente übergeben
werden (und nicht indirekt über einen va_list Zeiger), dann gibt
es keine portable Lösung. Das Problem ist in Assembler lösbar,
aber das ist selbstverständlich nicht mehr portabel.

7.6: Wie kann ich eine Funktion mit einer zur Laufzeit erzeugten,
variablen Argumentliste aufrufen?

A: Dieses Problem ist nicht portabel lösbar. Wer besonders neugierig
ist, der kann denn Maintainer der (englischen) FAQ
(scs@eskimo.com) fragen, der offensichtlich ein paar Ideen zu
diesem Thema hat... (Siehe auch Frage 16.11).

---------------------------------------------------------------------------

Abschnitt 8: Boolesche Ausdrücke und Variablen
==============================================

8.1: Welcher Typ wird am besten für boolesche Werte verwendet? Warum
gibt es dafür keinen eigenen Typ? Sollte "#define" oder "enum" für
TRUE und FALSE benutzt werden?

A: C stellt keinen Standardtyp zur Verfügung, weil diese Entscheidung
für die eine oder die andere Variante dem Programmierer überlassen
werden soll, da sie den Bedarf an Speicherplatz und/oder das
Laufzeitverhalten beeinflußt (die Wahl 'int' mag schneller sein,
dafür kann char evtl. Speicherplatz sparen).

Die Entscheidung für '#define' oder 'enum' ist willkürlich und
nicht sonderlich interessant (s.a. 9.1). Jede Entscheidung für

#define TRUE 1 #define YES 1
#define FALSE 0 #define NO 0

enum bool {false,true}; enum bool {no,yes};

ist gleich gut, solange sie innerhalb des Programms/Projekts
konsequent durchgehalten wird. (Möglicherweise ist 'enum'
vorzuziehen, wenn der verwendete Debugger die Werte dann
symbolisch anzeigen kann).

Einige Programmierer bevorzugen Varianten wie

#define TRUE (1==1)
#define FALSE (!TRUE)

oder basteln Hilfsmakros wie

#define Istrue(e) ((e) != 0).

Das bringt keinen Vorteil irgendeiner Art. (s.a. 8.2 sowie 1.6).


8.2: Ist '#define TRUE 1' nicht gefährlich, da jeder von 0 verschiedene
Wert in C als 'true' interpretiert wird? Was geschieht, wenn einer
der eingebauten Vergleichsoperatoren etwas anderes als 1 zurück
gibt?

A: Es stimmt zwar, dass jeder von 0 verschiedene Wert in C als 'true'
akzeptiert wird, aber dies bezieht sich nur auf die Eingabe, d.h.
auf die Stellen, an denen boolesche Werte erwartet werden. Wenn
ein boolescher Wert von einem eingebauten Operator erzeugt wird,
ist es definitiv 0 oder 1. Deshalb funktioniert der Test

if (( a==b ) == TRUE)

genau dann, wenn TRUE dem Wert 1 entspricht, hat aber
offensichtlich keinen weiteren Sinn. Allgemein sind ausdrückliche
Tests auf TRUE oder FALSE nicht sinnvoll, da einige
Bibliotheksfunktionen (namentlich isupper, isalpha, etc.) im
Erfolgsfall einen von 0 verschiedenen Wert zurückgeben, der nicht
unbedingt 1 ist. (Außerdem, wenn 'if((a==b)==TRUE)' eine
Verbesserung gegenüber 'if(a==b)' ist, warum sollte man dies
nicht noch durch 'if(((a==b)==TRUE)==TRUE)' weiter verbessern?)

Eine gute Faustregel sagt, dass TRUE, FALSE (oder ähnliches) nur
für Wertzuweisungen an boolesche Variablen oder als Rückgabewerte
für boolesche Funktionen, niemals aber in Vergleichen verwendet
werden sollten.

Die Präprozessormakros TRUE und FALSE sollen die Lesbarkeit des
Quelltexts steigern, nicht aber als Sicherheitsleine für eine
evtl. Änderung der zugrundeliegenden Werte dienen (s.a. 1.7 und
1.9).

References: K&R I Sec. 2.7 p. 41; K&R II Sec. 2.6 p. 42,
Sec. A7.4.7 p. 204, Sec. A7.9 p. 206; ANSI Secs. 3.3.3.3, 3.3.8,
3.3.9, 3.3.13, 3.3.14, 3.3.15, 3.6.4.1, 3.6.5; Achilles and the
Tortoise.

---------------------------------------------------------------------------


Abschnitt 9: Structs, Enums und Unions
======================================

9.1: Was ist der Unterschied zwischen einem Enum (Aufzählung) und
einer Reihe Präprozessor #defines?

A: Momentan ist da wenig Unterschied. Obwohl sich das sicher viele
Leute anders gewünscht hätten, besagt der ANSI Standard, dass Enums
ohne Casts mit integralen Typen gemischt werden dürfen, ohne dass
der Compiler Fehler meldet. (Wenn solches Mischen ohne explizite
Casts illegal wäre, könnte die grundsätzliche Verwendung von Enums
viele Programmierfehler auffangen.)

Einige Vorteile von Enums sind, dass Zahlenwerte automatisch
zugewiesen werden, dass ein Debugger Werte von Enumvariablen
symbolisch darstellen kann und dass sie den Sichtbarkeitsregeln
von C unterliegen. (Ein Compiler kann Warnungen erzeugen, wenn
Enums und Ints gemischt verwendet werden, da so etwas immer noch
als schlechter Stil angesehen werden kann, selbst wenn es nicht
strikt illegal ist.) Ein Nachteil ist, dass der Programmierer nur
wenig Kontrolle über die Größe von Enums hat (oder über diese
Warnungen).

Referenzen: K&R II Sec. 2.3 p. 39, Sec. A4.2 p. 196; H&S
Sec. 5.5 p. 100; ANSI Secs. 3.1.2.5, 3.5.2, 3.5.2.2 .

9.2: Ich habe gehört, Structs könnten Variablen zugewiesen werden und
an oder von Funktionen übergeben werden, aber K&R I spricht
dagegen.

A: Was K&R I sagte, war dass die Beschränkungen der Structoperationen
in einer nachfolgenden Version des Compilers behoben sein würden,
und tatsächlich waren Structzuweisung und -übergabe in Ritchies
Compiler bereits voll funktionstüchtig, als K&R I veröffentlicht
wurde. Obwohl einige wenige C Compiler keine Zuweisung von
Structs konnten, unterstützen es alle modernen Compiler, so dass
keine Probleme bei der Verwendung entstehen sollten.

Referenzen: K&R I Sec. 6.2 p. 121; K&R II Sec. 6.2 p. 129; H&S
Sec. 5.6.2 p. 103; ANSI Secs. 3.1.2.5, 3.2.2.1, 3.3.16 .

9.3: Wie funktioniert die Über- und Rückgabe von Structs?

A: Wenn Structs als Argumente an Funktionen übergeben werden, wird
typischerweise die gesamte Struct auf den Stack kopiert, dabei
werden so viele (Maschinen-)wörter wie nötig verwendet.
(Programmierer entscheiden sich häufig, Pointer auf Strukturen
zu verwenden, um diesen Overhead zu vermeiden.)

Für die Rückgabe von Structs als Funktionsergebnis wird oft ein
nicht sichtbarer Parameter an eine solche Funktion verwendet, der
vom Compiler automatisch übergeben wird. Dieser Parameter ist ein
Zeiger auf einen Speicherbereich, an dem das Funktionsergebnis
abgelegt wird. Einige ältere Compiler haben auch einen statischen
belegten Speicherplatz als Platz für das Rückgabeergebnis
verwendet, dadurch wurden solche Funktionen nicht-reentrant
(wiedereintrittsfähig), was ANSI verbietet.

Referenzen: ANSI Sec. 2.2.3 p. 13.


9.4: Das folgende Programm arbeitet korrekt, bricht jedoch nach dem
beenden mit einem core-dump ab. Warum?

struct list
{
char *item
struct list *next;
}

/* Nun das Hauptprogramm */

main(int argc, char *argv[])
...

A: Ein fehlendes Semikolon macht den Compiler glauben, main gebe
eine Struct zurück. (Die Verbindung ist wegen des dazwischen
liegenden Kommentars schwer zu erkennen.) Da Funktionen mit einer
Struct als Funktionsergebnis gewöhnlich durch Hinzufügen eines
versteckten Arguments implementiert werden, versucht der erzeugte
Code für main drei Argumente zu akzeptieren, obwohl nur zwei
übergeben wurden (in diesem Falle vom Startup-Code). Siehe auch
Frage 17.21.

Referenzen: CT&P Sec. 2.3 pp. 21-2.

9.5: Warum kann man Structs nicht vergleichen?

A: Es gibt keinen vernünftigen Weg für einen Compiler, Vergleiche
von Structs zu implementieren, der konsistent zu C's low-level
Konzept ist. Ein Byte für Byte Vergleich könnte durch zufällige
Bits in den Löchern einer Struktur (wenn Padding verwendet wird,
um die Ausrichtung der späteren Felder korrekt zu halten; siehe
Fragen 9.10, 9.11) verfälscht werden. Ein Feld für Feld Vergleich
würde bei großen Strukturen inakzeptable Mengen an wiederholtem
Inlinecode benötigen.

Zum Vergleich von zwei Structs kommt man nicht umhin, eine
Funktion zu schreiben, die das tut. Unter C++ kann dazu der ==
Operator überladen werden.

References: K&R II Sec. 6.2 p. 129; H&S Sec. 5.6.2 p. 103; ANSI
Rationale Sec. 3.3.9 p. 47.

9.6: Wie kann ich Stucts aus/in Datendateien lesen/schreiben?

A: Es ist relativ naheliegend, einen Struct mittels fwrite zu
schreiben:

fwrite((char*)&somestruct, sizeof(somestruct), 1, fp);

und ein passender Aufruf von fread kann es wieder einlesen.
Wie auch immer, Datendateien, die so geschrieben wurden sind
_nicht_ sehr portabel (Siehe auch Fragen 9.11 und 17.3). Bei vielen
Systemen muß das "b" flag beim fopen verwendet werden.

9.7: Ich stolperte über Code, der einen Struct wie diesen hier
deklarierte:

struct name
{
int namelen;
char name[1];
};

Und dann mittels trickreicher Allokation das Array name so tun
ließ als hätte es mehrere Elemente. Ist das legal und/oder
portabel?

A: Diese Technik ist verbreitet, obwohl Dennis Ritchie es "not
warranted chumminess with the C implementation" (frei:
Ausnutzen nicht garantierter Eigenschaften der C Implementation)
nannte. Eine ANSI Interpretationsregel meinte, es (präziser:
Zugriff über die deklarierte Feldgröße hinaus) sei nicht strikt
konform; obwohl eine gründliche Behandlung der Argumente um die
Legalität der Technik über den Rahmen dieser Liste hinausgeht.
Wie auch immer scheint es auf alle bekannten Implementationen
portabel zu sein. (Compiler, die Arraygrenzen sorgfältig
überprüften könnten Warnungen ausgeben.)

Um auf der sicheren Seite zu sein, ist es vorzuziehen das Element
variabler Größe sehr groß anstelle sehr klein zu deklarieren; für
das obige Beispiel:

...
char name[MAXSIZE]
...

wobei MAXSIZE größer als jedes zu speichernde Name ist. Dem so
angepaßten Trick wird ANSI-Konformität nachgesagt.

References: ANSI Rationale Sec. 3.5.4.2 pp. 54-5.

9.8: Wie kann ich den Byteoffset eines Elements in einer Struct
ermitteln?

A: ANSI C definiert das offsetof() Makro, das, so vorhanden,
verwendet werden sollte; siehe <stddef.h>. Wenn es nicht
existiert, hier ist eine mögliche Implementation:

#define offsetof(type, mem) ((size_t) \
((char *)&((type *) 0)->mem - (char *)((type *) 0)))

Diese Implementation ist nicht 100% portabel; einige Compiler
akzeptieren sie (legitimerweise) nicht.

Siehe die nächste Frage für einen Nutzungshinweis.

References: ANSI Sec. 4.1.5, Rationale Sec. 3.5.4.2 p. 55.

9.9: Wie kann ich auf Structfelder zur Laufzeit per Namen zugreifen?

A: Baue eine Tabelle mit Namen und Offsets, das offsetof() Makro
verwendend. Der Offset von Feld b in Struct a ist

offsetb = offsetof(struct a, b)

Wenn structp ein Pointer auf ein Exemplar dieser Struktur und b
ein Int-Feld mit offset, wie oben angegeben, kann b's Wert
indirekt mittels

*(int *)((char *)structp + offsetb) = value

gesetzt werden.

9.10: Warum ergibt sizeof für einen Strukturtyp, eine größere Größe als
ich erwarte, so als ob da Padding am Ende wäre?

A: Strukturen können dieses Padding haben (wie auch internes
Padding; siehe auch Frage 9.5), so dass Alignmenteigenschaften
erhalten bleiben, wenn ein Array von zusammenhängenden Strukturen
alloziert wird.

9.11: Mein Compiler läßt Löcher in Strukturen, was Platz verschwendet
und binäres I/O nach externen files verhindert. Kann ich das
Padding ausstellen, oder das Alignment von Strukturen anderweitig
kontrollieren?

A: Der Compiler hat möglicherweise eine Erweiterung, die eine
Kontrolle über das Alignment erlaubt (vielleicht ein #pragma),
aber es gibt keine Standardmethode. Siehe auch Frage 17.3

9.12: Kann ich Unions initialisieren?

A: Der ANSI C Standard erlaubt einen Initialisierer für das erste
Element eines Union. Es gibt keinen Standardweg, die anderen
Elemente zu initialisieren (unter einem prä-ANSI Compiler,
gibt es gar keinen Weg überhaupt eines der Elemente zu
initialisieren).

9.13: Wie kann ich konstante Werte an Routinen übergeben, die
Struct-Parameter akzeptieren?

A: C kennt keinen Weg, anonyme Struct-Werte zu erzeugen. Man muß
temporäre Structvariablen verwenden.

---------------------------------------------------------------------------

Abschnitt 10: Deklarationen
===========================

10.1: Wie entscheide ich mich für einen der Integer-Typen?

A: Der Typ 'long' sollte verwendet werden, wenn Werte benötigt werden,
die größer als 32767 oder kleiner als -32767 sind. Wenn Speicher
sehr wichtig ist - große Arrays oder sehr viele Strukturen -, dann
ist die Verwendung von 'short' sinnvoll. Trifft keiner dieser
Gründe zu, dann sollte 'int' verwendet werden. Falls ein
definiertes Verhalten bei einer Bereichsüberschreitung wichtig ist
und/oder keine negative Werte auftreten, so sind die jeweiligen
'unsigned' Typen von Vorteil. Die beiden Varianten solten aber auf
keinen Fall innerhalb eines Ausdrucks gemischt werden.

Obwohl 'char' und 'unsigned char' als 'kleine' Integer-Typen
verwendet werden können, handelt man sich mit dieser Vorgehensweise
meist mehr Nach- als Vorteile ein. Dies liegt an undefiniertem
Verhalten bei Vorzeichenbelegung und an einer Vergrößerung des
Programmcodes bei manchen Compilern (falls diese die vom Standard
vorgeschriebene Erweiterung nach int in allen Fällen durchführen).

Obige Regeln lassen sich natürlich nicht anwenden, wenn man mit
den Adressen von Variablen arbeitet und diese einen bestimmten
Typ erfordern.

Wenn aus irgendeinem Grund irgendetwas mit _genauer_ Größe
deklariert werden muß (die wohl einzig sinnvolle Situation dafür
ist die Kompatibilität mit einem von außen aufgezwungen
Speicherlayout), dann sollte das in passenden 'typedef's versteckt
werden.

10.2: Wie sollte der 64-bit Typ auf neuen, 64-bit-Rechnern aussehen?

A: Einige Hersteller von C-Produkten für 64-bit-Rechner unterstützen
64-bit große 'long int's. Andere fürchten, dass zuviele real
existierende Quelltexte auf sizeof(int) == sizeof(long) == 32 bits
vertrauen, und führen stattdessen einen neuen 64-bit 'long long'
oder '__longlong' Typ ein.

Programmierer, die daran interessiert sind, portierbaren Code
zu schreiben, sollten deshalb ihre 64-bit-Erfordernisse hinter
entsprechenden 'typedef's verstecken.

10.3: Ich habe Probleme mit der Definition von verketteten Listen. Bei

typedef struct
{
char *item;
NODEPTR next;
} *NODEPTR;

gibt der Compiler eine Fehlermeldung aus. Kann eine C-Struktur
keinen Zeiger auf sich enthalten?

A: C-Strukturen können selbstverständlich Zeiger auf sich enthalten,
Kap. 6.5 in K&R beschäftigt sich damit. Im Beispiel liegt das
Problem darin, dass die Definition für NODEPTR noch nicht
vollständig war, als sie für das Feld 'next' benötigt wurde. Eine
Verbesserung besteht darin, der Struktur sofort einen Namen
(tag,Etikett) 'struct node' zu geben, und anschließend bei der
Deklaration des Feldes 'next' 'struct node *next' anzugeben.
Zuletzt wird der 'typedef'-Teil hinter die Strukturdeklaration
geschoben (oder vor sie gezogen).

Eine berichtigte Version wäre

struct node
{
char *item;
struct node *next;
};

typedef struct node *NODEPTR;

Es gibt außerdem mindestens nochmal drei gleichwertige
Möglichkeiten.

Ein ähnliches Problem - mit ähnlicher Lösung - kann bei dem
Versuch auftreten, Typen zu definieren, die auf einander
verweisen.

References: K&R I Sec. 6.5 p. 101; K&R II Sec. 6.5 p. 139; H&S
Sec. 5.6.1 p. 102; ANSI Sec. 3.5.2.3 .

10.4: Wie kann ich ein Array von N Zeigern auf Funktionen deklarieren,
die Zeiger auf Funktionen zurückgeben, die Zeiger auf char
zurückgeben?

A: Es gibt mindestens 3 unterschiedliche Wege:

1. Man schreibt es einfach hin:
char *(*(*a[N])())();

2. Man baut die Deklaration unter Verwendung von 'typedef' in
Etappen auf:

typedef char *pc; /* Zeiger auf char */
typedef pc fpc(); /* Fun., die pc zurückgibt*/
typedef fpc *pfpc; /* Zeiger auf fpc */
typedef pfpc fpfpc(); /* Funk., die pfpc zurückgibt */
typedef fpfpc *pfpfpc; /* Zeiger auf .. */
pfpfpc a[N]; /* Array of ... */

3. Man benutzt das Programm 'cdecl', das Englisch in C umwandelt
und umgekehrt:

cdecl> declare a as array of pointer to function returning
pointer to function returning pointer to char
char *(*(*a[])())()

cdecl kann auch komplizierte Deklarationen erklären, bei casts
helfen und aufzeigen, in welche Klammern die Argumente gehören
(bei komplizierten Funktionsdeklarationen wie der obigen).

cdecl-Versionen sind in Volume 14 von comp.sources.unix
(siehe 17.12) und K&R II.

Jedes gute Buch über C sollte erläutern, wie man komplizierte
C-Deklarationen "von innen nach aussen" liest.

References: K&R II Sec. 5.12 p. 122; H&S Sec. 5.10.1 p. 116.

10.5: Ich programmiere gerade einen Automaten, bei dem jeder Zustand
durch eine eigene Funktion realisiert ist. Ich möchte die
einzelnen Zustandsübergänge dadurch implementieren, dass eine
Funktion einen Zeiger auf die Funktion des Folgezustands
übergibt. Dabei werde ich durch die Deklarationsmethode von C
behindert. Es gibt keine Möglichkeit, eine Funktion zu deklarieren,
die einen Zeiger auf eine Funktion zurückgibt, die einen Zeiger
auf eine Funktion ... .

A: Das geht nur auf einem Umweg. Entweder muß die Funktion einen
generischen Funktionszeiger zurückgeben, der vor Gebrauch
durch Typumwandlung angepasst wird, oder eine Struktur, die
lediglich einen Zeiger auf eine Funktion enthält, die diese
Struktur zurückgibt.

10.6: Mein Compiler beschwert sich über eine ungültige Redeklaration
einer Funktion, dabei definiere ich die Funktion nur einmal und
rufe sie einmal auf.

A: Funktionen, die aufgerufen werden, ohne dass ihre Deklaration
im Sichtbarkeitsbereich ist (oder _bevor_ sie deklariert wurden),
werden vom Compiler so eingesetzt, als ob sie den Typ int
zurückgeben. Dies führt zu Unstimmigkeiten, falls sie später
anders deklariert werden. Funktionen, die andere Typen als int
zurückgeben, _müssen_ _vor_ ihrem Aufruf deklariert werden.
Ansonsten kann man dies als Kompatibilitätsübung für C++
Projekte ansehen, in denen Prototypen für jegliche Art von
Funktionen Pflicht sind.

References: K&R I Sec. 4.2 pp. 70; K&R II Sec. 4.2 p. 72; ANSI
Sec. 3.3.2.2 .

10.7: Wie deklariert und definiert man globale Variablen am besten?

A: Zunächst sei festgestellt, dass es beliebig viele _Deklarationen_
einer 'globalen' (genauer gesagt: 'externen') Variablen geben
kann, aber genau eine _Definition_. (Die Definition ist diejenige
Deklaration, die letztendlich Speicher anfordert - und die
Variable eventuell initialisiert).

Am besten faßt man die Definitionen in einer zentralen C-Datei
zusammen (relativ zum Programm oder Modul) mit externen
Deklarationen in Header-Dateien, die per '#include' eingebunden
werden, wo immer man die Deklarationen braucht. Die o.a. C-Datei,
die die Definition enthält, sollte ebenfalls die Header-Datei
einbinden, damit der Compiler sie mit der entsprechenden
Deklaration vergleichen kann. Diese Vorgehensweise verspricht ein
Höchstmaß an Portierbarkeit, und entspricht den Anforderungen von
ANSI-C.

Nebenbei bemerkt, benutzen Compiler und Linker unter Unix
gewöhnlich ein 'Gemeinschaftsmodell', welches mehrfache
(nichtinitialisierende) Definitionen unterstützt. Hin und wieder
findet man Systeme, die ausdrückliche Initialisierung verlangen,
um eine Definition von einer externen Deklaration zu
unterscheiden.

Man kann mit Präprozessortricks (geschickte '#define's) erreichen,
dass eine Header-Datei mit Deklarationen genau einmal eingelesen
wird, und die Deklarationen zu Definitionen werden.

References: K&R I Sec. 4.5 pp. 76-7; K&R II Sec. 4.4 pp. 80-1;
ANSI Sec. 3.1.2.2 (esp. Rationale), Secs. 3.7, 3.7.2,
Sec. F.5.11; H&S Sec. 4.8 pp. 79-80; CT&P Sec. 4.2 pp. 54-56.

10.8: Was bedeutet 'extern' in einer Funktionsdeklaration?

A: Es kann als Hinweis (für menschliche Leser) dienen, dass die
Definition (wahrscheinlich) in einer anderen Quelldatei steht,
aber der Compiler sieht keinen Unterschied zwischen

extern int f();
und
int f();

References: ANSI Sec. 3.1.2.2 .

10.9: Ich habe herausgefunden, wie Zeiger auf Funktionen zu deklarieren
sind, nun würde ich gerne wissen, wie ich sie initialisieren kann.

A: Z.B.

extern int func();
int (*fp)() = func;

Wenn der Name einer Funktion in einem Ausdruck erscheint ohne als
Aufruf zu wirken (d.h. ohne folgende "(" ), dann wird die Funktion
in einen Zeiger umgewandelt (ihre Adresse wird implizit ermittelt
und eingesetzt), ähnlich wie es bei Arrays gehandhabt wird.

Eine ausdrückliche (externe) Deklaration ist i.a. notwendig, da
eine implizite (externe) Deklaration wegen der fehlenden Argumente
sowie der Information über deren Typ nicht möglich ist.

10.10: Ich habe unterschiedliche Wege gesehen, wie Routinen über
Zeiger auf Funktionen aufgerufen werden. Was steckt dahinter?

A: Ursprünglich mußte ein Zeiger auf eine Funktion vor dem
Aufruf durch Dereferenzieren ( *-Operator sowie ein
zusätzliches Klammerpaar, um der Hierarchie der Operatoren zu
genügen) in eine 'echte' Funktion umgewandelt werden:

int r, func(), (*fp)() = func;
r = (*fp)();

Man kann ebenso argumentieren, dass Funktionen immer über Zeiger
aufgerufen werden, und dass 'echte' Funktionen implizit zu Zeigern
umgewandelt werden, und sich folglich nicht abweichend verhalten.
Diese Überlegung, die in den ANSI-Standard aufgenommen wurde,
bedeutet, dass

r = fp();

regelgerecht ist, und korrekt abgearbeitet wird, unabhängig davon,
ob fp nun eine Funktion ist, oder ein Zeiger auf eine solche.
(Diese Schreibweise war immer eindeutig, man konnte mit einem
Zeiger auf eine Funktion nie etwas anderes machen, als eine
Funktion aufzurufen.) Ein ausdrückliches * ist harmlos, und immer
noch erlaubt (und empfohlen, falls Kompatibilität zu älteren
Compilern wichtig ist).

References: ANSI Sec. 3.3.2.2 p. 41, Rationale p. 41.

10.11: Welchen Sinn hat das Schlüsselwort 'auto'?

A: Keinen. Es ist veraltet.

---------------------------------------------------------------------------

Abschnitt 11: Stdio
===================

11.1: Was ist an diesem Code falsch:

char c;
while((c = getchar()) != EOF)...

A: Die Variable zur Aufnahme des Rückgabewertes von getchar muß
vom Typ int sein. getchar kann alle möglichen char- Werte,
aber auch EOF liefern. Wird der Rückgabewert in einem char
gespeichert, kann ein normales Zeichen als EOF mißverstanden
werden, oder das EOF wird als char interpretiert und deshalb
nicht als solches erkannt (insbesondere, wenn der Typ char
vorzeichenlos ist).

Referenz: CT&P Abschn. 5.1 S. 70.

11.2: Wie kann ich ein '%' Zeichen in einem printf Format String
drucken? Ich habe \% versucht, aber es hat nicht funktioniert.

A: Einfach das Prozent Zeichen verdoppeln: %% .

Referenzen: K&R I Abschn. 7.3 S. 147; K&R II Abschn. 7.2 S. 154
ANSI Abschn. 4.9.6.1 .

11.3: Warum funktioniert der Code scanf("%d", i); nicht?

A: scanf braucht Zeiger auf die Variablen, denen es die Werte
zuweisen soll; es muß also scanf("%d", &i); heissen.

11.4: Warum funktioniert der Code:

double d;
scanf("%f", &d);

nicht?

A: scanf benutzt %lf für Werte vom Typ double und %f für float.
(Man beachte den Unterschied zu printf, das %f sowohl für double
als auch für float benutzt, da Argumente in variablen
Argumentlisten in C den Default Promotions unterworfen werden).

11.5: Warum funktioniert der Code

while(!feof(infp)) {
fgets(buf, MAXLINE, infp);
fputs(buf, outfp);
}

nicht?

A: Die Ein-/Ausgabe von C ist anders als die von PASCAL. EOF wird
nur angezeigt _nachdem_ eine Eingabefunktion versucht hat zu
lesen und dabei das Dateiende erreicht wurde.
Üblicherweise sollte man den Rückgabewert der Eingabefunktion
prüfen (in diesem Fall fgets), dann braucht man feof() meist
nicht zu benutzen.

11.6: Warum sagt jeder, dass man gets() nicht benutzen soll?

A: Bei gets() kann die Größe des Eingabepuffers nicht übergeben
werden. Somit kann ein Überlaufen dieses Puffers nicht verhindert
werden. Siehe Frage 3.1, dort ist ein Code-Fragments gezeigt,
das die Verwendung von fgets() anstelle von gets() illustriert.

11.7: Warum enthält errno den Wert ENOTTY nach dem Aufruf von printf?

A: Viele Implementationen des stdio Pakets passen ihr Verhalten
geringfügig an, wenn stdout ein Terminal ist. Um die
Unterscheidung durchzuführen, führen diese Implementationen eine
Operation aus, die fehlschlägt (mit ENOTTY) wenn stdout kein
Terminal ist. Auch wenn die Ausgabeoperation ansonsten
erfolgreich durchgeführt wurde, enthält errno noch den Wert
ENOTTY.

Referenz: CT&P Abschn. 5.4 p. 73.

11.8: Mein Programm erwartet eine Eingabe, wobei zwischenzeitliche
Ausgaben nicht immer auf dem Bildschirm erscheinen, insbesondere
wenn die Ausgabe durch eine Pipe zu einem anderen Programm
geleitet wird.

A: Es ist das Beste, ein explizites fflush(stdout) zu benutzen, wenn
die Ausgabe definitiv sichtbar sein soll. Verschiedene
Mechanismen versuchen das fflush zur "rechten Zeit" auszuführen,
aber sie werden meist nur aktiv, wenn stdout ein Terminal ist.
(Siehe Frage 11.7.)

11.9: Wenn ich mit scanf von einer Tastatur einlese, dann scheint es
auf die Eingabe einer zusätzlichen Zeile zu warten.

A: scanf wurde für die Eingabe eines freien Formates entwickelt,
das man selten braucht, wenn man von einer Tastatur einliest.
Insbesondere ein "\n" in einem Formatstring bedeutet _nicht_,
dass auf ein Newline gewartet werden soll, sondern daß Zeichen
gelesen und verworfen werden sollen, bis ein whitespace Zeichen
kommt.

Ein damit zusammenhängendes Problem ist, dass unerwartete
nichtnumerische Eingaben scanf dazu veranlassen können zu
"Klemmen". Wegen dieser Probleme ist es üblicherweise besser,
fgets zum Lesen einer ganzen Zeile und anschließend sscanf oder
andere Stringfunktionen zum Zerlegen des Zeilenpuffers zu
verwenden. Wenn man sscanf benutzt, sollte man nicht vergessen,
den Rückgabewert zu überprüfen um sicherzugehen, dass die erwartete
Anzahl von Elementen gefunden wurde.

11.10: Ich versuche eine Datei direkt zu aktualisieren, indem ich sie
mit fopen Mode "r+" öffne, dann einen bestimmten String lese und
abschließend den modifizierten String zurückschreibe. Leider
scheint das nicht zu funktionieren.

A: Man muß vor dem Schreiben unbedingt fseek aufrufen, um erstens an
den Anfang des Strings zurückzukehren, der überschrieben werden
soll und zweitens weil ein Aufruf von fseek oder fflush immer
zwischen Lese- und Schreibvorgang erforderlich ist, wenn man die
Lese/ Schreib "+" Modi benutzt. Man sollte außerdem bedenken, dass
man nur Zeichen mit der gleichen Anzahl von Ersatzzeichen
überschreiben kann (siehe auch Frage 17.4).

Referenz: ANSI Abschn. 4.9.5.3 S. 131.

11.11: Wie kann ich jeweils ein einzelnes Zeichen einlesen, ohne auf die
Return-Taste zu warten?

A: Siehe Frage 16.1.

11.12: Wie kann ich bereitstehende Zeichen löschen, so dass die vom
Benutzer bereits eingegebenen Zeichen nicht bei der nächsten
Eingabeaufforderung eingelesen werden? Kann man dazu
fflush(stdin) verwenden?

A: Die Anweisung "fflush(stdin);" kann dafür nicht verwendet
werden. Die Funktionsweise von fflush ist nur für Ausgaben
festgelegt worden. Deshalb darf der an fflush übergebene Zeiger nur
auf einen Ein-/Ausgabestrom, dessen letzte Aktion keine Eingabe war,
oder einen Ausgabestrom verweisen(7.19.5.2p2 C99)[*].

Die Funktion fflush dient dazu, gepufferte Ausgabedaten eines
Stromes vorzeitig an die Laufzeitumgebung zu übergeben.
Normalerweise geschieht dies automatisch, wenn der Puffer voll ist
und weitere Daten geschrieben werden sollen, oder wenn fclose
aufgerufen wird. Das Verhalten von fflush auf einen
Ein-/Ausgabestrom, dessen letzte Aktion keine Ausgabe war, oder
einen Eingabestrom, ist undefiniert. Auf Eingabedaten ist fflush
somit nicht anwendbar!

Ein Pendant zu fflush im Sinne des Verwerfens ungelesener Eingaben
gibt es nicht. Eine solche Funktion wäre schwer
plattformübergreifend zu realisieren, weil sich ungelesene Zeichen
nicht nur in den Puffern des laufenden Programms, sondern auch in
denen der Laufzeitumgebung befinden können.

Eine Lösung ist das Einlesen sämtlicher Eingaben. Das Interpretieren
der Eingabe erfolgt zu einem späteren Zeitpunkt, und Fehleingaben
könnten dann ignoriert werden. Dazu bieten sich die fgets- und
sscanf-Funktionen an.

[*] An fflush kann auch ein Nullzeiger übergeben werden. Ein solcher
Aufruf entspricht einem Aufruf von fflush für alle aktuell
geöffneten Ein-/Ausgabeströme, deren letzte Aktion eine Ausgabe war,
bzw. alle geöffneten Ausgabeströme (7.19.5.2p3 C99).

11.13: Wie kann ich stdin oder stdout von einem Programm aus in eine
Datei umleiten?

A: Unter Verwendung von freopen.

11.14: Ich habe einmal freopen benutzt. Wie kann ich das originale stdin
oder stdout zurückbekommen?

A: Wenn man hin und herschalten muß, dann ist die Benutzung von
freopen nicht sinnvoll. Besser ist die Verwendung einer eigenen
Variable für das Ein- bzw. Ausgabefile. Dieser Variable kann man
je nach Bedarf einen Zeiger auf den einen oder anderen Stream
zuweisen, dabei bleiben die orginalen Variablen für stdin und
stdout unverändert.

11.15: Wie kann ich den Namen einer Datei ermitteln, der zu einem
bestimmten Dateihandle gehört?

A: Dieses Problem ist grundsätzlich unlösbar. Unter UNIX z.B. wäre
theoretisch ein Absuchen der gesamten Festplatte erforderlich (was
u.U. spezielle Zugriffsrechte erfordert) und dieser Versuch würde
fehlschlagen, wenn das Dateihandle eine Pipe oder eine gelöschte
Datei referenziert. Bei einer Datei mit mehreren Links ist
außerdem die Antwort irreführend. Es ist das Beste, sich die Namen
der Dateien selbst zu merken, wenn man sie öffnet (evtl. mit einer
Wrapper-Funktion um fopen).

---------------------------------------------------------------------------

Abschnitt 12: Bibliotheksfunktionen
===================================

12.1: Warum setzt strncpy nicht immer ein '\0' an das Ende des
Ziel-Strings?

A: strncpy wurde ursprünglich entwickelt, um eine inzwischen
veraltete Datenstruktur, nämlich einen String mit fester Länge,
der nicht unbedingt mit '\0' abgeschlossen sein muß, zu
verarbeiten. strncpy ist zugestandenermaßen ein bißchen
schwierig in anderen Zusammenhängen zu benutzen, weil man oft
ein '\0' per Hand an den Zielstring anhängen muß.

12.2: Ich versuche, ein Feld von Strings unter Verwendung von strcmp
als Vergleichsfunktion mit qsort zu sortieren, aber es
funktioniert nicht.

A: Mit einem "Feld von Strings" ist vermutlich ein "Feld von
Zeigern auf char" gemeint. Die Argumente für die
Vergleichsfunktion von qsort sind Zeiger auf die Objekte,
die sortiert werden sollen, in diesem Fall Zeiger auf Zeiger,
die auf char zeigen. (strcmp akzeptiert allerdings nur einfache
Zeiger auf char.)

Die Argumente der Vergleichsfunktion werden als "generische
Zeiger" bezeichnet, const void * oder char *. Sie müssen
wieder in das konvertiert werden, was sie "wirklich sind"
(char **) und dereferenziert, damit sie char * liefern, die
sinnvoll verglichen werden können. Man könnte eine
Vergleichsfunktion folgendermaßen schreiben:

int pstrcmp(p1, p2) /* vergleicht Strings mittels Zeigern */
char *p1, *p2; /* const void * für ANSI C */
{
return strcmp(*(char **)p1, *(char **)p2);
}

Man beachte bei der Diskussion in K&R II Abschn. 5.11 S. 119-20,
dass dort nicht die Standard Bibliothek qsort diskutiert wird.

12.3: Jetzt versuche ich ein Feld von Strukturen mit qsort zu sortieren.
Meine Vergleichsfunktion nimmt Zeiger auf Strukturen, aber der
Compiler beschwert sich, dass die Funktion vom falschen Typ für
qsort ist. Wie kann ich den Funktionszeiger umwandeln, um diese
Warnung abzuschalten?

A: Die Umwandlung muß in der Vergleichsfunktion stattfinden, welche
so deklariert sein muß, dass sie "generische Zeiger" (const void *
oder char *) akzeptiert, wie oben bei Frage 12.2 diskutiert. Der
Code könnte folgendermaßen aussehen:

int mystructcmp(p1, p2)
char *p1, *p2; /* const void * für ANSI C */
{
struct mystruct *sp1 = (struct mystruct *)p1;
struct mystruct *sp2 = (struct mystruct *)p2;
/* jetzt vergleiche sp1->whatever und sp2-> ... */
}

(Wenn man andererseits Zeiger auf Strukturen sortiert, dann
benötigt man einen Umweg, wie bei Frage 12.2:
sp1 = *(struct mystruct **)p1 .)

12.4: Wie kann ich Zahlen in Strings umwandeln (das Gegenteil zu atoi)?
Gibt es eine itoa Funktion?

A: Man kann dafür einfach sprintf benutzen. (Man muß irgendwo den
Platz für das Ergebnis reservieren, siehe Fragen 3.1 und 3.2.
Keine Sorge, dass sprintf Overkill wäre und möglicherweise
Laufzeit oder Speicherplatz verschwendet. Es funktioniert in der
Praxis recht gut.)

Referenzen: K&R I Abschn. 3.6 S. 60; K&R II Abschn. 3.6 S. 64.

12.5: Wie kann ich das aktuelle Datum oder die Tageszeit in einem C
Programm ermitteln?

A: Man benutze einfach die Funktionen time, ctime, und/oder
localtime. (Diese Funktionen gibt es bereits seit Jahren und
sie gehören zum ANSI Standard.)
Hier ist ein einfaches Beispiel:

#include <stdio.h>
#include <time.h>

main()
{
time_t now = time((time_t *)NULL);
printf("It's %.24s.\n", ctime(&now));
return 0;
}

Referenzen: ANSI Abschn. 4.12 .

12.6: Ich weiß, dass die Bibliotheksfunktion localtime ein time_t in
eine Struktur tm aufteilt, und dass ctime ein time_t in einen
druckbaren String umwandelt. Wie kann ich die entgegengesetzte
Operation, d.h. die Umwandlung einer Struktur tm oder eines
Strings in ein time_t, realisieren?

A: ANSI C spezifiziert eine Bibliotheksfunktion, mktime, die eine
Struktur tm in ein time_t umwandelt. Verschiedene Public Domain
Versionen dieser Funktion sind verfügbar, falls ein Compiler das
noch nicht unterstützen sollte.

Die Umwandlung eines Strings in ein time_t ist schwieriger, wegen
der großen Vielfältigkeit von Datums- und Zeitformaten, die dabei
berücksichtigt werden sollte. Manche Systeme stellen eine
Funktion strptime zur Verfügung; eine andere beliebte Funktion
ist partime (weit verbreitet mit dem RCS Paket), aber diese
werden wahrscheinlich nicht standardisiert werden.

Referenzen: K&R II Abschn. B10 S. 256; H&S Abschn. 20.4 S. 361;
ANSI Abschn. 4.12.2.3 .

12.7: Wie kann ich n Tage zu einem Datum addieren? Wie kann ich die
Differenz zwischen zwei Daten bestimmen?

A: Die ANSI/ISO Standard C Funktionen mktime und difftime liefern
eine gewisse Hilfe für beide Probleme. mktime() akzeptiert
nicht-normalisierte Daten, so dass es einfach ist, eine gefüllte
Struktur tm zu nehmen, etwas zu dem Element tm_mday zu addieren
bzw. davon zu subtrahieren und anschließend mktime() aufzurufen
um Jahr, Monat und Tag wieder zu normalisieren (und in einen
time_t Wert umzuwandeln). difftime() berechnet die Differenz in
Sekunden zwischen zwei time_t Werten; mktime() kann benutzt
werden, um die time_t Werte für zwei zu subtrahierende Daten zu
ermitteln. (Man beachte jedoch, dass diese Lösungen nur für Daten
funktionieren, die in dem Bereich liegen, der mit time_t Werten
dargestellt werden kann, und dass nicht alle Tage 86400 Sekunden
lang sind.) Siehe auch Frage 12.6 und 17.28.

Referenzen: K&R II Abschn. B10 S. 256; H&S Abschn. 20.4, 20.5
S. 361-362; ANSI Abschn. 4.12.2.2, 4.12.2.3 .

12.8: Ich brauche einen Zufallszahlengenerator.

A: Die Standard C Bibliothek hat einen: rand(). Die Implementation
ist nicht auf jedem System perfekt, aber einen besseren zu
schreiben ist nicht unbedingt einfach.

Referenzen: ANSI Abschn. 4.10.2.1 S. 154; Knuth Band 2 Kap. 3
S. 1-177.

12.9: Wie kann ich zufällige ganze Zahlen in einem bestimmten Bereich
erzeugen?

A: Der übliche Weg,

rand() % N

(wobei N natürlich der Bereich ist) ist dürftig, weil die unteren
Bits von vielen Zufallszahlengeneratoren genau genommen nicht
zufällig sind. (Siehe Frage 12.11.) Eine bessere Methode ist
folgende

(int)((double)rand() / ((double)RAND_MAX + 1) * N)

Wenn man Bedenken wegen der Benutzung von Gleitkommazahlen hat,
dann kann man folgendes versuchen

rand() / (RAND_MAX / N + 1)

Beide Methoden erfordern unverkennbar die Kenntnis von RAND_MAX
(was ANSI in <stdlib.h> definiert) und setzen voraus, dass N viel
kleiner ist als RAND_MAX.

12.10: Jedes mal, wenn ich mein Programm starte, bekomme ich die gleiche
Zahlenfolge von rand() geliefert.

A: Man kann srand() aufrufen, um den Pseudo-Zufallszahlengenerator
mit einem zufälligeren Startwert zu initialisieren. Beliebte
Startwerte sind die Tageszeit, oder die Zeit, die vergangen ist,
bevor der Benutzer eine Taste betätigt hat (allerdings ist der
Zeitpunkt einer Tastenbetätigung kaum portabel zu bestimmen;
siehe Frage 16.10).

Referenzen: ANSI Abschn. 4.10.2.2 S. 154.

12.11: Ich brauche einen zufälligen wahr/falsch Wert. Deshalb habe ich
rand() % 2 benutzt, erhalte aber nur abwechselnd 0, 1, 0, 1, 0...

A: Einfache Pseudo-Zufallszahlengeneratoren (wie die, die
unglücklicherweise mit manchen Systemen geliefert werden) sind in
den unteren Bits nicht sehr zufällig. Man sollte versuchen, die
oberen Bits zu benutzen. Siehe Frage 12.9.

12.12: Ich versuche ein altes Programm zu portieren. Warum bekomme
ich "undefined external" Fehlermeldungen für index(), rindex(),
bcopy(), bcmp() und bzero()?

A: Diese Funktionen sind unterschiedlich veraltet. Stattdessen
sollten folgende Funktionen benutzt werden:

index() : strchr()
rindex() : strrchr()
bcmp() : memcmp()
bzero() : memset() mit zweitem Argument 0

Statt bcopy() wird memmove() nach dem Vertauschen des ersten und
zweiten Arguments benutzt (siehe auch Frage 5.15).

12.13: Ich bekomme ständig Fehlermeldungen wegen undefinierter
Bibliotheksfunktionen, obwohl ich alle Headerfiles richtig
eingebunden habe.

A: In manchen Fällen (insbesondere, wenn es sich um nicht
standardisierte Funktionen handelt) muß man explizit die
richtigen Bibliotheken angeben, die beim Linken des Programms
durchsucht werden sollen. Siehe auch Frage 15.2.

12.14: Ich bekomme immer Errors wegen undefinierter
Bibliotheks-Funktionen, obwohl ich -l zur Anforderung der
Bibliotheken während des Linkens benutze.

A: Viele Linker führen nur einen Lauf über die Liste der angegebenen
Objektdateien und Bibliotheken aus und extrahieren aus den
Bibliotheken nur die Module, die den Referenzen entsprechen, die
bis dahin undefiniert waren. Deshalb ist die Reihenfolge, in der
die Bibliotheken aufgelistet werden hinsichtlich der
Objektdateien (und auch untereinander) wichtig; üblicherweise
läßt man die Bibliotheken zuletzt durchsuchen. (z.B. unter UNIX
gibt man die -l Schalter am Ende der Kommandozeile an.)

12.15: Ich brauche Quellcode zur Auswertung regulärer Ausdrücke.

A: Man sollte sich die regexp Bibliothek (wird mit vielen UNIX
Systemen geliefert) ansehen, oder sich Henry Spencer's regexp
Paket von cs.toronto.edu in pub/regexp.shar.Z beschaffen (siehe
auch Frage 17.12).

12.16: Wie kann ich die Kommandozeile in durch Whitespaces getrennte
Argumente aufteilen, wie die Parameter argc und argv von main()?

A: Die meisten Systeme haben eine Funktion namens strtok, allerdings
kann es schwer zu benutzen sein und es kann sein, dass es nicht
das tut, was man gern hätte (z.B. Quoting).

Referenzen: ANSI Abschn. 4.11.5.8; K&R II Abschn. B3 S. 250; H&S
Abschn. 15.7; PCS S. 178.

---------------------------------------------------------------------------


Abschnitt 13: Lint
==================

13.1: Ich habe dieses Programm eingetippt und es verhält sich seltsam.
Was könnte daran falsch sein?

A: Vielen Fehlern kommt man mit lint auf die Spur (evtl. mit -a, -c,
-h, -p und/oder anderen Optionen). Viele C Compiler sind in
Wirklichkeit nur halbe Compiler, nicht dazu ausersehen,
verschiedene Quellcode-Probleme zu erkennen, wodurch die
Erzeugung von nicht funktionierendem Code verhindert werden
könnte.

13.2: Wie kann ich die Message: "warning: possible pointer alignment
problem", die lint bei jedem Aufruf von malloc erzeugt,
abschalten.

A: Das Problem besteht darin, dass herkömmliche Versionen von lint
nicht wissen, und es ihnen auch nicht mitgeteilt werden kann,
dass malloc einen Zeiger auf einen Bereich zurückgibt, der zur
Speicherung beliebiger Objekttypen geeignet ist. Es ist möglich,
eine Pseudo-Implementation von malloc zu liefern, die innerhalb
eines "#ifdef lint" mit einem #define diese Warnung abschaltet.
Aber eine solch einfache Definition unterdrückt auch wichtige
Messages über wirklich fehlerhafte Aufrufe. Es dürfte einfacher
sein, diese Message zu ignorieren, vielleicht auf automatisierte
Art und Weise mit grep -v.

13.3: Wo kann ich ein ANSI-kompatibles lint bekommen ?

A: Ein Produkt namens FlexeLint ist erhältlich (als "verborgener
Quellcode", zur Übersetzung auf den meisten Systemen) bei

Gimpel Software
3207 Hogarth Lane
Collegeville, PA 19426 USA
(+1) 610 584 4261
gimpel@netaxs.com

Das lint von System V release 4 ist ANSI-kompatibel und ist
einzeln erhältlich (gebündelt mit anderen C-Tools) von den UNIX
Support Labs oder von den System V Wiederverkäufern.

Ein weiteres ANSI-kompatibles lint (das auch eine bessere formale
Prüfung durchführen kann) ist LCLint, erhältlich unter

ftp://ftp.sds.lcs.lcs.mit.edu/pub/lclint (Who's computer is this?)
http://www.sds.lcs.mit.edu/lclint (Who's computer is this?)

In Ermangelung von lint versuchen viele moderne Compiler die
meisten Probleme so zu erkennen, wie ein gutes lint es tun würde.

---------------------------------------------------------------------------

Abschnitt 14: Programmierstil
=============================

14.1: Hier ist ein hübscher Trick:

if (!strcmp(s1,s2))

Ist das guter Programmierstil?

A: Ein solches Konstrukt ist nicht unbedingt guter Stil, auch wenn
es oft verwendet wird. Der Test ist erfolgreich, wenn die beiden
Strings gleich sind, aber die Form suggeriert, dass hier auf
Ungleichheit getestet wird.

Eine andere Lösung ist ein Macro:

#define Streq(s1, s2) (strcmp((s1), (s2)) == 0)

Stilfragen können - ähnlich wie religiöse Themen - endlos
diskutiert werden. Guter Stil ist ein anzustrebendes Ziel und
kann durchaus erkannt werden, es ist aber praktisch unmöglich,
guten Programmierstil festzuschreiben.

14.2: Was ist der beste Codierstil für C?

A: "Programmieren in C" von K&R beschreibt einen Stil, der sehr
verbreitet ist, enthält aber gleichzeitig folgenden Absatz:

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äßig halten, und benutzen Sie ihn dann grundsätzlich.

Es ist viel wichtiger, dass das gewählte Code-Layout konsistent ist
(konsistent innerhalb des eigenen oder mitverwendeten Codes), als
das es "perfekt" ist. Wem die Umgebung (z.B. die Abteilung oder
Firma) keinen Stil vorgibt, und wer keine Lust hat, einen eigenen
Stil zu erfinden, der kann einfach den von K&R übernehmen. (Die
Vor- und Nachteile der verschiedenen Einrücktechniken und
Klammersetzungen können bis ins kleinste erörtert werden, sie sind
es aber nicht wert, hier Platz zu verschwenden. Siehe auch das
Indian Hill Style Guide.)

Der schwer zu fassende Begriff "Guter Programmierstil" beeinhaltet
sehr viel mehr als nur Regeln zur Formatierung von Quelltexten,
Zeit für die Formatierung von Quellcode unter Vernachlässigung
wichtigerer Punkte aufwenden ist Unfug.

References: K&R Sec. 1.2 p. 10.

14.3: Wo gibt es das "Indian Hill Style Guide" und die anderen
angesprochenen Standards?

A: Verschiedene Dokumente sind per anonymous ftp erhältlich von

Host: Datei oder Verzeichnis:

cs.washington.edu ~ftp/pub/cstyle.tar.Z
(128.95.1.4) (aktuelles Indian Hill Guide)

cs.toronto.edu doc/programming

ftp.cs.umd.edu pub/style-guide

---------------------------------------------------------------------------

Abschnitt 15: Gleitkomma-Probleme
=================================

15.1: Meine Gleitkommarechnungen zeigen ein eigenartiges Verhalten
und/oder führen auf unterschiedlichen Rechnern zu
unterschiedlichen Ergebnissen.

A: Bei Problemen sollte zunächst sichergestellt werden, dass <math.h>
mit

#include <math.h>

eingebunden wird, und dass alle Funktionen, die 'double'
zurückgeben auch korrekt deklariert sind.

Wenn sich das Problem nicht so einfach lösen läßt, erinnere Dich,
dass die von den meisten Digitalrechnern verwendeten
Gleitkommaformate zwar eine relativ gute, aber keine exakte
Simulation der Arithmetik für reelle Zahlen ermöglichen.
Gleitkommaunterlauf, Fortpflanzung von Rundungsfehlern und andere
Anomalien führen oft zu Problemen.

Gleitkommaergebnisse sind nie exakt, und es ist praktisch *immer*
ein Fehler, Gleitkommawerte auf Gleichheit zu prüfen.

Diese Probleme hat C mit anderen Sprachen gemeinsam.
Gleitkommasemantik ist üblicherweise definiert als "wie auch immer
der jeweilige Prozessor es macht", andernfalls hätte ein Compiler
für einen Rechner mit dem 'falschen' Modell extrem aufwendige
Emulationen einzubinden.

Dieser Artikel kann keine Liste der Fallgruben im Bereich der
Gleitkommaarithmetik bzw. der sicheren Umgehungen derselben sein.
Gute Programmierlehrbücher sollten entsprechende Einführungen
enthalten.

References: EoPS Sec. 6 pp. 115-8.

15.2: Ich arbeite mit trigonometrischen Funktionen, habe 'math.h'
geladen, erhalte aber "undefined: _sin" u.ä. als Fehlermeldung.

A: Wird die Mathe-Bibliothek dazugelinkt? Unter Unix z.B. ist '-lm'
als Linkeroption erforderlich, und zwar am Ende der Linker
Kommandozeile. (s.a. 12.14)

15.3: Wie runde ich Gleitkommazahlen?

A: Eine einfache Lösung, die aber nur für positive Zahlen korrekt
arbeitet ist:

(int)(x + 0.5)

Auch für negative Zahlen funktioniert

(int)floor (x + 0.5)

(letztere Antwort stammt aus einer Diskussion in d.c.l.c).

15.4: Wie teste ich auf IEEE NaN und andere spezielle Werte?

A: Viele Systeme mit hochwertigen IEEE Gleitkomma-Implementierungen
stellen entsprechende Tests zur Verfügung (z.B. ein Makro isnan()),
um sauber mit diesen 'Werten' umzugehen; die Numerical C Extensions
Group (NCEG) erarbeitet formelle Standards für derartige Hilfmittel.
Ein grober aber i.a. erfolgreicher Test auf NaN ist

#define isnan(x) ((x) != (x))

wenngleich ein eifriger Optimizer den Ausdruck u.U. wegoptimiert.

15.5: Mein Turbo C Programm stürzt mit der Meldung "floating point
formats not linked" ab.

A: Einige Compiler für Kleinrechner, einschließlich Turbo C (und
Ritchies ursprünglichem PDP-11 Compiler), binden keine
Gleitkommaunterstützung ein, wenn es so aussieht, als ob sie nicht
benötigt wird.
Insbesondere benötigen die Versionen von printf und scanf, die
ohne Gleitkommaunterstützung auskommen, indem sie auf die
Behandlung von %e, %f und %g verzichten, weniger Speicher.
Bei Turbo C scheint die Entscheidung über die Notwendigkeit der
Einbindung von Gleitkommacode unzuverlässig zu sein. Deshalb ist
manchmal ein (sonst überflüssiger) Aufruf einer
Gleitkomma-Bibliotheksfunktion nötig, um dem Erkennungsmechanismus
auf die Sprünge zu helfen.

15.6: Wie wird eine Variable nach IEEE-Gleitkommaformat im Rechner
dargestellt?

A: Diese Frage läßt sich ohne Kenntnis von CPU, Compiler und BS
nicht beantworten. IEEE-754 (der FP-Standard) definiert nur die
Bedeutung der Bits in ihrer logischen (nicht physikalischen)
Reihenfolge. IEEE-754 definiert 3 Gleitkommatypen : single, double
und double-extended. Single und double-Typen sind 32 bzw. 64 Bit
lang, double-extended ist nicht vollständig definiert und wird
von Compilerherstellern derzeit meist mit einer Länge von 80 Bit
implementiert. Es ist für die interne Darstellung in Koprozessoren
konzipiert worden und soll hier nicht weiter behandelt werden.

Das 32/64bit Format sieht wie folgt aus:

VZ Charakteristik Mantisse
I I // I // I
32: 31 .... ... 23 22 ... ... 0
64: 63 .... ... 52 51 ... ... 0

Das höchstwertige Bit gibt das Vorzeichen der Zahl an (wie
üblich : 0 <=> '+' ; 1 <=> '-' ).

Die Charakteristik entspricht einem derart geshifteten Exponenten,
dass der Wertebereich von 0 bis 2^N - 1 reicht. Hierbei zeigen
die Grenzwerte Unterlauf bzw. NaN an. Ansonsten gilt:

32: Char. = Exp + 127
64: Char. = Exp + 1023

Die Mantisse liege in normalisierter Form vor, d.h. 1.0 <= M < 2.0,
- 1.00...00 <= M <= 1.11...11 . Diese Information hält man fest,
läßt das höchstwertige Bit jedoch wegfallen. Dadurch hat man 1 Bit
Auflösung gewonnen, was im Falle des 'single'-Formats den
Unter schied zwischen sicheren 6 bzw. 7 Dezimalstellen bedeutet.

Die Auflösung beträgt 24 bzw 53 bit; dies entspricht 7 bzw. 15
signifikanten Dezimalstellen (Bitte nicht mit Nachkommastellen
verwechseln!).

---------------------------------------------------------------------------

Abschnitt 16: Systemabhängiges
==============================

16.1: Wie kann ich einen einzelnen Buchstaben von der Tastatur auslesen,
ohne auf RETURN zu warten?

A: Im Gegensatz zum allgemeinen Glauben und den Wünschen vieler
Programmierer ist dies keine C-Frage. (Genauso wenig wie ähnliche
Fragen, die das ECHO von Tastatur-Eingaben betreffen.) Der
Transport von Buchstaben von einer "Tastatur" zu einem C-Programm
ist eine Funktion des entsprechenden Betriebssystems, und wurde
nicht durch die Sprache C standardisiert. Manche Versionen von
CURSES haben die Funktion cbreak(), welche diese Funktionalität
beinhaltet. Um speziell ein kurzes Paßwort ohne ECHO einzulesen,
kann getpass() verwendet werden. Unter UNIX kann IOCTL verwendet
werden, um die Terminal-Treiber-Modi zu verändern (CBREAK oder RAW
bei "klassischen" Versionen; ICANON, c_cc[VMIN] und c_cc[VTIME] bei
System V- oder POSIX-Systemen). Unter MSDOS kann getch() benutzt
werden. Bei VMS sollten die Bildschirmsteuerungs-Routinen (SMG$)
oder CURSES benutzt oder systemnahe $QIOs mit den IO$_READVBLK (und
vielleicht IO$M_NOECHO) Funktionscodes verwendet werden, um
einzelne Zeichen abzufragen. Bei anderen Systemen müssen
entsprechende Funktionen selbst entwickelt werden. Man sollte
beachten, dass manche Betriebssysteme es generell nicht erlauben, da
das Einlesen der Zeichen in Eingabezeilen durch seperate
Prozessoren erfolgt, die nicht der direkten Kontrolle der CPU
unterstehen, auf der das Programm läuft.

Betriebssystemspezifische Fragen sind in de.comp.lang.c nicht
angebracht. Viele dieser Fragen werden in FAQs der Gruppen
comp.unix.questions, comp.os.msdos.programmer usw. beantwortet.
Viele Antworten sind nicht einmal zwischen verschiedenen
Betriebssystemvarianten gleich. Man sollte bedenken, dass die
Beantwortung von betriebssystemspezifischen Fragen auf anderen
Systemen anders ausfallen kann, als auf dem eigenen.

Querverweise: PCS Sek. 10 S. 128-9, Sek. 10.1 S. 130-1.

16.2: Wie finde ich heraus, ob Zeichen zum Einlesen zur Verfügung stehen
(und wenn, wieviele)? Oder wie kann ich Zeichen einlesen, ohne dass
mein Prozeß blockiert, wenn keine da sind?

A: Dies ist ebenfalls eine völlig betriebssystemspezifische Frage.
Manche Version von CURSES haben die nodelay() Funktion. Abhängig
vom System kann ebenso "nichtblockierende I/O" verwendet werden,
oder ein Systemaufruf namens "select" oder der FIONREAD
IOCTL-Aufruf oder kbhit() oder rdchk() oder die O_NDELAY Option
für open() oder fcntl().

16.3: Wie kann ich den Bildschirm löschen? Wie kann ich Text invers
darstellen?

A: So etwas hängt vom verwendeten Terminaltyp (oder dem Bildschirm)
ab. Zur Lösung des Problems kann eine Bibliothek wie TERMCAP oder
CURSES, oder einige systemspezifische Routinen verwendet werden.

16.4: Wie lese ich die Maus aus?

A: Hierüber informiert die Systembeschreibung oder eine geeignete
systemspezifische Newsgruppe (deren FAQ man zunächst anschauen
sollte). Die Maussteuerung ist beim X11-Window-System, MS-DOS,
Macintosh und vermutlich jedem anderen System völlig verschieden.

16.5: Wie kann mein Programm den kompletten Pfadnamen der ausführbaren
Datei ermitteln, von der es aufgerufen wurde?

A: argv[0] könnte den ganzen Pfadnamen oder einen Teil davon
beinhalten, oder auch gar nichts. Man kann die Such-Pfad Logik des
Befehlsinterpreters kopieren, wenn der Name in argv[0] zwar
vorhanden, aber nicht komplett ist. Es gibt aber keine garantierte
oder portable Lösung.

[Anmerkung Uz:]
Es gibt ein Paket namens selfdir, dass es z.B. irgendwo auf
ftp://ftp.uni-stuttgart.de (Who's computer is this?) gibt und das diese Problemstellung
für Unix-Systeme löst.
[Ende Anmerkung Uz]

16.6: Wie kann ein Prozeß eine Umgebungsvariable seines aufrufenden
Programmes verändern?

A: Im allgemeinen nicht. Verschiedene Betriebssysteme implementieren
eine Funktionalität ähnlich wie bei Unix-Umgebungen. Ob eine
"Umgebung" sinnvoll geändert werden kann, und wenn ja, wie, ist
eine systemspezifische Frage.

Unter Unix kann ein Prozeß seine eigene Umgebung verändern (einige
Systeme unterstützen hierfür die Funktionen setenv() und/oder
putenv()), und die veränderte Umgebung wird gewöhnlich an alle
Kind-Prozesse weitergegeben, jedoch _nicht_ zurück an den
Eltern-Prozeß.

16.7: Wie kann ich überprüfen ob eine Datei existiert? Ich möchte den
Anwender fragen, bevor eine vorhandene Datei überschrieben wird.

A: Unter Unix-Systemen kann man die Funktion access() verwenden,
obwohl es dabei ein paar Probleme gibt (sie bildet mit der
folgenden Aktion keine atomare [ununterbrechbare] Einheit und kann
Anomalien zeigen, wenn sie von setuid-Programmen aufgerufen wird).
Eine andere (vielleicht bessere) Möglichkeit ist es,
stat() für auf diese Datei aufzurufen. Ansonsten ist der
einzige, garantiert funktionierende und portable Weg, das
Vorhandensein einer Datei zu überprüfen, zu versuchen diese zu
öffnen (was jedoch nicht verhindert eine bestehende Datei zu
öffnen, es sei denn es gibt soetwas, wie die BSD-Unix O_EXCL
Option für die Funktion open()).

16.8: Wie finde ich die Größe einer Datei heraus, bevor ich diese
einlese?

A: Wenn mit "Größe einer Datei" die Anzahl der Bytes gemeint ist, die
man unter C einlesen kann, dann ist es unmöglich, deren Anzahl im
voraus festzustellen. Unter Unix gibt stat() die genaue Antwort,
viele andere Systeme unterstützen Unix-ähnliche stat()-Funktionen,
welche die annähernde Anzahl angeben. Man kann mit der Funktion
fseek() zum Ende der Datei gehen und dann ftell() verwenden (um die
absolute Position innerhalb der Datei zu bestimmen Anm. d. Übers),
aber diese Methode ist nicht portabel (sie ergibt lediglich unter
UNIX eine genaue Antwort und ansonsten eine quasi-genaue Antwort,
die nur für binäre Dateien im Sinne von ANSI-C gültig ist). Manche
Systeme unterstützen Routinen wie filesize() oder filelength().

Muß die Größe der Datei tatsächlich vorher bestimmt werden? Da der
genaueste Weg, die Größe einer Datei zu bestimmen, darin besteht,
diese zu öffnen und sie zu lesen, kann vielleicht der Programmcode
so umgestellt werden, dass die Größe während des Lesens festgestellt
wird.

16.9: Wie kann eine Datei gekürzt werden, ohne sie komplett zu löschen
oder erneut zu schreiben?

A: BSD-Systeme bieten ftruncate(), verschiedene andere unterstützen
chsize(), und ein paar unterstützen vielleicht eine (möglicherweise
undokumentierte) fcntl-Option namens F_FREESP. Unter MS-DOS kann
man manchmal write(fd, "", 0) verwenden. Eine portable Lösung gibt
es nicht.

16.10: Wie kann ich eine Pause (delay) oder die Messung einer
Anwender-Reaktion einbauen, welche eine Auflösung im Bereich
von Sekundenbruchteilen hat?

A: Leider gibt es keinen portablen Weg. V7 Unix und vergleichbare
System boten eine ziemlich brauchbare ftime() Funktion mit einer
Auflösung bis hin zu einer Millisekunde, aber diese ist bei System
V und Posix verschwunden. Andere Routinen, nach denen man suchen
kann, sind nap(), setitimer(), msleep(), usleep(), clock() und
gettimeofday(). Die select() und poll() Aufrufe können
zweckentfremdet werden, um einfache Pausen zu erzeugen. Auf MS-DOS
Maschinen ist es möglich, den System-Zeitgeber und die
Timer-Interrupts neu zu programmieren.

16.11: Wie kann ich Objekt-Dateien einlesen und zu Routinen darin
anspringen?

A: Das wäre dann ein dynamischer Linker und/oder Lader. Es ist
möglich, etwas Speicher zu belegen und dann die Objekt-Datei
einzulesen, aber man muß sehr viel über die Formate von
Objekt-Dateien, Relokationen usw. wissen. Unter BSD Unix kann man
system() und ld -A verwenden um das Linken zu bewerkstelligen.
Viele (die meisten?) Versionen von SunOS und System V haben eine
-ldl Bibliothek, die das dyamische Laden von Objekt-Dateien
ermöglicht. Es gibt außerdem ein GNU-Paket namens "dld". Siehe
dazu die Frage 7.6

16.12: Wie kann ich innerhalb eines C-Programmes ein Kommando des
Betriebssystems aufrufen?

A: Durch Verwendung von system(). Da das Programm aber damit
zwangsläufig Abhängigkeiten vom verwendeten Betriebssystem hat,
sollte wo möglich auf C-Funktionen ausgewichen werden.

Querverweise: K&R II Sek. B6 S. 253; ANSI Sek. 4.10.4.5; H&S
Sek. 21.2; PCS Sek. 11 S. 179;

16.13: Wie kann ich innerhalb eines C-Programmes ein Kommando des
Betriebssystems aufrufen und dessen Ausgaben auffangen?

A: Unix und einige andere Betriebssysteme unterstützen eine popen()
Routine, die einen stdio Stream bildet, welcher die Ausgaben des
entsprechenden Prozesses beinhaltet, so dass dessen Ausgaben
gelesen werden können. Alternativ kann man den Befehl einfach so
ausführen (siehe Frage 16.12), dass er seine Ausgaben in eine Datei
schreibt, diese anschließend öffnet und einliest.

(Anm. d. Übers.: Bei vielen Betriebssystemen ist es erforderlich,
den entsprechenden Befehl innerhalb einer Shell zu starten, da das
erforderliche Pipe-ing eine Eigenschaft der Shell ist.)

Querverweise: PCS Sek. 11 S. 169.

16.14: Wie kann ich in einem C-Programm ein Verzeichnis lesen?

A: Man sollte überprüfen, ob die Funktionen opendir() und readdir()
benutzt werden können, die bei den meisten Unix Systemen vorhanden
sind. Es gibt auch Implementationen für MS-DOS, VMS und andere
Systeme (MS-DOS hat Routinen namens FINDFIRST und FINDNEXT, die im
Grunde das gleiche machen.).

16.15: Wie kann ich serielle ("Comm") Ports nutzen?

A: Das ist systemabhängig. Unter Unix kann man normalerweise die
Gerätetreiber im Verzeichnis /dev öffnen, diese lesen und
schreiben und die Möglichkeiten des Treibers nutzen, um deren
Eigenschaften zu verändern. Unter MS-DOS kann man entweder einige
primitive BIOS Aufrufe nutzen, oder (wenn es besonders effizient
sein soll) eines von vielen Interrupt-gesteuerten seriellen
I/O-Paketen nutzen.

---------------------------------------------------------------------------

Abschnitt 17: Verschiedenes
===========================

17.1: Welche Annahmen über die automatische Initialisierung von Variablen
sind erlaubt? Reicht eine Initialisierung von Null-Zeigern und
Gleitkommavariablen mit 0 aus?

A: Variablen der Speicherklasse static (Variablen die außerhalb einer
Funktion deklariert werden und solche, die als static deklariert
werden) werden garantiert mit 0 initialisiert, und zwar genau
einmal beim Start des Programms, als wenn der Programmierer bei
der Deklaration "= 0" geschrieben hätte. Diese Variablen werden
daher mit Null-Zeigern (mit korrektem Typ - siehe Abschnitt 1),
wenn es sich um Zeiger handelt, und 0.0, wenn es sich um
Gleitkomma Variablen handelt, initialisiert.

Variablen der Speicherklasse automatic (lokale Variable ohne
statische Speicherklasse) werden beim Programmstart nicht
initialisiert, es sei denn, der Programmierer tut dies explizit.
Über den Inhalt dieser Variablen darf ansonsten keine Annahme
gemacht werden.

Der Inhalt von mittels malloc und realloc dynamisch belegtem
Speicher sollte ebenso als nicht initialisiert betrachtet werden.
Er muß durch das aufrufende Programm entsprechend initialisiert
werden. Speicherbereiche die mit calloc belegt werden, werden
bitweise mit 0 initialisiert. Dies muß nicht notwendigerweise
einem Null-Zeiger oder einer Gleitkomma 0 entsprechen (siehe auch
3.13 und Abschnitt 1).

17.2: Dieses Programm, direkt aus einem Buch, läßt sich nicht
kompilieren

f()
{
char a[] = "Hello, world!";
}

Dies könnte darn liegen, dass es sich um einen alten, nicht
ANSI-kompatiblen Compiler handelt und dieser die Initialisierung
von "automatic aggregates" (d.h. nicht statische lokale Arrays und
Strukturen) nicht erlaubt. Als Ausweg kann man das Array global
oder statisch deklarieren und mittels strcpy beim beim Aufruf von
f initialisieren. (Man kann lokale char * Variablen immer mit
String Konstanten initialisieren, siehe auch 17.20). Siehe auch
5.16 und 5.17.

17.3: Wie kann man Daten so in Dateien schreiben, dass sie auch auf
fremden Maschinen mit anderen Wortlängen, Byteorder oder anderem
Gleitkommaformat gelesen werden können?

Die beste Lösung ist es, Textdateien zu verwenden (i.d.R. ASCII),
die mit fprintf geschrieben und mit fscanf gelesen werden
(ähnliches gilt auch bei Netzwerkprotokollen). Die Argumente,
Textdateien seien zu groß und Lesen und Schreiben von Textdateien
sei zu langsam, sind nicht sehr stichhaltig. Normalerweise ist
ihre Effizienz in der Praxis akzeptabel. Darüberhinaus kann der
Vorteil, dass man die Daten mit Standardwerkzeugen bearbeiten
kann, eventuelle Nachteile meist mehr als wettmachen.

Soll dennoch ein Binärformat verwendet werden, kann die
Portabilität durch Verwenden von standardisierten Formaten, wie
SUN XDR (RFC 1014), OSI ASN.1, CCITT X.409, oder ISO 8825 "Basic
Encoding Rules" erhöht werden. Für diese Formate gibt es u.U. auch
bereits fertige E/A Bibliotheken. Siehe auch 9.11.

17.4: Wie kann man in der Mitte einer Datei eine Zeile (oder einen
Record) löschen oder einfügen?

A: Ausser durch ein Neuschreiben der Datei ist dies wahrscheinlich
nicht möglich. Siehe auch 16.9.

17.5: Wie kann man mehrere Werte aus einer Funktion zurückgeben?

A: Entweder wird der Funktion ein Zeiger auf Speicherstellen, die die
Funktion beschreiben kann, übergeben, oder die Funktion muß eine
struct zurückgeben, die die gewünschten Werte enthält. Als
Notlösung kommen auch globale Variable in Betracht. Siehe auch
2.17, 3.4 und 9.2.

17.6: Wenn man eine char * Variable hat, die auf den Namen einer
Funktion als String zeigt, wie kann man diese Funktion aufrufen?

Die offensichtlichste Lösung ist es, eine Tabelle mit den Namen
der Funktionen und den entsprechenden Zeigern anzulegen:

int function1(), function2();

struct {char *name; int (*funcptr)(); } symtab[] =
{
"function1", function1,
"function2", function2,
};

Nun braucht man nur die Tabelle zu durchsuchen und kann über den
entsprechenden Zeiger die Funktion aufrufen. Siehe auch 9.9 und
16.11.

17.7: Auf meinem System scheint die Headerdatei <sgtty.h> nicht
vorhanden zu sein. Kann mir jemand eine Kopie zuschicken?

Standard Headerdateien existieren unter anderem deswegen, damit
für den jeweiligen Compiler, das Betriebssystem und den Prozessor
entsprechende Definitionen zur Verfügung gestellt werden können.
Es ist nicht möglich, einfach eine Kopie einer fremden Header
Datei zu übernehmen und dann zu erwarten, diese würde
funktionieren, es sei denn, die Header Datei wurde von jemandem
mit der exakt gleichen Umgebung zur Verfügung gestellt. Hier ist
eine Nachfrage beim Compilerhersteller/Vertrieb notwendig, warum
die entsprechende Datei nicht zur Verfügung gestellt wurde.

17.8: Wie kann man FORTRAN (C++, BASIC, Pascal, Ada, LISP) Funktionen
aus C aufrufen (und umgekehrt)?

Die Antwort hängt von der verwendeten Hardware und den
Aufrufkonventionen der verschiedenen beteiligten Compiler ab, und
es kann sogar unmöglich sein. Man sollte in einem solchen Fall die
Dokumentation des Compilers besonders genau durcharbeiten.
Manchmal gibt es Kapitel über "mixed language" Programmierung.
Aber selbst hier werden wichtige Themen wie die Übergabe von
Argumenten und die Laufzeitinitialisierung oft nur oberflächlich
behandelt. Weitere (englischsprachige) Informationen hierzu sind
in Glenn Geers FORT.gz erhältlich, das man u.a. per ftp von
suphys.physics.su.oz.au im src Verzeichnis erhält.

cfortran.h, eine C Header Datei und vereinfacht die C/FORTRAN
Schnittstelle auf vielen gängigen Maschinen. Erhältlich ist sie
per ftp von zebra.desy.de (131.169.2.244).

In C++ verwendet man den "C" Modifier in einer external
Funktionsdeklaration um dem Compiler zu sagen, dass die Funktion
nach der C Konvention aufgerufen werden soll.

Bei Ada ist in der entsprechenden Norm (ISO/IEC 8652:1995) im
Abschnitt B.3 eine Beschreibung zu finden, die das Vorgehen in
allen Details erläutert. Alle gängigen Ada-Compiler unterstützen
diese Schnittstelle.

17.9: Kennt jemand ein Programm das Pascal oder Fortran (oder LISP, Ada,
awk, "altes C" (K&R), ...) nach C übersetzt?

A: Es sind verschiedene Public Domain Programme erhältlich:

p2c Ein Pascal/C Konverter von Dave Gillespie, veröffentlicht
in comp.os.sources im März 1990;
ftp: csvax.cs.caltech.edu, file pub/p2c-1.20.tar.Z.

ptoc Ein weiteres Pascal/C Konvertierprogramm, dieses ist in
Pascal geschrieben. (comp.sources.unix)

f2c Ein Fortran/C Konverter, gemeinsam von Angehörigen von
Bell Labs, Bellcore und Carnegie Mellon Universität
entwickelt. Um mehr Informationen über f2c zu erhalten,
braucht man nur eine e-mail mit dem Inhalt "send index
from f2c" an netlib@research.att.com oder research!netlib
zu schicken (außerdem ftp://netlib.att.com (Who's computer is this?) im Verzeichnis
netlib/f2c).

Beim Autor der englischen comp.lang.c FAQ ist außerdem eine Liste
weiterer kommerzieller Übersetzungsprodukte sowie einiger
Konverter für weniger verbreitete Sprachen erhältlich.

Siehe auch 5.3.

17.10: Ist C++ eine Obermenge von C? Kann ich einen C++ Compiler benutzen
um C Programme zu kompilieren?

A: C++ wurde von C abgeleitet und basiert größtenteils auf C, aber
es gibt zulässige C Programme, die in C++ nicht zulässig sind.
Außerdem gibt es Programme, die in C und C++ eine
unterschiedliche Semantik haben.

[Hinweis Jochen:]
Das folgende Programm erkennt zum Beispiel, ob es mit einem
C90-Compiler oder einem C++-Compiler übersetzt wurde:

#include <stdio.h>

int main(void)
{
int result = 1 //* */ 2
;
printf("Dies ist %s C-Compiler\n",(result?"kein":"ein"));

return 0;
}
[Ende Hinweis Jochen]

In der Praxis werden sich viele C-Programme auch mit einem
C++-Compiler anstandslos übersetzen lassen.

17.11: Ich brauche einen Crossreference-Generator (Beautifier, Pretty
Printer) für C.

A:

Ich brauche: Dafür sind folgende Programme
erhältlich (siehe auch 17.12):

---------------------------------------------------------------

Einen C Cross-Reference- cflow, calls, cscope
Generator

Einen C Beautifier/ cb, indent
Pretty-Printer

17.12: Wo sind all die erwähnten Public-Domain Programme erhältlich?

A: Im Usenet erscheinen regelmässig in den Newsgruppen
comp.sources.unix und comp.sources.misc Postings, die recht genau
beschreiben, wie man an bestimmte Programmarchive gelangen kann.
Normalerweise benutzt man ftp und/oder uucp, um die Archive von
einem ftp-Server zu bekommen, wie z.b. uunet (ftp.uu.net)
An dieser Stelle kann aber keine vollständige Liste der möglichen
Server und wie man auf sie zugreift, aufgeführt werden.

Ajay Shah führt eine Liste mit kostenloser numerischer Software.
Sie wird regelmäßig gepostet und ist mit dem FAQ zu comp.lang.c
auf rtfm.mit.edu in /pub/usenet-by-group/comp.lang.c erhältlich.
in der Newsgroup comp.archives werden darüberhinaus zahlreiche
Ankündigungen über verschiedenerlei Programme und auf welchen
Servern sie erhältlich sind veröffentlicht. Mit "archie" kann man
herausfinden, auf welchem ftp Server welche Programmpakete
archiviert werden; für weitere Informationen kann man eine e-mail
mit dem Subject "help" an archie@th-darmstadt.de schicken.
Grundsätzlich ist auch comp.sources.wanted ein guter Platz, um
nach Programmen und Programmpaketen zu fragen. Aber auch hier
gilt: Lieber erst die entsprechende FAQ lesen!

17.13: Wann findet der nächste "International Obfuscated C Contest"
(IOCC) statt? Wie bekommt man eine Kopie früherer und
gegenwärtiger Sieger?

A: Es gibt inzwischen einen eigenen Webserver für den Wettbewerb.
Unter http://www.ioccc.org/ (Who's computer is this?) findet man alle Informationen zu
vergangenen, aktuellen und zukünftigen Wettbewerben.

17.14: Warum gibt es in C keine verschachtelten Kommentare? Wie kann ich
Programmtext, der Kommentare enthält, auskommentieren? Darf ich
Kommentare in Stringkonstanten einfügen?

A: Verschachtelte Kommentare würden mehr schaden als nutzen, weil
die Gefahr groß ist, versehentlich Kommentare nicht zu schließen,
wenn die Zeichen "/*" in ihnen enthalten sind. Deshalb ist es
normalerweise besser, größere Programmteile, die auch Kommentare
enthalten, mit #ifdef oder #if 0 "auszukommentieren" (siehe hierzu
auch 5.11).

Die Zeichenfolgen /* und */ sind innerhalb von Stringliteralen
keine Sonderzeichen! Daher leiten sie auch keine Kommentare ein,
da es ja möglich wäre, dass ein Programm diese Zeichenfolge
ausgeben will (besonders, wenn es C Code erzeugt).

Referenz: ANSI Appendix E p. 198, Rationale Sec. 3.1.9 p. 33.

17.15: Wie kann man den ASCII Wert eines Zeichens und umgekehrt das
passende Zeichen zu einem ASCII Wert rausfinden?

A: In C werden Zeichen (char) generell durch Integer repräsentiert,
deren Wert direkt dem Wert des Zeichens im Zeichensatz der
jeweiligen Maschine entspricht. Man braucht deshalb keine
Konvertierungsroutine, wenn man ein Zeichen hat, hat man seinen
Wert!

17.16: Wie programmiert man Bit-Sets und/oder Bit-Arrays?

A: Man benutzt char oder int Arrays, mit ein paar Makros um ein
bestimmtes Bit an einer bestimmten Stelle zu manipulieren. (sollte
<limits.h> nicht implementiert sein, kann man es mit CHAR_BIT 8
probieren)


#include <limits.h> /* for CHAR_BIT */

#define BITMASK(bit) (1 << ((bit) % CHAR_BIT))
#define BITSLOT(bit) ((bit) / CHAR_BIT)
#define BITSET(ary, bit) ((ary)[BITSLOT(bit)] |= BITMASK(bit))
#define BITTEST(ary, bit) ((ary)[BITSLOT(bit)] & BITMASK(bit))

17.17: Wie kann man am effizientesten die Zahl der gesetzten Bits in
einer Variablen ermitteln?

A: Diese und viele andere Bit-Manipulationsprobleme können häufig
durch Tabellen beschleunigt werden.

17.18: Wie kann man dieses Programm effizienter machen?

A: Obwohl vielfach in de.comp.lang.c über Effizienz diskutiert wird,
ist sie nicht annähernd so häufig ein entscheidender Faktor, wie
viele denken. Der größte Teil eines Programm ist nicht
zeitkritisch. Ist aber ein Programmteil nicht zeitkritisch, so ist
es wesentlich wichtiger, dass dieser verständlich und portabel
geschrieben wird, als ihn auf Effizienz zu optimieren
(schliesslich sind Computer trotz allem ziemlich schnell, so dass
auch ineffiziente Programmteile ohne Verzögerung laufen können).

Es ist sehr schwierig vorherzusagen, welche Teile eines Programms
besonders laufzeitkritisch sind. Wenn das Laufzeitverhalten von
entscheidender Bedeutung ist, sollte man einen Profiler benutzen,
um diejenigen Programmteile zu finden, die besonderer
Aufmerksamkeit in Bezug auf ihre Optimierung bedürfen. Häufig ist
zu beobachten, dass E/A Operationen sich auf das Laufzeitverhalten
stärker auswirken als die eigentlichen Berechnungen. Dies kann
durch Cache Techniken beschleunigt werden.

Um den kleinen Anteil des wirklich zeitkritischen Codes in einem
Programm zu optimieren, ist es besonders wichtig, dass man einen
guten Algorithmus auswählt. Die "Mikrooptimierung" von
Programmteilen ist weniger entscheidend. Die meisten Tricks zur
Effizienz-Steigerung werden selbst von einfachen Compilern
automatisch durchgeführt (wie z.B. Multiplikationen mit
Zweierpotenzen durch Shiftoperationen zu ersetzen). Ungeschickte
Optimierungen können ein Programm so umständlich machen, dass das
Laufzeitverhalten darunter leidet.

Weitere Hinweise zu diesem Thema finden sich im Kapitel 7 von
Kernighan, Plauger "The Elements of Programming Style" sowie in
Jon Bentley "Writing Efficient Programs".

17.19: Sind Zeiger wirklich schneller als Arrays? Wie sehr verlangsamen
Funktionsaufrufe die Programmausführung? Ist ++i schneller als
i= i + 1?

A: Die Antworten hierauf hängen natürlich von dem verwendeten
Compiler und der Hardware ab. Die sicherste Antwort hierauf erhält
man, indem man es einfach selbst austestet (meistens werden die
Unterschiede so klein sein, dass man mehrere hunderttausend
Iterationen braucht, um irgendeinen Unterschied zu sehen. Wenn
möglich sollte man sich das Assemblerlisting anschauen, um zu
sehen, ob die beiden Alternativen nicht den gleichen Assemblercode
liefern).

Es ist "gewöhnlich" schneller, durch grosse Arrays mit Zeigern
als mit dem Arrayindex zu "wandern". Allerdings ist dies bei
einigen Prozessoren auch umgekehrt.

Funktionsaufrufe sind zwar unwesentlich langsamer als inline
Code, sie tragen aber so stark zur Modularität und Lesbarkeit
eines Programms bei, dass es selten einen vernünftgen Grund gibt,
auf sie zu verzichten.

Bevor man Ausdrücke wie i = i + 1 umschreibt, sollte man bedenken,
dass man mit einem C Compiler umgeht, nicht mit einem Taschenrechner!
Jeder halbwegs gute Compiler wird identischen Code für ++i, i+=1
und i = i + 1 erzeugen! Es ist nur eine Frage des Stils (bzw. des
Zusammenhangs) ob man i++, i+=1 oder i = i + 1 wählen sollte.

17.20: Warum stürzt dieser Code ab:

char *p = "Hello, world!";
p[0] = tolower(p[0]);

A: String Konstanten sind nicht notwendigerweise modifizierbar, außer
wenn sie zum Initialisieren von Arrays verwendet werden. Folgendes
Beispiel sollte funktionieren:

char a[] = "Hello, world!";

(Um ältere Programme zu kompilieren haben einige Compiler eine
Option, um zu steuern, ob Strings beschreibbar sind oder nicht).
Siehe auch 2.1, 2.2, 2.8 und 17.2

Referenz: ANSI Sec. 3.1.4 .

17.21: Mein Programm stürzt ab, bevor es überhaupt die erste Zeile
ausführt (Wenn man es mit einem Debugger untersucht bricht das
Programm vor der ersten Zeile von main ab).

A: In dem Programm gibt es wahrscheinlich einen oder mehrere sehr
grosse lokale Arrays. Der Stack hat auf vielen Systemen eine
feste Größe. Systeme, die den Stack dynamisch anlegen können,
können durcheinander kommen, wenn der Stack plötzlich um ein
sehr grosses Stück wächst.

Oft ist es besser, große Arrays als static zu deklarieren (außer
natürlich, wenn bei jedem neuen Aufruf ein neues Array benötigt
wird).

(Siehe auch 9.4.)

17.22: Was bedeuten "Segmentation Violation" und "Bus Error"?

A: Das Programm hat versucht, auf Speicher zuzugreifen, auf den es
nicht so zugreifen durfte, oft im Zusammenhang mit falscher
Benutzung von Zeigern, wobei diese meist nicht initialisiert oder
falsch belegt werden (siehe 3.1 und 3.2), oder von malloc (siehe
17.23) oder von scanf (siehe 11.3).

17.23: Hat jemand ein Programmpaket um den Compiler zu überprüfen?

A: Ein kommerzielles Produkt ist von Plum Hall erhältlich. Die
GNU C Distribution der FSF (gcc) enthält c-torture-test.tar.Z,
das eine Reihe von gängigen Compilerproblemen überprüft.
Der paranoia Test von Kahan, erhältlich in netlib/paranoia auf
netlib.att.com testet ausführlich die Gleitkomma Fähigkeiten
der jeweiligen Implementation.

17.24: Wo bekommt man eine YACC Grammatik für C her?

A: Die "definitive" Grammatik ist diejenige im ANSI Standard. Eine
weitere Grammatik von Jim Roskind ist in pub/*grammar* auf
ics.uci.edu erhältlich. Ein funktionierendes Beispiel der
ANSI Grammatik (verfasst von Jeff Lee) ist im uunet (siehe 17.12)
in usenet/net.sources/anis.c.grammar.Z (inklusive eines lexers)
erhältlich. Der GNU C Compiler der FSF enthält ebenso eine
Grammatik wie der Anhang von K&R II.

Referenz: ANSI Sec. A.2 .

17.25: Ich benötige Code um mathematische Ausdrücke zu parsen und
auszuwerten.

A: Zwei Pakete sind: "defunc", erhältlich auf sunsite.unc.edu
in pub/packages/development/libraries/defunc-1.3.tar.Z; sowie
"parse" auf lamont.ldgo.columbia.edu.

17.26: Wo bekommt man eine Routine für den "ungefähren" Vergleich von 2
Strings her, die ähnliche, aber nicht unbedingt exakt gleiche
Strings erkennt.

A: Gewöhnlich verwendet man hierfür den Soundex Algorithmus, welcher
ähnlich klingenden Worten identische Zahlenwerte zuordnet. Dieser
Algorithmus wird in Donald Knuth's "The Art of Computer
Programming" im Band "Searching and Sorting" beschrieben.

17.27: Wie kann man den Wochentag aus einem gegebenen Datum herausfinden?

A: Mittels mktime (Siehe 12.6 und 12.7) oder Zeller's congruence oder
im FAQ zu sci.math, oder mit folgendem Programm:

dayofweek(y, m, d) /* 0 = Sonntag */
int y, m, d; /* 1 <= m <= 12, y > 1752 oder so */
{
static int t[] = {0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4};
y -= m < 3;
return (y + y/4 - y/100 + y/400 + t[m-1] + d) % 7;
}

17.28: Ist das Jahr 2000 ein Schaltjahr gewesen? Ist (year % 4 == 0) ein
zuverlässiger Test für ein Schaltjahr?

A: Ja und Nein (in dieser Reihenfolge). Für den Gregorianischen
Kalender lautet der vollständige Test:

year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)

In jedem guten astronomischen Führer gibt es zu diesem Thema
weitere Informationen.

---------------------------------------------------------------------------

Das Urheberrecht für diese FAQ bzw. die deutsche Übersetzung (was auch
immer das heissen mag) liegt bei den Autoren, eine weitere Verbreitung ist
zulässig und erwünscht, vorausgesetzt dies geschieht komplett und
unverändert.

Die englische Version von Steve Summit (scs@eskimo.com) sagt weiterhin:

This article is Copyright 1988, 1990-1995 by Steve Summit.
It may be freely redistributed so long as the author's name, and this
notice, are retained.
The C code in this article (vstrcat(), error(), etc.) is public domain
and may be used without restriction.

---------------------------------------------------------------------------

Vielen Dank an

- Steve Summit (scs@eskimo.com) für die Erlaubnis, den Text der alten
comp.lang.c FAQ als Vorlage zu nutzen.

- Alle, die an der FAQ mitgearbeitet haben (siehe Liste am Anfang).

- Alle die mit Fragen und Hinweisen mitgeholfen haben, Fehler zu
entfernen und Formulierungen klarer zu machen.




Where you can get the newsgroup de.comp.lang.c