Pourquoi ne puis-je pas accéder à un pointeur vers un pointeur pour un tableau de pile?

35

Veuillez consulter le code suivant. Il essaie de passer un tableau en tant que char**à une fonction:

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

Le fait que je ne puisse le faire que compiler en faisant explicitement fond &test2sur des char**indices déjà que ce code est incorrect.

Pourtant, je me demande ce qui ne va pas exactement. Je peux passer un pointeur vers un pointeur vers un tableau alloué dynamiquement mais je ne peux pas passer un pointeur vers un pointeur pour un tableau sur la pile. Bien sûr, je peux facilement contourner le problème en affectant d'abord le tableau à une variable temporaire, comme ceci:

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

Pourtant, quelqu'un peut me expliquer pourquoi il ne fonctionne pas coulé char[256]à char**directement?

Andreas
la source

Réponses:

29

Parce que ce testn'est pas un pointeur.

&testvous obtient un pointeur vers le tableau, de type char (*)[256], qui n'est pas compatible avec char**(car un tableau n'est pas un pointeur). Il en résulte un comportement indéfini.

emlai
la source
3
Mais pourquoi le compilateur C permet alors passer quelque chose du type char (*)[256]à char**?
ComFreek
@ComFreek Je soupçonne qu'avec des avertissements max et -Werror, cela ne permet pas cela.
PiRocks
@ComFreek: Cela ne le permet pas vraiment. Je dois forcer le compilateur à l'accepter en le convertissant explicitement en char**. Sans cette distribution, il ne se compile pas.
Andreas
38

testest un tableau, pas un pointeur, et &testest un pointeur vers le tableau. Ce n'est pas un pointeur vers un pointeur.

Vous avez peut-être été informé qu'un tableau est un pointeur, mais cela est incorrect. Le nom d'un tableau est un nom de l'objet entier - tous les éléments. Ce n'est pas un pointeur sur le premier élément. Dans la plupart des expressions, un tableau est automatiquement converti en pointeur vers son premier élément. C'est une commodité qui est souvent utile. Mais il y a trois exceptions à cette règle:

  • Le tableau est l'opérande de sizeof.
  • Le tableau est l'opérande de &.
  • Le tableau est un littéral de chaîne utilisé pour initialiser un tableau.

Dans &test, le tableau est l'opérande de &, donc la conversion automatique ne se produit pas. Le résultat de &testest un pointeur vers un tableau de 256 char, qui a un type char (*)[256], non char **.

Pour obtenir un pointeur vers un pointeur vers charde test, vous devez d'abord créer un pointeur vers char. Par exemple:

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

Une autre façon de penser à cela est de réaliser que le testnom de l'objet entier - le tableau entier de 256 char. Il ne nomme pas de pointeur, donc, dans &test, il n'y a pas de pointeur dont l'adresse peut être prise, donc cela ne peut pas produire un char **. Pour créer un char **, vous devez d'abord avoir un char *.

Eric Postpischil
la source
1
Cette liste de trois exceptions est-elle exhaustive?
Ruslan
8
@Ruslan: Oui, selon C 2018 6.3.2.1 3.
Eric Postpischil
Oh, et en C11, il y avait aussi l' _Alignofopérateur mentionné en plus de sizeofet &. Je me demande pourquoi ils l'ont enlevé ...
Ruslan
@Ruslan: Cela a été supprimé car c'était une erreur. _Alignofaccepte uniquement un nom de type comme opérande et n'a jamais accepté un tableau ou tout autre objet comme opérande. (Je ne sais pas pourquoi; il semble que cela puisse être syntaxiquement et grammaticalement sizeof, mais ce n'est pas le cas.)
Eric Postpischil
6

Le type de test2est char *. Ainsi, le type de &test2sera char **compatible avec le type de paramètre xde printchar().
Le type de testest char [256]. Ainsi, le type de &testsera char (*)[256]qui n'est pas compatible avec le type de paramètre xde printchar().

Permettez-moi de vous montrer la différence en termes d'adresses de testet 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;
}

Production:

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

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

Point à noter ici:

La sortie (adresse mémoire) de test2et &test2[0]est numériquement identique et leur type est également le même char *.
Mais les test2et &test2sont des adresses différentes et leur type est également différent.
Le type de test2est char *.
Le type de &test2est char **.

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

La sortie (adresse mémoire) de test, &testet &test[0]est numériquement identique , mais leur type est différent .
Le type de testest char [256].
Le type de &testest char (*) [256].
Le type de &test[0]est char *.

Comme le montre la sortie &testest la même que &test[0].

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

Par conséquent, vous obtenez un défaut de segmentation.

HS
la source
3

Vous ne pouvez pas accéder à un pointeur vers un pointeur car ce &testn'est pas un pointeur, c'est un tableau.

Si vous prenez l'adresse d'un tableau, convertissez le tableau et l'adresse du tableau en (void *), et comparez-les, ils seront (sauf pédanterie de pointeur possible) équivalents.

Ce que vous faites vraiment est similaire à cela (encore une fois, sauf aliasing strict):

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

ce qui est manifestement faux.

SS Anne
la source
3

Votre code attend l'argument xde printcharpointer vers la mémoire qui contient un (char *).

Dans le premier appel, il pointe vers le stockage utilisé test2et est donc bien une valeur qui pointe vers a (char *), cette dernière pointant vers la mémoire allouée.

Dans le deuxième appel, cependant, il n'y a aucun endroit où une telle (char *)valeur pourrait être stockée et il est donc impossible de pointer vers une telle mémoire. Le casting que (char **)vous avez ajouté aurait supprimé une erreur de compilation (sur la conversion (char *)en (char **)), mais cela ne ferait pas apparaître le stockage de nulle part pour contenir un (char *)initialisé pointant vers les premiers caractères du test. La conversion du pointeur en C ne modifie pas la valeur réelle du pointeur.

Pour obtenir ce que vous voulez, vous devez le faire explicitement:

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

Je suppose que votre exemple est une distillation d'un morceau de code beaucoup plus grand; à titre d'exemple, vous souhaitez peut-être printcharincrémenter la (char *)valeur vers laquelle la xvaleur transmise pointe afin que lors du prochain appel, le caractère suivant soit imprimé. Si ce n'est pas le cas, pourquoi ne pas simplement passer un (char *)pointage vers le caractère à imprimer, ou même simplement passer le caractère lui-même?

Kevin Martin
la source
Bonne réponse; Je suis d'accord pour dire que la façon la plus simple de garder les choses au clair est de savoir s'il existe ou non un objet C qui contient l'adresse du tableau, c'est-à-dire un objet pointeur dont vous pouvez prendre l'adresse pour obtenir un char **. Les variables / objets de tableau sont simplement le tableau, l'adresse étant implicite, non stockée nulle part. Aucun niveau supplémentaire d'indirection pour y accéder, contrairement à une variable de pointeur qui pointe vers un autre stockage.
Peter Cordes
0

Apparemment, prendre l'adresse de testéquivaut à prendre l'adresse de 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;
}

Compilez cela et exécutez:

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

Ainsi, la cause ultime de l'erreur de segmentation est que ce programme essaiera de déréférencer l'adresse absolue 0x42(également connue sous le nom de 'B'), que votre programme n'a pas la permission de lire.

Bien qu'avec un compilateur / machine différent, les adresses seront différentes: essayez-le en ligne! , mais vous l'obtiendrez toujours, pour une raison quelconque:

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

Mais l'adresse qui cause le défaut de segmentation peut très bien être différente:

[printchar] Address of pointer to pointer: 0x7ffd4891b080
[printchar] Address of pointer: 0x9c000000942  [WAS 0x42 IN MY CASE]
ForceBru
la source
1
Prendre l'adresse de testn'est pas la même chose que prendre l'adresse de test[0]. Le premier a le type char (*)[256]et le second le type char *. Ils ne sont pas compatibles et la norme C leur permet d'avoir des représentations différentes.
Eric Postpischil
Lors du formatage d'un pointeur avec %p, il doit être converti en void *(à nouveau pour des raisons de compatibilité et de représentation).
Eric Postpischil
1
printchar(&test);peut se bloquer pour vous, mais le comportement n'est pas défini par la norme C et les gens peuvent observer d'autres comportements dans d'autres circonstances.
Eric Postpischil
Re "Donc, la cause ultime du défaut de segmentation est que ce programme essaiera de déréférencer l'adresse absolue 0x42 (également connue sous le nom de 'B'), qui est probablement occupée par le système d'exploitation.": S'il y a un défaut de segment essayant de lire un emplacement, cela signifie que rien n'y est mappé, pas qu'il soit occupé par le système d'exploitation. (Sauf qu'il pourrait y avoir quelque chose de mappé comme, par exemple, d'exécution uniquement sans autorisation de lecture, mais cela est peu probable.)
Eric Postpischil
1
&test == &test[0]viole les contraintes de C 2018 6.5.9 2 car les types ne sont pas compatibles. La norme C nécessite une implémentation pour diagnostiquer cette violation et le comportement résultant n'est pas défini par la norme C. Cela signifie que votre compilateur peut produire du code les évaluant comme égaux, mais pas un autre compilateur.
Eric Postpischil
-4

La représentation de char [256]dépend de l'implémentation. Ce ne doit pas être le même que char *.

Coulée &testdu type char (*)[256]de char **rendements comportement non défini.

Avec certains compilateurs, il peut faire ce que vous attendez, et pas d'autres.

ÉDITER:

Après avoir testé avec gcc 9.2.1, il semble qu'en printchar((char**)&test)fait test , la valeur soit convertie en char**. C'est comme si l'instruction était printchar((char**)test). Dans la printcharfonction, xest un pointeur sur le premier caractère du test de tableau, pas un double pointeur sur le premier caractère. Une double dé-référence xentraîne un défaut de segmentation car les 8 premiers octets du tableau ne correspondent pas à une adresse valide.

J'obtiens exactement le même comportement et le même résultat lors de la compilation du programme avec clang 9.0.0-2.

Cela peut être considéré comme un bogue du compilateur, ou le résultat d'un comportement non défini dont le résultat peut être spécifique au compilateur.

Un autre comportement inattendu est que le code

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

La sortie est

px: 0x7ffd92627370
x: 0x7ffd92627370
c: A

Le comportement étrange est cela xet *xa la même valeur.

C'est un truc de compilateur. Je doute que cela soit défini par la langue.

chmike
la source
1
Voulez-vous dire que la représentation de char (*)[256]dépend de la mise en œuvre? La représentation de char [256]n'est pas pertinente dans cette question - c'est juste un tas de bits. Mais, même si vous voulez dire que la représentation d'un pointeur sur un tableau est différente de la représentation d'un pointeur sur un pointeur, cela manque également le point. Même s'ils ont les mêmes représentations, le code de l'OP ne fonctionnerait pas, car le pointeur vers un pointeur peut être déréférencé deux fois, comme cela est fait dans printchar, mais le pointeur vers un tableau ne peut pas, quelle que soit la représentation.
Eric Postpischil
@EricPostpischil la conversion de char (*)[256]à char **est acceptée par le compilateur, mais ne donne pas le résultat attendu car a char [256]n'est pas identique à a char *. J'ai supposé que l'encodage est différent, sinon cela donnerait le résultat attendu.
chmike
Je ne sais pas ce que vous entendez par «résultat attendu». La seule spécification dans la norme C de ce que devrait être le résultat est que, si l'alignement est inadéquat char **, le comportement n'est pas défini, et que, sinon, si le résultat est reconverti en char (*)[256], il se compare au pointeur d'origine. Par «résultat attendu», vous pourriez signifier que, s'il (char **) &testest encore converti en a char *, il se compare à &test[0]. Ce n'est pas un résultat improbable dans les implémentations qui utilisent un espace d'adressage plat, mais ce n'est pas uniquement une question de représentation.
Eric Postpischil
2
De plus, «Casting et test de type char (*) [256] en char ** donne un comportement indéfini.» n'est pas correcte. C 2018 6.3.2.3 7 permet à un pointeur vers un type d'objet d'être converti en n'importe quel autre pointeur vers un type d'objet. Si le pointeur n'est pas correctement aligné pour le type référencé (le type référencé dechar ** est char *), le comportement n'est pas défini. Sinon, la conversion est définie, bien que la valeur ne soit que partiellement définie, par mon commentaire ci-dessus.
Eric Postpischil
char (*x)[256]n'est pas la même chose que char **x. La raison xet l' *ximpression de la même valeur de pointeur xest simplement un pointeur vers le tableau. Votre *x est le tableau , et son utilisation dans un contexte de pointeur se désintègre à l'adresse du tableau . Pas de bogue de compilateur là-bas (ou dans quoi (char **)&test), juste un peu de gymnastique mentale nécessaire pour comprendre ce qui se passe avec les types. (cdecl l'explique comme "déclarer x comme pointeur vers le tableau 256 de char"). Même utiliser char*pour accéder à la représentation d'objet d'un char**n'est pas UB; il peut tout alias.
Peter Cordes