Indexation du pointeur


11

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.


3
quelle norme C référez-vous? Je demande parce que, d'après mes souvenirs, "Recettes numériques en C" a été publié dans les années 1990, dans les temps anciens de K&R et peut-être ANSI C
gnat


3
"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." Ne supposez jamais que parce que votre compilateur le permet, le langage C le permet. À moins, bien sûr, que votre code ne casse à l'avenir.
Doval

5
Sans vouloir être sarcastique, les "Recettes numériques" sont généralement considérées comme un livre utile, rapide et sale, et non comme un paradigme de développement logiciel ou d'analyse numérique. Consultez l'article de Wikipedia sur "Recettes numériques" pour un résumé de certaines des critiques.
Charles E. Grant du

1
En passant, voici pourquoi nous indexons à
Russell Borogove

Réponses:


16

Vous avez raison ce code tel que

float a = malloc(size);
a--;

donne un comportement indéfini, conformément à la norme ANSI C, section 3.3.6:

À moins que l'opérande de pointeur et le résultat ne pointent vers un membre du même objet tableau, ou un au-delà du dernier membre de l'objet tableau, le comportement n'est pas défini

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' mallocimplé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.


4
Exactement, il est possible sur une architecture multi-banques que malloc puisse vous donner la 0ème adresse dans une banque et sa décrémentation peut provoquer un piège CPU avec un sous-dépassement pour une.
Vality

1
Je ne suis pas d'accord pour dire que c'est "chanceux". Je pense que ce serait beaucoup mieux si les compilateurs émettaient du code qui se bloquait immédiatement chaque fois que vous invoquiez un comportement non défini.
David Conrad

4
@DavidConrad: Alors C n'est pas le langage pour vous. Une grande partie du comportement indéfini dans C ne peut pas être facilement détecté ou uniquement avec un coup sévère de performance.
Bart van Ingen Schenau

Je pensais ajouter "avec un commutateur de compilation". De toute évidence, vous ne voudriez pas cela pour un code optimisé. Mais vous avez raison, et c'est pourquoi j'ai renoncé à écrire C il y a dix ans.
David Conrad

@BartvanIngenSchenau selon ce que vous entendez par `` coup de performance sévère '', il y a une exécution symbolique pour C (par exemple clang + klee) ainsi que pour les assainissants (asan, tsan, ubsan, valgrind, etc.) qui ont tendance à être très utiles pour le débogage.
Maciej Piechotka

10

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.


1
Ça a du sens. Malheureusement, c'est deux de trop si c'est à mon goût.
wolfPack88

3
Le dernier point est à mon humble avis le plus problématique. Étant donné que les compilateurs ne permettent pas simplement que la plate-forme se produise "naturellement" dans le cas d'UB, mais que les optimiseurs l' exploitent de manière agressive , je ne jouerais pas avec elle si légèrement.
Matteo Italia

3

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.


1

... 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?

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:

double *p = (double *)0xdeadbeef;
--p;  // p == 0xdeadbee7, assuming sizeof(double) == 8.
double d = p[0];

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, donc a[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 doubles mappés dans 0xdeadbee7et 0xdeadbeef. 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 appelant malloc(), 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 avec malloc(), car vous avez mis le fardeau sur celui qui appelle free()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é:

double *array_create(size_t size) {
    // Wasting one element, so don't allow it to be full-sized
    assert(size < SIZE_MAX);
    return malloc((size+1) * sizeof(double));
}

inline double array_index(double *array, size_t index) {
    assert(array != NULL);
    assert(index >= 1);  // This is a 1-based array
    return array[index];
}

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. -1est un entier, ce qui rend p[-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):

Lorsqu'une expression de type entier est ajoutée ou soustraite d'un pointeur, le résultat a le type de l'opérande du pointeur.

... si l'expression Ppointe vers le i-ème élément d'un objet tableau, les expressions (P)+N(de manière équivalente N+(P)) et (P)-N (où Na la valeur n) pointent respectivement vers les éléments i+n-th et i−n-th de l'objet tableau, à condition qu'elles existent .

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:

double a[20];

// This points to element 9 of a; behavior is defined.
double d = a[-1 + 10];

double *p = a - 1;  // This is just a pointer.  No dereferencing.

double e = p[0];   // Does not point at any element of a; behavior is undefined.
double f = p[1];   // Points at element 0 of a; behavior is defined.

Dois-je recommander de faire les choses de cette façon? Non, et ma réponse explique pourquoi.


8
-1 Une définition de «autorisé» qui inclut du code que la norme C déclare comme générant des résultats non définis n'est pas utile.
Pete Kirkham

D'autres ont souligné qu'il s'agit d'un comportement indéfini, vous ne devez donc pas dire qu'il est "autorisé". Cependant, la suggestion d'allouer un élément supplémentaire inutilisé 0 est bonne.
200_success

Ce n'est vraiment pas correct, notez au moins que cela est interdit par la norme C.
Vality

@PeteKirkham: Je ne suis pas d'accord. Voir l'addendum à ma réponse.
Blrfl

4
@Blrfl 6.5.6 de la norme ISO C11 indique dans le cas de l'ajout d'un entier à un pointeur: "Si l'opérande du pointeur et le résultat pointent tous les deux vers des éléments du même objet de tableau, ou au-delà du dernier élément de l'objet de tableau , l'évaluation ne doit pas produire de débordement, sinon le comportement n'est pas défini. "
Vality
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.