Dans le livre, "Comprendre et utiliser les pointeurs C" de Richard Reese , il est dit à la page 85,
int vector[5] = {1, 2, 3, 4, 5};
Le code généré par
vector[i]
Est différent du code généré par*(vector+i)
. La notationvector[i]
Génère un code machine qui commence au vecteur emplacement. se déplacei
à partir de cet emplacement et utilise ses contenu. La notation*(vector+i)
génère un code machine qui commence à l'emplacementvector
, ajoutei
à la adresse, puis utilise le contenu de cette adresse. Bien que le résultat soit identique, le code machine généré est différent. Cette différence est rarement significative pour la plupart des programmeurs.
Vous pouvez voir le extrait ici . Que signifie ce passage? Dans quel contexte un compilateur générerait-il un code différent pour ces deux-là? Existe-t-il une différence entre "move" from base et "add" to base? Je n'ai pas pu obtenir que cela fonctionne sur GCC - générer un code machine différent.
La citation est tout simplement fausse. Assez tragique que de telles ordures soient encore publiées au cours de cette décennie. En fait, la norme C définit x[y]
Comme *(x+y)
.
La partie sur les lvalues plus tard sur la page est complètement et totalement fausse aussi.
IMHO, la meilleure façon d'utiliser ce livre est de le mettre dans un bac de recyclage ou de le brûler.
J'ai 2 fichiers C: ex1.c
% cat ex1.c
#include <stdio.h>
int main (void) {
int vector[5] = { 1, 2, 3, 4, 5 };
printf("%d\n", vector[3]);
}
et ex2.c
,
% cat ex2.c
#include <stdio.h>
int main (void) {
int vector[5] = { 1, 2, 3, 4, 5 };
printf("%d\n", *(vector + 3));
}
Et je compile les deux dans Assembly, et montre la différence dans le code Assembly généré
% gcc -S ex1.c; gcc -S ex2.c; diff -u ex1.s ex2.s
--- ex1.s 2018-07-17 08:19:25.425826813 +0300
+++ ex2.s 2018-07-17 08:19:25.441826756 +0300
@@ -1,4 +1,4 @@
- .file "ex1.c"
+ .file "ex2.c"
.text
.section .rodata
.LC0:
Q.E.D.
La norme C stipule très explicitement (C11 n1570 6.5.2.1p2) :
- Une expression postfixée suivie d'une expression entre crochets
[]
est une désignation en indice d’un élément d’un objet tableau. La définition de l'opérateur en indice[]
est-ceE1[E2]
est identique à(*((E1)+(E2)))
_ . En raison des règles de conversion applicables au binaire+
opérateur, siE1
est un objet tableau (équivalent, un pointeur sur l’élément initial d’un objet tableau) etE2
est un entier,E1[E2]
désigne leE2
- ème élément deE1
(à partir de zéro).
De plus, la règle as-if s'applique ici - si le comportement du programme est identique, le compilateur peut générer le même code même si la sémantique n'était pas le même.
Le passage cité est tout à fait faux. Les expressions vector[i]
Et *(vector+i)
sont parfaitement identiques et peuvent générer un code identique dans toutes les circonstances.
Les expressions vector[i]
Et *(vector+i)
sont identiques par définition . Il s'agit d'une propriété centrale et fondamentale du langage de programmation C. Tout programmeur C compétent comprend cela. Tout auteur d'un livre intitulé Comprendre et utiliser les pointeurs C doit le comprendre. Tout auteur d'un compilateur C comprendra cela. Les deux fragments généreront un code identique non pas par accident, mais parce que pratiquement tous les compilateurs C traduiront en effet un formulaire dans un autre presque immédiatement, de sorte que, le moment venu, il ne saura même pas quelle forme avait été utilisée initialement. (Je serais assez surpris qu'un compilateur C ait jamais généré un code significativement différent pour vector[i]
Par opposition à *(vector+i)
.)
Et en fait, le texte cité se contredit. Comme vous l'avez noté, les deux passages
La notation
vector[i]
Génère un code machine qui commence à l'emplacementvector
, déplacei
à partir de cet emplacement et utilise son contenu.
et
La notation
*(vector+i)
génère un code machine qui commence à l'emplacementvector
, ajoutei
à l'adresse, puis utilise le contenu de cette adresse.
dire fondamentalement la même chose.
Son langage est étrangement similaire à celui de question 6.2 de l'ancien C FAQ liste :
... quand le compilateur voit l'expression
a[3]
, il émet du code pour qu'il commence à l'emplacement "a
", le dépasse de trois et y récupère le caractère. Lorsqu'il voit l'expressionp[3]
, Il émet le code pour qu'il commence à l'emplacement "p
", récupère la valeur du pointeur à cet endroit, ajoute trois points au pointeur et, finalement, récupère le caractère pointé.
Mais bien sûr, la principale différence est que a
est un tableau et p
est un pointeur . La liste FAQ ne parle pas de a[3]
Par rapport à *(a+3)
, mais plutôt de a[3]
(Ou *(a+3)
) où a
est un tableau, alors que p[3]
(ou *(p+3)
) où p
est un pointeur (bien entendu, ces deux cas génèrent un code différent, car les tableaux et les pointeurs sont différents. Comme l'explique la liste FAQ, extraire une adresse d'une variable de pointeur est fondamentalement différent d'utiliser l'adresse d'un tableau.)
La norme spécifie le comportement de arr[i]
Lorsque arr
est un objet de tableau équivalent à la décomposition de arr
en un pointeur, à l'ajout de i
et au déréférencement du résultat. . Bien que les comportements soient équivalents dans tous les cas définis par la norme, il existe des cas où les compilateurs traitent les actions de manière utile, même si la norme le requiert, et le traitement de arrayLvalue[i]
Et de *(arrayLvalue+i)
peut différer. en conséquence.
Par exemple, étant donné
char arr[5][5];
union { unsigned short h[4]; unsigned int w[2]; } u;
int atest1(int i, int j)
{
if (arr[1][i])
arr[0][j]++;
return arr[1][i];
}
int atest2(int i, int j)
{
if (*(arr[1]+i))
*((arr[0])+j)+=1;
return *(arr[1]+i);
}
int utest1(int i, int j)
{
if (u.h[i])
u.w[j]=1;
return u.h[i];
}
int utest2(int i, int j)
{
if (*(u.h+i))
*(u.w+j)=1;
return *(u.h+i);
}
Le code généré par GCC pour test1 supposera qu'arr [1] [i] et arr [0] [j] ne peuvent pas être alias, mais le code généré pour test2 permettra à l'arithmétique de pointeur d'accéder à l'ensemble du tableau, gcc reconnaissez que dans utest1, les expressions lvalue uh [i] et uw [j] accèdent toutes deux à la même union, mais que ce n'est pas assez sophistiqué pour remarquer la même chose de * (u.h + i) et * (u.w + j) dans utest2.
Je pense à quoi le texte original peut faire référence, ce sont des optimisations que certains compilateurs peuvent ou non effectuer.
Exemple:
for ( int i = 0; i < 5; i++ ) {
vector[i] = something;
}
vs.
for ( int i = 0; i < 5; i++ ) {
*(vector+i) = something;
}
Dans le premier cas, un compilateur optimiseur peut détecter que le tableau vector
est itéré élément par élément et générer ainsi quelque chose comme:
void* tempPtr = vector;
for ( int i = 0; i < 5; i++ ) {
*((int*)tempPtr) = something;
tempPtr += sizeof(int); // _move_ the pointer; simple addition of a constant.
}
Il est même possible d'utiliser les instructions de post-incrémentation du pointeur de la CPU cible, le cas échéant.
Dans le second cas, il est "plus difficile" pour le compilateur de voir que l'adresse calculée via une expression arithmétique de pointeur "arbitraire" présente la même propriété. d'avancer de manière monotone un montant fixe à chaque itération. Il pourrait donc ne pas trouver l'optimisation et calculer ((void*)vector+i*sizeof(int))
à chaque itération utilisant une multiplication supplémentaire. Dans ce cas, il n'y a pas de pointeur (temporaire) qui soit "déplacé" mais seulement une adresse temporaire recalculée.
Cependant, l'instruction ne s'applique probablement pas universellement à tous les compilateurs C de toutes les versions.
Mise à jour:
J'ai vérifié l'exemple ci-dessus. Il semble que sans optimisations activées au moins gcc-8.1 x86-64 génère plus de code (2 instructions supplémentaires) pour le second formulaire (arithmétique du pointeur) par rapport à le premier (index de tableau).
Voir: https://godbolt.org/g/7DaPHG
Cependant, avec toutes les optimisations activées sur (-O
...-O3
) le code généré est identique (longueur) pour les deux.
Permettez-moi de tenter de répondre à cette question "dans l'étroit" (d'autres ont déjà expliqué pourquoi la description "en l'état" est quelque peu absente/incomplète/trompeuse):
Dans quel contexte un compilateur générerait-il un code différent pour ces deux-là?
Un compilateur "pas très optimisant" peut générer un code différent dans à peu près n'importe quel contexte, car lors de l'analyse, il existe une différence: x[y]
Est une expression (index dans un tableau), tandis que *(x+y)
sont deux expressions (ajoute un entier à un pointeur, puis déréférence). Bien sûr, il n’est pas très difficile de reconnaître cela (même lors de l’analyse) et de le traiter de la même manière, mais si vous écrivez un compilateur simple/rapide, vous éviterez d’y mettre "trop de sagesse". Par exemple:
char vector[] = ...;
char f(int i) {
return vector[i];
}
char g(int i) {
return *(vector + i);
}
Lors de l'analyse de f()
, le compilateur voit l'indexation et peut générer quelque chose comme (pour un processeur similaire à 68000):
MOVE D0, [A0 + D1] ; A0/vector, D1/i, D0/result of function
OTOH, pour g()
, le compilateur voit deux choses: d'abord un déréférencement (de "quelque chose à venir") et ensuite l'ajout d'un entier au pointeur/à un tableau, donc ne pas être très optimisant, cela pourrait se terminer avec:
MOVE A1, A0 ; A1/t = A0/vector
ADD A1, D1 ; t += i/D1
MOVE D0, [A1] ; D0/result = *t
Évidemment, cela dépend beaucoup de la mise en oeuvre, certains compilateurs peuvent également ne pas aimer utiliser des instructions complexes telles que utilisées pour f()
(utiliser des instructions complexes rend plus difficile le débogage du compilateur), le processeur peut ne pas avoir de telles instructions complexes, etc.
Existe-t-il une différence entre "move" from base et "add" to base?
On peut soutenir que la description dans le livre n’est pas bien formulée. Mais je pense que l'auteur voulait décrire la distinction montrée ci-dessus: l'indexation ("déplacer" à partir de la base) est une expression, alors que "ajouter et ensuite déréférencer" sont deux expressions.
Il s’agit de implémentation du compilateur, pas définition de la langue, distinction qui aurait également dû être explicitement indiquée dans le livre.
J'ai testé le code pour certaines variantes du compilateur, la plupart me donnant le même code d'assemblage pour les deux instructions (testé pour x86 sans optimisation). Il est intéressant de noter que le gcc 4.4.7 fait exactement ce que vous avez mentionné: Exemple:
D'autres langages comme ARM ou MIPS font parfois la même chose, mais je n'ai pas tout testé. Il semble donc que leur différence était différente, mais les versions ultérieures de gcc ont "corrigé" ce bogue.