Je lis actuellement un livre intitulé "Recettes numériques en C". Dans ce livre, l'auteur détaille comment certains algorithmes fonctionnent intrinsèquement mieux si nous avions des indices commençant par 1 (je ne suis pas entièrement d'accord avec son argument et ce n'est pas le but de cet article), mais C indexe toujours ses tableaux en commençant par 0 Pour contourner ce problème, il suggère simplement de décrémenter le pointeur après l'allocation, par exemple:
float *a = malloc(size);
a--;
Cela, dit-il, vous donnera effectivement un pointeur qui a un index commençant par 1, qui sera ensuite libéré avec:
free(a + 1);
À ma connaissance, cependant, il s'agit d'un comportement indéfini par la norme C. Il s'agit apparemment d'un livre très réputé au sein de la communauté HPC, donc je ne veux pas simplement ignorer ce qu'il dit, mais simplement décrémenter un pointeur en dehors de la plage allouée me semble très sommaire. Ce comportement est-il "autorisé" en C? Je l'ai testé en utilisant à la fois gcc et icc, et ces deux résultats semblent indiquer que je ne m'inquiète pour rien, mais je veux être absolument positif.
Réponses:
Vous avez raison ce code tel que
donne un comportement indéfini, conformément à la norme ANSI C, section 3.3.6:
Pour un code comme celui-ci, la qualité du code C dans le livre (quand je l'ai utilisé à la fin des années 1990) n'était pas considérée comme très élevée.
Le problème avec un comportement indéfini est que, quel que soit le résultat produit par le compilateur, ce résultat est par définition correct (même s'il est hautement destructeur et imprévisible).
Heureusement, très peu de compilateurs s'efforcent de provoquer un comportement inattendu dans de tels cas et l'
malloc
implémentation typique sur les machines utilisées pour HPC contient des données de comptabilité juste avant l'adresse qu'elle renvoie, de sorte que la décrémentation vous donne généralement un pointeur sur ces données de comptabilité. Ce n'est pas une bonne idée d'y écrire, mais la simple création du pointeur est inoffensive sur ces systèmes.Sachez simplement que le code peut se casser lorsque l'environnement d'exécution est modifié ou lorsque le code est porté vers un autre environnement.
la source
Officiellement, c'est un comportement indéfini d'avoir un pointeur en dehors du tableau (sauf pour un après la fin), même s'il n'est jamais déréférencé .
En pratique, si votre processeur a un modèle de mémoire plate (par opposition à des modèles étranges comme x86-16 ), et si le compilateur ne vous donne pas d'erreur d'exécution ou d'optimisation incorrecte si vous créez un pointeur invalide, le code fonctionnera ça va.
la source
Tout d'abord, c'est un comportement indéfini. Certains compilateurs d'optimisation sont de nos jours très agressifs face à un comportement non défini. Par exemple, étant donné que a-- dans ce cas est un comportement indéfini, le compilateur peut décider d'enregistrer une instruction et un cycle de processeur et non décrémenter a. Ce qui est officiellement correct et légal.
En ignorant cela, vous pourriez soustraire 1, ou 2, ou 1980. Par exemple, si j'ai des données financières pour les années 1980 à 2013, je pourrais soustraire 1980. Maintenant, si nous prenons float * a = malloc (taille); il y a sûrement une grande constante k telle que a - k est un pointeur nul. Dans ce cas, nous nous attendons vraiment à ce que quelque chose tourne mal.
Maintenant, prenez une grosse structure, disons un mégaoctet. Allouez un pointeur p pointant vers deux structures. p - 1 peut être un pointeur nul. p - 1 peut se terminer (si une structure est un mégaoctet et que le bloc malloc est à 900 Ko depuis le début de l'espace d'adressage). Il pourrait donc être sans aucune malice du compilateur que p - 1> p. Les choses peuvent devenir intéressantes.
la source
Permis? Oui. Bonne idée? Pas habituellement.
C est un raccourci pour le langage d'assemblage, et dans le langage d'assemblage, il n'y a pas de pointeurs, juste des adresses mémoire. Les pointeurs de C sont des adresses de mémoire qui ont un comportement secondaire d'incrémentation ou de décrémentation de la taille de ce qu'ils pointent lorsqu'ils sont soumis à l'arithmétique. Cela rend les éléments suivants très bien d'un point de vue syntaxique:
Les tableaux ne sont pas vraiment une chose en C; ce ne sont que des pointeurs vers des plages de mémoire contiguës qui se comportent comme des tableaux. L'
[]
opérateur est un raccourci pour effectuer l'arithmétique des pointeurs et le déréférencement, donca[x]
signifie en fait*(a + x)
.Il existe des raisons valables de faire ce qui précède, comme certains périphériques d'E / S ayant un couple de
double
s mappés dans0xdeadbee7
et0xdeadbeef
. Très peu de programmes devraient le faire.Lorsque vous créez l'adresse de quelque chose, par exemple en utilisant l'
&
opérateur ou en appelantmalloc()
, vous souhaitez conserver intact le pointeur d'origine afin de savoir que ce qu'il pointe est en fait quelque chose de valide. La décrémentation du pointeur signifie qu'une partie du code erroné pourrait essayer de le déréférencer, d'obtenir des résultats erronés, d'altérer quelque chose ou, selon votre environnement, de commettre une violation de segmentation. Cela est particulièrement vrai avecmalloc()
, car vous avez mis le fardeau sur celui qui appellefree()
de se rappeler de transmettre la valeur d'origine et non une version modifiée qui entraînera la perte de tout.Si vous avez besoin de tableaux basés sur 1 en C, vous pouvez le faire en toute sécurité au détriment de l'allocation d'un élément supplémentaire qui ne sera jamais utilisé:
Notez que cela ne fait rien pour protéger contre le dépassement de la limite supérieure, mais c'est assez facile à gérer.
Addenda:
Quelques chapitres et versets du brouillon C99 (désolé, c'est tout ce que je peux lier):
Le §6.5.2.1.1 indique que la deuxième expression ("autre") utilisée avec l'opérateur d'indice est de type entier.
-1
est un entier, ce qui rendp[-1]
valide et rend donc également le pointeur&(p[-1])
valide. Cela n'implique pas que l'accès à la mémoire à cet emplacement produirait un comportement défini, mais le pointeur est toujours un pointeur valide.Le §6.5.2.2 dit que l'opérateur d'indice de tableau est évalué à l'équivalent de l'ajout du numéro d'élément au pointeur,
p[-1]
est donc équivalent à*(p + (-1))
. Toujours valide, mais peut ne pas produire de comportement souhaitable.Le §6.5.6.8 dit (c'est moi qui souligne):
Cela signifie que les résultats de l'arithmétique des pointeurs doivent pointer sur un élément d'un tableau. Il ne dit pas que l'arithmétique doit être faite en même temps. Donc:
Dois-je recommander de faire les choses de cette façon? Non, et ma réponse explique pourquoi.
la source