Je ne comprends pas cette façon de calculer le carré d'un nombre

135

J'ai trouvé une fonction qui calcule le carré d'un nombre:

int p(int n) {
    int a[n]; //works on C99 and above
    return (&a)[n] - a;
}

Il renvoie la valeur n 2 . La question est, comment fait-il cela? Après quelques tests, j'ai trouvé qu'entre (&a)[k]et (&a)[k+1]est sizeof(a)/ sizeof(int). Pourquoi donc?

Emanuel
la source
6
Avez-vous des liens vers où vous avez trouvé ces informations?
R Sahu
4
int p(n)? Cela compile-t-il même?
barak manos
78
C'est génial, ne l'utilisez plus jamais et utilisez n * n à la place ...
26
ou mieux:int q(int n) { return sizeof (char [n][n]); }
ouah
17
@ouah en supposant que cette question se réfère à codegolf.stackexchange.com/a/43262/967 la raison pour laquelle je n'ai pas utilisé sizeofétait pour enregistrer des caractères. Tout le monde: c'est du code intentionnellement obscur, c'est un comportement indéfini, la réponse de @ ouah est correcte.
ecatmur

Réponses:

117

Evidemment un hack ... mais une façon de quadriller un nombre sans utiliser l' *opérateur (c'était une exigence du concours de codage).

(&a)[n] 

équivaut à un pointeur vers un intemplacement

(a + sizeof(a[n])*n)

et donc l'expression entière est

  (&a)[n] -a 

= (a + sizeof(a[n])*n -a) /sizeof(int)

= sizeof(a[n])*n / sizeof(int)
= sizeof(int) * n * n / sizeof(int)
= n * n
Mark Lakata
la source
11
Et comme vous l'impliquez clairement mais je ressens le besoin de rendre explicite, c'est au mieux un hack de syntaxe. L'opération de multiplication sera toujours là-dessous; c'est juste l'opérateur qui est évité.
Tommy
J'ai ce qu'il se passe derrière, mais ma vraie question est de savoir pourquoi (& a) [k] est à la même adresse que a + k * sizeof (a) / sizeof (int)
Emanuel
33
En tant que vieux codeur, je suis décontenancé par le fait que le compilateur peut le traiter (&a)comme un pointeur vers un objet dont le n*sizeof(int)moment nn'est pas connu au moment de la compilation. C était un langage simple ...
Floris
C'est un hack assez intelligent, mais quelque chose que vous ne verriez pas dans le code de production (espérons-le).
John Odom
14
En passant, c'est aussi UB, car il incrémente un pointeur pour ne pointer ni vers un élément du tableau sous-jacent, ni juste au-delà.
Deduplicator
86

Pour comprendre ce hack, vous devez d'abord comprendre la différence de pointeur, c'est-à-dire que se passe-t-il lorsque deux pointeurs pointant vers des éléments du même tableau sont soustraits?

Lorsqu'un pointeur est soustrait d'un autre, le résultat est la distance (mesurée en éléments du tableau) entre les pointeurs. Donc, si ppointe vers a[i]et qpointe vers a[j], alors p - qest égal ài - j .

C11: 6.5.6 Opérateurs additifs (p9):

Lorsque deux pointeurs sont soustraits , les deux doivent pointer vers des éléments du même objet tableau, ou un après le dernier élément de l'objet tableau; le résultat est la différence des indices des deux éléments du tableau . [...].
En d'autres termes, si les expressions Pet Qpointent respectivement vers les i-ème et j-ème éléments d'un objet tableau, l'expression (P)-(Q)a la valeur ài−j condition que la valeur tienne dans un objet de type ptrdiff_t.

Maintenant, je m'attends à ce que vous soyez conscient de la conversion du nom du tableau en pointeur, aconvertit en pointeur vers le premier élément du tableau a. &aest l'adresse du bloc mémoire entier, c'est-à-dire que c'est une adresse de tableau a. La figure ci-dessous vous aidera à comprendre ( lisez cette réponse pour une explication détaillée ):

entrez la description de l'image ici

Cela vous aidera à comprendre pourquoi aet &aa la même adresse et comment (&a)[i]est l'adresse du i ème tableau (de même taille que celle de a).

Donc, la déclaration

return (&a)[n] - a; 

est équivalent à

return (&a)[n] - (&a)[0];  

et cette différence donnera le nombre d'éléments entre les pointeurs (&a)[n]et (&a)[0], qui sont des ntableaux de chacun des n intéléments. Par conséquent, le total des éléments du tableau est n*n= n2 .


REMARQUE:

C11: 6.5.6 Opérateurs additifs (p9):

Lorsque deux pointeurs sont soustraits, les deux doivent pointer vers des éléments du même objet tableau, ou un après le dernier élément de l'objet tableau ; le résultat est la différence des indices des deux éléments du tableau. La taille du résultat est définie par l'implémentation et son type (un type entier signé) est ptrdiff_tdéfini dans l'en- <stddef.h>tête. Si le résultat n'est pas représentable dans un objet de ce type, le comportement n'est pas défini.

Puisque (&a)[n]ni ne pointe vers des éléments du même objet de tableau ni un après le dernier élément de l'objet de tableau, (&a)[n] - ainvoquera un comportement indéfini .

Notez également que, mieux vaut changer le type de retour de la fonction pen ptrdiff_t.

piratages
la source
"les deux doivent pointer vers des éléments du même objet tableau" - ce qui me pose la question de savoir si ce "hack" n'est pas UB après tout. L'expression arithmétique du pointeur fait référence à la fin hypothétique d'un objet inexistant: est-ce même autorisé?
Martin Ba
Pour résumer, a est l'adresse d'un tableau de n éléments, donc & a [0] est l'adresse du premier élément de ce tableau, qui est identique à a; de plus, & a [k] sera toujours considérée comme une adresse d'un tableau de n éléments, indépendamment de k, et puisque & a [1..n] est également un vecteur, la "localisation" de ses éléments est consécutive, ce qui signifie le premier élément est à la position x, le second est à la position x + (nombre d'éléments du vecteur a qui est n) et ainsi de suite. Ai-je raison? De plus, il s'agit d'un espace de tas, cela signifie-t-il que si j'alloue un nouveau vecteur des mêmes n éléments, son adresse est la même que (& a) [1]?
Emanuel
1
@Emanuel; &a[k]est une adresse du ke élément du tableau a. C'est (&a)[k]cela qui sera toujours considéré comme l'adresse d'un tableau d' kéléments. Ainsi, le premier élément est à la position a(ou &a), le second est à la position a+ (nombre d'éléments du tableau aqui est n) * (taille d'un élément du tableau) et ainsi de suite. Et notez que la mémoire pour les tableaux de longueur variable est allouée sur la pile, pas sur le tas.
haccks le
@MartinBa; Est-ce même autorisé? Non, ce n'est pas autorisé. Son UB. Voir la modification.
haccks du
1
@haccks belle coïncidence entre la nature de la question et votre surnom
Dimitar Tsonev
35

a est un tableau (variable) de n int .

&a est un pointeur vers un tableau (variable) de n int .

(&a)[1]est un pointeur d' intun intaprès le dernier élément du tableau. Ce pointeur estn int éléments après &a[0].

(&a)[2]est un pointeur d' intun intaprès le dernier élément de tableau de deux tableaux. Ce pointeur est2 * n int éléments après &a[0].

(&a)[n]est un pointeur d' intun intaprès le dernier élément de tableau de ntableaux. Ce pointeur est les n * n intéléments après &a[0]. Soustrayez simplement &a[0]ou aet vous avez n.

Bien sûr, il s'agit d'un comportement techniquement indéfini même s'il fonctionne sur votre machine car (&a)[n]il ne pointe pas à l'intérieur du tableau ou au-delà du dernier élément du tableau (comme l'exigent les règles C d'arithmétique des pointeurs).

ouah
la source
Eh bien, j'ai compris, mais pourquoi cela se produit-il en C? Quelle est la logique derrière cela?
Emanuel
@Emanuel il n'y a pas de réponse plus stricte à cela que l'arithmétique des pointeurs est utile pour mesurer la distance (généralement dans un tableau), la [n]syntaxe déclare un tableau et les tableaux se décomposent en pointeurs. Trois choses utiles séparément avec cette conséquence.
Tommy
1
@Emanuel si vous demandez pourquoi quelqu'un ferait cela, il y a peu de raisons et toutes les raisons de ne pas le faire en raison de la nature UB de l'action. Et il est à noter que (&a)[n]c'est le type int[n], et cela s'exprime comme int*dû à des tableaux exprimant comme l'adresse de leur premier élément, au cas où cela ne serait pas clair dans la description.
WhozCraig
Non, je ne voulais pas dire pourquoi quelqu'un ferait ça, je voulais dire pourquoi le standard C se comporte comme ça dans cette situation.
Emanuel
1
@Emanuel Pointer Arithmetic (et dans ce cas un sous-chapitre de ce sujet: différenciation des pointeurs ). Cela vaut la peine de rechercher sur Google et de lire les questions et réponses sur ce site. il présente de nombreux avantages utiles et est défini concrètement dans les normes lorsqu'il est utilisé correctement. Afin de le saisir pleinement, vous devez comprendre comment les types du code que vous avez répertorié sont créés.
WhozCraig
12

Si vous avez deux pointeurs qui pointent vers deux éléments du même tableau, sa différence donnera le nombre d'éléments entre ces pointeurs. Par exemple, cet extrait de code affichera 2.

int a[10];

int *p1 = &a[1];
int *p2 = &a[3];

printf( "%d\n", p2 - p1 ); 

Considérons maintenant l'expression

(&a)[n] - a;

Dans cette expression aa le typeint * et pointe vers son premier élément.

L'expression &aa un type int ( * )[n]et pointe vers la première ligne du tableau bidimensionnel imagé. Sa valeur correspond à la valeur de asi les types sont différents.

( &a )[n]

est le n-ième élément de ce tableau bidimensionnel imagé et a le type int[n]C'est-à-dire qu'il s'agit de la n-ième ligne du tableau imagé. Dans l'expression, (&a)[n] - ail est converti en l'adresse de son premier élément et a le type `int *.

Donc, entre (&a)[n]et ail y a n rangées de n éléments. La différence sera donc égale à n * n.

Vlad de Moscou
la source
Donc derrière chaque tableau il y a une matrice de taille n * n?
Emanuel
@Emanuel Entre ces deux pointeurs se trouve une matrice de nxn éléments. Et la différence des pointeurs donne une valeur égale à n * n qui est le nombre d'éléments entre les pointeurs.
Vlad de Moscou le
Mais pourquoi cette matrice de taille n * n est-elle derrière? A-t-il une utilité en C? Je veux dire, c'est comme C "alloué" plus de tableaux de la taille n, sans que je le sache? Si oui, puis-je les utiliser? Sinon, pourquoi cette matrice serait-elle formée (je veux dire, elle doit avoir un but pour qu'elle soit là).
Emanuel
2
@Emanuel - Cette matrice n'est qu'une explication du fonctionnement de l'arithmétique des pointeurs dans ce cas. Cette matrice n'est pas allouée et vous ne pouvez pas l'utiliser. Comme cela a déjà été dit à quelques reprises, 1) cet extrait de code est un hack qui n'a aucune utilité pratique; 2) vous devez apprendre comment fonctionne l'arithmétique des pointeurs pour comprendre ce hack.
void_ptr
@Emanuel Ceci explique l'arithmétique du pointeur. Expreesion (& a) [n] est un pointeur sur n-élément du tableau bidimensionnel imagé en raison de l'arithmétique du pointeur.
Vlad de Moscou le
4
Expression     | Value                | Explanation
a              | a                    | point to array of int elements
a[n]           | a + n*sizeof(int)    | refer to n-th element in array of int elements
-------------------------------------------------------------------------------------------------
&a             | a                    | point to array of (n int elements array)
(&a)[n]        | a + n*sizeof(int[n]) | refer to n-th element in array of (n int elements array)
-------------------------------------------------------------------------------------------------
sizeof(int[n]) | n * sizeof(int)      | int[n] is a type of n-int-element array

Donc,

  1. type de pointeur (&a)[n]estint[n]
  2. type de pointeur aestint

Maintenant, l'expression (&a)[n]-aeffectue une soustraction de pointeur:

  (&a)[n]-a
= ((a + n*sizeof(int[n])) - a) / sizeof(int)
= (n * sizeof(int[n])) / sizeof(int)
= (n * n * sizeof(int)) / sizeof(int)
= n * n
seulement
la source