Perché non riesco ad accedere a un puntatore a puntatore per un array di stack?


35

Si prega di dare un'occhiata al seguente codice. Prova a passare un array come char**a una funzione:

#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;
}

Il fatto che riesca a farlo compilare esplicitamente eseguendo il casting &test2su char**già suggerisce che questo codice è sbagliato.

Tuttavia, mi chiedo cosa ci sia esattamente di sbagliato in questo. Posso passare un puntatore a un puntatore a un array allocato dinamicamente ma non riesco a passare un puntatore a un puntatore per un array nello stack. Certo, posso facilmente aggirare il problema assegnando prima l'array a una variabile temporanea, in questo modo:

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

Eppure, qualcuno può spiegare a me perché non funziona a getto char[256]a char**direttamente?

Risposte:


29

Perché testnon è un puntatore.

&testti char (*)[256]dà un puntatore alla matrice, di tipo , che non è compatibile con char**(perché una matrice non è un puntatore). Ciò comporta un comportamento indefinito.


3
Ma perché il compilatore C consente quindi di passare qualcosa di tipo char (*)[256]a char**?
ComFreek,

@ComFreek Sospetto che con un numero massimo di avvertimenti e -Werror, ciò non lo consente.
PiRocks

@ComFreek: non lo consente davvero. Devo costringere il compilatore ad accettarlo lanciandolo esplicitamente char**. Senza quel cast, non si compila.
Andreas

38

testè un array, non un puntatore ed &testè un puntatore all'array. Non è un puntatore a un puntatore.

Potrebbe esserti stato detto che un array è un puntatore, ma questo non è corretto. Il nome di un array è il nome dell'intero oggetto, tutti gli elementi. Non è un puntatore al primo elemento. Nella maggior parte delle espressioni, un array viene automaticamente convertito in un puntatore al suo primo elemento. Questa è una comodità che è spesso utile. Ma ci sono tre eccezioni a questa regola:

  • La matrice è l'operando di sizeof.
  • La matrice è l'operando di &.
  • La matrice è una stringa letterale utilizzata per inizializzare una matrice.

In &test, l'array è l'operando di &, quindi la conversione automatica non si verifica. Il risultato di &testè un puntatore a un array di 256 char, che ha tipo char (*)[256]e non char **.

Per ottenere un puntatore a un puntatore a charda test, si dovrebbe prima bisogno di fare un puntatore a char. Per esempio:

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

Un altro modo di pensarci è rendersi conto che testnomina l'intero oggetto, l'intero array di 256 char. Non nomina un puntatore, quindi, in &test, non esiste un puntatore il cui indirizzo può essere preso, quindi questo non può produrre un char **. Per creare un char **, devi prima avere un char *.


1
Questo elenco di tre eccezioni è esaustivo?
Ruslan,

8
@Ruslan: Sì, per C 2018 6.3.2.1 3.
Eric Postpischil,

Oh, e in C11 c'era anche l' _Alignofoperatore menzionato oltre a sizeofe &. Mi chiedo perché l'hanno rimosso ...
Ruslan,

@Ruslan: è stato rimosso perché è stato un errore. _Alignofaccetta solo un nome di tipo come operando e non accetta mai un array o qualsiasi altro oggetto come operando. (Non so perché; sembra sintatticamente e grammaticalmente potrebbe essere come sizeof, ma non lo è.)
Eric Postpischil,

6

Il tipo di test2è char *. Quindi, il tipo di &test2sarà char **compatibile con il tipo di parametro xdi printchar().
Il tipo di testè char [256]. Quindi, il tipo di &testsarà char (*)[256]che non è compatibile con il tipo di parametro xdi printchar().

Lascia che ti mostri la differenza in termini di indirizzi di teste 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;
}

Produzione:

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

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

Nota qui:

L'output (indirizzo di memoria) di test2e &test2[0]è numericamente uguale e anche il loro tipo è lo stesso char *.
Ma l' test2e &test2sono diversi gli indirizzi e il loro tipo è anche diverso.
Il tipo di test2è char *.
Il tipo di &test2è char **.

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

L'output (indirizzo di memoria) di test, &tested &test[0]è numericamente uguale ma il loro tipo è diverso .
Il tipo di testè char [256].
Il tipo di &testè char (*) [256].
Il tipo di &test[0]è char *.

Come mostra l'output &testè lo stesso di &test[0].

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

Quindi stai riscontrando un errore di segmentazione.


3

Non è possibile accedere a un puntatore a un puntatore perché &testnon è un puntatore: è un array.

Se si prende l'indirizzo di un array (void *), si esegue il cast dell'array e l'indirizzo dell'array e si confrontano, saranno (escluso l'eventuale pedanteria del puntatore) equivalenti.

Quello che stai davvero facendo è simile a questo (di nuovo, a parte il rigoroso aliasing):

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

che è ovviamente sbagliato.


3

Il tuo codice si aspetta che l'argomento xdi printcharper punto alla memoria che contiene una (char *).

Nella prima chiamata, punta alla memoria utilizzata per test2ed è quindi davvero un valore che punta a a (char *), quest'ultimo punta alla memoria allocata.

Nella seconda chiamata, tuttavia, non esiste alcun luogo in cui tale (char *)valore possa essere archiviato e quindi è impossibile puntare a tale memoria. Il cast che (char **)hai aggiunto avrebbe rimosso un errore di compilazione (sulla conversione (char *)in (char **)) ma non avrebbe fatto apparire la memoria dal nulla per contenere un (char *)inizializzato che punta ai primi caratteri del test. Il casting del puntatore in C non modifica il valore effettivo del puntatore.

Per ottenere ciò che vuoi, devi farlo esplicitamente:

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

Presumo che il tuo esempio sia una distillazione di un codice molto più grande; ad esempio, forse si desidera printcharaumentare il (char *)valore a cui xpunta il valore passato in modo che sulla chiamata successiva venga stampato il carattere successivo. In caso contrario, perché non passi semplicemente un (char *)puntamento al personaggio da stampare, o semplicemente passi il personaggio stesso?


Buona risposta; Sono d'accordo che il modo più semplice per mantenerlo dritto sia pensare se esiste o meno un oggetto C che contiene l'indirizzo dell'array, ovvero un oggetto puntatore di cui puoi prendere l'indirizzo per ottenere un char **. Le variabili / gli oggetti dell'array sono semplicemente l'array, con l'indirizzo implicito, non memorizzato da nessuna parte. Nessun livello aggiuntivo di riferimento indiretto per accedervi, diversamente da una variabile puntatore che punta ad altro spazio di archiviazione.
Peter Cordes,

0

Apparentemente, prendere l'indirizzo di testè lo stesso di prendere l'indirizzo di 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;
}

Compilalo ed esegui:

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

Quindi la causa ultima dell'errore di segmentazione è che questo programma proverà a dereferenziare l'indirizzo assoluto 0x42(noto anche come 'B'), che il tuo programma non ha il permesso di leggere.

Sebbene con un compilatore / macchina diverso, gli indirizzi saranno diversi: provalo online! , ma otterrai comunque questo, per qualche motivo:

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

Ma l'indirizzo che causa l'errore di segmentazione potrebbe benissimo essere diverso:

[printchar] Address of pointer to pointer: 0x7ffd4891b080
[printchar] Address of pointer: 0x9c000000942  [WAS 0x42 IN MY CASE]

1
Prendere l'indirizzo di testnon è lo stesso di prendere l'indirizzo di test[0]. Il primo ha tipo char (*)[256]e il secondo ha tipo char *. Non sono compatibili e lo standard C consente loro di avere rappresentazioni diverse.
Eric Postpischil,

Quando si formatta un puntatore con %p, dovrebbe essere convertito in void *(di nuovo per motivi di compatibilità e rappresentazione).
Eric Postpischil,

1
printchar(&test);potrebbe bloccarsi, ma il comportamento non è definito dallo standard C e le persone potrebbero osservare altri comportamenti in altre circostanze.
Eric Postpischil,

Ri "Quindi la causa ultima dell'errore di segmentazione è che questo programma proverà a dereferenziare l'indirizzo assoluto 0x42 (noto anche come" B "), che è probabilmente occupato dal sistema operativo.": Se c'è un errore di segmento che tenta di leggere una posizione, significa che nulla è mappato lì, non che sia occupato dal sistema operativo. (Tranne il fatto che potrebbe esserci qualcosa mappato lì come, diciamo, solo esecuzione senza permessi di lettura, ma è improbabile.)
Eric Postpischil

1
&test == &test[0]viola i vincoli in C 2018 6.5.9 2 perché i tipi non sono compatibili. Lo standard C richiede un'implementazione per diagnosticare questa violazione e il comportamento risultante non è definito dallo standard C. Ciò significa che il tuo compilatore potrebbe produrre un codice che li valuta come uguali, ma un altro compilatore potrebbe non farlo.
Eric Postpischil,

-4

La rappresentazione char [256]dipende dall'implementazione. Non deve essere lo stesso di char *.

Casting &testdi tipo char (*)[256]per char **produrre un comportamento indefinito.

Con alcuni compilatori, può fare quello che ti aspetti, e altri no.

MODIFICARE:

Dopo aver testato con gcc 9.2.1, sembra che printchar((char**)&test)passi effettivamente test come valore cast a char**. È come se l'istruzione fosse printchar((char**)test). Nella printcharfunzione, xè un puntatore al primo carattere del test dell'array, non un doppio puntatore al primo carattere. Un doppio de-riferimento xprovoca un errore di segmentazione perché gli 8 primi byte dell'array non corrispondono a un indirizzo valido.

Ottengo lo stesso identico comportamento e risultato quando compilo il programma con clang 9.0.0-2.

Questo può essere considerato un bug del compilatore o il risultato di un comportamento indefinito il cui risultato potrebbe essere specifico del compilatore.

Un altro comportamento imprevisto è che il codice

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

L'output è

px: 0x7ffd92627370
x: 0x7ffd92627370
c: A

Lo strano comportamento è quello xe *xhanno lo stesso valore.

Questa è una cosa da compilatore. Dubito che questo sia definito dalla lingua.


1
Vuoi dire che la rappresentazione char (*)[256]dipende dall'implementazione? La rappresentazione di char [256]non è rilevante in questa domanda: è solo un mucchio di bit. Ma, anche se intendi che la rappresentazione di un puntatore a un array è diversa dalla rappresentazione di un puntatore a un puntatore, anche questo manca il punto. Anche se hanno le stesse rappresentazioni, il codice dell'OP non funzionerebbe, poiché il puntatore a un puntatore può essere dereferenziato due volte, come avviene in printchar, ma il puntatore a un array non può, indipendentemente dalla rappresentazione.
Eric Postpischil,

@EricPostpischil il cast da char (*)[256]a char **è accettato dal compilatore, ma non produce il risultato atteso perché a char [256]non è lo stesso di a char *. Ho ipotizzato che la codifica fosse diversa, altrimenti avrebbe prodotto il risultato atteso.
chmike,

Non so cosa intendi per "risultato atteso". L'unica specifica nello standard C di ciò che dovrebbe essere il risultato è che, se l'allineamento è inadeguato char **, il comportamento non è definito e che, altrimenti, se il risultato viene riconvertito in char (*)[256], si confronta uguale al puntatore originale. Per "risultato atteso", si potrebbe significare che, se (char **) &testulteriormente convertito in a char *, confronta uguale a &test[0]. Questo non è un risultato improbabile in implementazioni che utilizzano uno spazio di indirizzamento piatto, ma non è puramente una questione di rappresentazione.
Eric Postpischil,

2
Inoltre, "Casting e test di tipo char (*) [256] in char ** produce un comportamento indefinito." non è corretto. C 2018 6.3.2.3 7 consente a un puntatore a un tipo di oggetto di essere convertito in qualsiasi altro puntatore a un tipo di oggetto. Se il puntatore non è allineato correttamente per il tipo di riferimento (il tipo di riferimento char **è char *), il comportamento non è definito. Altrimenti, la conversione è definita, sebbene il valore sia definito solo parzialmente, per il mio commento sopra.
Eric Postpischil,

char (*x)[256]non è la stessa cosa di char **x. Il motivo xe la *xstampa dello stesso valore del puntatore è che xè semplicemente un puntatore alla matrice. Il tuo *x è l'array e il suo utilizzo in un contesto di puntatore decade all'indirizzo della matrice . Nessun bug del compilatore lì (o in quello che (char **)&testfa), solo un po 'di ginnastica mentale necessaria per capire che sta succedendo con i tipi. (cdecl lo spiega come "dichiara x come puntatore all'array 256 di char"). Anche usando char*per accedere alla rappresentazione dell'oggetto di achar** non è UB; può alias qualsiasi cosa.
Peter Cordes,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.