wake-up-neo.com

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

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

In C ist es möglich, einen Zeiger wie folgt zu deklarieren:

char (* p)[10];

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

Es scheint mir, als wäre es sehr nützlich, einen Zeiger wie diesen zu deklarieren - insbesondere, wenn man einen Zeiger an eine Funktion übergibt. Normalerweise würden die Leute den Prototyp einer solchen Funktion wie folgt schreiben:

void foo(char * p, int plen);

Wenn Sie einen Puffer einer bestimmten Größe erwarten, testen Sie einfach den Wert von plen. Es kann jedoch nicht garantiert werden, dass die Person, die p an Sie weitergibt, Ihnen wirklich gültige Speicherstellen in diesem Puffer gibt. Sie müssen darauf vertrauen, dass die Person, die diese Funktion aufgerufen hat, das Richtige tut. Auf der anderen Seite:

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

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

Dies scheint sehr nützlich zu sein, aber ich habe in keinem Code, auf den ich jemals gestoßen bin, einen so deklarierten Zeiger gesehen.

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

105
figurassa

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 Erklärungen zu spielen, stelle ich fest, dass mit ihnen ein schwerwiegendes Handicap in C verbunden ist (offensichtlich nicht in C++). Es kommt ziemlich häufig vor, dass Sie einem Aufrufer einen const-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 - Paragraph 8) widerspricht ungefähr dem Folgenden:


   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 müssen Sie jedoch immer dann auf eine reguläre Zeigerdeklaration zurückgreifen, wenn Sie einen konstanten Zeiger auf den Puffer mit fester Größe möchten (es sei denn, der Puffer selbst wurde zu Beginn als konstante deklariert). Weitere Informationen finden Sie in diesem E-Mail-Thread: Linktext

Dies ist meiner Meinung nach eine schwerwiegende Einschränkung, und dies könnte einer der Hauptgründe sein, warum Leute in C normalerweise keine Zeiger wie diesen deklarieren AndreyT hat darauf hingewiesen.

9
figurassa

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 ein bestimmtes Niveau an Kenntnissen in C-Sprache erreicht.

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

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

(In C++ wird dies auch mit Referenzen durchgeführt

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

).

Auf diese Weise wird die Typüberprüfung auf Sprachebene aktiviert, wodurch sichergestellt wird, dass das Array mit der genau richtigen Größe als Argument angegeben wird. Tatsächlich wird diese Technik in vielen Fällen implizit verwendet, ohne es überhaupt zu bemerken, und der Array-Typ wird hinter einem typedef-Namen verborgen

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 Typ Vector3d, Der ein Array oder eine struct ist, unveränderlich ist. Sie können die Definition von Vector3d Jederzeit von einem Array in ein struct und zurück ändern, ohne die Funktionsdeklaration ändern zu müssen. In beiden Fällen erhalten die Funktionen ein aggregiertes Objekt "per Verweis" (es gibt Ausnahmen, aber im Kontext dieser Diskussion ist dies wahr).

Diese Methode der Array-Übergabe wird jedoch nicht allzu oft verwendet, da zu viele Benutzer durch eine verwickelte Syntax verwirrt werden und sich mit solchen Funktionen der C-Sprache einfach nicht gut genug auskennen, um sie ordnungsgemäß zu verwenden. Aus diesem Grund ist es im normalen Leben populärer, 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 Übergabe von Arrays eine sehr nischenhafte Technik, ein Trick, der einem ganz bestimmten Zweck dient: Sein einziger Zweck ist es, die Übergabe von Arrays von nterschiedlicher Größe zu erleichtern (dh Laufzeitgröße). Wenn Sie in der Lage sein müssen, Arrays mit einer Laufzeitgröße zu verarbeiten, können Sie ein solches Array am besten über einen Zeiger auf das erste Element übergeben, dessen 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 auch zur Beliebtheit der Methode beiträgt. Viele C-Entwickler haben einfach nie die Notwendigkeit, ein Array mit fester Größe zu verarbeiten (oder zu erkennen), und sind sich daher der richtigen Technik mit fester Größe nicht bewusst.

Wenn die Array-Größe jedoch festgelegt ist, wird sie als Zeiger auf ein Element übergeben

void foo(char p[])

ist ein schwerwiegender Fehler auf Technikebene, der heutzutage leider eher verbreitet ist. Eine Pointer-to-Array-Technik ist in solchen Fällen ein viel besserer Ansatz.

Ein weiterer Grund, der die Einführung der Array-Übergabetechnik mit fester Größe behindern könnte, ist die Dominanz eines naiven Ansatzes zur Typisierung dynamisch zugewiesener Arrays. Wenn das Programm beispielsweise feste Arrays vom Typ char[10] (Wie in Ihrem Beispiel) aufruft, wird ein durchschnittlicher Entwickler Arrays wie malloc verwenden

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 der Realität liegt die Wurzel des Problems jedoch im naiven malloc Ansatz. Das oben gezeigte Format malloc sollte für Arrays mit Laufzeitgröße reserviert werden. Wenn der Array-Typ eine Größe zur Kompilierungszeit hat, sieht der bessere Weg zu malloc folgendermaßen aus

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

Dies kann natürlich einfach an die oben deklarierte foo übergeben werden.

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.

158
AnT

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

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

Die standardmäßige Heraufstufung eines Arrays erfolgt zu einem nicht qualifizierten Zeiger.

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

4
Keith Randall

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

1
Per Knytt

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

Ich stimme jedoch 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 die Verwendung von (*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 ein Array zugewiesen wird:

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

    Der address-of-Operator erhält die Adresse des gesamten Arrays in &a, mit dem richtigen Typ, um ihn p zuzuweisen. Ohne den Operator wird a automatisch in die Adresse des ersten Elements des Arrays konvertiert, genau wie in &a[0], der einen anderen Typ hat.

    Da diese automatische Konvertierung bereits stattfindet, bin ich immer verwundert, dass das & ist notwendig. Es steht im Einklang mit der Verwendung von & auf Variablen anderer Typen, aber ich muss bedenken, dass ein Array etwas Besonderes ist und dass ich das &, um den richtigen Adresstyp zu erhalten, obwohl der Adreßwert derselbe ist.

    Ein Grund für mein Problem könnte sein, dass ich in den 80er Jahren K & R C gelernt habe, was es nicht erlaubte, das & -Operator für ganze Arrays (obwohl einige Compiler dies ignorierten oder die Syntax tolerierten). Was übrigens ein weiterer Grund sein kann, warum es schwierig ist, Zeiger auf Arrays zu implementieren: Sie funktionieren nur ordnungsgemäß seit ANSI C, und das & Einschränkung des Operators kann ein weiterer Grund gewesen sein, sie als zu umständlich einzustufen.

  • Wenn typedefnicht verwendet wird, um einen Typ für den Zeiger auf ein Array (in einer gemeinsamen Header-Datei) zu erstellen, benötigt ein globaler Zeiger auf ein Array ein komplizierteres extern Deklaration, um es zwischen Dateien zu teilen:

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

Einfach ausgedrückt, C macht die Dinge nicht so. Ein Array vom Typ T wird als Zeiger auf das erste T im Array übergeben, und das ist alles, was Sie erhalten.

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

*dst++ = *src++

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

In der Nähe dessen, was Sie in C verlangen, müssen Sie einen struct (nach Wert) oder einen Zeiger auf einen (nach Verweis) übergeben. Solange auf beiden Seiten dieser Operation derselbe Strukturtyp verwendet wird, stimmen sowohl der Code, der die Referenz ausgibt, als auch der Code, der sie verwendet, über die Größe der zu verarbeitenden Daten überein.

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

Dennoch hindert nichts Sie oder einen inkompetenten oder böswilligen Programmierer daran, Casts zu verwenden, um den Compiler dazu zu bringen, Ihre Struktur als eine von einer anderen Größe zu behandeln. Die fast uneingeschränkte Fähigkeit, solche Dinge zu tun, ist Teil des Designs von C.

1
Carl Smotricz

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 per Referenz:

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

oder über die Array-Syntax:

void foo(char p[]); //same as char*
1
s1n