Tableaux, tas et pile et types de valeur

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

Dans le code ci-dessus, new int [100] génère-t-il le tableau sur le tas? D'après ce que j'ai lu sur CLR via c #, la réponse est oui. Mais ce que je ne peux pas comprendre, c'est ce qui arrive aux int réels à l'intérieur du tableau. Comme ce sont des types de valeur, je suppose qu'ils devraient être encadrés, comme 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 ai-je tort? Je suppose qu'ils seraient simplement mis en boîte et vivraient sur le tas aussi longtemps que le tableau existait.

dévoré d'elysium
la source

Réponses:

289

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

La source de votre confusion est probablement parce que les gens ont dit que les types de référence sont alloués sur le tas et que les types de valeur sont alloués sur la pile. Ce n'est pas une représentation entièrement exacte.

Toutes les variables et paramètres locaux sont alloués sur la pile. Cela inclut à la fois 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 valeur, la valeur du type est stockée directement dans la variable, et pour un type référence, la valeur du type est stockée sur le tas, et une référence à cette valeur est ce qui est stocké dans la variable.

Il en va de même pour les champs. Lorsque la mémoire est allouée pour une instance d'un type d'agrégat (a classou a struct), elle doit inclure le 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 serait elle-même allouée ultérieurement sur le tas. 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 Idans chaque cas prend 4 octets pour stocker sa valeur, le champ Sprend 4 octets pour stocker sa référence et le champ Lprend 8 octets pour stocker sa valeur. Donc, la mémoire pour la valeur des deux RefTypeet ValTyperessemble à ceci:

 0 ┌───────────────────┐
   │ je │
 4 ├───────────────────┤
   │ S │
 8 ├───────────────────┤
   │ L │
   │ │
16 └───────────────────

Maintenant , si vous avez eu trois variables locales dans une fonction, des types RefType, ValTypeet int[], comme celui - ci:

RefType refType;
ValType valType;
int[]   intArray;

alors votre pile pourrait ressembler à ceci:

 0 ┌───────────────────┐
   │ refType │
 4 ├───────────────────┤
   │ valType │
   │ │
   │ │
   │ │
20 ├───────────────────┤
   │ intArray │
24 └───────────────────

Si vous avez attribué 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;

Ensuite, votre pile pourrait ressembler à ceci:

 0 ┌───────────────────┐
   │ 0x4A963B68 │ - adresse de tas de `refType`
 4 ├───────────────────┤
   │ 200 │ - valeur de `valType.I`
   │ 0x4A984C10 │ - adresse de tas de `valType.S`
   │ 0x44556677 │ - bas 32 bits de `valType.L`
   │ 0x00112233 │ - 32 bits de haut de `valType.L`
20 ├───────────────────┤
   │ 0x4AA4C288 │ - adresse de tas de `intArray`
24 └───────────────────

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

 0 ┌───────────────────┐
   │ 100 │ - valeur de `refType.I`
 4 ├───────────────────┤
   │ 0x4A984D88 │ - adresse de tas de `refType.S`
 8 ├───────────────────┤
   │ 0x89ABCDEF │ - 32 bits bas de `refType.L`
   │ 0x01234567 │ - 32 bits de haut de `refType.L`
16 └───────────────────

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

 0 ┌───────────────────┐
   │ 4 │ - longueur du tableau
 4 ├───────────────────┤
   │ 300 │ - `intArray [0]`
 8 ├───────────────────┤
   │ 301 │ - `intArray [1]`
12 ├───────────────────
   │ 302 │ - `intArray [2]`
16 ├───────────────────
   │ 303 │ - `intArray [3]`
20 └───────────────────┘

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

Papa
la source
52
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 d'itérateur sont stockées sur le tas. Les variables locales des blocs asynchrones sont stockées sur le tas. Les variables locales qui sont enregistrées ne sont stockées ni sur la pile ni sur le tas. Les variables locales élidées ne sont stockées ni sur la pile ni sur le tas.
Eric Lippert
5
LOL, toujours le pinailleur, M. Lippert. :) Je me sens obligé de souligner qu'à l'exception de vos deux derniers cas, les soi-disant «locaux» cessent d'être des locaux 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. C'est donc simplement un détail d'implémentation (ricanement). Bien sûr, le stockage des registres est un détail d'implémentation encore plus bas, et l'élision ne compte pas.
P Daddy
3
Bien sûr, tout mon article concerne les détails de la mise en œuvre, mais, comme vous le savez sûrement, tout était dans le but de séparer les concepts de variables et de valeurs . Une variable (appelez-la un local, un champ, un paramètre, peu importe) peut être stockée sur la pile, le tas ou un 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 copie: si la copie de cette variable copie sa valeur ou son adresse.
P Daddy
16
Apparemment, vous avez une idée différente de ce que signifie être une «variable locale» que moi. Vous semblez croire qu'une "variable locale" est caractérisée par ses détails d'implémentation . Cette croyance n'est justifiée par rien que je sache dans la spécification C #. Une variable locale est en fait une variable déclarée à l'intérieur d'un bloc dont le nom n'est dans la portée que dans tout l'espace de déclaration associé au bloc. Je vous assure que les variables locales qui sont, en tant que détail d'implémentation, hissées dans les champs d'une classe de fermeture, sont toujours des variables locales selon les règles de C #.
Eric Lippert
15
Cela dit, bien entendu, votre réponse est généralement excellente; le fait que les valeurs sont conceptuellement différentes des variables doit être souligné aussi souvent et aussi fort que possible, car il est fondamental. Et pourtant, beaucoup de gens croient aux mythes les plus étranges à leur sujet! Tellement bon à toi d'avoir combattu le bon combat.
Eric Lippert
23

Oui, le tableau sera situé sur le tas.

Les entiers à l'intérieur du tableau ne seront pas encadrés. Le simple fait qu'un type valeur existe sur le tas ne signifie pas nécessairement qu'il sera encadré. L'encadrement ne se produira que lorsqu'un type valeur, tel que int, est affecté à une référence de type objet.

Par exemple

Ne boxe pas:

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

Des boites:

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

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

JaredPar
la source
1
Mais je ne comprends pas. Les types de valeur ne devraient-ils pas être alloués sur la pile? Ou les deux types valeur et référence peuvent être alloués à la fois sur le tas ou sur la pile et c'est juste qu'ils sont généralement stockés à un endroit ou à un autre?
dévoré elysium le
4
@Jorge, un type valeur sans wrapper / conteneur de type référence vivra sur la pile. Cependant, une fois qu'il est utilisé dans un conteneur de type référence, il vivra dans le tas. Un tableau est un type de référence et, par conséquent, la mémoire de l'int doit être dans le tas.
JaredPar
2
@Jorge: les types de référence ne vivent que dans le tas, jamais sur la pile. Au contraire, il est impossible (en code vérifiable) de stocker un pointeur vers un emplacement de pile dans un objet de type référence.
Anton Tykhyy
1
Je pense que vous vouliez attribuer i à arr [0]. L'affectation constante provoquera toujours la boxe de "42", mais vous avez créé i, vous pouvez donc aussi bien l'utiliser ;-)
Marcus Griep
@AntonTykhyy: Il n'y a pas de règle dont je suis consciente disant qu'un CLR ne peut pas faire une analyse d'évasion. S'il détecte qu'un objet ne sera jamais référencé après la durée de vie de la fonction qui l'a créé, il est tout à fait légitime - et même préférable - de construire l'objet sur la pile, qu'il s'agisse d'un type valeur ou non. Le "type de valeur" et le "type de référence" décrivent essentiellement ce qui est occupé par la variable dans la mémoire, et non une règle absolue sur l'endroit où se trouve l'objet.
cHao
21

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 valeur 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 valeur.
  • Une référence d'objet est un type 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, pas en tant qu'objets séparés, ils ne sont donc pas encadrés.

Si vous avez un tableau de chaînes, c'est vraiment un tableau de références de chaînes. Les références étant des types valeur, 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 distinct sur le tas.

Guffa
la source
Oui, les références se comportent exactement comme les types valeur, mais j'ai remarqué qu'elles ne sont généralement pas appelées de cette façon ou incluses dans les types valeur. Voir par exemple (mais il y en a beaucoup plus comme ça) msdn.microsoft.com/en-us/library/s1ax56ch.aspx
Henk Holterman
@Henk: Oui, vous avez raison de dire que les références ne sont pas répertoriées parmi les variables de type valeur, mais en ce qui concerne 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 mémoire tout va ensemble. :)
Guffa
Je doute du 5ème point, "Un tableau ne peut contenir que des types valeur." Qu'en est-il du tableau de chaînes? string [] strings = nouvelle chaîne [4];
Sunil Purushothaman le
9

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 probablement quelque chose avec lequel tous les développeurs .NET et Java ont eu du mal.

Un tableau n'est qu'une liste de valeurs. S'il s'agit d'un tableau d'un type référence (disons a string[]), alors le tableau est une liste de références à divers stringobjets sur le tas, car une référence est la valeur d'un type 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 ceci, un tel tableau ressemblerait à ceci en mémoire (sur le tas):

[ 00000000, 00000000, 00000000, F8AB56AA ]

Il s'agit d'un tableau stringcontenant 4 références à des stringobjets sur le tas (les nombres ici sont hexadécimaux). Actuellement, seul le dernier stringpointe vers quelque chose (la mémoire est initialisée à tous les zéros lorsqu'elle est allouée), ce tableau serait essentiellement 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 (le F8AB56AAseraient 00000000F8AB56AA).

Si vous avez un tableau de types valeur (disons an int[]), alors le tableau est une liste d'entiers, car la valeur d'un type valeur est 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 entier reçoit 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 a produit ce tableau serait:

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

Ce int[]tableau serait également stocké sur le tas.

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

[ 0000, 0000, 0000, 0000 ]

Comme la valeur de a shortest un nombre de 2 octets.

L'endroit où un type valeur est stocké n'est qu'un détail d'implémentation comme Eric Lippert l'explique très bien ici , non inhérent aux différences entre les types valeur et référence (qui est la différence de comportement).

Lorsque vous passez quelque chose à une méthode (qu'il s'agisse d'un type référence ou d'un type valeur), une copie de la valeur du type est en fait transmise à la méthode. Dans le cas d'un type référence, la valeur est une référence (pensez-y comme un pointeur vers un morceau de mémoire, bien que ce soit également un détail d'implémentation) et dans le cas d'un type 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){}

L'encadrement ne se produit que si vous convertissez un type valeur en type référence. Ce code encadre:

object o = 5;
JulianR
la source
Je crois que "un détail d'implémentation" devrait être une taille de police: 50px. ;)
sisve le
2

Ce sont des illustrations illustrant la réponse ci-dessus de @P Daddy

entrez la description de l'image ici

entrez la description de l'image ici

Et j'ai illustré le contenu correspondant dans mon style.

entrez la description de l'image ici

Parc YoungMin
la source
@P Daddy J'ai fait des illustrations. Veuillez vérifier s'il y a une mauvaise pièce. Et j'ai quelques questions supplémentaires. 1. Lorsque je crée un tableau de type int de 4 longueurs, les informations de longueur (4) sont également toujours stockées dans la mémoire?
YoungMin Park
2. Sur la deuxième illustration, l'adresse de la baie copiée est stockée où? Est-ce la même zone de pile dans laquelle l'adresse intArray est stockée? S'agit-il d'une autre pile mais du même type de pile? Est-ce un type de pile différent? 3. Que signifie 32 bits faible / 32 bits élevé? 4. Quelle est la valeur de retour lorsque j'alloue un type de valeur (dans cet exemple, structure) sur la pile en utilisant un nouveau mot-clé? Est-ce aussi l'adresse? Quand je vérifiais par cette déclaration Console.WriteLine (valType), il montrait le nom complet comme objet comme ConsoleApp.ValType.
YoungMin Park
5. valType.I = 200; Est-ce que cette déclaration signifie que j'obtiens l'adresse de valType, par cette adresse j'accède au I et là, je stocke 200 mais "sur la pile".
YoungMin Park
1

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

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

Pour résumer, si vous passez myIntegers à certaines fonctions, vous ne transmettez la référence qu'à l'endroit où le vrai groupe d'entiers est alloué.

Dykam
la source
1

Il n'y a pas de boxe dans votre exemple de code.

Les types de valeur peuvent vivre sur le tas comme ils le font dans votre tableau d'entiers. Le tableau est alloué sur le tas et il stocke des entiers, qui se trouvent être des types valeur. Le contenu du tableau est initialisé à la valeur par défaut (int), qui se trouve être zéro.

Considérez une classe qui contient un type valeur:


    class HasAnInt
    {
        int i;
    }

    HasAnInt h = new HasAnInt();

La variable h fait référence à une instance de HasAnInt qui vit sur le tas. Il se trouve juste qu'il contient un type valeur. C'est parfaitement normal, «i» se trouve juste à vivre sur le tas car il est contenu dans une classe. Il n'y a pas non plus de boxe dans cet exemple.

Curt Nichols
la source
1

Tout le monde en a assez dit, mais si quelqu'un cherche un exemple clair (mais non officiel) et une documentation sur le tas, la pile, les variables locales et les variables statiques, reportez-vous à l'article complet de Jon Skeet sur la mémoire en .NET - ce qui se passe où

Extrait:

  1. Chaque variable locale (c'est-à-dire déclarée dans une méthode) est stockée sur la pile. Cela inclut les variables de type référence - la variable elle-même est sur la pile, mais rappelez-vous que la valeur d'une variable de type référence n'est qu'une référence (ou null), pas l'objet lui-même. Les paramètres de méthode comptent également comme des variables locales, mais s'ils sont déclarés avec le modificateur ref, ils n'obtiennent pas leur propre emplacement, mais partagent un emplacement avec la variable utilisée dans le code appelant. Voir mon article sur le passage de paramètres pour plus de détails.

  2. Les variables d'instance pour un type de référence sont toujours sur le tas. C'est là que l'objet lui-même "vit".

  3. Les variables d'instance pour un type valeur sont stockées dans le même contexte que la variable qui déclare le type valeur. L'emplacement de mémoire de l'instance contient effectivement les emplacements pour chaque champ de l'instance. Cela signifie (étant donné les deux points précédents) qu'une variable struct déclarée dans une méthode sera toujours sur la pile, alors qu'une variable struct qui est un champ d'instance d'une classe sera sur le tas.

  4. Chaque variable statique est stockée sur le tas, qu'elle soit déclarée dans un type référence ou un type valeur. Il n'y a qu'un seul emplacement au total, quel que soit le nombre d'instances créées. (Il n'est cependant pas nécessaire de créer des instances pour que cet emplacement existe.) Les détails sur quel tas les variables vivent sont compliqués, mais expliqués en détail dans un article MSDN sur le sujet.

gmaran23
la source