151 votes

Tableaux, tas et pile et types de valeurs

int[] myIntegers;
myIntegers = new int[100];

Dans le code ci-dessus, est-ce que new int[100] génère le tableau sur le tas ? D'après ce que j'ai lu sur le CLR via c#, la réponse est oui. Mais ce que je n'arrive pas à comprendre, c'est ce qui se passe avec les int à l'intérieur du tableau. Comme ce sont des types de valeur, je suppose qu'ils doivent être mis en boîte, car je peux, par exemple, passer mesIntegers à d'autres parties du programme et cela encombrerait la pile s'ils étaient laissés dessus tout le temps. Ou est-ce que je me trompe ? Je suppose qu'ils seraient juste mis en boîte et qu'ils vivraient sur la pile aussi longtemps que le tableau existerait.

327voto

P Daddy Points 14228

Votre tableau est alloué sur le tas, et les ints ne sont pas encadrés.

La source de votre confusion est probablement due au fait que les gens ont dit que les types de référence sont alloués sur le tas, et les types de valeur sur la pile. Ce n'est pas une représentation tout à fait exacte.

Toutes les variables locales et les paramètres sont alloués sur la pile. Cela inclut les types de valeur et les types de référence. La différence entre les deux est uniquement ce qui est stocké dans la variable. Sans surprise, pour un type de valeur, la variable valeur du type est stockée directement dans la variable, et pour un type de référence, la valeur du type est stockée sur le tas, et un référence à cette valeur est ce qui est stocké dans la variable.

Il en va de même pour les champs. Lorsque de la mémoire est allouée pour une instance d'un type d'agrégat (un champ de type class ou un struct ), il doit inclure un stockage pour chacun de ses champs d'instance. Pour les champs de type référence, ce stockage contient simplement une référence à la valeur, qui sera elle-même allouée sur le tas ultérieurement. Pour les champs de type valeur, ce stockage contient la valeur réelle.

Donc, étant donné les types suivants :

class RefType{
    public int    I;
    public string S;
    public long   L;
}

struct ValType{
    public int    I;
    public string S;
    public long   L;
}

Les valeurs de chacun de ces types nécessiteraient 16 octets de mémoire (en supposant une taille de mot de 32 bits). Le champ I dans chaque cas, il faut 4 octets pour stocker sa valeur, le champ S prend 4 octets pour stocker sa référence, et le champ L prend 8 octets pour stocker sa valeur. Ainsi, la mémoire pour la valeur des deux RefType y ValType ressemble à ça :

 0 
           I          
 4 
           S          
 8 
           L          

16 

Maintenant, si vous avez trois variables locales dans une fonction, de types RefType , ValType y int[] comme ceci :

RefType refType;
ValType valType;
int[]   intArray;

alors votre pile pourrait ressembler à ceci :

 0 
        refType       
 4 
        valType       

20 
        intArray      
24 

Si vous avez assigné des valeurs à ces variables locales, comme ceci :

refType = new RefType();
refType.I = 100;
refType.S = "refType.S";
refType.L = 0x0123456789ABCDEF;

valType = new ValType();
valType.I = 200;
valType.S = "valType.S";
valType.L = 0x0011223344556677;

intArray = new int[4];
intArray[0] = 300;
intArray[1] = 301;
intArray[2] = 302;
intArray[3] = 303;

Votre pile pourrait alors ressembler à ceci :

 0 
       0x4A963B68      -- heap address of \`refType\`
 4 
          200          -- value of \`valType.I\`
       0x4A984C10      -- heap address of \`valType.S\`
       0x44556677      -- low 32-bits of \`valType.L\`
       0x00112233      -- high 32-bits of \`valType.L\`
20 
       0x4AA4C288      -- heap address of \`intArray\`
24 

Mémoire à l'adresse 0x4A963B68 (valeur de refType ) serait quelque chose comme :

 0 
          100          -- value of \`refType.I\`
 4 
       0x4A984D88      -- heap address of \`refType.S\`
 8 
       0x89ABCDEF      -- low 32-bits of \`refType.L\`
       0x01234567      -- high 32-bits of \`refType.L\`
16 

Mémoire à l'adresse 0x4AA4C288 (valeur de intArray ) serait quelque chose comme :

 0 
           4           -- length of array
 4 
          300          -- \`intArray\[0\]\`
 8 
          301          -- \`intArray\[1\]\`
12 
          302          -- \`intArray\[2\]\`
16 
          303          -- \`intArray\[3\]\`
20 

Maintenant, si vous avez passé intArray à une autre fonction, la valeur poussée sur la pile serait 0x4AA4C288 l'adresse du tableau, no une copie du tableau.

60 votes

Je note que l'affirmation selon laquelle toutes les variables locales sont stockées sur la pile est inexacte. Les variables locales qui sont des variables externes d'une fonction anonyme sont stockées sur le tas. Les variables locales des blocs itérateurs sont stockées dans le tas. Les variables locales des blocs asynchrones sont stockées dans le tas. Les variables locales qui sont enregistrées ne sont stockées ni sur la pile ni sur le tas. Les variables locales qui sont élidées ne sont stockées ni sur la pile ni sur le tas.

5 votes

LOL, toujours le pinailleur, M. Lippert :) Je me sens obligé de souligner qu'à l'exception de vos deux derniers cas, les "locales" cessent de l'être au moment de la compilation. L'implémentation les élève au statut de membres de classe, ce qui est la seule raison pour laquelle ils sont stockés sur le tas. Il s'agit donc simplement d'un détail d'implémentation (snicker). Bien sûr, le stockage des registres est un détail d'implémentation de niveau encore plus bas, et l'élision ne compte pas.

4 votes

Bien sûr, tout mon post est constitué de détails de mise en œuvre, mais, comme vous le réalisez certainement, il s'agissait de tenter de séparer les concepts de variables y valeurs . Une variable (appelée locale, champ, paramètre, etc.) peut être stockée sur la pile, le tas, ou tout autre endroit défini par l'implémentation, mais ce n'est pas vraiment ce qui est important. Ce qui est important, c'est de savoir si cette variable stocke directement la valeur qu'elle représente, ou simplement une référence à cette valeur, stockée ailleurs. C'est important car cela affecte la sémantique de la copie : si la copie de cette variable copie sa valeur ou son adresse.

24voto

JaredPar Points 333733

Oui, le tableau sera situé sur le tas.

Les ints à l'intérieur du tableau ne seront pas encadrés. Ce n'est pas parce qu'un type de valeur existe sur le tas qu'il sera nécessairement mis en boîte. Le boxing ne se produira que lorsqu'un type de valeur, tel que int, est assigné à une référence de type object.

Par exemple

Ne contient pas de boîte :

int i = 42;
myIntegers[0] = 42;

Boîtes :

object i = 42;
object[] arr = new object[10];  // no boxing here 
arr[0] = 42;

Vous pouvez également consulter le billet d'Eric sur ce sujet :

1 votes

Mais je ne comprends pas. Les types de valeur ne devraient-ils pas être alloués sur la pile ? Ou bien les types valeur et référence peuvent être alloués à la fois sur le tas et sur la pile et c'est juste qu'ils sont généralement stockés à un endroit ou à un autre ?

6 votes

@Jorge, un type de valeur sans wrapper / conteneur de type de référence vivra sur la pile. Cependant, une fois qu'il est utilisé dans un conteneur de type de référence, il se trouve dans le tas. Un tableau est un type de référence et donc la mémoire pour le int doit être dans le tas.

2 votes

@Jorge : les types de référence ne vivent que dans le tas, jamais sur la pile. A l'inverse, il est impossible (dans un code vérifiable) de stocker un pointeur vers un emplacement de la pile dans un objet de type référence.

23voto

Guffa Points 308133

Pour comprendre ce qui se passe, voici quelques faits :

  • Les objets sont toujours alloués sur le tas.
  • Le tas ne contient que des objets.
  • Les types de valeurs sont soit alloués sur la pile, soit font partie d'un objet sur le tas.
  • Un tableau est un objet.
  • Un tableau ne peut contenir que des types de valeurs.
  • Une référence d'objet est un type de valeur.

Ainsi, si vous avez un tableau d'entiers, le tableau est alloué sur le tas et les entiers qu'il contient font partie de l'objet tableau sur le tas. Les entiers résident à l'intérieur de l'objet tableau sur le tas, et non en tant qu'objets séparés, ils ne sont donc pas encadrés.

Si vous avez un tableau de chaînes de caractères, il s'agit en fait d'un tableau de références de chaînes de caractères. Comme les références sont des types de valeurs, elles feront partie de l'objet tableau sur le tas. Si vous placez un objet chaîne dans le tableau, vous placez en fait la référence à l'objet chaîne dans le tableau, et la chaîne est un objet séparé sur le tas.

0 votes

Oui, les références se comportent exactement comme les types de valeurs, mais j'ai remarqué qu'elles ne sont généralement pas appelées de cette façon, ou incluses dans les types de valeurs. Voir par exemple (mais il y en a beaucoup plus comme ça) msdn.microsoft.com/fr/us/library/s1ax56ch.aspx

0 votes

@Henk : Oui, vous avez raison, les références ne sont pas listées parmi les variables de type valeur, mais lorsqu'il s'agit de la façon dont la mémoire leur est allouée, elles sont à tous égards des types de valeur, et il est très utile de s'en rendre compte pour comprendre comment l'allocation de la mémoire s'articule :)

1 votes

Je doute du 5ème point, "Un tableau ne peut contenir que des types de valeurs". Qu'en est-il des tableaux de chaînes de caractères ? string[] strings = new string[4] ;

11voto

JulianR Points 7257

Je pense qu'au cœur de votre question se trouve un malentendu sur les types de référence et de valeur. C'est une question à laquelle tous les développeurs .NET et Java ont dû faire face.

Un tableau est juste une liste de valeurs. S'il s'agit d'un tableau de type référence (disons un string[] ), alors le tableau est une liste de références à diverses string sur le tas, car une référence est la valeur d'un type de référence. En interne, ces références sont implémentées comme des pointeurs vers une adresse en mémoire. Si vous souhaitez visualiser cela, un tel tableau ressemblerait à ceci en mémoire (sur le tas) :

[ 00000000, 00000000, 00000000, F8AB56AA ]

Il s'agit d'un tableau de string qui contient 4 références à string objets sur le tas (les chiffres ici sont hexadécimaux). Actuellement, seul le dernier string ne pointe en fait sur rien (la mémoire est initialisée à tous les zéros lorsqu'elle est allouée), ce tableau serait en fait le résultat de ce code en C# :

string[] strings = new string[4];
strings[3] = "something"; // the string was allocated at 0xF8AB56AA by the CLR

Le tableau ci-dessus serait dans un programme 32 bits. Dans un programme 64 bits, les références seraient deux fois plus grandes ( F8AB56AA serait 00000000F8AB56AA ).

Si vous avez un tableau de types de valeurs (disons un int[] ), alors le tableau est une liste d'entiers, comme l'indique l'option valeur d'une catégorie de valeur es la valeur elle-même (d'où le nom). La visualisation d'un tel tableau serait la suivante :

[ 00000000, 45FF32BB, 00000000, 00000000 ]

Il s'agit d'un tableau de 4 entiers, où seul le deuxième int se voit attribuer une valeur (à 1174352571, qui est la représentation décimale de ce nombre hexadécimal) et le reste des entiers serait 0 (comme je l'ai dit, la mémoire est initialisée à zéro et 00000000 en hexadécimal est 0 en décimal). Le code qui produit ce tableau serait :

 int[] integers = new int[4];
 integers[1] = 1174352571; // integers[1] = 0x45FF32BB would be valid too

Ce site int[] sera également stocké dans le tas.

Autre exemple, la mémoire d'un short[4] ressemblerait à ceci :

[ 0000, 0000, 0000, 0000 ]

Comme le valeur d'un short est un nombre de 2 octets.

L'endroit où un type de valeur est stocké n'est qu'un détail de mise en œuvre, comme l'explique très bien Eric Lippert. aquí Les différences entre les types de valeurs et de références ne sont pas inhérentes aux différences entre les types de valeurs et de références (qui sont des différences de comportement).

Lorsque vous passez quelque chose à une méthode (qu'il s'agisse d'un type de référence ou d'un type de valeur), un message de type copie de la valeur du type est effectivement passé à la méthode. Dans le cas d'un type de référence, le valeur est une référence (pensez-y comme un pointeur vers un élément de la mémoire, bien qu'il s'agisse également d'un détail de mise en œuvre) et dans le cas d'un type de valeur, la valeur est la chose elle-même.

// Calling this method creates a copy of the *reference* to the string
// and a copy of the int itself, so copies of the *values*
void SomeMethod(string s, int i){}

La boxe ne se produit que si vous convertir un type de valeur à un type de référence. Ce code encadre :

object o = 5;

0 votes

Je pense que "un détail de mise en œuvre" devrait être une police-size : 50px. ;)

1voto

Dykam Points 5343

Un tableau d'entiers est alloué sur le tas, rien de plus, rien de moins. myIntegers fait référence au début de la section où les ints sont alloués. Cette référence est située sur la pile.

Si vous avez un tableau d'objets de type référence, comme le type Object, myObjects[], situé sur la pile, fera référence au groupe de valeurs qui font référence aux objets eux-mêmes.

En résumé, si vous passez myIntegers à certaines fonctions, vous ne passez que la référence à l'endroit où le vrai paquet d'entiers est alloué.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X