40 votes

Une comparaison d'égalité entre des pointeurs non liés peut-elle être évaluée comme vraie ?

La section 6.5.9 de la C standard concernant le == y != les opérateurs déclarent ce qui suit :

2 L'une des situations suivantes s'applique :

  • les deux opérandes sont de type arithmétique ;
  • les deux opérandes sont des pointeurs vers des versions qualifiées ou non qualifiées de types compatibles ;
  • un opérande est un pointeur vers un type d'objet et l'autre est un pointeur vers une version qualifiée ou non qualifiée de void ; ou
  • un opérande est un pointeur et l'autre est une constante de type pointeur nul.

...

6 Deux pointeurs sont égaux si et seulement si les deux sont des pointeurs nuls, les deux sont des pointeurs vers le même objet (y compris un pointeur vers un objet et un sous-objet à son début) ou une fonction, les deux sont des pointeurs vers l'avant-dernier élément du même objet de type tableau, ou l'un est un l'un est un pointeur vers la fin d'un objet du tableau et l'autre est un l'autre est un pointeur vers le début d'un autre objet tableau qui se trouve être immédiatement après le premier objet de tableau dans l'espace d'adressage. 109)

7 Aux fins de ces opérateurs, un pointeur sur un objet qui n'est pas qui n'est pas un élément d'un tableau se comporte de la même manière qu'un pointeur vers le premier élément du tableau. élément d'un tableau de longueur un avec le type de l'objet comme type d'élément. type d'élément.

Note de bas de page 109 :

109) Deux objets peuvent être adjacents en mémoire parce qu'il s'agit d'éléments adjacents éléments adjacents d'un tableau plus grand ou des membres adjacents d'une structure sans aucun de remplissage entre eux, ou parce que l'implémentation a choisi de les placer ainsi, même s'ils ne sont pas liés . Si des opérations antérieures de pointeurs invalides invalides (comme les accès en dehors des limites d'un tableau) ont donné lieu à des comportement indéfini, les comparaisons suivantes produisent également un comportement indéfini.

Cela semble indiquer que vous pourriez faire ce qui suit :

int a;
int b;
printf("a precedes b: %d\n", (&a + 1) == &b);
printf("b precedes a: %d\n", (&b + 1) == &a);

Cela devrait être légal puisque nous utilisons une adresse un élément après la fin d'un tableau (qui dans ce cas est un objet unique traité comme un tableau de taille 1) sans le déréférencer. Plus important encore, l'une de ces deux instructions serait nécessaire pour afficher 1 si une variable suit immédiatement l'autre en mémoire.

Cependant, les tests n'ont pas semblé le confirmer. Étant donné le programme de test suivant :

#include <stdio.h>

struct s {
    int a;
    int b;
};

int main()
{
    int a;
    int b;
    int *x = &a;
    int *y = &b;

    printf("sizeof(int)=%zu\n", sizeof(int));
    printf("&a=%p\n", (void *)&a);
    printf("&b=%p\n", (void *)&b);
    printf("x=%p\n", (void *)x);
    printf("y=%p\n", (void *)y);

    printf("addr: a precedes b: %d\n", ((&a)+1) == &b);
    printf("addr: b precedes a: %d\n", &a == ((&b)+1));
    printf("pntr: a precedes b: %d\n", (x+1) == y);
    printf("pntr: b precedes a: %d\n", x == (y+1));

    printf("  x=%p,   &a=%p\n", (void *)(x), (void *)(&a));
    printf("y+1=%p, &b+1=%p\n", (void *)(y+1), (void *)(&b+1));

    struct s s1;
    x=&s1.a;
    y=&s1.b;
    printf("addr: s.a precedes s.b: %d\n", ((&s1.a)+1) == &s1.b);
    printf("pntr: s.a precedes s.b: %d\n", (x+1) == y);
    return 0;
}

Le compilateur est gcc 4.8.5, le système est CentOS 7.2 x64.

Avec -O0 j'obtiens le résultat suivant :

sizeof(int)=4
&a=0x7ffe9498183c
&b=0x7ffe94981838
x=0x7ffe9498183c
y=0x7ffe94981838
addr: a precedes b: 0
addr: b precedes a: 0
pntr: a precedes b: 0
pntr: b precedes a: 1
  x=0x7ffe9498183c,   &a=0x7ffe9498183c
y+1=0x7ffe9498183c, &b+1=0x7ffe9498183c
addr: s.a precedes s.b: 1

Nous pouvons voir ici qu'un int est de 4 octets et que l'adresse de a est 4 octets après l'adresse de b et que x contient l'adresse de a tandis que y contient l'adresse de b . Cependant, la comparaison &a == ((&b)+1) évalue à false alors que la comparaison (x+1) == y est évalué à true. Je m'attendrais à ce que les deux soient vrais puisque les adresses comparées semblent identiques.

Avec -O1 je reçois ça :

sizeof(int)=4
&a=0x7ffca96e30ec
&b=0x7ffca96e30e8
x=0x7ffca96e30ec
y=0x7ffca96e30e8
addr: a precedes b: 0
addr: b precedes a: 0
pntr: a precedes b: 0
pntr: b precedes a: 0
  x=0x7ffca96e30ec,   &a=0x7ffca96e30ec
y+1=0x7ffca96e30ec, &b+1=0x7ffca96e30ec
addr: s.a precedes s.b: 1
pntr: s.a precedes s.b: 1

Maintenant, les deux comparaisons donnent un résultat faux, même si (comme avant) l'adresse comparée semble être la même.

Cela semble indiquer que comportement indéfini mais d'après la façon dont j'ai lu le passage ci-dessus, il semble que cela devrait être autorisé.

Notez également que la comparaison des adresses d'objets adjacents de même type dans un fichier de type struct imprime le résultat attendu dans tous les cas.

Est-ce que je lis mal quelque chose ici concernant ce qui est autorisé (ce qui signifie que c'est UB), ou est-ce que cette version de gcc est non-conforme dans ce cas ?

25voto

Keith Thompson Points 85120

Une comparaison d'égalité entre des pointeurs non liés peut-elle être évaluée comme vraie ?

Oui, mais...

int a;
int b;
printf("a precedes b: %d\n", (&a + 1) == &b);
printf("b precedes a: %d\n", (&b + 1) == &a);

Il y a, selon mon interprétation de la norme C, trois possibilités :

  • a précède immédiatement b
  • b précède immédiatement a
  • ni a ni b ne précèdent immédiatement l'autre (il peut y avoir un vide, ou un autre objet, entre eux).

J'ai joué avec cela il y a quelque temps et j'ai conclu que GCC effectuait une optimisation invalide sur le fichier == pour les pointeurs, ce qui donne un résultat faux même lorsque les adresses sont les mêmes, j'ai donc soumis un rapport de bogue :

https://gcc.gnu.org/bugzilla/show_bug.cgi?id=63611

Ce bogue a été fermé en tant que doublon d'un autre rapport :

https://gcc.gnu.org/bugzilla/show_bug.cgi?id=61502

Les mainteneurs de GCC qui ont répondu à ces rapports de bogue semblent être d'avis que l'adjacence de deux objets n'a pas besoin d'être cohérente et que la comparaison de leurs adresses peut montrer qu'ils sont adjacents ou non, au cours de la même exécution du programme. Comme vous pouvez le voir dans mes commentaires sur le deuxième ticket Bugzilla, je ne suis pas du tout d'accord. À mon avis, sans un comportement cohérent de la fonction == opérateur, les exigences de la norme concernant les objets adjacents n'ont aucun sens, et je pense que nous devons supposer que ces mots ne sont pas simplement décoratifs.

Voici un programme de test simple :

#include <stdio.h>
int main(void) {
    int x;
    int y;
    printf("&x = %p\n&y = %p\n", (void*)&x, (void*)&y);
    if (&y == &x + 1) {
        puts("y immediately follows x");
    }
    else if (&x == &y + 1) {
        puts("x immediately follows y");
    }
    else {
        puts("x and y are not adjacent");
    }
}

Lorsque je le compile avec GCC 6.2.0, les adresses imprimées de x y y diffèrent d'exactement 4 octets à tous les niveaux d'optimisation, mais j'obtiens y immediately follows x seulement à -O0 ; à -O1 , -O2 y -O3 Je reçois x and y are not adjacent . Je pense que ce comportement est incorrect, mais apparemment, il ne sera pas corrigé.

clang 3.8.1, à mon avis, se comporte correctement, en montrant x immediately follows y à tous les niveaux d'optimisation. Clang avait précédemment un problème avec ceci ; je l'ai signalé :

https://bugs.llvm.org/show_bug.cgi?id=21327

et il a été corrigé.

Je suggère de ne pas se fier aux comparaisons d'adresses d'objets éventuellement adjacents qui se comportent de manière cohérente.

(Notez que les opérateurs relationnels ( < , <= , > , >= ) sur des pointeurs vers des objets non liés ont un comportement non défini, mais les opérateurs d'égalité ( == , != ) sont généralement tenus de se comporter de manière cohérente).

14voto

Bathsheba Points 23209
int a;
int b;
printf("a precedes b: %d\n", (&a + 1) == &b);
printf("b precedes a: %d\n", (&b + 1) == &a);

est un code parfaitement bien défini, mais probablement plus par chance que par jugement.

Vous êtes autorisé à prendre l'adresse d'un scalaire et à placer un pointeur un au-delà de cette adresse. Ainsi, &a + 1 est valide, mais &a + 2 ne l'est pas. Vous êtes également autorisé à comparer la valeur d'un pointeur du même type avec la valeur de tout autre pointeur valide à l'aide de la fonction == y != bien que l'arithmétique des pointeurs ne soit valable que dans les tableaux.

Votre affirmation selon laquelle l'adresse de a y b vous dit quoi que ce soit sur la façon dont ils sont placés dans la mémoire est une foutaise. Pour être clair, vous ne pouvez pas "atteindre" b par arithmétique de pointeur sur l'adresse de a .

Quant à

struct s {
    int a;
    int b;
};

La norme garantit que l'adresse du struct est la même que l'adresse de a mais une quantité arbitraire de remplissage peut être insérée entre a y b . Encore une fois, vous ne pouvez pas atteindre l'adresse de b par toute arithmétique de pointeur sur l'adresse de a .

8voto

chux Points 13185

Une comparaison d'égalité entre des pointeurs non liés peut-elle être évaluée comme vraie ?

Oui. C précise quand cela est vrai.

Deux pointeurs se comparent de manière égale si et seulement si ... ou l'un d'eux est un pointeur vers une fin passée d'un objet tableau et l'autre est un pointeur vers le début d'un objet tableau différent qui se trouve suivre immédiatement le premier objet tableau dans l'espace d'adressage. C11dr §6.5.9 6

Pour être clair : les variables adjacentes dans le code ne doivent pas nécessairement être adjacentes en mémoire, mais elles peuvent l'être.


Le code ci-dessous démontre que c'est possible . Il utilise un vidage de la mémoire d'un int* en plus de la convention "%p" y (void*) .

Pourtant, le code et la sortie de l'OP ne reflètent pas cela. Étant donné la partie "comparer égal si et seulement si" de la spécification ci-dessus, IMO, La compilation du PO est non conforme . Adjacent en mémoire variables p,q du même type, soit &p+1 == &q o &p == &q+1 doit être vrai.

Pas d'avis si les objets sont de type différent - le PO ne demande pas cette CAI.


void print_int_ptr(const char *prefix, int *p) {
  printf("%s %p", prefix, (void *) p);
  union {
    int *ip;
    unsigned char uc[sizeof (int*)];
  } u = {p};
  for (size_t i=0; i< sizeof u; i++) {
    printf(" %02X", u.uc[i]);
  }
  printf("\n");
}

int main(void) {
  int b = rand();
  int a = rand();
  printf("sizeof(int) = %zu\n", sizeof a);
  print_int_ptr("&a     =", &a);
  print_int_ptr("&a + 1 =", &a + 1);
  print_int_ptr("&b     =", &b);
  print_int_ptr("&b + 1 =", &b + 1);
  printf("&a + 1 == &b: %d\n", &a + 1 == &b);
  printf("&a == &b + 1: %d\n", &a == &b + 1);
  return a + b;
}

Sortie

sizeof(int) = 4
&a     = 0x28cc28 28 CC 28 00
&a + 1 = 0x28cc2c 2C CC 28 00  <-- same bit pattern
&b     = 0x28cc2c 2C CC 28 00  <-- same bit pattern
&b + 1 = 0x28cc30 30 CC 28 00
&a + 1 == &b: 1                <-- compare equal
&a == &b + 1: 0

3voto

supercat Points 25534

Les auteurs de la norme n'ont pas essayé de la rendre "à l'épreuve des juristes", et par conséquent, elle est quelque peu ambiguë. Cette ambiguïté n'est généralement pas un problème lorsque les auteurs de compilateurs s'efforcent de bonne foi de respecter le principe du moindre étonnement, puisqu'il existe un comportement clair et non étonnant, et que tout autre comportement aurait des conséquences étonnantes. D'un autre côté, cela signifie que les auteurs de compilateurs qui sont plus intéressés par la question de savoir si les optimisations peuvent être justifiées dans n'importe quelle lecture de la norme que par celle de savoir si elles seront compatibles avec le code existant peuvent trouver des occasions intéressantes de justifier l'incompatibilité.

La norme n'exige pas que la représentation des pointeurs ait un lien quelconque avec l'architecture physique sous-jacente. Il serait parfaitement légitime pour un système de représenter chaque pointeur comme une combinaison d'un handle et d'un offset. Un système qui représenterait les pointeurs de cette manière serait libre de déplacer les objets ainsi représentés dans la mémoire physique comme bon lui semble. Sur un tel système, le premier octet de l'objet n° 57 pourrait suivre immédiatement le dernier octet de l'objet n° 23 à un moment donné, mais pourrait se trouver à un autre moment à un endroit complètement différent. Je ne vois rien dans la norme qui empêcherait une telle mise en oeuvre de signaler un pointeur "juste après" pour l'objet n° 23 comme étant égal à un pointeur vers l'objet n° 57 lorsque les deux objets sont adjacents, et comme n'étant pas égal lorsqu'ils ne le sont pas.

En outre, en vertu de la règle "as-if", une implémentation qui serait justifiée de déplacer des objets de cette manière et d'avoir un opérateur d'égalité bizarre, en conséquence, serait autorisée à avoir un opérateur d'égalité bizarre, qu'elle déplace physiquement ou non des objets dans le stockage.

Si, toutefois, une implémentation spécifie la manière dont les pointeurs sont stockés en RAM, et qu'une telle définition serait incompatible avec le comportement décrit ci-dessus, cela obligerait l'implémentation à mettre en œuvre l'opérateur d'égalité d'une manière compatible avec cette spécification. Tout compilateur qui souhaite avoir un opérateur d'égalité original doit s'abstenir de spécifier un format de stockage des pointeurs qui serait incompatible avec ce comportement.

De plus, la norme semble impliquer que si le code observe que deux pointeurs avec des valeurs définies ont une représentation identique, ils doivent se comparer de manière égale. La lecture d'un objet à l'aide d'un type de caractère, puis l'écriture de cette même séquence de valeurs de type de caractère dans un autre objet devraient donner un objet équivalent à l'original ; cette équivalence est une caractéristique fondamentale du langage. Si p est un pointeur "juste après" un objet, et q est un pointeur vers un autre objet, et leurs représentations sont copiées dans le fichier p2 y q2 respectivement, alors p1 doit être égal à p y q2 a q . Si les représentations décomposées de type caractère de p y q sont égales, cela impliquerait que q2 a été écrit avec la même séquence de valeurs de type de caractère que p1 ce qui impliquerait, à son tour, que les quatre pointeurs doivent être égaux.

Par conséquent, alors qu'il serait permis à un compilateur d'avoir une sémantique d'égalité bizarre pour les pointeurs qui ne sont jamais exposés au code qui pourrait observer leur représentation au niveau de l'octet, une telle licence comportementale ne s'étendrait pas aux pointeurs qui sont ainsi exposés. Si une implémentation définit une directive ou un paramètre qui invite les compilateurs à faire en sorte que les comparaisons individuelles rapportent arbitrairement l'égalité ou l'inégalité lorsqu'on leur donne des pointeurs vers la fin d'un objet et le début d'un autre dont le placement ne peut être observé que par une telle comparaison, l'implémentation n'aurait pas à se soucier de la conformité dans les cas où les représentations des pointeurs sont observées. Autrement, même s'il y a des cas où les implémentations conformes seraient autorisées à avoir une sémantique de comparaison bizarre, cela ne signifie pas que les implémentations de qualité devraient le faire, sauf si un pointeur juste après la fin d'un objet a naturellement une représentation différente de celle d'un pointeur au début du suivant.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X