|
Diese FAQ bezieht sich in ihrer Gänze auf den inzwischen nicht mehr aktuellen ISO-C Standard 9899:1990, vielfach auch als C90 bezeichnet. Der seit Dezember 1999 existierende neue ISO 9899:1999 Standard (oder auch C99) wird nicht berücksichtigt.
Frage 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? Antwort: 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. Frage 2.2: Aber ich habe gehört dass char a[] das gleiche wie char *a ist. Antwort: Ü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) Frage 2.3: Was ist dann mit der "Äquivalenz von Zeigern und Arrays" in C gemeint? Antwort: 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 Frage 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. Frage 2.5: Wie kann ein Array ein Lvalue sein, wenn man ihm nichts zuweisen kann? Antwort: Der ANSI Standard definiert einen "veränderbaren Lvalue", und das ist ein Array nicht. Literatur: ANSI Sec. 3.2.2.1 p. 37. Frage 2.6: Warum liefert sizeof nicht die wirkliche Größe eines Arrays das ein Parameter einer Funktion ist? Antwort: Der sizeof-Operator liefert die Größe des tatsächlich an die Funktion übergebenen Parameters, der ein Zeiger ist. (vgl. Frage 2.4) Frage 2.7: Jemand hat mir erklärt, dass Arrays in Wirklichkeit nur konstante Zeiger sind. Antwort: 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. Frage 2.8: Vom praktischen Standpunkt betrachtet, was ist der Unterschied zwischen Arrays und Zeigern? Antwort: 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) Frage 2.9: Ich bin auf "scherzhaften" Quelltext gestoßen, in dem der Ausdruck 5["abcdef"] vorkam. Wie kann so etwas in C erlaubt sein? Antwort: 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. Frage 2.10: Mein Compiler beschwert sich, wenn ich ein zweidimensionales Array an eine Funktion übergebe, die einen Zeiger auf einen Zeiger erwartet. Antwort: 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. Frage 2.11: Wie schreibe ich Funktionen, die zweidimensionale Arrays als Argumente annehmen, wenn die "Breite" zum Zeitpunkt der Übersetzung unbekannt ist? Antwort: 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. Frage 2.12: Wie vereinbare ich einen Zeiger auf ein Array? Antwort: 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.] Frage 2.13: Nachdem Bezeichner von Arrays zu Zeigern zerfallen, was ist bei int array[NROWS][NCOLUMNS]; der Unterschied zwischen array und &array? Antwort: 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.) Frage 2.14: Wie kann ich dynamisch ein mehrdimensionales Array allozieren? Antwort: 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) Frage 2.15: Wie können sowohl statisch als auch dynamisch allozierte Arrays an die gleiche Funktion übergeben werden? Antwort: 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. Frage 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. Antwort: 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. Frage 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. Antwort: 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. Frage 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? Antwort: 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. Frage 2.19: Kann ein Zeiger vom Typ void ** verwendet werden, um einen Zeiger von beliebigem Typ per Referenz an eine Funktion zu übergeben? Antwort: 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.
© 1997-2004 Jochen Schoof (joscho@bigfoot.de) Diese Version wurde am 14. März 2004 erzeugt. Sie wird zukünftig nicht weiter gepflegt. |