C-Zeiger: Zeigen auf ein Array fester Größe

118

Diese Frage geht an die C-Gurus da draußen:

In C kann ein Zeiger wie folgt deklariert werden:

char (* p)[10];

.. was im Grunde besagt, dass dieser Zeiger auf ein Array von 10 Zeichen zeigt. Das Schöne an der Deklaration eines solchen Zeigers ist, dass Sie einen Fehler bei der Kompilierung erhalten, wenn Sie versuchen, p einen Zeiger eines Arrays unterschiedlicher Größe zuzuweisen. Es wird auch ein Fehler bei der Kompilierung angezeigt, wenn Sie versuchen, p den Wert eines einfachen Zeichenzeigers zuzuweisen. Ich habe dies mit gcc versucht und es scheint mit ANSI, C89 und C99 zu funktionieren.

Es scheint mir sehr nützlich zu sein, einen Zeiger wie diesen zu deklarieren - insbesondere, wenn ein Zeiger an eine Funktion übergeben wird. Normalerweise schreiben die Leute den Prototyp einer solchen Funktion wie folgt:

void foo(char * p, int plen);

Wenn Sie einen Puffer einer bestimmten Größe erwarten würden, würden Sie einfach den Wert von plen testen. Es kann jedoch nicht garantiert werden, dass die Person, die p an Sie weitergibt, Ihnen tatsächlich gültige Speicherplätze in diesem Puffer gibt. Sie müssen darauf vertrauen, dass die Person, die diese Funktion aufgerufen hat, das Richtige tut. Andererseits:

void foo(char (*p)[10]);

..wurde den Anrufer zwingen, Ihnen einen Puffer der angegebenen Größe zu geben.

Dies scheint sehr nützlich zu sein, aber ich habe noch nie einen Zeiger gesehen, der in einem Code, auf den ich jemals gestoßen bin, so deklariert wurde.

Meine Frage ist: Gibt es einen Grund, warum Leute solche Zeiger nicht deklarieren? Sehe ich keine offensichtliche Falle?

figurassa
quelle
3
Hinweis: Da C99 nicht wie im Titel angegeben eine feste Größe haben muss, 10kann das Array durch eine beliebige Variable im Gültigkeitsbereich ersetzt werden
MM

Antworten:

172

Was Sie in Ihrem Beitrag sagen, ist absolut richtig. Ich würde sagen, dass jeder C-Entwickler zu genau der gleichen Entdeckung und zu genau der gleichen Schlussfolgerung kommt, wenn (wenn) er bestimmte Kenntnisse der C-Sprache erreicht.

Wenn die Besonderheiten Ihres Anwendungsbereichs ein Array mit einer bestimmten festen Größe erfordern (die Arraygröße ist eine Konstante für die Kompilierungszeit), können Sie ein solches Array nur mithilfe eines Zeiger-zu-Array-Parameters an eine Funktion übergeben

void foo(char (*p)[10]);

(In der C ++ - Sprache erfolgt dies auch mit Referenzen

void foo(char (&p)[10]);

).

Dies ermöglicht die Typprüfung auf Sprachebene, wodurch sichergestellt wird, dass das Array mit genau der richtigen Größe als Argument angegeben wird. Tatsächlich verwenden Menschen diese Technik in vielen Fällen implizit, ohne es überhaupt zu merken, und verstecken den Array-Typ hinter einem typedef-Namen

typedef int Vector3d[3];

void transform(Vector3d *vector);
/* equivalent to `void transform(int (*vector)[3])` */
...
Vector3d vec;
...
transform(&vec);

Beachten Sie außerdem, dass der obige Code in Bezug auf den Vector3dTyp, der ein Array oder ein Array ist, unveränderlich ist struct. Sie können die Definition von Vector3djederzeit von einem Array in ein structund zurück ändern, ohne die Funktionsdeklaration ändern zu müssen. In beiden Fällen erhalten die Funktionen ein aggregiertes Objekt "als Referenz" (es gibt Ausnahmen, aber im Kontext dieser Diskussion ist dies wahr).

Sie werden diese Methode der Array-Übergabe jedoch nicht allzu oft explizit verwenden, einfach weil zu viele Leute durch eine ziemlich verworrene Syntax verwirrt sind und mit solchen Funktionen der C-Sprache einfach nicht vertraut genug sind, um sie richtig zu verwenden. Aus diesem Grund ist es im durchschnittlichen realen Leben ein beliebterer Ansatz, ein Array als Zeiger auf sein erstes Element zu übergeben. Es sieht einfach "einfacher" aus.

In Wirklichkeit ist die Verwendung des Zeigers auf das erste Element für die Array-Übergabe eine sehr Nischentechnik, ein Trick, der einem ganz bestimmten Zweck dient: Sein einziger Zweck besteht darin, die Übergabe von Arrays unterschiedlicher Größe (dh Laufzeitgröße) zu erleichtern. . Wenn Sie wirklich in der Lage sein müssen, Arrays mit Laufzeitgröße zu verarbeiten, können Sie ein solches Array am besten durch einen Zeiger auf sein erstes Element übergeben, wobei die konkrete Größe durch einen zusätzlichen Parameter angegeben wird

void foo(char p[], unsigned plen);

Tatsächlich ist es in vielen Fällen sehr nützlich, Arrays mit Laufzeitgröße verarbeiten zu können, was ebenfalls zur Popularität der Methode beiträgt. Viele C-Entwickler stoßen einfach nie auf die Notwendigkeit (oder erkennen sie nie), ein Array mit fester Größe zu verarbeiten, und sind sich daher der richtigen Technik mit fester Größe nicht bewusst.

Wenn die Arraygröße jedoch fest ist, übergeben Sie sie als Zeiger auf ein Element

void foo(char p[])

ist ein schwerwiegender Fehler auf technischer Ebene, der heutzutage leider ziemlich weit verbreitet ist. Eine Zeiger-zu-Array-Technik ist in solchen Fällen ein viel besserer Ansatz.

Ein weiterer Grund, der die Einführung der Array-Passing-Technik mit fester Größe behindern könnte, ist die Dominanz des naiven Ansatzes zur Typisierung dynamisch zugeordneter Arrays. Wenn das Programm beispielsweise feste Arrays vom Typ char[10](wie in Ihrem Beispiel) aufruft , verwendet ein durchschnittlicher Entwickler mallocArrays wie

char *p = malloc(10 * sizeof *p);

Dieses Array kann nicht an eine als deklarierte Funktion übergeben werden

void foo(char (*p)[10]);

Dies verwirrt den durchschnittlichen Entwickler und veranlasst ihn, die Parameterdeklaration mit fester Größe aufzugeben, ohne weiter darüber nachzudenken. In Wirklichkeit liegt die Wurzel des Problems jedoch im naiven mallocAnsatz. Das mallocoben gezeigte Format sollte für Arrays mit Laufzeitgröße reserviert werden. Wenn der Array-Typ eine Größe zur Kompilierungszeit hat, mallocsieht der folgende Weg besser aus

char (*p)[10] = malloc(sizeof *p);

Dies kann natürlich leicht an die oben angegebene weitergegeben werden foo

foo(p);

und der Compiler führt die richtige Typprüfung durch. Aber auch dies ist für einen unvorbereiteten C-Entwickler zu verwirrend, weshalb Sie es im "typischen" durchschnittlichen Alltagscode nicht allzu oft sehen.

Ameise
quelle
2
Die Antwort liefert eine sehr präzise und informative Beschreibung, wie sizeof () erfolgreich ist, wie oft es fehlschlägt und wie es immer fehlschlägt. Ihre Beobachtungen der meisten C / C ++ - Ingenieure, die nicht verstehen, und daher etwas zu tun, von dem sie glauben, dass sie es verstehen, ist eines der prophetischeren Dinge, die ich seit einiger Zeit gesehen habe, und der Schleier ist nichts im Vergleich zu der Genauigkeit, die er beschreibt. im Ernst, Sir. gute Antwort.
WhozCraig
Ich Refactoring nur einige Code auf der Grundlage dieser Antwort, bravo und dankt beiden für die Q und die A.
Perry
1
Ich bin gespannt, wie Sie constmit dieser Technik mit Eigentum umgehen . Ein const char (*p)[N]Argument scheint nicht mit einem Zeiger auf kompatibel zu sein. char table[N];Im Gegensatz dazu char*bleibt ein einfaches ptr mit einem const char*Argument kompatibel .
Cyan
4
Es kann hilfreich sein zu beachten, dass Sie dies tun müssen (*p)[i]und nicht , um auf ein Element Ihres Arrays zuzugreifen *p[i]. Letzteres springt um die Größe des Arrays, was mit ziemlicher Sicherheit nicht das ist, was Sie wollen. Zumindest für mich hat das Erlernen dieser Syntax eher einen Fehler verursacht als verhindert; Ich hätte schneller korrekten Code erhalten, wenn ich nur einen Float * übergeben hätte.
Andrew Wagner
1
Ja @mickey, was Sie vorgeschlagen haben, ist ein constZeiger auf ein Array von veränderlichen Elementen. Und ja, das ist völlig anders als ein Zeiger auf ein Array unveränderlicher Elemente.
Cyan
11

Ich möchte die Antwort von AndreyT ergänzen (falls jemand auf diese Seite stößt und nach weiteren Informationen zu diesem Thema sucht):

Als ich anfange, mehr mit diesen Deklarationen zu spielen, stelle ich fest, dass mit ihnen in C ein großes Handicap verbunden ist (anscheinend nicht in C ++). Es kommt häufig vor, dass Sie einem Aufrufer einen konstanten Zeiger auf einen Puffer geben möchten, in den Sie geschrieben haben. Leider ist dies nicht möglich, wenn ein Zeiger wie dieser in C deklariert wird. Mit anderen Worten, der C-Standard (6.7.3 - Absatz 8) steht im Widerspruch zu so etwas:


   int array[9];

   const int (* p2)[9] = &array;  /* Not legal unless array is const as well */

Diese Einschränkung scheint in C ++ nicht vorhanden zu sein, was diese Art von Deklarationen weitaus nützlicher macht. Im Fall von C ist es jedoch erforderlich, immer dann auf eine reguläre Zeigerdeklaration zurückzugreifen, wenn Sie einen const-Zeiger auf den Puffer mit fester Größe wünschen (es sei denn, der Puffer selbst wurde zunächst als const deklariert). Weitere Informationen finden Sie in diesem Mail-Thread: Link-Text

Dies ist meiner Meinung nach eine schwerwiegende Einschränkung und könnte einer der Hauptgründe sein, warum Menschen in C normalerweise keine Zeiger wie diesen deklarieren. Der andere Grund ist die Tatsache, dass die meisten Menschen nicht einmal wissen, dass Sie einen Zeiger wie diesen als deklarieren können AndreyT hat darauf hingewiesen.

figurassa
quelle
2
Dies scheint ein compilerspezifisches Problem zu sein. Ich konnte mit gcc 4.9.1 duplizieren, aber clang 3.4.2 konnte problemlos von einer Nicht-Const- zu einer Const-Version wechseln. Ich habe die C11-Spezifikation gelesen (S. 9 in meiner Version ... der Teil, in dem davon gesprochen wird, dass zwei qualifizierte Typen kompatibel sind) und bin damit einverstanden, dass diese Konvertierungen anscheinend illegal sind. In der Praxis wissen wir jedoch, dass Sie ohne Vorwarnung immer automatisch von char * nach char const * konvertieren können. IMO, Clang lässt dies konsequenter zu als gcc, obwohl ich Ihnen zustimme, dass die Spezifikation jede dieser automatischen Konvertierungen zu verbieten scheint.
Doug Richardson
4

Der offensichtliche Grund ist, dass dieser Code nicht kompiliert wird:

extern void foo(char (*p)[10]);
void bar() {
  char p[10];
  foo(p);
}

Die Standard-Heraufstufung eines Arrays erfolgt auf einen nicht qualifizierten Zeiger.

Siehe auch diese Frage , foo(&p)sollte funktionieren.

Keith Randall
quelle
3
Natürlich funktioniert foo (p) nicht, foo bittet um einen Zeiger auf ein Array von 10 Elementen, daher müssen Sie die Adresse Ihres Arrays übergeben ...
Brian R. Bondy
8
Wie ist das der "offensichtliche Grund"? Es versteht sich offensichtlich, dass der richtige Weg, die Funktion aufzurufen, ist foo(&p).
AnT
3
Ich denke "offensichtlich" ist das falsche Wort. Ich meinte "am einfachsten". Die Unterscheidung zwischen p und & p ist in diesem Fall für den durchschnittlichen C-Programmierer ziemlich dunkel. Jemand, der versucht, das zu tun, was das Poster vorgeschlagen hat, schreibt, was ich geschrieben habe, erhält einen Fehler beim Kompilieren und gibt auf.
Keith Randall
1

Einfach gesagt, C macht die Dinge nicht so. Ein Array vom Typ Twird als Zeiger auf das erste Tim Array herumgereicht, und das ist alles, was Sie erhalten.

Dies ermöglicht einige coole und elegante Algorithmen, wie das Durchlaufen des Arrays mit Ausdrücken wie

*dst++ = *src++

Der Nachteil ist, dass die Verwaltung der Größe bei Ihnen liegt. Leider hat das Versäumnis, dies gewissenhaft zu tun, auch zu Millionen von Fehlern in der C-Codierung und / oder zu Möglichkeiten für böswillige Ausbeutung geführt.

Was in C dem nahe kommt, was Sie verlangen, ist, einen struct(nach Wert) oder einen Zeiger auf einen (nach Referenz) zu übergeben. Solange auf beiden Seiten dieses Vorgangs derselbe Strukturtyp verwendet wird, stimmen sowohl der Code, der die Referenz ausgibt, als auch der Code, der sie verwendet, über die Größe der verarbeiteten Daten überein.

Ihre Struktur kann beliebige Daten enthalten. Es könnte Ihr Array mit einer genau definierten Größe enthalten.

Nichts hindert Sie oder einen inkompetenten oder böswilligen Codierer daran, Casts zu verwenden, um den Compiler dazu zu bringen, Ihre Struktur als eine andere Größe zu behandeln. Die fast ungebundene Fähigkeit, so etwas zu tun, ist Teil von Cs Design.

Carl Smotricz
quelle
1

Sie können ein Array von Zeichen auf verschiedene Arten deklarieren:

char p[10];
char* p = (char*)malloc(10 * sizeof(char));

Der Prototyp einer Funktion, die ein Array nach Wert annimmt, lautet:

void foo(char* p); //cannot modify p

oder durch Bezugnahme:

void foo(char** p); //can modify p, derefernce by *p[0] = 'f';

oder nach Array-Syntax:

void foo(char p[]); //same as char*
s1n
quelle
2
Vergessen Sie nicht, dass ein Array mit fester Größe auch dynamisch als zugewiesen werden kann char (*p)[10] = malloc(sizeof *p).
AnT
Eine ausführlichere Beschreibung der Unterschiede zwischen char array [] und char * ptr finden Sie hier. stackoverflow.com/questions/1807530/…
t0mm13b
1

Ich würde diese Lösung nicht empfehlen

typedef int Vector3d[3];

da es die Tatsache verdeckt, dass Vector3D einen Typ hat, den Sie kennen müssen. Programmierer erwarten normalerweise nicht, dass Variablen desselben Typs unterschiedliche Größen haben. Erwägen :

void foo(Vector3d a) {
   Vector3D b;
}

wobei sizeof a! = sizeof b

Per Knytt
quelle
Er schlug dies nicht als Lösung vor. Er benutzte dies einfach als Beispiel.
figurassa
Hm. Warum ist das sizeof(a)nicht dasselbe wie sizeof(b)?
Sherlellbc
1

Ich möchte diese Syntax auch verwenden, um mehr Typprüfung zu ermöglichen.

Ich stimme aber auch zu, dass die Syntax und das mentale Modell der Verwendung von Zeigern einfacher und leichter zu merken sind.

Hier sind einige weitere Hindernisse, auf die ich gestoßen bin.

  • Der Zugriff auf das Array erfordert Folgendes (*p)[]:

    void foo(char (*p)[10])
    {
        char c = (*p)[3];
        (*p)[0] = 1;
    }

    Es ist verlockend, stattdessen einen lokalen Zeiger auf Zeichen zu verwenden:

    void foo(char (*p)[10])
    {
        char *cp = (char *)p;
        char c = cp[3];
        cp[0] = 1;
    }

    Dies würde jedoch den Zweck der Verwendung des richtigen Typs teilweise zunichte machen.

  • Man muss daran denken, den Operator address-of zu verwenden, wenn die Adresse eines Arrays einem Zeiger auf das Array zugewiesen wird:

    char a[10];
    char (*p)[10] = &a;

    Der Adressoperator erhält die Adresse des gesamten Arrays &amit dem richtigen Typ, dem er zugewiesen werden soll p. Ohne den Operator awird automatisch in die Adresse des ersten Elements des Arrays konvertiert, wie in&a[0] , das einen anderen Typ hat.

    Da diese automatische Konvertierung bereits stattfindet, bin ich immer verwirrt, dass dies &notwendig ist. Es ist konsistent mit der Verwendung von &on-Variablen anderer Typen, aber ich muss bedenken, dass ein Array etwas Besonderes ist und dass ich das benötige &, um den richtigen Adresstyp zu erhalten, obwohl der Adresswert der gleiche ist.

    Ein Grund für mein Problem könnte sein, dass ich K & R C bereits in den 80er Jahren gelernt habe, was die Verwendung des &Operators für ganze Arrays noch nicht erlaubte (obwohl einige Compiler dies ignorierten oder die Syntax tolerierten). Dies ist übrigens möglicherweise ein weiterer Grund, warum es schwierig ist, Zeiger auf Arrays zu übernehmen: Sie funktionieren erst seit ANSI C ordnungsgemäß, und die &Bedienerbeschränkung war möglicherweise ein weiterer Grund, sie als zu umständlich zu betrachten.

  • Wenn typedefist nicht eine Art für das Zeiger-to-Array verwendet zu erstellen (in einer gemeinsamen Header - Datei), dann ein globaler Zeiger-to-Array muss eine kompliziertere externErklärung es über Dateien zu teilen:

    fileA:
    char (*p)[10];
    
    fileB:
    extern char (*p)[10];
Orafu
quelle
0

Vielleicht fehlt mir etwas, aber ... da Arrays konstante Zeiger sind, bedeutet dies im Grunde, dass es keinen Sinn macht, Zeiger auf sie herumzugeben.

Könntest du nicht einfach benutzen void foo(char p[10], int plen);?

fortran
quelle
4
Arrays sind KEINE konstanten Zeiger. Lesen Sie bitte einige FAQ zu Arrays.
AnT
2
Für das, was hier wichtig ist (eindimensionale Arrays als Parameter), ist die Tatsache, dass sie zu konstanten Zeigern zerfallen. Lesen Sie bitte eine FAQ, wie Sie weniger pedantisch sein können.
Fortan
-2

Auf meinem Compiler (vs2008) wird es char (*p)[10]als Array von Zeichenzeigern behandelt , als ob es keine Klammern gäbe, selbst wenn ich als C-Datei kompiliere. Unterstützt der Compiler diese "Variable"? Wenn ja, ist dies ein Hauptgrund, es nicht zu verwenden.

Tyson Jacobs
quelle
1
-1 Falsch. Es funktioniert gut auf vs2008, vs2010, gcc. Insbesondere funktioniert dieses Beispiel gut: stackoverflow.com/a/19208364/2333290
kotlomoy