Bevor Sie die verschiedenen in C # verfügbaren Datentypen erläutern, müssen Sie erwähnen, dass C # eine stark typisierte Sprache ist. Dies bedeutet, dass jede Variable, Konstante, Eingabeparameter, Rückgabetyp und im Allgemeinen jeder Ausdruck, der einen Wert ergibt, einen Typ hat.
Jeder Typ enthält Informationen, die vom Compiler als Metadaten in die ausführbare Datei eingebettet werden und von der Common Language Runtime (CLR) verwendet werden, um die Typensicherheit beim Zuweisen und Zurückfordern von Speicher zu gewährleisten.
Wenn Sie wissen möchten, wie viel Speicher ein bestimmter Typ zuweist, können Sie den Operator sizeof wie folgt verwenden:
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}");
}
Die Ausgabe zeigt die Anzahl der von jeder Variablen zugewiesenen Bytes.
int size:4
bool size:1
double size:8
char size:2
Die Informationen zu jedem Typ sind:
- Der benötigte Speicherplatz.
- Die Maximal- und Minimalwerte. Beispielsweise akzeptiert der Typ Int32 Werte zwischen 2147483648 und 2147483647.
- Der Basistyp, von dem es erbt.
- Der Speicherort, an dem der Speicher für Variablen zur Laufzeit zugewiesen wird.
- Die Arten von Operationen, die zulässig sind.
Die im Typ enthaltenen Elemente (Methoden, Felder, Ereignisse usw.). Wenn wir beispielsweise die Definition des Typs int überprüfen, finden wir die folgende Struktur und die folgenden Elemente:
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);
...
}
}
Speicherverwaltung
Wenn auf einem Betriebssystem mehrere Prozesse ausgeführt werden und die RAM-Größe nicht ausreicht, um alles zu speichern, ordnet das Betriebssystem Teile der Festplatte dem RAM zu und beginnt mit dem Speichern von Daten auf der Festplatte. Das Betriebssystem verwendet als bestimmte Tabellen, in denen virtuelle Adressen ihren entsprechenden physischen Adressen zugeordnet sind, um die Anforderung auszuführen. Diese Funktion zum Verwalten des Speichers wird als virtueller Speicher bezeichnet.
In jedem Prozess ist der verfügbare virtuelle Speicher in den folgenden 6 Abschnitten organisiert. Für die Relevanz dieses Themas konzentrieren wir uns jedoch nur auf den Stapel und den Heap.
Stapel
Der Stapel ist eine LIFO-Datenstruktur (last in, first out) mit einer vom Betriebssystem abhängigen Größe (standardmäßig für ARM-, x86- und x64-Computer). Windows reserviert 1 MB, während Linux je nach Betriebssystem zwischen 2 MB und 8 MB reserviert Ausführung).
Dieser Speicherbereich wird automatisch von der CPU verwaltet. Jedes Mal, wenn eine Funktion eine neue Variable deklariert, weist der Compiler einen neuen Speicherblock zu, der so groß ist wie seine Größe auf dem Stapel, und wenn die Funktion beendet ist, wird der Speicherblock für die Variable freigegeben.
Heap
Dieser Speicherbereich wird von der CPU nicht automatisch verwaltet und ist größer als der Stapel. Wenn das neue Schlüsselwort aufgerufen wird, sucht der Compiler nach dem ersten freien Speicherblock, der der Größe der Anforderung entspricht. und wenn es es findet, wird es mit der eingebauten C-Funktion malloc () als reserviert markiert und gibt den Zeiger auf diese Position zurück. Es ist auch möglich, die Zuweisung eines Speicherblocks mithilfe der integrierten C-Funktion free () aufzuheben. Dieser Mechanismus verursacht eine Speicherfragmentierung und muss Zeiger verwenden, um auf den richtigen Speicherblock zuzugreifen. Er ist langsamer als der Stapel, um die Lese- / Schreiboperationen auszuführen.
Benutzerdefinierte und integrierte Typen
Während C # einen Standardsatz integrierter Typen bereitstellt, die Ganzzahlen, Boolesche Werte, Textzeichen usw. darstellen, können Sie Konstrukte wie struct, class, interface und enum verwenden, um Ihre eigenen Typen zu erstellen.
Ein Beispiel für einen benutzerdefinierten Typ, der das Strukturkonstrukt verwendet, ist:
struct Point
{
public int X;
public int Y;
};
Wert- und Referenztypen
Wir können den C # -Typ in die folgenden Kategorien einteilen:
Werttypen
Werttypen ergeben sich aus der System.ValueType Klasse und Variablen dieses Typs enthalten ihre Werte innerhalb ihrer Speicherzuweisung in dem Stapel. Die beiden Kategorien von Werttypen sind struct und enum.
Das folgende Beispiel zeigt das Mitglied vom Typ Boolean. Wie Sie sehen, gibt es keinen expliziten Verweis auf die System.ValueType-Klasse. Dies geschieht, weil diese Klasse von der Struktur geerbt wird.
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);
...
}
}
Referenztypen
Andererseits enthalten die Referenztypen nicht die tatsächlichen Daten, die in einer Variablen gespeichert sind, sondern die Speicheradresse des Heaps, in dem der Wert gespeichert ist. Die Kategorien von Referenztypen sind Klassen, Delegaten, Arrays und Schnittstellen.
Wenn zur Laufzeit eine Referenztypvariable deklariert wird, enthält sie den Wert null, bis ihr ein Objekt zugewiesen wird, das mit den Schlüsselwörtern new erstellt wurde.
Das folgende Beispiel zeigt die Mitglieder des generischen Typs 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);
...
}
}
Wenn Sie die Speicheradresse eines bestimmten Objekts ermitteln möchten, bietet die Klasse System.Runtime.InteropServices eine Möglichkeit, aus dem nicht verwalteten Speicher auf verwaltete Objekte zuzugreifen. Im folgenden Beispiel verwenden wir die statische Methode GCHandle.Alloc (), um einem String ein Handle zuzuweisen, und dann die Methode AddrOfPinnedObject, um seine Adresse abzurufen.
string s1 = "Hello World";
GCHandle gch = GCHandle.Alloc(s1, GCHandleType.Pinned);
IntPtr pObj = gch.AddrOfPinnedObject();
Console.WriteLine($"Memory address:{pObj.ToString()}");
Die Ausgabe wird sein
Memory address:39723832
Referenzen
Offizielle Dokumentation: https://docs.microsoft.com/en-us/cpp/build/reference/stack-stack-allocations?view=vs-2019