在解释 C# 中可用的不同数据类型之前,重要的是要提到 C# 是一种强类型语言。这意味着每个变量、常量、输入参数、返回类型以及通常每个计算结果为值的表达式都有一个类型。
每种类型都包含编译器将作为元数据嵌入到可执行文件中的信息,公共语言运行时 (CLR) 将使用这些信息在分配和回收内存时确保类型安全。
如果您想知道特定类型分配了多少内存,可以使用 sizeof 运算符,如下所示:
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}");
}
输出将显示每个变量分配的字节数。
int size:4
bool size:1
double size:8
char size:2
每种类型的相关信息有:
- 所需的存储空间。
- 最大值和最小值。例如,Int32 类型接受 2147483648 和 2147483647 之间的值。
- 它继承自的基类型。
- 在运行时分配变量内存的位置。
- 允许的操作种类。
-
类型包含的成员(方法、字段、事件等)。例如,如果我们检查类型 int 的定义,我们会发现以下结构和成员:
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);
...
}
}
内存管理
当多个进程在操作系统上运行并且 RAM 的数量不足以容纳所有进程时,操作系统会将部分硬盘映射到 RAM 并开始在硬盘中存储数据。操作系统将使用虚拟地址映射到其对应物理地址的特定表来执行请求。这种管理内存的能力称为虚拟内存。
在每个进程中,可用的虚拟内存分为以下 6 个部分,但为了本主题的相关性,我们将仅关注堆栈和堆。
堆栈
栈是一种 LIFO(后进先出)数据结构,大小取决于操作系统(默认情况下,对于 ARM、x86 和 x64 机器,Windows 预留 1MB,而 Linux 根据版本预留 2MB 到 8MB )。
这部分内存由 CPU 自动管理。每次函数声明一个新变量时,编译器都会在堆栈上分配一个与其大小一样大的新内存块,当函数结束时,该变量的内存块被释放。
堆
这个内存区域不是由 CPU 自动管理的,它的大小比堆栈大。当调用 new 关键字时,编译器开始寻找适合请求大小的第一个空闲内存块。当它找到它时,使用内置的 C 函数 malloc() 将其标记为保留,并返回指向该位置的指针。也可以使用内置的 C 函数 free() 释放一块内存。这种机制会导致内存碎片,并且必须使用指针来访问正确的内存块,它比堆栈执行读/写操作要慢。
自定义和内置类型
虽然 C# 提供了一组表示整数、布尔值、文本字符等的标准内置类型,但您可以使用结构、类、接口和枚举等构造来创建自己的类型。
使用 struct 构造的自定义类型的示例是:
struct Point
{
public int X;
public int Y;
};
值和引用类型
我们可以将 C# 类型分为以下几类:
值类型
值类型派生自 System.ValueType 类,并且此类型的变量在堆栈中的内存分配中包含它们的值。值类型的两大类是struct和enum。
以下示例显示了布尔类型的成员。如您所见,没有显式引用 System.ValueType 类,这是因为该类是由结构继承的。
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);
...
}
}
参考类型
另一方面,引用类型不包含存储在变量中的实际数据,而是存储值的堆的内存地址。引用类型的类别有类、委托、数组和接口。
在运行时,当引用类型变量被声明时,它包含值 null,直到使用关键字 new 创建的对象被分配给它。
以下示例显示了泛型类型 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);
...
}
}
如果您想找出特定对象的内存地址,System.Runtime.InteropServices 类提供了一种从非托管内存访问托管对象的方法。在下面的示例中,我们将使用静态方法 GCHandle.Alloc() 为字符串分配句柄,然后使用 AddrOfPinnedObject 方法检索其地址。
string s1 = "Hello World";
GCHandle gch = GCHandle.Alloc(s1, GCHandleType.Pinned);
IntPtr pObj = gch.AddrOfPinnedObject();
Console.WriteLine($"Memory address:{pObj.ToString()}");
输出将是
Memory address:39723832
参考文献
官方文档:https://docs.microsoft.com/en-us/cpp/build/reference/stack-stack-allocations?view=vs-2019