Avant d'expliquer les différents types de données disponibles en C #, il est important de mentionner que C # est un langage fortement typé. Cela signifie que chaque variable, constante, paramètre d'entrée, type de retour et en général chaque expression qui évalue une valeur, a un type.
Chaque type contient des informations qui seront incorporées par le compilateur dans le fichier exécutable en tant que métadonnées qui seront utilisées par le Common Language Runtime (CLR) pour garantir la sécurité du type lorsqu'il alloue et récupère de la mémoire.
Si vous voulez savoir combien de mémoire un type spécifique alloue, vous pouvez utiliser l'opérateur sizeof comme suit:
static void Main()
{
var size = sizeof(int);
Console.WriteLine($"int size:{size}");
size = sizeof(bool);
Console.WriteLine($"bool size:{size}");
size = sizeof(double);
Console.WriteLine($"double size:{size}");
size = sizeof(char);
Console.WriteLine($"char size:{size}");
}
La sortie affichera le nombre d'octets alloués par chaque variable.
int size:4
bool size:1
double size:8
char size:2
Les informations relatives à chaque type sont:
- L'espace de stockage requis.
- Les valeurs maximum et minimum. Par exemple, le type Int32 accepte des valeurs comprises entre 2147483648 et 2147483647.
- Le type de base dont il hérite.
- Emplacement où la mémoire des variables sera allouée au moment de l'exécution.
- Les types d'opérations autorisées.
Les membres (méthodes, champs, événements, etc.) contenus par le type. Par exemple, si nous vérifions la définition du type int, nous trouverons la structure et les membres suivants:
namespace System
{
[ComVisible(true)]
public struct Int32 : IComparable, IFormattable, IConvertible, IComparable<Int32>, IEquatable<Int32>
{
public const Int32 MaxValue = 2147483647;
public const Int32 MinValue = -2147483648;
public static Int32 Parse(string s, NumberStyles style, IFormatProvider provider);
...
}
}
Gestion de la mémoire
Lorsque plusieurs processus s'exécutent sur un système d'exploitation et que la quantité de RAM n'est pas suffisante pour tout contenir, le système d'exploitation mappe des parties du disque dur avec la RAM et commence à stocker les données sur le disque dur. Le système d'exploitation utilisera des tables spécifiques où les adresses virtuelles sont mappées à leurs adresses physiques correspondantes pour exécuter la demande. Cette capacité de gestion de la mémoire est appelée mémoire virtuelle.
Dans chaque processus, la mémoire virtuelle disponible est organisée dans les 6 sections suivantes mais pour la pertinence de ce sujet, nous nous concentrerons uniquement sur la pile et le tas.
Pile
La pile est une structure de données LIFO (dernier entré, premier sorti), dont la taille dépend du système d'exploitation (par défaut, pour les machines ARM, x86 et x64, Windows réserve 1 Mo, tandis que Linux réserve de 2 Mo à 8 Mo selon le version).
Cette partie de la mémoire est gérée automatiquement par la CPU. Chaque fois qu'une fonction déclare une nouvelle variable, le compilateur alloue un nouveau bloc de mémoire aussi grand que sa taille sur la pile, et lorsque la fonction est terminée, le bloc de mémoire de la variable est désalloué.
Heap
Cette région de mémoire n'est pas gérée automatiquement par le CPU et sa taille est plus grande que la pile. Lorsque le nouveau mot clé est appelé, le compilateur commence à rechercher le premier bloc de mémoire libre qui correspond à la taille de la demande. et quand il le trouve, il est marqué comme réservé en utilisant la fonction C intégrée malloc () et renvoie le pointeur vers cet emplacement. Il est également possible de désallouer un bloc de mémoire en utilisant la fonction C intégrée free (). Ce mécanisme provoque une fragmentation de la mémoire et doit utiliser des pointeurs pour accéder au bon bloc de mémoire, il est plus lent que la pile pour effectuer les opérations de lecture / écriture.
Types personnalisés et intégrés
intégrés Alors que C # fournit un ensemble standard de types intégrés représentant des entiers, des booléens, des caractères de texte, etc., vous pouvez utiliser des constructions telles que struct, class, interface et enum pour créer vos propres types.
Un exemple de type personnalisé utilisant la construction struct est:
struct Point
{
public int X;
public int Y;
};
Types de valeur et de référence
Nous pouvons classer le type C # dans les catégories suivantes:
- Types de valeur
- Types de référence
Types de valeur Les types de
valeur dérivent de la classe System.ValueType et les variables de ce type contiennent leurs valeurs dans leur allocation de mémoire dans la pile. Les deux catégories de types valeur sont struct et enum.
L'exemple suivant montre le membre de type boolean. Comme vous pouvez le voir, il n'y a pas de référence explicite à la classe System.ValueType, cela se produit car cette classe est héritée par la structure.
namespace System
{
[ComVisible(true)]
public struct Boolean : IComparable, IConvertible, IComparable<Boolean>, IEquatable<Boolean>
{
public static readonly string TrueString;
public static readonly string FalseString;
public static Boolean Parse(string value);
...
}
}
Types de référence
D'autre part, les types de référence ne contiennent pas les données réelles stockées dans une variable, mais l'adresse mémoire du tas où la valeur est stockée. Les catégories de types de référence sont les classes, les délégués, les tableaux et les interfaces.
Lors de l'exécution, lorsqu'une variable de type référence est déclarée, elle contient la valeur null jusqu'à ce qu'un objet qui a été créé à l'aide des mots-clés new lui soit affecté.
L'exemple suivant montre les membres du type générique List.
namespace System.Collections.Generic
{
[DebuggerDisplay("Count = {Count}")]
[DebuggerTypeProxy(typeof(Generic.Mscorlib_CollectionDebugView<>))]
[DefaultMember("Item")]
public class List<T> : IList<T>, ICollection<T>, IEnumerable<T>, IEnumerable, IList, ICollection, IReadOnlyList<T>, IReadOnlyCollection<T>
{
...
public T this[int index] { get; set; }
public int Count { get; }
public int Capacity { get; set; }
public void Add(T item);
public void AddRange(IEnumerable<T> collection);
...
}
}
Au cas où vous souhaiteriez connaître l'adresse mémoire d'un objet spécifique, la classe System.Runtime.InteropServices fournit un moyen d'accéder aux objets gérés à partir de la mémoire non gérée. Dans l'exemple suivant, nous allons utiliser la méthode statique GCHandle.Alloc () pour allouer un handle à une chaîne, puis la méthode AddrOfPinnedObject pour récupérer son adresse.
string s1 = "Hello World";
GCHandle gch = GCHandle.Alloc(s1, GCHandleType.Pinned);
IntPtr pObj = gch.AddrOfPinnedObject();
Console.WriteLine($"Memory address:{pObj.ToString()}");
La sortie sera
Memory address:39723832
Références
Documentation officielle: https://docs.microsoft.com/en-us/cpp/build/reference/stack-stack-allocations?view=vs-2019