12

operator new()与引用类型一起使用时,实例的空间在堆上分配,引用变量本身放在堆栈上。除此之外,在堆上分配的引用类型实例中的所有内容都被清零。
例如这里是一个类:

class Person
{
    public int id;
    public string name;
}

在以下代码中:

class PersonDemo
{
    static void Main()
    {
        Person p = new Person();
        Console.WriteLine("id: {0}  name: {1}", p.id, p.name);
    }
}

p变量在堆栈上,并且Person(其所有成员)的创建实例在堆上。p.id将是0 并且p.name将是null。之所以会出现这种情况,是因为在堆上分配的所有内容都归零。

现在我感到困惑的是,如果我使用带有new运算符的值类型。例如,考虑以下结构:

struct Date
{
    public int year;
    public int month;
    public int day;
}

class DateDemo
{
    static void Main()
    {
        Date someDate;
        someDate= new Date();

        Console.WriteLine("someDate is: {0}/{1}/{2}", 
            someDate.month, someDate.day, someDate.year);
    }
}

现在我想知道 main 中的以下几行是做什么的:

        Date someDate;
        someDate= new Date();

在第一行someDate变量分配在堆栈上。正好是 12 个字节。
我的问题是第二行会发生什么?做什么operator new()?它只是将Date结构的成员清零还是在堆上分配空间?一方面,我不希望new在堆上分配空间,当然因为在第一行内存已经在堆栈上分配给结构实例。另一方面,我希望new在堆上分配空间并返回该空间的地址,因为这是new应该做的。也许这是因为我来自 C++ 背景。

然而,如果答案是:“当new与值类型一起使用时,它只会将对象的成员清零”,那么new运算符的含义有点不一致,因为:

  1. 与值类型一起使用时new,它只会将堆栈上的对象成员清零
  2. 与引用类型一起使用时new,它会在堆上为实例分配内存并将其成员归零

在此先感谢,
干杯

4

5 回答 5

23

首先让我纠正你的错误。

当 operator new() 与引用类型一起使用时,实例的空间被分配在堆上,引用变量本身被放置在堆栈上。

作为“新”结果的引用是一个,而不是一个变量。该值是指存储位置。

引用当然是在 CPU 寄存器中返回的。该 CPU 寄存器的内容是否被复制到调用堆栈是由抖动的优化器决定的。它不需要永远存在于堆栈中;它可以永远存在于寄存器中,也可以直接从寄存器复制到托管堆,或者在不安全的代码中,它可以直接复制到非托管内存。

堆栈是一个实现细节。除非您查看 jit 代码,否则您不知道何时使用堆栈。

p 变量在堆栈上,而创建的 Person 实例(它的所有成员)在堆上。p.id 为 0,p.name 为空。

正确,但如果抖动如此决定,当然 p 可以再次实现为寄存器。如果有可用的寄存器,它不需要使用堆栈。

您似乎对正在使用堆栈的想法很感兴趣。抖动可能有大量的寄存器可供使用,而这些寄存器可能非常大。

我来自 C++ 背景。

啊,这就解释了为什么你如此沉迷于堆栈与堆的事情。学会停止担心它。我们设计了一个托管内存环境,让事物可以根据需要生存。管理器是否选择使用堆栈、堆或寄存器来有效地管理内存取决于它。

在第一行 someDate 变量被分配在堆栈上。正好是 12 个字节。

让我们假设这个 12 字节的结构是在堆栈上分配的。似乎有道理。

我的问题是第二行会发生什么?运算符 new() 是做什么的?它只是将 Date 结构的成员清零还是在堆上分配空间?

该问题以错误的二分法为前提,因此无法如所述回答。这个问题提出了两种非此即彼的选择,但都不一定是正确的。

一方面,我不希望 new 在堆上分配空间,当然因为在第一行内存已经在堆栈上分配给结构实例。

正确的结论,似是而非的推理。不执行堆分配,因为编译器知道此操作的任何部分都不需要长期存储。这就是堆的用途;当编译器确定给定变量的寿命可能比当前方法激活的时间长时,它会生成代码,该代码会在长期“堆”存储上为该变量分配存储空间。如果它确定变量的生命周期肯定很短,那么它使用堆栈(或寄存器)作为优化。

另一方面,我希望 new 在堆上分配空间并返回该空间的地址,因为那是 new 应该做的。

不正确。“新”不保证堆已分配。相反,“new”保证在清零内存上调用构造函数。

让我们回到你的问题:

它只是将 Date 结构的成员清零还是在堆上分配空间?

我们知道它不会在堆上分配空间。它是否将日期结构的成员清零?

这是一个复杂的问题。规范说当你说时会发生什么

someDate = new Date();    
  • someDate 的地址已确定
  • 为新对象分配空间(离开“堆栈”)。它被归零。
  • 然后调用构造函数(如果有),其中“this”是对新堆栈存储的引用
  • 然后将新堆栈存储的字节复制到 someDate 的地址。

现在,这真的会发生吗?您完全有权注意到,无法判断新堆栈空间是否已分配、初始化和复制,或者“旧”堆栈空间是否已初始化。

答案是,如果编译器推断用户不可能注意到现有堆栈空间正在发生变化,则现有堆栈空间会发生变化,额外分配和后续副本将被忽略

如果编译器无法推断出这一点,则会创建一个临时堆栈槽,将其初始化为零,由构造函数构造、变异,然后将结果值复制到变量中。这确保了如果构造函数抛出异常,您无法观察到变量中的不一致状态。

有关此问题及其编译器分析的更多详细信息,请参阅我关于该主题的文章。

https://ericlippert.com/2010/10/11/debunking-another-myth-about-value-types/

于 2011-04-07T17:03:18.810 回答
5

好的,这是一个简单的:

class Program
{
    static void Main(string[] args)
    {
        DateTime dateTime = new DateTime();
        dateTime = new DateTime();
        Console.Read();
    }
}

编译为此 IL 代码:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       24 (0x18)
  .maxstack  1
  .locals init ([0] valuetype [mscorlib]System.DateTime dateTime)
  IL_0000:  nop
  IL_0001:  ldloca.s   dateTime
  IL_0003:  initobj    [mscorlib]System.DateTime
  IL_0009:  ldloca.s   dateTime
  IL_000b:  initobj    [mscorlib]System.DateTime
  IL_0011:  call       int32 [mscorlib]System.Console::Read()
  IL_0016:  pop
  IL_0017:  ret
} // end of method Program::Main

正如您所看到的,CLR 将使用相同的局部变量来存储新的值类型,尽管它会再次运行构造函数——这很可能只是将内存归零。我们看不到是什么initobj,这是一个CLR 实现

现实是,正如 Eric Lippert在这里解释的那样,没有关于在堆栈上分配值类型的一般规则。这完全取决于 CLR 的实现。

于 2011-04-06T09:02:45.283 回答
2

结构的默认构造函数返回一个所有内存都清零的结构。即,new SomeStruct()与 相同default(SomeStruct)

然后,您的代码将该默认结构分配给您的变量。

这就是你肯定知道的一切。

编译器如何实现这一点完全是编译器的事情。

但是,如果您对幕后感兴趣,编译器很可能会直接清除该变量的堆栈位置:假设该变量存储在堆栈中。有很多事情可以防止这种情况发生——一个例子是匿名函数访问它,即:

Func<Person> PersonFactory()
{
  Person p = new Person();
  return () => p;
}

这里 p 需要存储在堆上,以便在函数返回等后能够存在,因此new Person()将清除该堆位置。

反正。与 C/C++ 不同,对于 C#,最好忘记“堆栈”、“堆”等。AFAIK,语言规范没有这些概念——它们都是特定于实现的。谁知道,在逃逸分析允许的情况下,未来的某些实现可能会将一些堆值放在堆栈上以节省 GC 的一些工作量。最好不要针对 C# 规范的给定实现做出特定的设计决策。

于 2011-04-06T09:23:05.667 回答
1

从开发人员的角度来看,您不知道它的分配位置。例如,具有 CLR 的奇异设备不知道堆栈 -> 一切都在堆上。即使您考虑桌面 CLR,在某些情况下 JITer 也可以将变量从堆栈移动到堆。

更多信息。

于 2011-04-06T09:05:46.387 回答
0

关于结构的归零。

无参数构造函数将成员归零。

如果不使用new(),则无法访问 struct 成员,除非您先自行初始化它们。否则你会得到“使用可能未分配的字段”。

于 2011-04-06T09:16:19.003 回答