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 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 dans le tableau. Comme ce sont des types de valeur, je suppose qu'ils devraient être encadrés, comme je peux, par exemple, passer mes entiers à 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 juste en boîte et vivraient sur le tas aussi longtemps que le tableau existerait.
Votre tableau est alloué sur le tas et les entrées ne sont pas encadrées.
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 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 n'est que ce qui est stocké dans la variable. Sans surprise, pour un type de valeur, la 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 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 agrégé (une classe ou une structure), elle doit inclure du stockage pour chacun de ses champs d'instance. Pour les champs de type référence, ce stockage contient juste une référence à la valeur, qui serait elle-même allouée au 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 32 bits). Le champ I
dans chaque cas prend 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 son valeur. Ainsi, la mémoire de la valeur de RefType
et ValType
ressemble à ceci:
0 ┌───────────────────┐ │ I │ 4 ├───────── ───────────┤ │ S │ 8 ├────────────────────┤ │ L │ │ │ 16 └───────────────────┘
Maintenant, si vous aviez trois variables locales dans une fonction, de types RefType
, ValType
et int[]
, comme ça:
RefType refType;
ValType valType;
int[] intArray;
alors votre pile pourrait ressembler à ceci:
0 ┌───────────────────┐ │ refType │ 4 ├───────── ───────────┤┤ │ valType │ │ │ │ │ │ │ 20 ├── ─────────────────┤ │ intArray │ 24 └────────────────── ──┘
Si vous avez affecté 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 │ - adresse de tas de `refType` 4 ├ ───────────────────┤ │ 200 │ - valeur de `valType.I` │ 0x4A984C10 │ - adresse de tas de `valType.S` │ 0x44556677 │ - 32 bits bas de` valType.L` │ 0x00112233 │ - 32 bits hauts 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 │ - bas 32 bits de `refType.L` │ 0x01234567 │ - haut 32 bits 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 avez passé intArray
à une autre fonction, la valeur insérée dans la pile serait 0x4AA4C288, l'adresse du tableau, pas une copie du tableau.
Oui, le tableau sera situé sur le tas.
Les entrées à l'intérieur du tableau ne seront pas encadrées. Le fait qu'un type de valeur existe sur le tas ne signifie pas nécessairement qu'il sera encadré. La boxe ne se produira que lorsqu'un type de valeur, tel que int, est affecté à une référence de type objet.
Par exemple
Ne contient 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 à ce sujet:
Pour comprendre ce qui se passe, voici quelques faits:
Donc, 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. 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 réellement la référence à l'objet chaîne dans le tableau, et la chaîne est un objet distinct sur le tas.
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 pour chaque .NET et Java développeur a eu du mal avec.
Un tableau n'est qu'une liste de valeurs. S'il s'agit d'un tableau d'un type de référence (disons un string[]
) alors le tableau est une liste de références à divers objets 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 en tant que 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 aux objets string
sur le tas (les nombres ici sont hexadécimaux). Actuellement, seul le dernier string
pointe réellement vers quoi que ce soit (la mémoire est initialisée à tous les zéro lors de l'allocation), 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 (F8AB56AA
serait 00000000F8AB56AA
).
Si vous avez un tableau de types de valeurs (disons un int[]
) alors le tableau est une liste d'entiers, comme la valeur d'un type de valeur is 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 est affecté d'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
Cette int[]
array serait également stocké sur le tas.
Comme autre exemple, la mémoire d'un short[4]
array ressemblerait à ceci:
[ 0000, 0000, 0000, 0000 ]
Comme la valeur d'un short
est un nombre de 2 octets.
Lorsqu'un type de valeur est stocké, ce n'est qu'un détail d'implémentation, comme l'explique très bien Eric Lippert ici , non inhérent aux différences entre les types de valeur et de référence (ce qui est une différence de comportement).
Lorsque vous passez quelque chose à une méthode (que ce soit un type de référence ou un type de valeur), une copie de la valeur de le type est en fait passé à la méthode. Dans le cas d'un type de référence, le valeur est une référence (pensez à cela 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 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 convertissez un type de valeur en un type de référence. Ce code encadre:
object o = 5;
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 les entiers, qui se trouvent être des types de valeur. Le contenu du tableau est initialisé par défaut (int), qui se trouve être zéro.
Considérez une classe qui contient un type de valeur:
class HasAnInt
{
int i;
}
HasAnInt h = new HasAnInt();
La variable h fait référence à une instance de HasAnInt qui réside sur le tas. Il se trouve qu'il contient simplement un type de valeur. C'est parfaitement correct, "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.
Tout le monde en a assez dit, mais si quelqu'un cherche un échantillon et une documentation clairs (mais non officiels) sur le tas, la pile, les variables locales et les variables statiques, reportez-vous à l'article complet de Jon Skeet sur Memory in. NET - ce qui va où
Extrait:
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 n'oubliez pas 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 variables locales, mais s'ils sont déclarés avec le modificateur ref, ils n'ont 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.
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".
Les variables d'instance pour un type de valeur sont stockées dans le même contexte que la variable qui déclare le type de valeur. L'emplacement de mémoire pour 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, tandis qu'une variable struct qui est un champ d'instance d'une classe sera sur le tas.
Chaque variable statique est stockée sur le tas, qu'elle soit déclarée dans un type de référence ou un type de 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 d'instances pour que cet emplacement existe.) Les détails de la pile exacte des variables sont compliqués, mais expliqués en détail dans un article MSDN sur le sujet.
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 entrées sont allouées. 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 tas de valeurs qui référencent les objets eux-mêmes.
Pour résumer, si vous passez myIntegers à certaines fonctions, vous ne passez que la référence à l'endroit où le vrai groupe d'entiers est alloué.