slx0009 Asked:2023-12-12 23:42:14 +0800 CST2023-12-12 23:42:14 +0800 CST 2023-12-12 23:42:14 +0800 CST C# 中的变量是如何初始化的以及它们在创建之前“位于”何处? 772 有几个变量: var a = 1; var b = "string"; var c = false; var d = new MyClass(); 你能解释一下这些变量在初始化之前的值在哪里吗? 我是否正确理解,在编译阶段将值放置在堆中,而在运行时将值从堆中取出? c# 1 个回答 Voted Best Answer Швеев Алексей 2023-12-13T01:23:38+08:002023-12-13T01:23:38+08:00 全局来说,内存可以分为两个区域:栈和堆。 堆栈大小始终是已知的,并且在特定代码块内的编译阶段是固定的。它存储局部范围变量 堆存储大小(在编译阶段)未知的数据。 为了便于理解,我会稍微简化一切,但基本概念是可以理解的 让我们通过一个例子来看看内存是如何分配的。假设我们有一段代码: { int x; string str = "Hello world!"; List<int> intList = new() {1, 2, 3}; MyClass anyClass = new(); MyStruct anyStruct = new(); } 让我们将堆栈想象为一个字节数组:[...]。当我们打开一个新的代码块时,我们粗略地声明N现在将在堆栈上使用更多字节。为此,我们必须提前(在编译阶段)知道给定代码块的大小。 然而,并非所有类型都是固定大小的。在编译阶段大小未知的类型存储在堆上。因此,在 C# 中,分为引用类型和值类型 我们在编译时知道值类型的大小。例如int,4 个字节。 引用类型转换为对象本身内存中的地址值,即转换为nint(大小取决于系统位容量的类型)。对象本身位于堆中,访问它时,我们将其从堆中取出。 PS:为了简单起见,我们假设字符串类型是引用类型,因为它有自己的微妙之处。 根据这些信息,我们将上面的代码重写为相同的,但更接近编译器: { int x; nint strPointer; nint listPoiner; nint classPointer; MyStruct anyStruct; // Структуры являются типом значений } 现在我们可以通过将所有本地字段的大小相加来计算给定代码块的大小(我们可以使用 this 的内置函数sizeof(T),但对于引用类型,它返回对象本身的大小,而不是其引用) : int32 (4 байта) + int64(8 байтов) + int64(8 байтов) + int64(8 байтов) + MyStruct (сумма размеров всех полей, допустим 4 * 3 байта) = 40 байтов 现在,在堆栈上,我们按照相同的顺序为我们的类型分配这 40 个字节: [..., int32_0, int32_1, int32_2, int32_3, int64_0, int64_1, ...] 要访问变量 x,我们需要获取sizeof(int)相对于当前代码块移位 0 的 (4) 个字节。 要访问对字符串的引用,我们需要获取sizeof(int64)(8) 个字节,并移动所有先前大小的总和(sizeof(int)字节,因为它前面只有 int) 要访问anyStruct变量数据,我们需要接收sizeof(MyStruct)(12) 个字节并移位sizeof(int) + sizeof(int64) + sizeof(int64) + sizeof(int64)(28) 个字节 也就是说,粗略地说,对于相对于堆栈内的开头的给定代码块,堆栈将始终如下所示: [..., int, int64, int64, int64, MyStruct, ...] 回答主要问题: 在初始化之前,它们位于堆栈上,并且 C# 默认情况下为它们分配默认值。 如果我们谈论对象,那么在堆中初始化之前它们可能根本不存在(null意味着空引用,即什么都没有)
全局来说,内存可以分为两个区域:栈和堆。
堆栈大小始终是已知的,并且在特定代码块内的编译阶段是固定的。它存储局部范围变量
堆存储大小(在编译阶段)未知的数据。
为了便于理解,我会稍微简化一切,但基本概念是可以理解的
让我们通过一个例子来看看内存是如何分配的。假设我们有一段代码:
让我们将堆栈想象为一个字节数组:
[...]
。当我们打开一个新的代码块时,我们粗略地声明N
现在将在堆栈上使用更多字节。为此,我们必须提前(在编译阶段)知道给定代码块的大小。然而,并非所有类型都是固定大小的。在编译阶段大小未知的类型存储在堆上。因此,在 C# 中,分为引用类型和值类型
我们在编译时知道值类型的大小。例如
int
,4 个字节。引用类型转换为对象本身内存中的地址值,即转换为
nint
(大小取决于系统位容量的类型)。对象本身位于堆中,访问它时,我们将其从堆中取出。PS:为了简单起见,我们假设字符串类型是引用类型,因为它有自己的微妙之处。
根据这些信息,我们将上面的代码重写为相同的,但更接近编译器:
现在我们可以通过将所有本地字段的大小相加来计算给定代码块的大小(我们可以使用 this 的内置函数
sizeof(T)
,但对于引用类型,它返回对象本身的大小,而不是其引用) :现在,在堆栈上,我们按照相同的顺序为我们的类型分配这 40 个字节:
[..., int32_0, int32_1, int32_2, int32_3, int64_0, int64_1, ...]
要访问变量 x,我们需要获取
sizeof(int)
相对于当前代码块移位 0 的 (4) 个字节。要访问对字符串的引用,我们需要获取
sizeof(int64)
(8) 个字节,并移动所有先前大小的总和(sizeof(int)
字节,因为它前面只有 int)要访问
anyStruct
变量数据,我们需要接收sizeof(MyStruct)
(12) 个字节并移位sizeof(int) + sizeof(int64) + sizeof(int64) + sizeof(int64)
(28) 个字节也就是说,粗略地说,对于相对于堆栈内的开头的给定代码块,堆栈将始终如下所示:
[..., int, int64, int64, int64, MyStruct, ...]
回答主要问题:
在初始化之前,它们位于堆栈上,并且 C# 默认情况下为它们分配默认值。
如果我们谈论对象,那么在堆中初始化之前它们可能根本不存在(
null
意味着空引用,即什么都没有)