Comment ce morceau de code détermine-t-il la taille du tableau sans utiliser sizeof ()?

134

En parcourant quelques questions d'entrevue C, j'ai trouvé une question indiquant "Comment trouver la taille d'un tableau en C sans utiliser l'opérateur sizeof?", Avec la solution suivante. Cela fonctionne, mais je ne comprends pas pourquoi.

#include <stdio.h>

int main() {
    int a[] = {100, 200, 300, 400, 500};
    int size = 0;

    size = *(&a + 1) - a;
    printf("%d\n", size);

    return 0;
}

Comme prévu, il renvoie 5.

edit: les gens ont souligné cette réponse, mais la syntaxe diffère un peu, c'est-à-dire la méthode d'indexation

size = (&arr)[1] - arr;

Je pense donc que les deux questions sont valables et ont une approche légèrement différente du problème. Merci à tous pour votre aide immense et votre explication approfondie!

janojlic
la source
13
Eh bien, je ne peux pas le trouver, mais on dirait à proprement parler que c'est le cas. L'Annexe J.2 indique explicitement: L'opérande de l'opérateur unaire * a une valeur invalide est un comportement indéfini. Ici &a + 1ne pointe vers aucun objet valide, il est donc invalide.
Eugene Sh.
5
Possibilité de dupliquer la taille du tableau de recherche sans utiliser sizeof en C
Alma Do
@AlmaDo bien la syntaxe diffère un peu, c'est-à-dire la partie indexation, donc je crois que cette question est toujours valable en soi, mais je me trompe peut-être. Je vous remercie de le faire remarquer!
janojlic
1
@janojlicz Ils sont essentiellement les mêmes, car (ptr)[x]c'est la même chose que *((ptr) + x).
SS Anne

Réponses:

135

Lorsque vous ajoutez 1 à un pointeur, le résultat est l'emplacement de l'objet suivant dans une séquence d'objets de type pointé (c'est-à-dire un tableau). Si ppointe vers un intobjet, alors p + 1pointera vers le suivant intdans une séquence. Si ppointe vers un tableau à 5 éléments de int(dans ce cas, l'expression &a), alors p + 1pointera vers le tableau à 5 éléments suivant deint dans une séquence.

La soustraction de deux pointeurs (à condition qu'ils pointent tous les deux dans le même objet de tableau, ou que l'un pointe un au-delà du dernier élément du tableau) donne le nombre d'objets (éléments de tableau) entre ces deux pointeurs.

L'expression &arenvoie l'adresse de aet a le type int (*)[5](pointeur vers un tableau à 5 éléments de int). L'expression &a + 1renvoie l'adresse du prochain tableau à 5 éléments de la intsuite a, et a également le type int (*)[5]. L'expression *(&a + 1)déréférence le résultat de &a + 1, de telle sorte qu'elle donne l'adresse du premier intsuivant le dernier élément de a, et a un type int [5], qui dans ce contexte «se désintègre» en une expression de type int *.

De même, l'expression a«se désintègre» en un pointeur vers le premier élément du tableau et a un type int *.

Une image peut aider:

int [5]  int (*)[5]     int      int *

+---+                   +---+
|   | <- &a             |   | <- a
| - |                   +---+
|   |                   |   | <- a + 1
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
+---+                   +---+
|   | <- &a + 1         |   | <- *(&a + 1)
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
| - |                   +---+
|   |                   |   |
+---+                   +---+

Il s'agit de deux vues du même stockage - sur la gauche, nous le visualisons comme une séquence de tableaux à 5 éléments int, tandis que sur la droite, nous le visualisons comme une séquence de int. Je montre également les différentes expressions et leurs types.

Attention, l'expression *(&a + 1)entraîne un comportement indéfini :

...
Si le résultat pointe un après le dernier élément de l'objet tableau, il ne doit pas être utilisé comme opérande d'un opérateur unaire * évalué.

C 2011 Online Draft , 6.5.6 / 9

John Bode
la source
13
Ce texte «ne doit pas être utilisé» est officiel: C 2018 6.5.6 8.
Eric Postpischil
@EricPostpischil: Avez-vous un lien vers le brouillon pré-pub 2018 (similaire à N1570.pdf)?
John Bode
1
@JohnBode: Cette réponse a un lien vers la Wayback Machine . J'ai vérifié la norme officielle dans mon exemplaire acheté.
Eric Postpischil
7
Alors si on écrivait size = (int*)(&a + 1) - a;ce code serait tout à fait valide? : o
Gizmo
@Gizmo, ils n'ont probablement pas écrit cela à l'origine parce que de cette façon, vous devez spécifier le type d'élément; l'original a probablement été écrit défini comme une macro pour une utilisation générique de type sur différents types d'élément.
Leushenko
35

Cette ligne est la plus importante:

size = *(&a + 1) - a;

Comme vous pouvez le voir, il prend d'abord l'adresse de aet y ajoute une. Ensuite, il déréférence ce pointeur et en soustrait la valeur d'origine a.

L'arithmétique du pointeur en C provoque le renvoi du nombre d'éléments dans le tableau, ou 5. L'ajout d'un et &aest un pointeur vers le tableau suivant de 5 ints après a. Après cela, ce code déréférence le pointeur résultant et soustrait a(un type de tableau qui s'est désintégré en pointeur) de cela, en donnant le nombre d'éléments dans le tableau.

Détails sur le fonctionnement de l'arithmétique des pointeurs:

Supposons que vous ayez un pointeur xyzqui pointe vers un inttype et contient la valeur (int *)160. Lorsque vous soustrayez un nombre de xyz, C spécifie que le montant réel soustrait xyzcorrespond à ce nombre multiplié par la taille du type vers lequel il pointe. Par exemple, si vous soustrayez 5de xyz, la valeur de xyzrésultat serait xyz - (sizeof(*xyz) * 5)si l'arithmétique du pointeur ne s'applique pas.

Comme apour un tableau de 5 inttypes, la valeur résultante sera 5. Cependant, cela ne fonctionnera pas avec un pointeur, uniquement avec un tableau. Si vous essayez ceci avec un pointeur, le résultat sera toujours 1.

Voici un petit exemple qui montre les adresses et comment cela n'est pas défini. Le côté gauche montre les adresses:

a + 0 | [a[0]] | &a points to this
a + 1 | [a[1]]
a + 2 | [a[2]]
a + 3 | [a[3]]
a + 4 | [a[4]] | end of array
a + 5 | [a[5]] | &a+1 points to this; accessing past array when dereferenced

Cela signifie que le code soustrait ade &a[5](ou a+5), donne 5.

Notez que ce comportement n'est pas défini et ne doit en aucun cas être utilisé. Ne vous attendez pas à ce que ce comportement soit cohérent sur toutes les plates-formes et ne l'utilisez pas dans les programmes de production.

SS Anne
la source
27

Hmm, je soupçonne que c'est quelque chose qui n'aurait pas fonctionné dans les premiers jours de C. C'est intelligent cependant.

Faire les étapes une par une:

  • &a obtient un pointeur vers un objet de type int [5]
  • +1 obtient le prochain objet de ce type en supposant qu'il existe un tableau de ces
  • * convertit efficacement cette adresse en pointeur de type vers int
  • -a soustrait les deux pointeurs int, renvoyant le nombre d'instances int entre eux.

Je ne suis pas sûr que ce soit tout à fait légal (en cela je veux dire la langue-avocat juridique - cela ne fonctionnera pas dans la pratique), étant donné certaines opérations de type en cours. Par exemple, vous n'êtes "autorisé" à soustraire deux pointeurs que lorsqu'ils pointent vers des éléments du même tableau. *(&a+1)a été synthétisé en accédant à un autre tableau, bien qu'un tableau parent, il n'est donc pas réellement un pointeur dans le même tableau que a. De plus, alors que vous êtes autorisé à synthétiser un pointeur au-delà du dernier élément d'un tableau, et que vous pouvez traiter n'importe quel objet comme un tableau de 1 élément, l'opération de déréférencement ( *) n'est pas "autorisée" sur ce pointeur synthétisé, même si elle n'a aucun comportement dans ce cas!

Je soupçonne que dans les premiers jours de C (syntaxe K&R, n'importe qui?), Un tableau s'est décomposé en un pointeur beaucoup plus rapidement, donc il *(&a+1)pourrait ne renvoyer que l'adresse du pointeur suivant de type int **. Les définitions plus rigoureuses du C ++ moderne permettent définitivement au pointeur sur le type de tableau d'exister et de connaître la taille du tableau, et probablement les normes C ont emboîté le pas. Tout le code de fonction C ne prend que des pointeurs comme arguments, donc la différence technique visible est minime. Mais je ne fais que deviner ici.

Ce type de question de légalité détaillée s'applique généralement à un interpréteur C, ou à un outil de type lint, plutôt qu'au code compilé. Un interpréteur peut implémenter un tableau 2D comme un tableau de pointeurs vers des tableaux, car il y a une fonctionnalité d'exécution de moins à implémenter, auquel cas le déréférencement du +1 serait fatal, et même si cela fonctionnait, cela donnerait la mauvaise réponse.

Une autre faiblesse possible peut être que le compilateur C peut aligner le tableau externe. Imaginez s'il s'agissait d'un tableau de 5 caractères ( char arr[5]), lorsque le programme s'exécute, &a+1il invoque le comportement "tableau de tableau". Le compilateur peut décider qu'un tableau de tableau de 5 chars ( char arr[][5]) est en fait généré comme un tableau de tableau de 8 chars ( char arr[][8]), de sorte que le tableau externe s'aligne bien. Le code dont nous discutons indiquerait maintenant la taille du tableau comme 8, et non 5. Je ne dis pas qu'un compilateur particulier ferait certainement cela, mais il le pourrait.

Gem Taylor
la source
C'est suffisant. Cependant, pour des raisons difficiles à expliquer, tout le monde utilise sizeof () / sizeof ()?
Gem Taylor
5
La plupart des gens le font. Par exemple, sizeof(array)/sizeof(array[0])donne le nombre d'éléments dans un tableau.
SS Anne
Le compilateur C est autorisé à aligner le tableau, mais je ne suis pas convaincu qu'il soit autorisé à changer le type du tableau après cela. L'alignement serait mis en œuvre de manière plus réaliste en insérant des octets de remplissage.
Kevin
1
La soustraction de pointeurs n'est pas limitée à seulement deux pointeurs dans le même tableau - les pointeurs sont également autorisés à se trouver un après la fin du tableau. &a+1est défini. Comme le note John Bollinger, ce *(&a+1)n'est pas le cas, car il tente de déréférencer un objet qui n'existe pas.
Eric Postpischil
5
Un compilateur ne peut pas implémenter un fichier char [][5]as char arr[][8]. Un tableau n'est que les objets répétés qu'il contient; il n'y a pas de rembourrage. De plus, cela casserait l'exemple (non normatif) 2 de C 2018 6.5.3.4 7, qui nous dit que nous pouvons calculer le nombre d'éléments dans un tableau avec sizeof array / sizeof array[0].
Eric Postpischil