web-dev-qa-db-fra.com

L'incrémentation d'un pointeur nul est-elle bien définie?

Il existe de nombreux exemples de comportement indéfini/non spécifié lors de l’arithmétique de pointeur - les pointeurs doivent pointer dans le même tableau (ou au-delà de la fin), ou dans le même objet, avec des restrictions quant au moment où vous pouvez effectuer des comparaisons/opérations en fonction , etc.

L'opération suivante est-elle bien définie?

int* p = 0;
p++;
48
Luchian Grigore

§5.2.6/1:

La valeur de l'objet opérande est modifiée en y ajoutant 1, sauf si l'objet est de type bool [..]

Et les expressions additives impliquant des pointeurs sont définies au §5.7/5:

Si l'opérande de pointeur et le résultat pointent tous deux sur des éléments du même objet tableau, même après le dernier élément de l'objet tableau, , L'évaluation ne produira pas de dépassement de capacité; sinon, le comportement n'est pas défini.

37
Columbo

Il semble y avoir une compréhension assez faible de ce que signifie "comportement indéfini". 

En C, C++ et les langages apparentés comme Objective-C, il existe quatre types de comportement: Il existe un comportement défini par le standard de langage. Il y a un comportement défini par l'implémentation, ce qui signifie que le standard de langage dit explicitement que l'implémentation doit définir le comportement. Il existe un comportement non spécifié, où la norme de langage dit que plusieurs comportements sont possibles. Et il y a un comportement indéfini, où le standard de langage ne dit rien sur le résultat . Comme le langage standard ne dit rien sur le résultat, tout comportement peut se produire avec un comportement indéfini. 

Certaines personnes ici supposent que "comportement indéfini" signifie "que quelque chose de mauvais se passe". C'est faux. Cela signifie "tout peut arriver", et cela inclut "quelque chose de mauvais peut arriver", pas "il faut que quelque chose de mauvais arrive". En pratique, cela signifie "rien de mal ne se produit lorsque vous testez votre programme, mais dès qu'il est expédié à un client, l'enfer se déchaîne". Comme tout peut arriver, le compilateur peut en réalité supposer qu'il n'y a pas de comportement indéfini dans votre code - parce que c'est soit vrai, soit faux, auquel cas tout peut arriver, ce qui signifie que tout ce qui se passe à cause de la mauvaise supposition du compilateur est toujours correct. 

Quelqu'un a affirmé que lorsque p pointe sur un tableau de 3 éléments et que p + 4 est calculé, rien de grave ne se passera. Faux. Voici votre compilateur d'optimisation. Dis que c'est ton code: 

int f (int x)
{
    int a [3], b [4];
    int* p = (x == 0 ? &a [0] : &b [0]);
    p + 4;
    return x == 0 ? 0 : 1000000 / x;
}

Évaluer p + 4 est un comportement indéfini si p pointe sur a [0], mais pas s'il pointe sur b [0]. Le compilateur est donc autorisé à supposer que p pointe sur b [0]. Le compilateur est donc autorisé à supposer que x! = 0, car x == 0 conduit à un comportement indéfini. Le compilateur est donc autorisé à supprimer la vérification x == 0 de l'instruction return et à renvoyer 1000000/x. Ce qui signifie que votre programme se bloque lorsque vous appelez f (0) au lieu de renvoyer 0. 

Une autre hypothèse avancée est que, si vous incrémentez un pointeur null puis le décrémentez à nouveau, le résultat est à nouveau un pointeur nul. Encore faux. Mis à part la possibilité que l'incrémentation d'un pointeur null puisse simplement planter sur un matériel, que diriez-vous de ceci: comme l'incrémentation d'un pointeur null est un comportement indéfini, le compilateur vérifie si un pointeur est nul et ne l'incrémente que s'il n'est pas un pointeur nul , donc p + 1 est à nouveau un pointeur nul. Et normalement, cela ferait la même chose pour la décrémentation, mais étant un compilateur intelligent, il remarquera que p + 1 est toujours un comportement indéfini si le résultat est un pointeur nul. Par conséquent, on peut supposer que p + 1 n'est pas un pointeur nul, par conséquent, la vérification du pointeur nul peut être omise. Ce qui signifie (p + 1) - 1 n'est pas un pointeur nul si p était un pointeur nul. 

15
gnasher729

Les opérations sur un pointeur (comme l’incrémentation, l’ajout, etc.) ne sont généralement valides que si la valeur initiale du pointeur et le résultat pointent vers des éléments du même tableau (ou vers un élément après le dernier élément). Sinon, le résultat est indéfini. La norme contient diverses clauses pour les différents opérateurs, y compris pour l’incrémentation et l’ajout.

(Il existe quelques exceptions telles que l'ajout de zéro à NULL ou la soustraction de zéro de NULL, mais cela ne s'applique pas ici).

Un pointeur NULL ne pointe vers rien, donc son incrémentation donne un comportement indéfini (la clause "sinon" s'applique).

13
Peter

Il s'avère que c'est en fait non défini. Il existe des systèmes pour lesquels cela est vrai

int *p = NULL;
if (*(int *)&p == 0xFFFF)

Par conséquent, ++ p déclencherait la règle de dépassement de capacité non définie (s'avère que sizeof (int *) == 2)). Il n'est pas garanti que les pointeurs soient des entiers non signés, la règle d'encapsulation non signée ne s'applique donc pas.

1
Joshua

Comme l'a dit Columbo, c'est UB. Et du point de vue des juristes spécialistes des langues, c’est la réponse définitive.

Cependant, toutes les implémentations de compilateur C++ que je connais donneront le même résultat:

int *p = 0;
intptr_t ip = (intptr_t) p + 1;

cout << ip - sizeof(int) << endl;

donne 0, ce qui signifie que p a la valeur 4 sur une implémentation 32 bits et 8 sur une 64 bits

Dit autrement:

int *p = 0;
intptr_t ip = (intptr_t) p; // well defined behaviour
ip += sizeof(int); // integer addition : well defined behaviour 
int *p2 = (int *) ip;      // formally UB
p++;               // formally UB
assert ( p2 == p) ;  // works on all major implementation
0
Serge Ballesta

De ISO IEC 14882-2011 §5.2.6 :

La valeur d'une expression postfix ++ est la valeur de son opérande. [Remarque: la valeur obtenue est une copie de De la valeur d'origine - note finale] L'opérande doit être une valeur modifiable. Le type de l'opérande doit être Un type arithmétique ou un pointeur sur un type d'objet complet.

Depuis un nullptr est un pointeur sur un type d'objet complet. Donc, je ne verrais pas pourquoi ce serait un comportement indéfini.

Comme il a été dit précédemment, le même document indique également dans §5.2.6/1 :

Si l'opérande de pointeur et le résultat pointent tous deux sur des éléments du même objet de tableau, ou d'un dernier Dernier élément de l'objet de tableau, l'évaluation ne doit pas produire de dépassement de capacité; sinon, le comportement est non défini.

Cette expression semble un peu ambiguë. Dans mon interprétation, la partie non définie pourrait très bien être l'évaluation de l'objet. Et je pense que personne ne serait en désaccord avec cela. Cependant, l'arithmétique au pointeur semble ne nécessiter qu'un objet complet. 

Bien sûr, les opérateurs postfix [] et les soustractions ou multiplications sur les objets pointeur vers tableau ne sont bien définis que s’ils pointent vers le même tableau. Surtout important parce que l'on peut être tenté de penser que 2 tableaux définis successivement dans 1 objet peuvent être itérés comme s'ils étaient un tableau unique. 

Donc, ma conclusion serait que l'opération est bien définie, mais pas l'évaluation. 

0
laurisvr

La norme C exige qu'aucun objet créé via des moyens définis par la norme ne puisse avoir une adresse égale à un pointeur nul. Les implémentations peuvent permettre l'existence d'objets qui ne sont pas créés via des moyens définis par la norme. Toutefois, la norme ne précise pas si un tel objet peut avoir une adresse qui (probablement à cause de problèmes de conception matérielle) est identique à un pointeur nul. .

Si une implémentation documente l'existence d'un objet multi-octets dont l'adresse serait égale à null, alors sur cette implémentation, dire que char *p = (char*)0; ferait à p de détenir un pointeur sur le premier octet de cet objet [qui se comparerait à un pointeur nul], et p++ ferait pointer vers le deuxième octet. Cependant, à moins qu'une implémentation ne documente l'existence d'un tel objet ou ne spécifie pas qu'elle effectuera une arithmétique de pointeur comme si un tel objet existait, il n'y a aucune raison de s'attendre à un comportement particulier. Avoir délibérément des tentatives d'implémentation pour effectuer tout type d'arithmétique sur des pointeurs nuls autres que l'ajout ou la soustraction de zéro ou d'autres pointeurs nuls peut être une mesure de sécurité utile, et un code qui incrémenterait des pointeurs nuls dans un but utile serait incompatible avec cette fin. Pire encore, certains compilateurs «intelligents» peuvent décider d’omettre les contrôles nuls dans les cas où des pointeurs seraient incrémentés même s’ils étaient nuls, permettant ainsi toutes sortes de ravages.

0
supercat