Warum kann ich nicht auf einen Zeiger auf Zeiger für ein Stapelarray zugreifen?

35

Bitte schauen Sie sich den folgenden Code an. Es wird versucht, ein Array als char**an eine Funktion zu übergeben:

#include <stdio.h>
#include <stdlib.h>

static void printchar(char **x)
{
    printf("Test: %c\n", (*x)[0]);
}

int main(int argc, char *argv[])
{
    char test[256];
    char *test2 = malloc(256);

    test[0] = 'B';
    test2[0] = 'A';

    printchar(&test2);            // works
    printchar((char **) &test);   // crashes because *x in printchar() has an invalid pointer

    free(test2);

    return 0;
}

Die Tatsache, dass ich es nur durch explizites Casting &test2zum Kompilieren bringen kann, char**deutet bereits darauf hin, dass dieser Code falsch ist.

Trotzdem frage ich mich, was genau daran falsch ist. Ich kann einen Zeiger auf einen Zeiger auf ein dynamisch zugewiesenes Array übergeben, aber ich kann keinen Zeiger auf einen Zeiger für ein Array auf dem Stapel übergeben. Natürlich kann ich das Problem leicht umgehen, indem ich das Array zuerst einer temporären Variablen zuweise, wie folgt:

char test[256];
char *tmp = test;
test[0] = 'B';
printchar(&tmp);

Kann mir trotzdem jemand erklären, warum es nicht funktioniert char[256], char**direkt zu besetzen?

Andreas
quelle

Antworten:

29

Weil testist kein Zeiger.

&testSie erhalten einen Zeiger auf das Array vom Typ char (*)[256], der nicht kompatibel ist char**(da ein Array kein Zeiger ist). Dies führt zu undefiniertem Verhalten.

Emlai
quelle
3
Aber warum erlaubt der C-Compiler dann, etwas vom Typ an char (*)[256]zu übergeben char**?
ComFreek
@ComFreek Ich vermute, dass es mit maximalen Warnungen und -Werror das nicht erlaubt.
PiRocks
@ ComFreek: Es erlaubt es nicht wirklich. Ich muss den Compiler zwingen, es zu akzeptieren, indem ich es explizit an wirke char**. Ohne diese Besetzung wird es nicht kompiliert.
Andreas
38

testist ein Array, kein Zeiger und &testein Zeiger auf das Array. Es ist kein Zeiger auf einen Zeiger.

Möglicherweise wurde Ihnen mitgeteilt, dass ein Array ein Zeiger ist, dies ist jedoch falsch. Der Name eines Arrays ist ein Name des gesamten Objekts - aller Elemente. Es ist kein Zeiger auf das erste Element. In den meisten Ausdrücken wird ein Array automatisch in einen Zeiger auf sein erstes Element konvertiert. Das ist eine Annehmlichkeit, die oft nützlich ist. Es gibt jedoch drei Ausnahmen von dieser Regel:

  • Das Array ist der Operand von sizeof.
  • Das Array ist der Operand von &.
  • Das Array ist ein String-Literal, mit dem ein Array initialisiert wird.

In &testist das Array der Operand von &, sodass die automatische Konvertierung nicht erfolgt. Das Ergebnis von &testist ein Zeiger auf ein Array von 256 char, das den Typ char (*)[256]nicht hat char **.

Um einen Zeiger auf einen Zeiger charvon zu erhalten test, müssten Sie zuerst einen Zeiger auf erstellen char. Zum Beispiel:

char *p = test; // Automatic conversion of test to &test[0] occurs.
printchar(&p);  // Passes a pointer to a pointer to char.

Eine andere Möglichkeit, darüber nachzudenken, besteht darin, zu erkennen, dass testdas gesamte Objekt benannt wird - das gesamte Array von 256 char. Es wird kein Zeiger benannt, daher &testgibt es in keinen Zeiger, dessen Adresse verwendet werden kann, sodass dies keinen erzeugen kann char **. Um ein zu erstellen char **, müssen Sie zuerst ein haben char *.

Eric Postpischil
quelle
1
Ist diese Liste mit drei Ausnahmen vollständig?
Ruslan
8
@ Ruslan: Ja, gemäß C 2018 6.3.2.1 3.
Eric Postpischil
Oh, und in C11 wurde _Alignofneben sizeofund auch der Operator erwähnt &. Ich frage mich, warum sie es entfernt haben ...
Ruslan
@ Ruslan: Das wurde entfernt, weil es ein Fehler war. _AlignofAkzeptiert nur einen Typnamen als Operanden und niemals ein Array oder ein anderes Objekt als Operanden. (Ich weiß nicht warum; es scheint syntaktisch und grammatikalisch so zu sein sizeof, aber es ist nicht so.)
Eric Postpischil
6

Die Art von test2ist char *. Der Typ von &test2wird also char **mit dem Parametertyp xvon kompatibel sein printchar().
Die Art von testist char [256]. Der Typ von &testwird also sein, char (*)[256]der nicht mit dem Typ des Parameters xvon kompatibel ist printchar().

Lassen Sie mich Ihnen den Unterschied in Bezug auf Adressen von testund zeigen test2.

#include <stdio.h>
#include <stdlib.h>

static void printchar(char **x)
{
    printf("x = %p\n", (void*)x);
    printf("*x  = %p\n", (void*)(*x));
    printf("Test: %c\n", (*x)[0]);
}

int main(int argc, char *argv[])
{
    char test[256];
    char *test2 = malloc(256);

    test[0] = 'B';
    test2[0] = 'A';

    printf ("test2 : %p\n", (void*)test2);
    printf ("&test2 : %p\n", (void*)&test2);
    printf ("&test2[0] : %p\n", (void*)&test2[0]);
    printchar(&test2);            // works

    printf ("\n");
    printf ("test : %p\n", (void*)test);
    printf ("&test : %p\n", (void*)&test);
    printf ("&test[0] : %p\n", (void*)&test[0]);

    // Commenting below statement
    //printchar((char **) &test);   // crashes because *x in printchar() has an invalid pointer

    free(test2);

    return 0;
}

Ausgabe:

$ ./a.out 
test2 : 0x7fe974c02970
&test2 : 0x7ffee82eb9e8
&test2[0] : 0x7fe974c02970
x = 0x7ffee82eb9e8
*x  = 0x7fe974c02970
Test: A

test : 0x7ffee82eba00
&test : 0x7ffee82eba00
&test[0] : 0x7ffee82eba00

Hier ist zu beachten:

Die Ausgabe (Speicheradresse) von test2und &test2[0]ist numerisch gleich und ihr Typ ist ebenfalls gleich char *.
Aber die test2und &test2sind unterschiedliche Adressen und ihr Typ ist auch unterschiedlich.
Die Art von test2ist char *.
Die Art von &test2ist char **.

x = &test2
*x = test2
(*x)[0] = test2[0] 

Die Ausgang (Speicheradresse) aus test, &testund &test[0]ist numerisch gleiche aber ihre Art unterscheidet .
Die Art von testist char [256].
Die Art von &testist char (*) [256].
Die Art von &test[0]ist char *.

Wie die Ausgabe zeigt, &testist das gleiche wie &test[0].

x = &test[0]
*x = test[0]       //first element of test array which is 'B'
(*x)[0] = ('B')[0]   // Not a valid statement

Daher erhalten Sie einen Segmentierungsfehler.

HS
quelle
3

Sie können nicht auf einen Zeiger auf einen Zeiger zugreifen, da &testes sich nicht um einen Zeiger handelt, sondern um ein Array.

Wenn Sie die Adresse eines Arrays verwenden, das Array und die Adresse des Arrays umwandeln (void *)und vergleichen, sind sie (mit Ausnahme möglicher Zeigerpedanterie) gleichwertig.

Was Sie wirklich tun, ist ähnlich (abgesehen von striktem Aliasing):

putchar(**(char **)test);

das ist ganz offensichtlich falsch.

SS Anne
quelle
3

Ihr Code erwartet, dass das Argument xvon printcharauf den Speicher verweist, der a enthält (char *).

Beim ersten Aufruf zeigt es auf den Speicher, für den es verwendet wird, test2und ist somit tatsächlich ein Wert, der auf a zeigt (char *), wobei letzterer auf den zugewiesenen Speicher zeigt.

Beim zweiten Aufruf gibt es jedoch keinen Ort, an dem ein solcher (char *)Wert gespeichert werden könnte, und daher ist es unmöglich, auf einen solchen Speicher zu verweisen. Die von (char **)Ihnen hinzugefügte Besetzung hätte einen Kompilierungsfehler (beim Konvertieren (char *)in (char **)) beseitigt , aber der Speicher würde nicht aus dem Nichts erscheinen und eine (char *)Initialisierung enthalten , die auf die ersten Zeichen des Tests verweist. Das Zeiger-Casting in C ändert den tatsächlichen Wert des Zeigers nicht.

Um zu bekommen, was Sie wollen, müssen Sie es explizit tun:

char *tempptr = &temp;
printchar(&tempptr);

Ich gehe davon aus, dass Ihr Beispiel eine Destillation eines viel größeren Codeteils ist. Als Beispiel möchten Sie möglicherweise printcharden (char *)Wert erhöhen, auf den der übergebene xWert zeigt, damit beim nächsten Aufruf das nächste Zeichen gedruckt wird. Wenn dies nicht der Fall ist, geben Sie dann einfach einen (char *)Hinweis auf das zu druckende Zeichen oder geben Sie das Zeichen selbst weiter.

Kevin Martin
quelle
Gute Antwort; Ich bin damit einverstanden, dass der einfachste Weg, dies klar zu halten, darin besteht, darüber nachzudenken, ob es ein C-Objekt gibt, das die Adresse des Arrays enthält, dh ein Zeigerobjekt, dessen Adresse Sie verwenden können, um ein zu erhalten char **. Array - Variablen / Objekte einfach sind die Array, mit der Adresse ist implizit, nicht gespeichert überall. Keine zusätzliche Indirektionsebene für den Zugriff, im Gegensatz zu einer Zeigervariablen, die auf einen anderen Speicher verweist.
Peter Cordes
0

Anscheinend ist die Adresse von testdie gleiche wie die Adresse von test[0]:

#include <stdio.h>
#include <stdlib.h>

static void printchar(char **x)
{
    printf("[printchar] Address of pointer to pointer: %p\n", (void *)x);
    printf("[printchar] Address of pointer: %p\n", (void *)*x);
    printf("Test: %c\n", **x);
}

int main(int argc, char *argv[])
{
    char test[256];
    char *test2 = malloc(256);

    printf("[main] Address of test: %p\n", (void *)test);
    printf("[main] Address of the address of test: %p\n", (void *)&test);
    printf("[main] Address of test2: %p\n", (void *)test2);
    printf("[main] Address of the address of test2: %p\n", (void *)&test2);

    test[0] = 'B';
    test2[0] = 'A';

    printchar(&test2);            // works
    printchar(&test);   // crashes because *x in printchar() has an invalid pointer

    free(test2);

    return 0;
}

Kompilieren Sie das und führen Sie Folgendes aus:

forcebru$ clang test.c -Wall && ./a.out
test.c:25:15: warning: incompatible pointer types passing 'char (*)[256]' to
      parameter of type 'char **' [-Wincompatible-pointer-types]
    printchar(&test);   // crashes because *x in printchar() has an inva...
              ^~~~~
test.c:4:30: note: passing argument to parameter 'x' here
static void printchar(char **x)
                             ^
1 warning generated.
[main] Address of test: 0x7ffeeed039c0
[main] Address of the address of test: 0x7ffeeed039c0 [THIS IS A PROBLEM]
[main] Address of test2: 0x7fbe20c02aa0
[main] Address of the address of test2: 0x7ffeeed039a8
[printchar] Address of pointer to pointer: 0x7ffeeed039a8
[printchar] Address of pointer: 0x7fbe20c02aa0
Test: A
[printchar] Address of pointer to pointer: 0x7ffeeed039c0
[printchar] Address of pointer: 0x42 [THIS IS THE ASCII CODE OF 'B' in test[0] = 'B';]
Segmentation fault: 11

Die ultimative Ursache für den Segmentierungsfehler ist also, dass dieses Programm versucht, die absolute Adresse 0x42(auch bekannt als 'B') zu dereferenzieren , die Ihr Programm nicht lesen darf.

Obwohl bei einem anderen Compiler / Computer die Adressen unterschiedlich sind: Probieren Sie es online aus! , aber Sie werden dies aus irgendeinem Grund immer noch bekommen:

[main] Address of test: 0x7ffd4891b080
[main] Address of the address of test: 0x7ffd4891b080  [SAME ADDRESS!]

Die Adresse, die den Segmentierungsfehler verursacht, kann jedoch sehr unterschiedlich sein:

[printchar] Address of pointer to pointer: 0x7ffd4891b080
[printchar] Address of pointer: 0x9c000000942  [WAS 0x42 IN MY CASE]
ForceBru
quelle
1
Die Adresse von zu nehmen testist nicht dasselbe wie die Adresse von zu nehmen test[0]. Ersteres hat Typ char (*)[256]und letzteres hat Typ char *. Sie sind nicht kompatibel und der C-Standard erlaubt ihnen unterschiedliche Darstellungen.
Eric Postpischil
Wenn Sie einen Zeiger mit formatieren %p, sollte er in konvertiert werden void *(wiederum aus Gründen der Kompatibilität und Darstellung).
Eric Postpischil
1
printchar(&test);kann für Sie abstürzen, aber das Verhalten ist nicht durch den C-Standard definiert, und Personen können unter anderen Umständen andere Verhaltensweisen beobachten.
Eric Postpischil
Betreff „Die ultimative Ursache für den Segmentierungsfehler ist also, dass dieses Programm versucht, die absolute Adresse 0x42 (auch als 'B' bekannt) zu dereferenzieren, die wahrscheinlich vom Betriebssystem belegt wird.”: Wenn ein Segmentfehler beim Lesen auftritt Ein Standort bedeutet, dass dort nichts zugeordnet ist und nicht, dass er vom Betriebssystem belegt ist. (Außer, dass dort etwas abgebildet sein könnte, beispielsweise "Nur ausführen" ohne Leseberechtigung, aber das ist unwahrscheinlich.)
Eric Postpischil
1
&test == &test[0]verstößt gegen die Einschränkungen in C 2018 6.5.9 2, da die Typen nicht kompatibel sind. Der C-Standard erfordert eine Implementierung, um diese Verletzung zu diagnostizieren, und das resultierende Verhalten wird vom C-Standard nicht definiert. Das bedeutet, dass Ihr Compiler möglicherweise Code erstellt, der sie als gleich bewertet, ein anderer Compiler jedoch möglicherweise nicht.
Eric Postpischil
-4

Die Darstellung von char [256]ist implementierungsabhängig. Es darf nicht dasselbe sein wie char *.

Gießen &testdes Typs char (*)[256]zu char **Ausbeuten nicht definiertes Verhalten.

Bei einigen Compilern kann es das tun, was Sie erwarten, bei anderen nicht.

BEARBEITEN:

Nach dem Testen mit gcc 9.2.1 scheint dies printchar((char**)&test)tatsächlich test als Wert übergeben zu werden char**. Es ist, als ob die Anweisung war printchar((char**)test). In der printcharFunktion xist ein Zeiger auf das erste Zeichen des Array-Tests, kein Doppelzeiger auf das erste Zeichen. Eine doppelte Referenzentfernung xführt zu einem Segmentierungsfehler, da die 8 ersten Bytes des Arrays keiner gültigen Adresse entsprechen.

Ich bekomme genau das gleiche Verhalten und Ergebnis beim Kompilieren des Programms mit clang 9.0.0-2.

Dies kann als Compiler-Fehler oder als Ergebnis eines undefinierten Verhaltens angesehen werden, dessen Ergebnis möglicherweise compilerspezifisch ist.

Ein weiteres unerwartetes Verhalten ist der Code

void printchar2(char (*x)[256]) {
    printf("px: %p\n", *x);
    printf("x: %p\n", x);
    printf("c: %c\n", **x);
}

Die Ausgabe ist

px: 0x7ffd92627370
x: 0x7ffd92627370
c: A

Das seltsame Verhalten ist das xund *xhat den gleichen Wert.

Dies ist eine Compilersache. Ich bezweifle, dass dies durch die Sprache definiert ist.

chmike
quelle
1
Meinen Sie, dass die Darstellung von char (*)[256]implementierungsabhängig ist? Die Darstellung von char [256]ist in dieser Frage nicht relevant - es handelt sich nur um eine Reihe von Bits. Aber selbst wenn Sie meinen, dass sich die Darstellung eines Zeigers auf ein Array von der Darstellung eines Zeigers auf einen Zeiger unterscheidet, verfehlt dies auch den Punkt. Selbst wenn sie dieselben Darstellungen haben, würde der OP-Code nicht funktionieren, da der Zeiger auf einen Zeiger wie in zweimal zweimal dereferenziert werden kann, printcharder Zeiger auf ein Array jedoch nicht, unabhängig von der Darstellung.
Eric Postpischil
@EricPostpischil Die Umwandlung von char (*)[256]bis char **wird vom Compiler akzeptiert, liefert jedoch nicht das erwartete Ergebnis, da a char [256]nicht mit a identisch ist char *. Ich nahm an, dass die Codierung anders ist, sonst würde es das erwartete Ergebnis liefern.
chmike
Ich weiß nicht, was Sie unter "erwartetem Ergebnis" verstehen. Die einzige Spezifikation im C-Standard für das Ergebnis ist, dass char **das Verhalten bei unzureichender Ausrichtung undefiniert ist und ansonsten, wenn das Ergebnis zurück in konvertiert wird char (*)[256], dem ursprünglichen Zeiger entspricht. Mit "erwartetes Ergebnis" meinen Sie möglicherweise, dass, wenn (char **) &testes weiter in a konvertiert wird char *, es gleich ist &test[0]. Dies ist bei Implementierungen, die einen flachen Adressraum verwenden, kein unwahrscheinliches Ergebnis, aber keine reine Repräsentationssache.
Eric Postpischil
2
Außerdem führt "Casting & Test vom Typ char (*) [256] zu char ** zu undefiniertem Verhalten." das ist nicht richtig. C 2018 6.3.2.3 7 ermöglicht die Konvertierung eines Zeigers auf einen Objekttyp in einen anderen Zeiger auf einen Objekttyp. Wenn der Zeiger für den referenzierten Typ (der referenzierte Typ von char **is char *) nicht korrekt ausgerichtet ist , ist das Verhalten undefiniert. Andernfalls wird die Konvertierung gemäß meinem obigen Kommentar definiert, obwohl der Wert nur teilweise definiert ist.
Eric Postpischil
char (*x)[256]ist nicht dasselbe wie char **x. Der Grund xund der *xDruck des gleichen Zeigerwerts ist, dass xes sich lediglich um einen Zeiger auf das Array handelt. Ihr *x ist das Array , und wenn Sie es in einem Zeigerkontext verwenden, wird es auf die Adresse des Arrays zurückgesetzt . Kein Compiler-Fehler (oder was (char **)&testauch immer), nur ein wenig mentale Gymnastik, um herauszufinden, was mit Typen los ist. (cdecl erklärt es als "deklariere x als Zeiger auf das Array 256 von char"). Selbst die Verwendung char*für den Zugriff auf die Objektdarstellung von a char**ist nicht UB; es kann alles alias.
Peter Cordes