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 oder 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 (im Prinzip ein nicht existierendes 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 #include 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 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 (oder notfalls unter Benutzung des älteren Files ). Hier ist als Beispiel eine Funktion, die eine beliebige Anzahl von Strings in einem neu belegten Speicherbereich aneinanderhängt: #include /* malloc, NULL, size_t */ #include /* va_ Zeugs */ #include /* 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 sollte 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 #include 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 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 . Wenn es nicht existiert, hier ist eine mögliche Implementation: #define offsetof(type, mem) ((size_t) \ ((char *)&((type *) 0)->mem - (char *)((type *) 0))) Diese Implementation ist nicht 100% portabel; einige Compiler akzeptieren sie (legitimerweise) nicht. Siehe die nächste Frage für einen Nutzungshinweis. References: ANSI Sec. 4.1.5, Rationale Sec. 3.5.4.2 p. 55. 9.9: Wie kann ich auf Structfelder zur Laufzeit per Namen zugreifen? A: Baue eine Tabelle mit Namen und Offsets, das offsetof() Makro verwendend. Der Offset von Feld b in Struct a ist offsetb = offsetof(struct a, b) Wenn structp ein Pointer auf ein Exemplar dieser Struktur und b ein Int-Feld mit offset, wie oben angegeben, kann b's Wert indirekt mittels *(int *)((char *)structp + offsetb) = value gesetzt werden. 9.10: Warum ergibt sizeof für einen Strukturtyp, eine größere Größe als ich erwarte, so als ob da Padding am Ende wäre? A: Strukturen können dieses Padding haben (wie auch internes Padding; siehe auch Frage 9.5), so dass Alignmenteigenschaften erhalten bleiben, wenn ein Array von zusammenhängenden Strukturen alloziert wird. 9.11: Mein Compiler läßt Löcher in Strukturen, was Platz verschwendet und binäres I/O nach externen files verhindert. Kann ich das Padding ausstellen, oder das Alignment von Strukturen anderweitig kontrollieren? A: Der Compiler hat möglicherweise eine Erweiterung, die eine Kontrolle über das Alignment erlaubt (vielleicht ein #pragma), aber es gibt keine Standardmethode. Siehe auch Frage 17.3 9.12: Kann ich Unions initialisieren? A: Der ANSI C Standard erlaubt einen Initialisierer für das erste Element eines Union. Es gibt keinen Standardweg, die anderen Elemente zu initialisieren (unter einem prä-ANSI Compiler, gibt es gar keinen Weg überhaupt eines der Elemente zu initialisieren). 9.13: Wie kann ich konstante Werte an Routinen übergeben, die Struct-Parameter akzeptieren? A: C kennt keinen Weg, anonyme Struct-Werte zu erzeugen. Man muß temporäre Structvariablen verwenden. --------------------------------------------------------------------------- Abschnitt 10: Deklarationen =========================== 10.1: Wie entscheide ich mich für einen der Integer-Typen? A: Der Typ 'long' sollte verwendet werden, wenn Werte benötigt werden, die größer als 32767 oder kleiner als -32767 sind. Wenn Speicher sehr wichtig ist - große Arrays oder sehr viele Strukturen -, dann ist die Verwendung von 'short' sinnvoll. Trifft keiner dieser Gründe zu, dann sollte 'int' verwendet werden. Falls ein definiertes Verhalten bei einer Bereichsüberschreitung wichtig ist und/oder keine negative Werte auftreten, so sind die jeweiligen 'unsigned' Typen von Vorteil. Die beiden Varianten solten aber auf keinen Fall innerhalb eines Ausdrucks gemischt werden. Obwohl 'char' und 'unsigned char' als 'kleine' Integer-Typen verwendet werden können, handelt man sich mit dieser Vorgehensweise meist mehr Nach- als Vorteile ein. Dies liegt an undefiniertem Verhalten bei Vorzeichenbelegung und an einer Vergrößerung des Programmcodes bei manchen Compilern (falls diese die vom Standard vorgeschriebene Erweiterung nach int in allen Fällen durchführen). Obige Regeln lassen sich natürlich nicht anwenden, wenn man mit den Adressen von Variablen arbeitet und diese einen bestimmten Typ erfordern. Wenn aus irgendeinem Grund irgendetwas mit _genauer_ Größe deklariert werden muß (die wohl einzig sinnvolle Situation dafür ist die Kompatibilität mit einem von außen aufgezwungen Speicherlayout), dann sollte das in passenden 'typedef's versteckt werden. 10.2: Wie sollte der 64-bit Typ auf neuen, 64-bit-Rechnern aussehen? A: Einige Hersteller von C-Produkten für 64-bit-Rechner unterstützen 64-bit große 'long int's. Andere fürchten, dass zuviele real existierende Quelltexte auf sizeof(int) == sizeof(long) == 32 bits vertrauen, und führen stattdessen einen neuen 64-bit 'long long' oder '__longlong' Typ ein. Programmierer, die daran interessiert sind, portierbaren Code zu schreiben, sollten deshalb ihre 64-bit-Erfordernisse hinter entsprechenden 'typedef's verstecken. 10.3: Ich habe Probleme mit der Definition von verketteten Listen. Bei typedef struct { char *item; NODEPTR next; } *NODEPTR; gibt der Compiler eine Fehlermeldung aus. Kann eine C-Struktur keinen Zeiger auf sich enthalten? A: C-Strukturen können selbstverständlich Zeiger auf sich enthalten, Kap. 6.5 in K&R beschäftigt sich damit. Im Beispiel liegt das Problem darin, dass die Definition für NODEPTR noch nicht vollständig war, als sie für das Feld 'next' benötigt wurde. Eine Verbesserung besteht darin, der Struktur sofort einen Namen (tag,Etikett) 'struct node' zu geben, und anschließend bei der Deklaration des Feldes 'next' 'struct node *next' anzugeben. Zuletzt wird der 'typedef'-Teil hinter die Strukturdeklaration geschoben (oder vor sie gezogen). Eine berichtigte Version wäre struct node { char *item; struct node *next; }; typedef struct node *NODEPTR; Es gibt außerdem mindestens nochmal drei gleichwertige Möglichkeiten. Ein ähnliches Problem - mit ähnlicher Lösung - kann bei dem Versuch auftreten, Typen zu definieren, die auf einander verweisen. References: K&R I Sec. 6.5 p. 101; K&R II Sec. 6.5 p. 139; H&S Sec. 5.6.1 p. 102; ANSI Sec. 3.5.2.3 . 10.4: Wie kann ich ein Array von N Zeigern auf Funktionen deklarieren, die Zeiger auf Funktionen zurückgeben, die Zeiger auf char zurückgeben? A: Es gibt mindestens 3 unterschiedliche Wege: 1. Man schreibt es einfach hin: char *(*(*a[N])())(); 2. Man baut die Deklaration unter Verwendung von 'typedef' in Etappen auf: typedef char *pc; /* Zeiger auf char */ typedef pc fpc(); /* Fun., die pc zurückgibt*/ typedef fpc *pfpc; /* Zeiger auf fpc */ typedef pfpc fpfpc(); /* Funk., die pfpc zurückgibt */ typedef fpfpc *pfpfpc; /* Zeiger auf .. */ pfpfpc a[N]; /* Array of ... */ 3. Man benutzt das Programm 'cdecl', das Englisch in C umwandelt und umgekehrt: cdecl> declare a as array of pointer to function returning pointer to function returning pointer to char char *(*(*a[])())() cdecl kann auch komplizierte Deklarationen erklären, bei casts helfen und aufzeigen, in welche Klammern die Argumente gehören (bei komplizierten Funktionsdeklarationen wie der obigen). cdecl-Versionen sind in Volume 14 von comp.sources.unix (siehe 17.12) und K&R II. Jedes gute Buch über C sollte erläutern, wie man komplizierte C-Deklarationen "von innen nach aussen" liest. References: K&R II Sec. 5.12 p. 122; H&S Sec. 5.10.1 p. 116. 10.5: Ich programmiere gerade einen Automaten, bei dem jeder Zustand durch eine eigene Funktion realisiert ist. Ich möchte die einzelnen Zustandsübergänge dadurch implementieren, dass eine Funktion einen Zeiger auf die Funktion des Folgezustands übergibt. Dabei werde ich durch die Deklarationsmethode von C behindert. Es gibt keine Möglichkeit, eine Funktion zu deklarieren, die einen Zeiger auf eine Funktion zurückgibt, die einen Zeiger auf eine Funktion ... . A: Das geht nur auf einem Umweg. Entweder muß die Funktion einen generischen Funktionszeiger zurückgeben, der vor Gebrauch durch Typumwandlung angepasst wird, oder eine Struktur, die lediglich einen Zeiger auf eine Funktion enthält, die diese Struktur zurückgibt. 10.6: Mein Compiler beschwert sich über eine ungültige Redeklaration einer Funktion, dabei definiere ich die Funktion nur einmal und rufe sie einmal auf. A: Funktionen, die aufgerufen werden, ohne dass ihre Deklaration im Sichtbarkeitsbereich ist (oder _bevor_ sie deklariert wurden), werden vom Compiler so eingesetzt, als ob sie den Typ int zurückgeben. Dies führt zu Unstimmigkeiten, falls sie später anders deklariert werden. Funktionen, die andere Typen als int zurückgeben, _müssen_ _vor_ ihrem Aufruf deklariert werden. Ansonsten kann man dies als Kompatibilitätsübung für C++ Projekte ansehen, in denen Prototypen für jegliche Art von Funktionen Pflicht sind. References: K&R I Sec. 4.2 pp. 70; K&R II Sec. 4.2 p. 72; ANSI Sec. 3.3.2.2 . 10.7: Wie deklariert und definiert man globale Variablen am besten? A: Zunächst sei festgestellt, dass es beliebig viele _Deklarationen_ einer 'globalen' (genauer gesagt: 'externen') Variablen geben kann, aber genau eine _Definition_. (Die Definition ist diejenige Deklaration, die letztendlich Speicher anfordert - und die Variable eventuell initialisiert). Am besten faßt man die Definitionen in einer zentralen C-Datei zusammen (relativ zum Programm oder Modul) mit externen Deklarationen in Header-Dateien, die per '#include' eingebunden werden, wo immer man die Deklarationen braucht. Die o.a. C-Datei, die die Definition enthält, sollte ebenfalls die Header-Datei einbinden, damit der Compiler sie mit der entsprechenden Deklaration vergleichen kann. Diese Vorgehensweise verspricht ein Höchstmaß an Portierbarkeit, und entspricht den Anforderungen von ANSI-C. Nebenbei bemerkt, benutzen Compiler und Linker unter Unix gewöhnlich ein 'Gemeinschaftsmodell', welches mehrfache (nichtinitialisierende) Definitionen unterstützt. Hin und wieder findet man Systeme, die ausdrückliche Initialisierung verlangen, um eine Definition von einer externen Deklaration zu unterscheiden. Man kann mit Präprozessortricks (geschickte '#define's) erreichen, dass eine Header-Datei mit Deklarationen genau einmal eingelesen wird, und die Deklarationen zu Definitionen werden. References: K&R I Sec. 4.5 pp. 76-7; K&R II Sec. 4.4 pp. 80-1; ANSI Sec. 3.1.2.2 (esp. Rationale), Secs. 3.7, 3.7.2, Sec. F.5.11; H&S Sec. 4.8 pp. 79-80; CT&P Sec. 4.2 pp. 54-56. 10.8: Was bedeutet 'extern' in einer Funktionsdeklaration? A: Es kann als Hinweis (für menschliche Leser) dienen, dass die Definition (wahrscheinlich) in einer anderen Quelldatei steht, aber der Compiler sieht keinen Unterschied zwischen extern int f(); und int f(); References: ANSI Sec. 3.1.2.2 . 10.9: Ich habe herausgefunden, wie Zeiger auf Funktionen zu deklarieren sind, nun würde ich gerne wissen, wie ich sie initialisieren kann. A: Z.B. extern int func(); int (*fp)() = func; Wenn der Name einer Funktion in einem Ausdruck erscheint ohne als Aufruf zu wirken (d.h. ohne folgende "(" ), dann wird die Funktion in einen Zeiger umgewandelt (ihre Adresse wird implizit ermittelt und eingesetzt), ähnlich wie es bei Arrays gehandhabt wird. Eine ausdrückliche (externe) Deklaration ist i.a. notwendig, da eine implizite (externe) Deklaration wegen der fehlenden Argumente sowie der Information über deren Typ nicht möglich ist. 10.10: Ich habe unterschiedliche Wege gesehen, wie Routinen über Zeiger auf Funktionen aufgerufen werden. Was steckt dahinter? A: Ursprünglich mußte ein Zeiger auf eine Funktion vor dem Aufruf durch Dereferenzieren ( *-Operator sowie ein zusätzliches Klammerpaar, um der Hierarchie der Operatoren zu genügen) in eine 'echte' Funktion umgewandelt werden: int r, func(), (*fp)() = func; r = (*fp)(); Man kann ebenso argumentieren, dass Funktionen immer über Zeiger aufgerufen werden, und dass 'echte' Funktionen implizit zu Zeigern umgewandelt werden, und sich folglich nicht abweichend verhalten. Diese Überlegung, die in den ANSI-Standard aufgenommen wurde, bedeutet, dass r = fp(); regelgerecht ist, und korrekt abgearbeitet wird, unabhängig davon, ob fp nun eine Funktion ist, oder ein Zeiger auf eine solche. (Diese Schreibweise war immer eindeutig, man konnte mit einem Zeiger auf eine Funktion nie etwas anderes machen, als eine Funktion aufzurufen.) Ein ausdrückliches * ist harmlos, und immer noch erlaubt (und empfohlen, falls Kompatibilität zu älteren Compilern wichtig ist). References: ANSI Sec. 3.3.2.2 p. 41, Rationale p. 41. 10.11: Welchen Sinn hat das Schlüsselwort 'auto'? A: Keinen. Es ist veraltet. --------------------------------------------------------------------------- Abschnitt 11: Stdio =================== 11.1: Was ist an diesem Code falsch: char c; while((c = getchar()) != EOF)... A: Die Variable zur Aufnahme des Rückgabewertes von getchar muß vom Typ int sein. getchar kann alle möglichen char- Werte, aber auch EOF liefern. Wird der Rückgabewert in einem char gespeichert, kann ein normales Zeichen als EOF mißverstanden werden, oder das EOF wird als char interpretiert und deshalb nicht als solches erkannt (insbesondere, wenn der Typ char vorzeichenlos ist). Referenz: CT&P Abschn. 5.1 S. 70. 11.2: Wie kann ich ein '%' Zeichen in einem printf Format String drucken? Ich habe \% versucht, aber es hat nicht funktioniert. A: Einfach das Prozent Zeichen verdoppeln: %% . Referenzen: K&R I Abschn. 7.3 S. 147; K&R II Abschn. 7.2 S. 154 ANSI Abschn. 4.9.6.1 . 11.3: Warum funktioniert der Code scanf("%d", i); nicht? A: scanf braucht Zeiger auf die Variablen, denen es die Werte zuweisen soll; es muß also scanf("%d", &i); heissen. 11.4: Warum funktioniert der Code: double d; scanf("%f", &d); nicht? A: scanf benutzt %lf für Werte vom Typ double und %f für float. (Man beachte den Unterschied zu printf, das %f sowohl für double als auch für float benutzt, da Argumente in variablen Argumentlisten in C den Default Promotions unterworfen werden). 11.5: Warum funktioniert der Code while(!feof(infp)) { fgets(buf, MAXLINE, infp); fputs(buf, outfp); } nicht? A: Die Ein-/Ausgabe von C ist anders als die von PASCAL. EOF wird nur angezeigt _nachdem_ eine Eingabefunktion versucht hat zu lesen und dabei das Dateiende erreicht wurde. Üblicherweise sollte man den Rückgabewert der Eingabefunktion prüfen (in diesem Fall fgets), dann braucht man feof() meist nicht zu benutzen. 11.6: Warum sagt jeder, dass man gets() nicht benutzen soll? A: Bei gets() kann die Größe des Eingabepuffers nicht übergeben werden. Somit kann ein Überlaufen dieses Puffers nicht verhindert werden. Siehe Frage 3.1, dort ist ein Code-Fragments gezeigt, das die Verwendung von fgets() anstelle von gets() illustriert. 11.7: Warum enthält errno den Wert ENOTTY nach dem Aufruf von printf? A: Viele Implementationen des stdio Pakets passen ihr Verhalten geringfügig an, wenn stdout ein Terminal ist. Um die Unterscheidung durchzuführen, führen diese Implementationen eine Operation aus, die fehlschlägt (mit ENOTTY) wenn stdout kein Terminal ist. Auch wenn die Ausgabeoperation ansonsten erfolgreich durchgeführt wurde, enthält errno noch den Wert ENOTTY. Referenz: CT&P Abschn. 5.4 p. 73. 11.8: Mein Programm erwartet eine Eingabe, wobei zwischenzeitliche Ausgaben nicht immer auf dem Bildschirm erscheinen, insbesondere wenn die Ausgabe durch eine Pipe zu einem anderen Programm geleitet wird. A: Es ist das Beste, ein explizites fflush(stdout) zu benutzen, wenn die Ausgabe definitiv sichtbar sein soll. Verschiedene Mechanismen versuchen das fflush zur "rechten Zeit" auszuführen, aber sie werden meist nur aktiv, wenn stdout ein Terminal ist. (Siehe Frage 11.7.) 11.9: Wenn ich mit scanf von einer Tastatur einlese, dann scheint es auf die Eingabe einer zusätzlichen Zeile zu warten. A: scanf wurde für die Eingabe eines freien Formates entwickelt, das man selten braucht, wenn man von einer Tastatur einliest. Insbesondere ein "\n" in einem Formatstring bedeutet _nicht_, dass auf ein Newline gewartet werden soll, sondern daß Zeichen gelesen und verworfen werden sollen, bis ein whitespace Zeichen kommt. Ein damit zusammenhängendes Problem ist, dass unerwartete nichtnumerische Eingaben scanf dazu veranlassen können zu "Klemmen". Wegen dieser Probleme ist es üblicherweise besser, fgets zum Lesen einer ganzen Zeile und anschließend sscanf oder andere Stringfunktionen zum Zerlegen des Zeilenpuffers zu verwenden. Wenn man sscanf benutzt, sollte man nicht vergessen, den Rückgabewert zu überprüfen um sicherzugehen, dass die erwartete Anzahl von Elementen gefunden wurde. 11.10: Ich versuche eine Datei direkt zu aktualisieren, indem ich sie mit fopen Mode "r+" öffne, dann einen bestimmten String lese und abschließend den modifizierten String zurückschreibe. Leider scheint das nicht zu funktionieren. A: Man muß vor dem Schreiben unbedingt fseek aufrufen, um erstens an den Anfang des Strings zurückzukehren, der überschrieben werden soll und zweitens weil ein Aufruf von fseek oder fflush immer zwischen Lese- und Schreibvorgang erforderlich ist, wenn man die Lese/ Schreib "+" Modi benutzt. Man sollte außerdem bedenken, dass man nur Zeichen mit der gleichen Anzahl von Ersatzzeichen überschreiben kann (siehe auch Frage 17.4). Referenz: ANSI Abschn. 4.9.5.3 S. 131. 11.11: Wie kann ich jeweils ein einzelnes Zeichen einlesen, ohne auf die Return-Taste zu warten? A: Siehe Frage 16.1. 11.12: Wie kann ich bereitstehende Zeichen löschen, so dass die vom Benutzer bereits eingegebenen Zeichen nicht bei der nächsten Eingabeaufforderung eingelesen werden? Kann man dazu fflush(stdin) verwenden? A: Die Anweisung "fflush(stdin);" kann dafür nicht verwendet werden. Die Funktionsweise von fflush ist nur für Ausgaben festgelegt worden. Deshalb darf der an fflush übergebene Zeiger nur auf einen Ein-/Ausgabestrom, dessen letzte Aktion keine Eingabe war, oder einen Ausgabestrom verweisen(7.19.5.2p2 C99)[*]. Die Funktion fflush dient dazu, gepufferte Ausgabedaten eines Stromes vorzeitig an die Laufzeitumgebung zu übergeben. Normalerweise geschieht dies automatisch, wenn der Puffer voll ist und weitere Daten geschrieben werden sollen, oder wenn fclose aufgerufen wird. Das Verhalten von fflush auf einen Ein-/Ausgabestrom, dessen letzte Aktion keine Ausgabe war, oder einen Eingabestrom, ist undefiniert. Auf Eingabedaten ist fflush somit nicht anwendbar! Ein Pendant zu fflush im Sinne des Verwerfens ungelesener Eingaben gibt es nicht. Eine solche Funktion wäre schwer plattformübergreifend zu realisieren, weil sich ungelesene Zeichen nicht nur in den Puffern des laufenden Programms, sondern auch in denen der Laufzeitumgebung befinden können. Eine Lösung ist das Einlesen sämtlicher Eingaben. Das Interpretieren der Eingabe erfolgt zu einem späteren Zeitpunkt, und Fehleingaben könnten dann ignoriert werden. Dazu bieten sich die fgets- und sscanf-Funktionen an. [*] An fflush kann auch ein Nullzeiger übergeben werden. Ein solcher Aufruf entspricht einem Aufruf von fflush für alle aktuell geöffneten Ein-/Ausgabeströme, deren letzte Aktion eine Ausgabe war, bzw. alle geöffneten Ausgabeströme (7.19.5.2p3 C99). 11.13: Wie kann ich stdin oder stdout von einem Programm aus in eine Datei umleiten? A: Unter Verwendung von freopen. 11.14: Ich habe einmal freopen benutzt. Wie kann ich das originale stdin oder stdout zurückbekommen? A: Wenn man hin und herschalten muß, dann ist die Benutzung von freopen nicht sinnvoll. Besser ist die Verwendung einer eigenen Variable für das Ein- bzw. Ausgabefile. Dieser Variable kann man je nach Bedarf einen Zeiger auf den einen oder anderen Stream zuweisen, dabei bleiben die orginalen Variablen für stdin und stdout unverändert. 11.15: Wie kann ich den Namen einer Datei ermitteln, der zu einem bestimmten Dateihandle gehört? A: Dieses Problem ist grundsätzlich unlösbar. Unter UNIX z.B. wäre theoretisch ein Absuchen der gesamten Festplatte erforderlich (was u.U. spezielle Zugriffsrechte erfordert) und dieser Versuch würde fehlschlagen, wenn das Dateihandle eine Pipe oder eine gelöschte Datei referenziert. Bei einer Datei mit mehreren Links ist außerdem die Antwort irreführend. Es ist das Beste, sich die Namen der Dateien selbst zu merken, wenn man sie öffnet (evtl. mit einer Wrapper-Funktion um fopen). --------------------------------------------------------------------------- Abschnitt 12: Bibliotheksfunktionen =================================== 12.1: Warum setzt strncpy nicht immer ein '\0' an das Ende des Ziel-Strings? A: strncpy wurde ursprünglich entwickelt, um eine inzwischen veraltete Datenstruktur, nämlich einen String mit fester Länge, der nicht unbedingt mit '\0' abgeschlossen sein muß, zu verarbeiten. strncpy ist zugestandenermaßen ein bißchen schwierig in anderen Zusammenhängen zu benutzen, weil man oft ein '\0' per Hand an den Zielstring anhängen muß. 12.2: Ich versuche, ein Feld von Strings unter Verwendung von strcmp als Vergleichsfunktion mit qsort zu sortieren, aber es funktioniert nicht. A: Mit einem "Feld von Strings" ist vermutlich ein "Feld von Zeigern auf char" gemeint. Die Argumente für die Vergleichsfunktion von qsort sind Zeiger auf die Objekte, die sortiert werden sollen, in diesem Fall Zeiger auf Zeiger, die auf char zeigen. (strcmp akzeptiert allerdings nur einfache Zeiger auf char.) Die Argumente der Vergleichsfunktion werden als "generische Zeiger" bezeichnet, const void * oder char *. Sie müssen wieder in das konvertiert werden, was sie "wirklich sind" (char **) und dereferenziert, damit sie char * liefern, die sinnvoll verglichen werden können. Man könnte eine Vergleichsfunktion folgendermaßen schreiben: int pstrcmp(p1, p2) /* vergleicht Strings mittels Zeigern */ char *p1, *p2; /* const void * für ANSI C */ { return strcmp(*(char **)p1, *(char **)p2); } Man beachte bei der Diskussion in K&R II Abschn. 5.11 S. 119-20, dass dort nicht die Standard Bibliothek qsort diskutiert wird. 12.3: Jetzt versuche ich ein Feld von Strukturen mit qsort zu sortieren. Meine Vergleichsfunktion nimmt Zeiger auf Strukturen, aber der Compiler beschwert sich, dass die Funktion vom falschen Typ für qsort ist. Wie kann ich den Funktionszeiger umwandeln, um diese Warnung abzuschalten? A: Die Umwandlung muß in der Vergleichsfunktion stattfinden, welche so deklariert sein muß, dass sie "generische Zeiger" (const void * oder char *) akzeptiert, wie oben bei Frage 12.2 diskutiert. Der Code könnte folgendermaßen aussehen: int mystructcmp(p1, p2) char *p1, *p2; /* const void * für ANSI C */ { struct mystruct *sp1 = (struct mystruct *)p1; struct mystruct *sp2 = (struct mystruct *)p2; /* jetzt vergleiche sp1->whatever und sp2-> ... */ } (Wenn man andererseits Zeiger auf Strukturen sortiert, dann benötigt man einen Umweg, wie bei Frage 12.2: sp1 = *(struct mystruct **)p1 .) 12.4: Wie kann ich Zahlen in Strings umwandeln (das Gegenteil zu atoi)? Gibt es eine itoa Funktion? A: Man kann dafür einfach sprintf benutzen. (Man muß irgendwo den Platz für das Ergebnis reservieren, siehe Fragen 3.1 und 3.2. Keine Sorge, dass sprintf Overkill wäre und möglicherweise Laufzeit oder Speicherplatz verschwendet. Es funktioniert in der Praxis recht gut.) Referenzen: K&R I Abschn. 3.6 S. 60; K&R II Abschn. 3.6 S. 64. 12.5: Wie kann ich das aktuelle Datum oder die Tageszeit in einem C Programm ermitteln? A: Man benutze einfach die Funktionen time, ctime, und/oder localtime. (Diese Funktionen gibt es bereits seit Jahren und sie gehören zum ANSI Standard.) Hier ist ein einfaches Beispiel: #include #include main() { time_t now = time((time_t *)NULL); printf("It's %.24s.\n", ctime(&now)); return 0; } Referenzen: ANSI Abschn. 4.12 . 12.6: Ich weiß, dass die Bibliotheksfunktion localtime ein time_t in eine Struktur tm aufteilt, und dass ctime ein time_t in einen druckbaren String umwandelt. Wie kann ich die entgegengesetzte Operation, d.h. die Umwandlung einer Struktur tm oder eines Strings in ein time_t, realisieren? A: ANSI C spezifiziert eine Bibliotheksfunktion, mktime, die eine Struktur tm in ein time_t umwandelt. Verschiedene Public Domain Versionen dieser Funktion sind verfügbar, falls ein Compiler das noch nicht unterstützen sollte. Die Umwandlung eines Strings in ein time_t ist schwieriger, wegen der großen Vielfältigkeit von Datums- und Zeitformaten, die dabei berücksichtigt werden sollte. Manche Systeme stellen eine Funktion strptime zur Verfügung; eine andere beliebte Funktion ist partime (weit verbreitet mit dem RCS Paket), aber diese werden wahrscheinlich nicht standardisiert werden. Referenzen: K&R II Abschn. B10 S. 256; H&S Abschn. 20.4 S. 361; ANSI Abschn. 4.12.2.3 . 12.7: Wie kann ich n Tage zu einem Datum addieren? Wie kann ich die Differenz zwischen zwei Daten bestimmen? A: Die ANSI/ISO Standard C Funktionen mktime und difftime liefern eine gewisse Hilfe für beide Probleme. mktime() akzeptiert nicht-normalisierte Daten, so dass es einfach ist, eine gefüllte Struktur tm zu nehmen, etwas zu dem Element tm_mday zu addieren bzw. davon zu subtrahieren und anschließend mktime() aufzurufen um Jahr, Monat und Tag wieder zu normalisieren (und in einen time_t Wert umzuwandeln). difftime() berechnet die Differenz in Sekunden zwischen zwei time_t Werten; mktime() kann benutzt werden, um die time_t Werte für zwei zu subtrahierende Daten zu ermitteln. (Man beachte jedoch, dass diese Lösungen nur für Daten funktionieren, die in dem Bereich liegen, der mit time_t Werten dargestellt werden kann, und dass nicht alle Tage 86400 Sekunden lang sind.) Siehe auch Frage 12.6 und 17.28. Referenzen: K&R II Abschn. B10 S. 256; H&S Abschn. 20.4, 20.5 S. 361-362; ANSI Abschn. 4.12.2.2, 4.12.2.3 . 12.8: Ich brauche einen Zufallszahlengenerator. A: Die Standard C Bibliothek hat einen: rand(). Die Implementation ist nicht auf jedem System perfekt, aber einen besseren zu schreiben ist nicht unbedingt einfach. Referenzen: ANSI Abschn. 4.10.2.1 S. 154; Knuth Band 2 Kap. 3 S. 1-177. 12.9: Wie kann ich zufällige ganze Zahlen in einem bestimmten Bereich erzeugen? A: Der übliche Weg, rand() % N (wobei N natürlich der Bereich ist) ist dürftig, weil die unteren Bits von vielen Zufallszahlengeneratoren genau genommen nicht zufällig sind. (Siehe Frage 12.11.) Eine bessere Methode ist folgende (int)((double)rand() / ((double)RAND_MAX + 1) * N) Wenn man Bedenken wegen der Benutzung von Gleitkommazahlen hat, dann kann man folgendes versuchen rand() / (RAND_MAX / N + 1) Beide Methoden erfordern unverkennbar die Kenntnis von RAND_MAX (was ANSI in definiert) und setzen voraus, dass N viel kleiner ist als RAND_MAX. 12.10: Jedes mal, wenn ich mein Programm starte, bekomme ich die gleiche Zahlenfolge von rand() geliefert. A: Man kann srand() aufrufen, um den Pseudo-Zufallszahlengenerator mit einem zufälligeren Startwert zu initialisieren. Beliebte Startwerte sind die Tageszeit, oder die Zeit, die vergangen ist, bevor der Benutzer eine Taste betätigt hat (allerdings ist der Zeitpunkt einer Tastenbetätigung kaum portabel zu bestimmen; siehe Frage 16.10). Referenzen: ANSI Abschn. 4.10.2.2 S. 154. 12.11: Ich brauche einen zufälligen wahr/falsch Wert. Deshalb habe ich rand() % 2 benutzt, erhalte aber nur abwechselnd 0, 1, 0, 1, 0... A: Einfache Pseudo-Zufallszahlengeneratoren (wie die, die unglücklicherweise mit manchen Systemen geliefert werden) sind in den unteren Bits nicht sehr zufällig. Man sollte versuchen, die oberen Bits zu benutzen. Siehe Frage 12.9. 12.12: Ich versuche ein altes Programm zu portieren. Warum bekomme ich "undefined external" Fehlermeldungen für index(), rindex(), bcopy(), bcmp() und bzero()? A: Diese Funktionen sind unterschiedlich veraltet. Stattdessen sollten folgende Funktionen benutzt werden: index() : strchr() rindex() : strrchr() bcmp() : memcmp() bzero() : memset() mit zweitem Argument 0 Statt bcopy() wird memmove() nach dem Vertauschen des ersten und zweiten Arguments benutzt (siehe auch Frage 5.15). 12.13: Ich bekomme ständig Fehlermeldungen wegen undefinierter Bibliotheksfunktionen, obwohl ich alle Headerfiles richtig eingebunden habe. A: In manchen Fällen (insbesondere, wenn es sich um nicht standardisierte Funktionen handelt) muß man explizit die richtigen Bibliotheken angeben, die beim Linken des Programms durchsucht werden sollen. Siehe auch Frage 15.2. 12.14: Ich bekomme immer Errors wegen undefinierter Bibliotheks-Funktionen, obwohl ich -l zur Anforderung der Bibliotheken während des Linkens benutze. A: Viele Linker führen nur einen Lauf über die Liste der angegebenen Objektdateien und Bibliotheken aus und extrahieren aus den Bibliotheken nur die Module, die den Referenzen entsprechen, die bis dahin undefiniert waren. Deshalb ist die Reihenfolge, in der die Bibliotheken aufgelistet werden hinsichtlich der Objektdateien (und auch untereinander) wichtig; üblicherweise läßt man die Bibliotheken zuletzt durchsuchen. (z.B. unter UNIX gibt man die -l Schalter am Ende der Kommandozeile an.) 12.15: Ich brauche Quellcode zur Auswertung regulärer Ausdrücke. A: Man sollte sich die regexp Bibliothek (wird mit vielen UNIX Systemen geliefert) ansehen, oder sich Henry Spencer's regexp Paket von cs.toronto.edu in pub/regexp.shar.Z beschaffen (siehe auch Frage 17.12). 12.16: Wie kann ich die Kommandozeile in durch Whitespaces getrennte Argumente aufteilen, wie die Parameter argc und argv von main()? A: Die meisten Systeme haben eine Funktion namens strtok, allerdings kann es schwer zu benutzen sein und es kann sein, dass es nicht das tut, was man gern hätte (z.B. Quoting). Referenzen: ANSI Abschn. 4.11.5.8; K&R II Abschn. B3 S. 250; H&S Abschn. 15.7; PCS S. 178. --------------------------------------------------------------------------- Abschnitt 13: Lint ================== 13.1: Ich habe dieses Programm eingetippt und es verhält sich seltsam. Was könnte daran falsch sein? A: Vielen Fehlern kommt man mit lint auf die Spur (evtl. mit -a, -c, -h, -p und/oder anderen Optionen). Viele C Compiler sind in Wirklichkeit nur halbe Compiler, nicht dazu ausersehen, verschiedene Quellcode-Probleme zu erkennen, wodurch die Erzeugung von nicht funktionierendem Code verhindert werden könnte. 13.2: Wie kann ich die Message: "warning: possible pointer alignment problem", die lint bei jedem Aufruf von malloc erzeugt, abschalten. A: Das Problem besteht darin, dass herkömmliche Versionen von lint nicht wissen, und es ihnen auch nicht mitgeteilt werden kann, dass malloc einen Zeiger auf einen Bereich zurückgibt, der zur Speicherung beliebiger Objekttypen geeignet ist. Es ist möglich, eine Pseudo-Implementation von malloc zu liefern, die innerhalb eines "#ifdef lint" mit einem #define diese Warnung abschaltet. Aber eine solch einfache Definition unterdrückt auch wichtige Messages über wirklich fehlerhafte Aufrufe. Es dürfte einfacher sein, diese Message zu ignorieren, vielleicht auf automatisierte Art und Weise mit grep -v. 13.3: Wo kann ich ein ANSI-kompatibles lint bekommen ? A: Ein Produkt namens FlexeLint ist erhältlich (als "verborgener Quellcode", zur Übersetzung auf den meisten Systemen) bei Gimpel Software 3207 Hogarth Lane Collegeville, PA 19426 USA (+1) 610 584 4261 gimpel@netaxs.com Das lint von System V release 4 ist ANSI-kompatibel und ist einzeln erhältlich (gebündelt mit anderen C-Tools) von den UNIX Support Labs oder von den System V Wiederverkäufern. Ein weiteres ANSI-kompatibles lint (das auch eine bessere formale Prüfung durchführen kann) ist LCLint, erhältlich unter ftp://ftp.sds.lcs.lcs.mit.edu/pub/lclint http://www.sds.lcs.mit.edu/lclint In Ermangelung von lint versuchen viele moderne Compiler die meisten Probleme so zu erkennen, wie ein gutes lint es tun würde. --------------------------------------------------------------------------- Abschnitt 14: Programmierstil ============================= 14.1: Hier ist ein hübscher Trick: if (!strcmp(s1,s2)) Ist das guter Programmierstil? A: Ein solches Konstrukt ist nicht unbedingt guter Stil, auch wenn es oft verwendet wird. Der Test ist erfolgreich, wenn die beiden Strings gleich sind, aber die Form suggeriert, dass hier auf Ungleichheit getestet wird. Eine andere Lösung ist ein Macro: #define Streq(s1, s2) (strcmp((s1), (s2)) == 0) Stilfragen können - ähnlich wie religiöse Themen - endlos diskutiert werden. Guter Stil ist ein anzustrebendes Ziel und kann durchaus erkannt werden, es ist aber praktisch unmöglich, guten Programmierstil festzuschreiben. 14.2: Was ist der beste Codierstil für C? A: "Programmieren in C" von K&R beschreibt einen Stil, der sehr verbreitet ist, enthält aber gleichzeitig folgenden Absatz: Die Position von geschweiften Klammern ist weniger wichtig, obwohl manche Leute da ganz fanatisch werden. Wir haben in diesem Buch eine von mehreren populären Stilrichtungen benutzt. Gewöhnen Sie sich einen Stil an, den Sie für zweckmäßig halten, und benutzen Sie ihn dann grundsätzlich. Es ist viel wichtiger, dass das gewählte Code-Layout konsistent ist (konsistent innerhalb des eigenen oder mitverwendeten Codes), als das es "perfekt" ist. Wem die Umgebung (z.B. die Abteilung oder Firma) keinen Stil vorgibt, und wer keine Lust hat, einen eigenen Stil zu erfinden, der kann einfach den von K&R übernehmen. (Die Vor- und Nachteile der verschiedenen Einrücktechniken und Klammersetzungen können bis ins kleinste erörtert werden, sie sind es aber nicht wert, hier Platz zu verschwenden. Siehe auch das Indian Hill Style Guide.) Der schwer zu fassende Begriff "Guter Programmierstil" beeinhaltet sehr viel mehr als nur Regeln zur Formatierung von Quelltexten, Zeit für die Formatierung von Quellcode unter Vernachlässigung wichtigerer Punkte aufwenden ist Unfug. References: K&R Sec. 1.2 p. 10. 14.3: Wo gibt es das "Indian Hill Style Guide" und die anderen angesprochenen Standards? A: Verschiedene Dokumente sind per anonymous ftp erhältlich von Host: Datei oder Verzeichnis: cs.washington.edu ~ftp/pub/cstyle.tar.Z (128.95.1.4) (aktuelles Indian Hill Guide) cs.toronto.edu doc/programming ftp.cs.umd.edu pub/style-guide --------------------------------------------------------------------------- Abschnitt 15: Gleitkomma-Probleme ================================= 15.1: Meine Gleitkommarechnungen zeigen ein eigenartiges Verhalten und/oder führen auf unterschiedlichen Rechnern zu unterschiedlichen Ergebnissen. A: Bei Problemen sollte zunächst sichergestellt werden, dass mit #include eingebunden wird, und dass alle Funktionen, die 'double' zurückgeben auch korrekt deklariert sind. Wenn sich das Problem nicht so einfach lösen läßt, erinnere Dich, dass die von den meisten Digitalrechnern verwendeten Gleitkommaformate zwar eine relativ gute, aber keine exakte Simulation der Arithmetik für reelle Zahlen ermöglichen. Gleitkommaunterlauf, Fortpflanzung von Rundungsfehlern und andere Anomalien führen oft zu Problemen. Gleitkommaergebnisse sind nie exakt, und es ist praktisch *immer* ein Fehler, Gleitkommawerte auf Gleichheit zu prüfen. Diese Probleme hat C mit anderen Sprachen gemeinsam. Gleitkommasemantik ist üblicherweise definiert als "wie auch immer der jeweilige Prozessor es macht", andernfalls hätte ein Compiler für einen Rechner mit dem 'falschen' Modell extrem aufwendige Emulationen einzubinden. Dieser Artikel kann keine Liste der Fallgruben im Bereich der Gleitkommaarithmetik bzw. der sicheren Umgehungen derselben sein. Gute Programmierlehrbücher sollten entsprechende Einführungen enthalten. References: EoPS Sec. 6 pp. 115-8. 15.2: Ich arbeite mit trigonometrischen Funktionen, habe 'math.h' geladen, erhalte aber "undefined: _sin" u.ä. als Fehlermeldung. A: Wird die Mathe-Bibliothek dazugelinkt? Unter Unix z.B. ist '-lm' als Linkeroption erforderlich, und zwar am Ende der Linker Kommandozeile. (s.a. 12.14) 15.3: Wie runde ich Gleitkommazahlen? A: Eine einfache Lösung, die aber nur für positive Zahlen korrekt arbeitet ist: (int)(x + 0.5) Auch für negative Zahlen funktioniert (int)floor (x + 0.5) (letztere Antwort stammt aus einer Diskussion in d.c.l.c). 15.4: Wie teste ich auf IEEE NaN und andere spezielle Werte? A: Viele Systeme mit hochwertigen IEEE Gleitkomma-Implementierungen stellen entsprechende Tests zur Verfügung (z.B. ein Makro isnan()), um sauber mit diesen 'Werten' umzugehen; die Numerical C Extensions Group (NCEG) erarbeitet formelle Standards für derartige Hilfmittel. Ein grober aber i.a. erfolgreicher Test auf NaN ist #define isnan(x) ((x) != (x)) wenngleich ein eifriger Optimizer den Ausdruck u.U. wegoptimiert. 15.5: Mein Turbo C Programm stürzt mit der Meldung "floating point formats not linked" ab. A: Einige Compiler für Kleinrechner, einschließlich Turbo C (und Ritchies ursprünglichem PDP-11 Compiler), binden keine Gleitkommaunterstützung ein, wenn es so aussieht, als ob sie nicht benötigt wird. Insbesondere benötigen die Versionen von printf und scanf, die ohne Gleitkommaunterstützung auskommen, indem sie auf die Behandlung von %e, %f und %g verzichten, weniger Speicher. Bei Turbo C scheint die Entscheidung über die Notwendigkeit der Einbindung von Gleitkommacode unzuverlässig zu sein. Deshalb ist manchmal ein (sonst überflüssiger) Aufruf einer Gleitkomma-Bibliotheksfunktion nötig, um dem Erkennungsmechanismus auf die Sprünge zu helfen. 15.6: Wie wird eine Variable nach IEEE-Gleitkommaformat im Rechner dargestellt? A: Diese Frage läßt sich ohne Kenntnis von CPU, Compiler und BS nicht beantworten. IEEE-754 (der FP-Standard) definiert nur die Bedeutung der Bits in ihrer logischen (nicht physikalischen) Reihenfolge. IEEE-754 definiert 3 Gleitkommatypen : single, double und double-extended. Single und double-Typen sind 32 bzw. 64 Bit lang, double-extended ist nicht vollständig definiert und wird von Compilerherstellern derzeit meist mit einer Länge von 80 Bit implementiert. Es ist für die interne Darstellung in Koprozessoren konzipiert worden und soll hier nicht weiter behandelt werden. Das 32/64bit Format sieht wie folgt aus: VZ Charakteristik Mantisse I I // I // I 32: 31 .... ... 23 22 ... ... 0 64: 63 .... ... 52 51 ... ... 0 Das höchstwertige Bit gibt das Vorzeichen der Zahl an (wie üblich : 0 <=> '+' ; 1 <=> '-' ). Die Charakteristik entspricht einem derart geshifteten Exponenten, dass der Wertebereich von 0 bis 2^N - 1 reicht. Hierbei zeigen die Grenzwerte Unterlauf bzw. NaN an. Ansonsten gilt: 32: Char. = Exp + 127 64: Char. = Exp + 1023 Die Mantisse liege in normalisierter Form vor, d.h. 1.0 <= M < 2.0, - 1.00...00 <= M <= 1.11...11 . Diese Information hält man fest, läßt das höchstwertige Bit jedoch wegfallen. Dadurch hat man 1 Bit Auflösung gewonnen, was im Falle des 'single'-Formats den Unter schied zwischen sicheren 6 bzw. 7 Dezimalstellen bedeutet. Die Auflösung beträgt 24 bzw 53 bit; dies entspricht 7 bzw. 15 signifikanten Dezimalstellen (Bitte nicht mit Nachkommastellen verwechseln!). --------------------------------------------------------------------------- Abschnitt 16: Systemabhängiges ============================== 16.1: Wie kann ich einen einzelnen Buchstaben von der Tastatur auslesen, ohne auf RETURN zu warten? A: Im Gegensatz zum allgemeinen Glauben und den Wünschen vieler Programmierer ist dies keine C-Frage. (Genauso wenig wie ähnliche Fragen, die das ECHO von Tastatur-Eingaben betreffen.) Der Transport von Buchstaben von einer "Tastatur" zu einem C-Programm ist eine Funktion des entsprechenden Betriebssystems, und wurde nicht durch die Sprache C standardisiert. Manche Versionen von CURSES haben die Funktion cbreak(), welche diese Funktionalität beinhaltet. Um speziell ein kurzes Paßwort ohne ECHO einzulesen, kann getpass() verwendet werden. Unter UNIX kann IOCTL verwendet werden, um die Terminal-Treiber-Modi zu verändern (CBREAK oder RAW bei "klassischen" Versionen; ICANON, c_cc[VMIN] und c_cc[VTIME] bei System V- oder POSIX-Systemen). Unter MSDOS kann getch() benutzt werden. Bei VMS sollten die Bildschirmsteuerungs-Routinen (SMG$) oder CURSES benutzt oder systemnahe $QIOs mit den IO$_READVBLK (und vielleicht IO$M_NOECHO) Funktionscodes verwendet werden, um einzelne Zeichen abzufragen. Bei anderen Systemen müssen entsprechende Funktionen selbst entwickelt werden. Man sollte beachten, dass manche Betriebssysteme es generell nicht erlauben, da das Einlesen der Zeichen in Eingabezeilen durch seperate Prozessoren erfolgt, die nicht der direkten Kontrolle der CPU unterstehen, auf der das Programm läuft. Betriebssystemspezifische Fragen sind in de.comp.lang.c nicht angebracht. Viele dieser Fragen werden in FAQs der Gruppen comp.unix.questions, comp.os.msdos.programmer usw. beantwortet. Viele Antworten sind nicht einmal zwischen verschiedenen Betriebssystemvarianten gleich. Man sollte bedenken, dass die Beantwortung von betriebssystemspezifischen Fragen auf anderen Systemen anders ausfallen kann, als auf dem eigenen. Querverweise: PCS Sek. 10 S. 128-9, Sek. 10.1 S. 130-1. 16.2: Wie finde ich heraus, ob Zeichen zum Einlesen zur Verfügung stehen (und wenn, wieviele)? Oder wie kann ich Zeichen einlesen, ohne dass mein Prozeß blockiert, wenn keine da sind? A: Dies ist ebenfalls eine völlig betriebssystemspezifische Frage. Manche Version von CURSES haben die nodelay() Funktion. Abhängig vom System kann ebenso "nichtblockierende I/O" verwendet werden, oder ein Systemaufruf namens "select" oder der FIONREAD IOCTL-Aufruf oder kbhit() oder rdchk() oder die O_NDELAY Option für open() oder fcntl(). 16.3: Wie kann ich den Bildschirm löschen? Wie kann ich Text invers darstellen? A: So etwas hängt vom verwendeten Terminaltyp (oder dem Bildschirm) ab. Zur Lösung des Problems kann eine Bibliothek wie TERMCAP oder CURSES, oder einige systemspezifische Routinen verwendet werden. 16.4: Wie lese ich die Maus aus? A: Hierüber informiert die Systembeschreibung oder eine geeignete systemspezifische Newsgruppe (deren FAQ man zunächst anschauen sollte). Die Maussteuerung ist beim X11-Window-System, MS-DOS, Macintosh und vermutlich jedem anderen System völlig verschieden. 16.5: Wie kann mein Programm den kompletten Pfadnamen der ausführbaren Datei ermitteln, von der es aufgerufen wurde? A: argv[0] könnte den ganzen Pfadnamen oder einen Teil davon beinhalten, oder auch gar nichts. Man kann die Such-Pfad Logik des Befehlsinterpreters kopieren, wenn der Name in argv[0] zwar vorhanden, aber nicht komplett ist. Es gibt aber keine garantierte oder portable Lösung. [Anmerkung Uz:] Es gibt ein Paket namens selfdir, dass es z.B. irgendwo auf ftp://ftp.uni-stuttgart.de gibt und das diese Problemstellung für Unix-Systeme löst. [Ende Anmerkung Uz] 16.6: Wie kann ein Prozeß eine Umgebungsvariable seines aufrufenden Programmes verändern? A: Im allgemeinen nicht. Verschiedene Betriebssysteme implementieren eine Funktionalität ähnlich wie bei Unix-Umgebungen. Ob eine "Umgebung" sinnvoll geändert werden kann, und wenn ja, wie, ist eine systemspezifische Frage. Unter Unix kann ein Prozeß seine eigene Umgebung verändern (einige Systeme unterstützen hierfür die Funktionen setenv() und/oder putenv()), und die veränderte Umgebung wird gewöhnlich an alle Kind-Prozesse weitergegeben, jedoch _nicht_ zurück an den Eltern-Prozeß. 16.7: Wie kann ich überprüfen ob eine Datei existiert? Ich möchte den Anwender fragen, bevor eine vorhandene Datei überschrieben wird. A: Unter Unix-Systemen kann man die Funktion access() verwenden, obwohl es dabei ein paar Probleme gibt (sie bildet mit der folgenden Aktion keine atomare [ununterbrechbare] Einheit und kann Anomalien zeigen, wenn sie von setuid-Programmen aufgerufen wird). Eine andere (vielleicht bessere) Möglichkeit ist es, stat() für auf diese Datei aufzurufen. Ansonsten ist der einzige, garantiert funktionierende und portable Weg, das Vorhandensein einer Datei zu überprüfen, zu versuchen diese zu öffnen (was jedoch nicht verhindert eine bestehende Datei zu öffnen, es sei denn es gibt soetwas, wie die BSD-Unix O_EXCL Option für die Funktion open()). 16.8: Wie finde ich die Größe einer Datei heraus, bevor ich diese einlese? A: Wenn mit "Größe einer Datei" die Anzahl der Bytes gemeint ist, die man unter C einlesen kann, dann ist es unmöglich, deren Anzahl im voraus festzustellen. Unter Unix gibt stat() die genaue Antwort, viele andere Systeme unterstützen Unix-ähnliche stat()-Funktionen, welche die annähernde Anzahl angeben. Man kann mit der Funktion fseek() zum Ende der Datei gehen und dann ftell() verwenden (um die absolute Position innerhalb der Datei zu bestimmen Anm. d. Übers), aber diese Methode ist nicht portabel (sie ergibt lediglich unter UNIX eine genaue Antwort und ansonsten eine quasi-genaue Antwort, die nur für binäre Dateien im Sinne von ANSI-C gültig ist). Manche Systeme unterstützen Routinen wie filesize() oder filelength(). Muß die Größe der Datei tatsächlich vorher bestimmt werden? Da der genaueste Weg, die Größe einer Datei zu bestimmen, darin besteht, diese zu öffnen und sie zu lesen, kann vielleicht der Programmcode so umgestellt werden, dass die Größe während des Lesens festgestellt wird. 16.9: Wie kann eine Datei gekürzt werden, ohne sie komplett zu löschen oder erneut zu schreiben? A: BSD-Systeme bieten ftruncate(), verschiedene andere unterstützen chsize(), und ein paar unterstützen vielleicht eine (möglicherweise undokumentierte) fcntl-Option namens F_FREESP. Unter MS-DOS kann man manchmal write(fd, "", 0) verwenden. Eine portable Lösung gibt es nicht. 16.10: Wie kann ich eine Pause (delay) oder die Messung einer Anwender-Reaktion einbauen, welche eine Auflösung im Bereich von Sekundenbruchteilen hat? A: Leider gibt es keinen portablen Weg. V7 Unix und vergleichbare System boten eine ziemlich brauchbare ftime() Funktion mit einer Auflösung bis hin zu einer Millisekunde, aber diese ist bei System V und Posix verschwunden. Andere Routinen, nach denen man suchen kann, sind nap(), setitimer(), msleep(), usleep(), clock() und gettimeofday(). Die select() und poll() Aufrufe können zweckentfremdet werden, um einfache Pausen zu erzeugen. Auf MS-DOS Maschinen ist es möglich, den System-Zeitgeber und die Timer-Interrupts neu zu programmieren. 16.11: Wie kann ich Objekt-Dateien einlesen und zu Routinen darin anspringen? A: Das wäre dann ein dynamischer Linker und/oder Lader. Es ist möglich, etwas Speicher zu belegen und dann die Objekt-Datei einzulesen, aber man muß sehr viel über die Formate von Objekt-Dateien, Relokationen usw. wissen. Unter BSD Unix kann man system() und ld -A verwenden um das Linken zu bewerkstelligen. Viele (die meisten?) Versionen von SunOS und System V haben eine -ldl Bibliothek, die das dyamische Laden von Objekt-Dateien ermöglicht. Es gibt außerdem ein GNU-Paket namens "dld". Siehe dazu die Frage 7.6 16.12: Wie kann ich innerhalb eines C-Programmes ein Kommando des Betriebssystems aufrufen? A: Durch Verwendung von system(). Da das Programm aber damit zwangsläufig Abhängigkeiten vom verwendeten Betriebssystem hat, sollte wo möglich auf C-Funktionen ausgewichen werden. Querverweise: K&R II Sek. B6 S. 253; ANSI Sek. 4.10.4.5; H&S Sek. 21.2; PCS Sek. 11 S. 179; 16.13: Wie kann ich innerhalb eines C-Programmes ein Kommando des Betriebssystems aufrufen und dessen Ausgaben auffangen? A: Unix und einige andere Betriebssysteme unterstützen eine popen() Routine, die einen stdio Stream bildet, welcher die Ausgaben des entsprechenden Prozesses beinhaltet, so dass dessen Ausgaben gelesen werden können. Alternativ kann man den Befehl einfach so ausführen (siehe Frage 16.12), dass er seine Ausgaben in eine Datei schreibt, diese anschließend öffnet und einliest. (Anm. d. Übers.: Bei vielen Betriebssystemen ist es erforderlich, den entsprechenden Befehl innerhalb einer Shell zu starten, da das erforderliche Pipe-ing eine Eigenschaft der Shell ist.) Querverweise: PCS Sek. 11 S. 169. 16.14: Wie kann ich in einem C-Programm ein Verzeichnis lesen? A: Man sollte überprüfen, ob die Funktionen opendir() und readdir() benutzt werden können, die bei den meisten Unix Systemen vorhanden sind. Es gibt auch Implementationen für MS-DOS, VMS und andere Systeme (MS-DOS hat Routinen namens FINDFIRST und FINDNEXT, die im Grunde das gleiche machen.). 16.15: Wie kann ich serielle ("Comm") Ports nutzen? A: Das ist systemabhängig. Unter Unix kann man normalerweise die Gerätetreiber im Verzeichnis /dev öffnen, diese lesen und schreiben und die Möglichkeiten des Treibers nutzen, um deren Eigenschaften zu verändern. Unter MS-DOS kann man entweder einige primitive BIOS Aufrufe nutzen, oder (wenn es besonders effizient sein soll) eines von vielen Interrupt-gesteuerten seriellen I/O-Paketen nutzen. --------------------------------------------------------------------------- Abschnitt 17: Verschiedenes =========================== 17.1: Welche Annahmen über die automatische Initialisierung von Variablen sind erlaubt? Reicht eine Initialisierung von Null-Zeigern und Gleitkommavariablen mit 0 aus? A: Variablen der Speicherklasse static (Variablen die außerhalb einer Funktion deklariert werden und solche, die als static deklariert werden) werden garantiert mit 0 initialisiert, und zwar genau einmal beim Start des Programms, als wenn der Programmierer bei der Deklaration "= 0" geschrieben hätte. Diese Variablen werden daher mit Null-Zeigern (mit korrektem Typ - siehe Abschnitt 1), wenn es sich um Zeiger handelt, und 0.0, wenn es sich um Gleitkomma Variablen handelt, initialisiert. Variablen der Speicherklasse automatic (lokale Variable ohne statische Speicherklasse) werden beim Programmstart nicht initialisiert, es sei denn, der Programmierer tut dies explizit. Über den Inhalt dieser Variablen darf ansonsten keine Annahme gemacht werden. Der Inhalt von mittels malloc und realloc dynamisch belegtem Speicher sollte ebenso als nicht initialisiert betrachtet werden. Er muß durch das aufrufende Programm entsprechend initialisiert werden. Speicherbereiche die mit calloc belegt werden, werden bitweise mit 0 initialisiert. Dies muß nicht notwendigerweise einem Null-Zeiger oder einer Gleitkomma 0 entsprechen (siehe auch 3.13 und Abschnitt 1). 17.2: Dieses Programm, direkt aus einem Buch, läßt sich nicht kompilieren f() { char a[] = "Hello, world!"; } Dies könnte darn liegen, dass es sich um einen alten, nicht ANSI-kompatiblen Compiler handelt und dieser die Initialisierung von "automatic aggregates" (d.h. nicht statische lokale Arrays und Strukturen) nicht erlaubt. Als Ausweg kann man das Array global oder statisch deklarieren und mittels strcpy beim beim Aufruf von f initialisieren. (Man kann lokale char * Variablen immer mit String Konstanten initialisieren, siehe auch 17.20). Siehe auch 5.16 und 5.17. 17.3: Wie kann man Daten so in Dateien schreiben, dass sie auch auf fremden Maschinen mit anderen Wortlängen, Byteorder oder anderem Gleitkommaformat gelesen werden können? Die beste Lösung ist es, Textdateien zu verwenden (i.d.R. ASCII), die mit fprintf geschrieben und mit fscanf gelesen werden (ähnliches gilt auch bei Netzwerkprotokollen). Die Argumente, Textdateien seien zu groß und Lesen und Schreiben von Textdateien sei zu langsam, sind nicht sehr stichhaltig. Normalerweise ist ihre Effizienz in der Praxis akzeptabel. Darüberhinaus kann der Vorteil, dass man die Daten mit Standardwerkzeugen bearbeiten kann, eventuelle Nachteile meist mehr als wettmachen. Soll dennoch ein Binärformat verwendet werden, kann die Portabilität durch Verwenden von standardisierten Formaten, wie SUN XDR (RFC 1014), OSI ASN.1, CCITT X.409, oder ISO 8825 "Basic Encoding Rules" erhöht werden. Für diese Formate gibt es u.U. auch bereits fertige E/A Bibliotheken. Siehe auch 9.11. 17.4: Wie kann man in der Mitte einer Datei eine Zeile (oder einen Record) löschen oder einfügen? A: Ausser durch ein Neuschreiben der Datei ist dies wahrscheinlich nicht möglich. Siehe auch 16.9. 17.5: Wie kann man mehrere Werte aus einer Funktion zurückgeben? A: Entweder wird der Funktion ein Zeiger auf Speicherstellen, die die Funktion beschreiben kann, übergeben, oder die Funktion muß eine struct zurückgeben, die die gewünschten Werte enthält. Als Notlösung kommen auch globale Variable in Betracht. Siehe auch 2.17, 3.4 und 9.2. 17.6: Wenn man eine char * Variable hat, die auf den Namen einer Funktion als String zeigt, wie kann man diese Funktion aufrufen? Die offensichtlichste Lösung ist es, eine Tabelle mit den Namen der Funktionen und den entsprechenden Zeigern anzulegen: int function1(), function2(); struct {char *name; int (*funcptr)(); } symtab[] = { "function1", function1, "function2", function2, }; Nun braucht man nur die Tabelle zu durchsuchen und kann über den entsprechenden Zeiger die Funktion aufrufen. Siehe auch 9.9 und 16.11. 17.7: Auf meinem System scheint die Headerdatei nicht vorhanden zu sein. Kann mir jemand eine Kopie zuschicken? Standard Headerdateien existieren unter anderem deswegen, damit für den jeweiligen Compiler, das Betriebssystem und den Prozessor entsprechende Definitionen zur Verfügung gestellt werden können. Es ist nicht möglich, einfach eine Kopie einer fremden Header Datei zu übernehmen und dann zu erwarten, diese würde funktionieren, es sei denn, die Header Datei wurde von jemandem mit der exakt gleichen Umgebung zur Verfügung gestellt. Hier ist eine Nachfrage beim Compilerhersteller/Vertrieb notwendig, warum die entsprechende Datei nicht zur Verfügung gestellt wurde. 17.8: Wie kann man FORTRAN (C++, BASIC, Pascal, Ada, LISP) Funktionen aus C aufrufen (und umgekehrt)? Die Antwort hängt von der verwendeten Hardware und den Aufrufkonventionen der verschiedenen beteiligten Compiler ab, und es kann sogar unmöglich sein. Man sollte in einem solchen Fall die Dokumentation des Compilers besonders genau durcharbeiten. Manchmal gibt es Kapitel über "mixed language" Programmierung. Aber selbst hier werden wichtige Themen wie die Übergabe von Argumenten und die Laufzeitinitialisierung oft nur oberflächlich behandelt. Weitere (englischsprachige) Informationen hierzu sind in Glenn Geers FORT.gz erhältlich, das man u.a. per ftp von suphys.physics.su.oz.au im src Verzeichnis erhält. cfortran.h, eine C Header Datei und vereinfacht die C/FORTRAN Schnittstelle auf vielen gängigen Maschinen. Erhältlich ist sie per ftp von zebra.desy.de (131.169.2.244). In C++ verwendet man den "C" Modifier in einer external Funktionsdeklaration um dem Compiler zu sagen, dass die Funktion nach der C Konvention aufgerufen werden soll. Bei Ada ist in der entsprechenden Norm (ISO/IEC 8652:1995) im Abschnitt B.3 eine Beschreibung zu finden, die das Vorgehen in allen Details erläutert. Alle gängigen Ada-Compiler unterstützen diese Schnittstelle. 17.9: Kennt jemand ein Programm das Pascal oder Fortran (oder LISP, Ada, awk, "altes C" (K&R), ...) nach C übersetzt? A: Es sind verschiedene Public Domain Programme erhältlich: p2c Ein Pascal/C Konverter von Dave Gillespie, veröffentlicht in comp.os.sources im März 1990; ftp: csvax.cs.caltech.edu, file pub/p2c-1.20.tar.Z. ptoc Ein weiteres Pascal/C Konvertierprogramm, dieses ist in Pascal geschrieben. (comp.sources.unix) f2c Ein Fortran/C Konverter, gemeinsam von Angehörigen von Bell Labs, Bellcore und Carnegie Mellon Universität entwickelt. Um mehr Informationen über f2c zu erhalten, braucht man nur eine e-mail mit dem Inhalt "send index from f2c" an netlib@research.att.com oder research!netlib zu schicken (außerdem ftp://netlib.att.com im Verzeichnis netlib/f2c). Beim Autor der englischen comp.lang.c FAQ ist außerdem eine Liste weiterer kommerzieller Übersetzungsprodukte sowie einiger Konverter für weniger verbreitete Sprachen erhältlich. Siehe auch 5.3. 17.10: Ist C++ eine Obermenge von C? Kann ich einen C++ Compiler benutzen um C Programme zu kompilieren? A: C++ wurde von C abgeleitet und basiert größtenteils auf C, aber es gibt zulässige C Programme, die in C++ nicht zulässig sind. Außerdem gibt es Programme, die in C und C++ eine unterschiedliche Semantik haben. [Hinweis Jochen:] Das folgende Programm erkennt zum Beispiel, ob es mit einem C90-Compiler oder einem C++-Compiler übersetzt wurde: #include int main(void) { int result = 1 //* */ 2 ; printf("Dies ist %s C-Compiler\n",(result?"kein":"ein")); return 0; } [Ende Hinweis Jochen] In der Praxis werden sich viele C-Programme auch mit einem C++-Compiler anstandslos übersetzen lassen. 17.11: Ich brauche einen Crossreference-Generator (Beautifier, Pretty Printer) für C. A: Ich brauche: Dafür sind folgende Programme erhältlich (siehe auch 17.12): --------------------------------------------------------------- Einen C Cross-Reference- cflow, calls, cscope Generator Einen C Beautifier/ cb, indent Pretty-Printer 17.12: Wo sind all die erwähnten Public-Domain Programme erhältlich? A: Im Usenet erscheinen regelmässig in den Newsgruppen comp.sources.unix und comp.sources.misc Postings, die recht genau beschreiben, wie man an bestimmte Programmarchive gelangen kann. Normalerweise benutzt man ftp und/oder uucp, um die Archive von einem ftp-Server zu bekommen, wie z.b. uunet (ftp.uu.net) An dieser Stelle kann aber keine vollständige Liste der möglichen Server und wie man auf sie zugreift, aufgeführt werden. Ajay Shah führt eine Liste mit kostenloser numerischer Software. Sie wird regelmäßig gepostet und ist mit dem FAQ zu comp.lang.c auf rtfm.mit.edu in /pub/usenet-by-group/comp.lang.c erhältlich. in der Newsgroup comp.archives werden darüberhinaus zahlreiche Ankündigungen über verschiedenerlei Programme und auf welchen Servern sie erhältlich sind veröffentlicht. Mit "archie" kann man herausfinden, auf welchem ftp Server welche Programmpakete archiviert werden; für weitere Informationen kann man eine e-mail mit dem Subject "help" an archie@th-darmstadt.de schicken. Grundsätzlich ist auch comp.sources.wanted ein guter Platz, um nach Programmen und Programmpaketen zu fragen. Aber auch hier gilt: Lieber erst die entsprechende FAQ lesen! 17.13: Wann findet der nächste "International Obfuscated C Contest" (IOCC) statt? Wie bekommt man eine Kopie früherer und gegenwärtiger Sieger? A: Es gibt inzwischen einen eigenen Webserver für den Wettbewerb. Unter http://www.ioccc.org/ findet man alle Informationen zu vergangenen, aktuellen und zukünftigen Wettbewerben. 17.14: Warum gibt es in C keine verschachtelten Kommentare? Wie kann ich Programmtext, der Kommentare enthält, auskommentieren? Darf ich Kommentare in Stringkonstanten einfügen? A: Verschachtelte Kommentare würden mehr schaden als nutzen, weil die Gefahr groß ist, versehentlich Kommentare nicht zu schließen, wenn die Zeichen "/*" in ihnen enthalten sind. Deshalb ist es normalerweise besser, größere Programmteile, die auch Kommentare enthalten, mit #ifdef oder #if 0 "auszukommentieren" (siehe hierzu auch 5.11). Die Zeichenfolgen /* und */ sind innerhalb von Stringliteralen keine Sonderzeichen! Daher leiten sie auch keine Kommentare ein, da es ja möglich wäre, dass ein Programm diese Zeichenfolge ausgeben will (besonders, wenn es C Code erzeugt). Referenz: ANSI Appendix E p. 198, Rationale Sec. 3.1.9 p. 33. 17.15: Wie kann man den ASCII Wert eines Zeichens und umgekehrt das passende Zeichen zu einem ASCII Wert rausfinden? A: In C werden Zeichen (char) generell durch Integer repräsentiert, deren Wert direkt dem Wert des Zeichens im Zeichensatz der jeweiligen Maschine entspricht. Man braucht deshalb keine Konvertierungsroutine, wenn man ein Zeichen hat, hat man seinen Wert! 17.16: Wie programmiert man Bit-Sets und/oder Bit-Arrays? A: Man benutzt char oder int Arrays, mit ein paar Makros um ein bestimmtes Bit an einer bestimmten Stelle zu manipulieren. (sollte nicht implementiert sein, kann man es mit CHAR_BIT 8 probieren) #include /* for CHAR_BIT */ #define BITMASK(bit) (1 << ((bit) % CHAR_BIT)) #define BITSLOT(bit) ((bit) / CHAR_BIT) #define BITSET(ary, bit) ((ary)[BITSLOT(bit)] |= BITMASK(bit)) #define BITTEST(ary, bit) ((ary)[BITSLOT(bit)] & BITMASK(bit)) 17.17: Wie kann man am effizientesten die Zahl der gesetzten Bits in einer Variablen ermitteln? A: Diese und viele andere Bit-Manipulationsprobleme können häufig durch Tabellen beschleunigt werden. 17.18: Wie kann man dieses Programm effizienter machen? A: Obwohl vielfach in de.comp.lang.c über Effizienz diskutiert wird, ist sie nicht annähernd so häufig ein entscheidender Faktor, wie viele denken. Der größte Teil eines Programm ist nicht zeitkritisch. Ist aber ein Programmteil nicht zeitkritisch, so ist es wesentlich wichtiger, dass dieser verständlich und portabel geschrieben wird, als ihn auf Effizienz zu optimieren (schliesslich sind Computer trotz allem ziemlich schnell, so dass auch ineffiziente Programmteile ohne Verzögerung laufen können). Es ist sehr schwierig vorherzusagen, welche Teile eines Programms besonders laufzeitkritisch sind. Wenn das Laufzeitverhalten von entscheidender Bedeutung ist, sollte man einen Profiler benutzen, um diejenigen Programmteile zu finden, die besonderer Aufmerksamkeit in Bezug auf ihre Optimierung bedürfen. Häufig ist zu beobachten, dass E/A Operationen sich auf das Laufzeitverhalten stärker auswirken als die eigentlichen Berechnungen. Dies kann durch Cache Techniken beschleunigt werden. Um den kleinen Anteil des wirklich zeitkritischen Codes in einem Programm zu optimieren, ist es besonders wichtig, dass man einen guten Algorithmus auswählt. Die "Mikrooptimierung" von Programmteilen ist weniger entscheidend. Die meisten Tricks zur Effizienz-Steigerung werden selbst von einfachen Compilern automatisch durchgeführt (wie z.B. Multiplikationen mit Zweierpotenzen durch Shiftoperationen zu ersetzen). Ungeschickte Optimierungen können ein Programm so umständlich machen, dass das Laufzeitverhalten darunter leidet. Weitere Hinweise zu diesem Thema finden sich im Kapitel 7 von Kernighan, Plauger "The Elements of Programming Style" sowie in Jon Bentley "Writing Efficient Programs". 17.19: Sind Zeiger wirklich schneller als Arrays? Wie sehr verlangsamen Funktionsaufrufe die Programmausführung? Ist ++i schneller als i= i + 1? A: Die Antworten hierauf hängen natürlich von dem verwendeten Compiler und der Hardware ab. Die sicherste Antwort hierauf erhält man, indem man es einfach selbst austestet (meistens werden die Unterschiede so klein sein, dass man mehrere hunderttausend Iterationen braucht, um irgendeinen Unterschied zu sehen. Wenn möglich sollte man sich das Assemblerlisting anschauen, um zu sehen, ob die beiden Alternativen nicht den gleichen Assemblercode liefern). Es ist "gewöhnlich" schneller, durch grosse Arrays mit Zeigern als mit dem Arrayindex zu "wandern". Allerdings ist dies bei einigen Prozessoren auch umgekehrt. Funktionsaufrufe sind zwar unwesentlich langsamer als inline Code, sie tragen aber so stark zur Modularität und Lesbarkeit eines Programms bei, dass es selten einen vernünftgen Grund gibt, auf sie zu verzichten. Bevor man Ausdrücke wie i = i + 1 umschreibt, sollte man bedenken, dass man mit einem C Compiler umgeht, nicht mit einem Taschenrechner! Jeder halbwegs gute Compiler wird identischen Code für ++i, i+=1 und i = i + 1 erzeugen! Es ist nur eine Frage des Stils (bzw. des Zusammenhangs) ob man i++, i+=1 oder i = i + 1 wählen sollte. 17.20: Warum stürzt dieser Code ab: char *p = "Hello, world!"; p[0] = tolower(p[0]); A: String Konstanten sind nicht notwendigerweise modifizierbar, außer wenn sie zum Initialisieren von Arrays verwendet werden. Folgendes Beispiel sollte funktionieren: char a[] = "Hello, world!"; (Um ältere Programme zu kompilieren haben einige Compiler eine Option, um zu steuern, ob Strings beschreibbar sind oder nicht). Siehe auch 2.1, 2.2, 2.8 und 17.2 Referenz: ANSI Sec. 3.1.4 . 17.21: Mein Programm stürzt ab, bevor es überhaupt die erste Zeile ausführt (Wenn man es mit einem Debugger untersucht bricht das Programm vor der ersten Zeile von main ab). A: In dem Programm gibt es wahrscheinlich einen oder mehrere sehr grosse lokale Arrays. Der Stack hat auf vielen Systemen eine feste Größe. Systeme, die den Stack dynamisch anlegen können, können durcheinander kommen, wenn der Stack plötzlich um ein sehr grosses Stück wächst. Oft ist es besser, große Arrays als static zu deklarieren (außer natürlich, wenn bei jedem neuen Aufruf ein neues Array benötigt wird). (Siehe auch 9.4.) 17.22: Was bedeuten "Segmentation Violation" und "Bus Error"? A: Das Programm hat versucht, auf Speicher zuzugreifen, auf den es nicht so zugreifen durfte, oft im Zusammenhang mit falscher Benutzung von Zeigern, wobei diese meist nicht initialisiert oder falsch belegt werden (siehe 3.1 und 3.2), oder von malloc (siehe 17.23) oder von scanf (siehe 11.3). 17.23: Hat jemand ein Programmpaket um den Compiler zu überprüfen? A: Ein kommerzielles Produkt ist von Plum Hall erhältlich. Die GNU C Distribution der FSF (gcc) enthält c-torture-test.tar.Z, das eine Reihe von gängigen Compilerproblemen überprüft. Der paranoia Test von Kahan, erhältlich in netlib/paranoia auf netlib.att.com testet ausführlich die Gleitkomma Fähigkeiten der jeweiligen Implementation. 17.24: Wo bekommt man eine YACC Grammatik für C her? A: Die "definitive" Grammatik ist diejenige im ANSI Standard. Eine weitere Grammatik von Jim Roskind ist in pub/*grammar* auf ics.uci.edu erhältlich. Ein funktionierendes Beispiel der ANSI Grammatik (verfasst von Jeff Lee) ist im uunet (siehe 17.12) in usenet/net.sources/anis.c.grammar.Z (inklusive eines lexers) erhältlich. Der GNU C Compiler der FSF enthält ebenso eine Grammatik wie der Anhang von K&R II. Referenz: ANSI Sec. A.2 . 17.25: Ich benötige Code um mathematische Ausdrücke zu parsen und auszuwerten. A: Zwei Pakete sind: "defunc", erhältlich auf sunsite.unc.edu in pub/packages/development/libraries/defunc-1.3.tar.Z; sowie "parse" auf lamont.ldgo.columbia.edu. 17.26: Wo bekommt man eine Routine für den "ungefähren" Vergleich von 2 Strings her, die ähnliche, aber nicht unbedingt exakt gleiche Strings erkennt. A: Gewöhnlich verwendet man hierfür den Soundex Algorithmus, welcher ähnlich klingenden Worten identische Zahlenwerte zuordnet. Dieser Algorithmus wird in Donald Knuth's "The Art of Computer Programming" im Band "Searching and Sorting" beschrieben. 17.27: Wie kann man den Wochentag aus einem gegebenen Datum herausfinden? A: Mittels mktime (Siehe 12.6 und 12.7) oder Zeller's congruence oder im FAQ zu sci.math, oder mit folgendem Programm: dayofweek(y, m, d) /* 0 = Sonntag */ int y, m, d; /* 1 <= m <= 12, y > 1752 oder so */ { static int t[] = {0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4}; y -= m < 3; return (y + y/4 - y/100 + y/400 + t[m-1] + d) % 7; } 17.28: Ist das Jahr 2000 ein Schaltjahr gewesen? Ist (year % 4 == 0) ein zuverlässiger Test für ein Schaltjahr? A: Ja und Nein (in dieser Reihenfolge). Für den Gregorianischen Kalender lautet der vollständige Test: year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) In jedem guten astronomischen Führer gibt es zu diesem Thema weitere Informationen. --------------------------------------------------------------------------- Das Urheberrecht für diese FAQ bzw. die deutsche Übersetzung (was auch immer das heissen mag) liegt bei den Autoren, eine weitere Verbreitung ist zulässig und erwünscht, vorausgesetzt dies geschieht komplett und unverändert. Die englische Version von Steve Summit (scs@eskimo.com) sagt weiterhin: This article is Copyright 1988, 1990-1995 by Steve Summit. It may be freely redistributed so long as the author's name, and this notice, are retained. The C code in this article (vstrcat(), error(), etc.) is public domain and may be used without restriction. --------------------------------------------------------------------------- Vielen Dank an - Steve Summit (scs@eskimo.com) für die Erlaubnis, den Text der alten comp.lang.c FAQ als Vorlage zu nutzen. - Alle, die an der FAQ mitgearbeitet haben (siehe Liste am Anfang). - Alle die mit Fragen und Hinweisen mitgeholfen haben, Fehler zu entfernen und Formulierungen klarer zu machen.