de.comp.lang.c FAQ
Von: Martin Woelfel (dclcfaq@zwillingsskorpion.de) [Profil]
Datum: 01.07.2008 03:12
Message-ID: <dclc_FAQ_102_Part_1@dclc-faq.de>
Followup-to: de.comp.lang.c
Newsgroup: de.comp.lang.c
Datum: 01.07.2008 03:12
Message-ID: <dclc_FAQ_102_Part_1@dclc-faq.de>
Followup-to: de.comp.lang.c
Newsgroup: de.comp.lang.c
Archive-Name: C-faq/german/part1
Posting-Frequency: monthly
Last-modified: 2004-03-14
URL: http://home.pages.de/~c-faq/
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/
bzw.
http://home.pages.de/~c-faq/
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