c# - 在結構上使用“新”是否將它分配到堆或棧上?




.net memory-management (8)

當你用new操作符創建一個類的實例時,內存將被分配到堆上。 當你使用new運算符創建一個結構實例時,內存分配的位置,堆或堆棧上?


Answers

簡而言之,新結構對於結構體來說是錯誤的,稱新結構體只是簡單地稱之為構造函數。 結構的唯一存儲位置是它所定義的位置。

如果它是一個成員變量,它將直接存儲在其中定義的任何變量中,如果它是一個局部變量或參數,則它將存儲在堆棧中。

將它與類進行對比,該類在結構完全存儲的任何地方都有引用,而參考點位於堆的某處。 (成員內,本地/參數堆棧)

它可能有助於查看C ++中的哪些類/結構之間沒有真正的區別。 (在語言中有類似的名字,但它們只是指事物的默認可訪問性)當你調用new時,你得到一個指向堆位置的指針,而如果你有一個非指針引用,它直接存儲在堆棧中,或者在另一個對像中,C#中的ala結構。


結構被分配給堆棧。 這是一個有用的解釋:

Structs

此外,在.NET中實例化的類會在堆或.NET的保留內存空間上分配內存。 鑑於由於堆棧上的分配而實例化時,結構會產生更高的效率。 此外,應該注意的是,在結構中傳遞參數是通過值來完成的。


包含struct字段的內存可以根據具體情況分配在堆棧或堆上。 如果struct-type變量是某個匿名委託或迭代器類未捕獲的局部變量或參數,則它將被分配到堆棧上。 如果變量是某個類的一部分,那麼它將在堆中的類中分配。

如果結構被分配在堆上,那麼調用new運算符實際上並不是分配內存所必需的。 唯一的目的是根據構造函數中的內容設置字段值。 如果構造函數沒有被調用,那麼所有的字段將得到它們的默認值(0或null)。

同樣,對於在堆棧中分配的結構,除了C#要求所有局部變量在使用前都設置為某個值之外,因此您必須調用自定義構造函數或默認構造函數(不帶參數的構造函數始終可用於結構)。


幾乎所有被視為值類型的結構都分配在堆棧上,而對象則分配在堆上,而對象引用(指針)則分配到堆棧上。


classstruct聲明就像是用於在運行時創建實例或對象的藍圖。 如果您定義了一個名為Person的classstruct ,Person就是該類型的名稱。 如果聲明並初始化Person類型的變量p,則稱p為Person的對像或實例。 可以創建同一Person類型的多個實例,並且每個實例在其propertiesfields可以具有不同的值。

一個class是一個引用類型。 當class一個對像被創建時,該對像被分配到的變量只保存對該內存的引用。 將對象引用分配給新變量時,新變量引用原始對象。 通過一個變量所做的更改反映在另一個變量中,因為它們都指向相同的數據。

struct是一個值類型。 當一個struct被創建時, struct被分配的變量保存結構的實際數據。 當struct被分配給一個新的變量時,它被複製。 因此新變量和原始變量包含相同數據的兩個獨立副本。 對一個副本所做的更改不會影響其他副本。

通常, classes用於建模更複雜的行為,或用於在創建class對像後修改的數據。 Structs最適合小數據結構,主要包含在創建struct後不打算修改的數據。

更多...


與所有值類型一樣,結構總是在聲明的位置

有關何時使用結構的更多詳細信息,請參閱here的問題。 這裡的這個問題有關結構的更多信息。

編輯:我錯誤地回答說,他們總是在堆棧中。 這是incorrect


好吧,讓我們看看我能否做得更清楚。

首先,Ash是正確的:問題在於分配值類型變量的位置。 這是一個不同的問題 - 答案不僅僅是“在堆疊上”。 它比這更複雜(並且由C#2變得更加複雜)。 我有一篇關於這個話題文章,如果需要的話,將會擴展它,但是我們只處理new運營商。

其次,這一切都取決於你在談論什麼水平。 我正在研究編譯器對源代碼所做的事情,就它創建的IL而言。 JIT編譯器在優化相當多的“邏輯”分配方面會做很多巧妙的事情。

第三,我忽略了泛型,主要是因為我實際上並不知道答案,部分原因是它會讓事情變得複雜。

最後,所有這些都與當前的實施一致。 C#規範沒有詳細說明 - 它實際上是一個實現細節。 有些人認為託管代碼開發人員真的不應該在意。 我不確定我會走多遠,但值得一想的是,一個實際上所有局部變量都在堆上的世界 - 這仍然符合規範。

new運算符在值類型上有兩種不同的情況:可以調用無參數構造函數(例如new Guid() )或有參數的構造函數(例如new Guid(someString) )。 這些產生顯著不同的IL。 要理解為什麼,您需要比較C#和CLI規範:根據C#,所有值類型都有一個無參數構造函數。 根據CLI規範, 沒有值類型具有無參數的構造函數。 (一段時間用反射獲取值類型的構造函數 - 你不會找到無參數的。)

對於C#來說,將“用零初始化一個值”作為構造函數是有道理的,因為它保持了語言的一致性 - 您可以將new(...)視為始終調用構造函數。 CLI有不同的想法是有道理的,因為沒有真正的代碼可以調用 - 當然也沒有類型特定的代碼。

在初始化它之後,您將如何處理該值也會產生影響。 IL用於

Guid localVariable = new Guid(someString);

與用於以下情況的IL不同:

myInstanceOrStaticVariable = new Guid(someString);

另外,如果該值用作中間值,例如方法調用的參數,則事情會再次略有不同。 為了顯示所有這些差異,這裡有一個簡短的測試程序。 它沒有顯示靜態變量和實例變量之間的差異:IL會在stfldstsfld之間不同,但僅此stsfld

using System;

public class Test
{
    static Guid field;

    static void Main() {}
    static void MethodTakingGuid(Guid guid) {}


    static void ParameterisedCtorAssignToField()
    {
        field = new Guid("");
    }

    static void ParameterisedCtorAssignToLocal()
    {
        Guid local = new Guid("");
        // Force the value to be used
        local.ToString();
    }

    static void ParameterisedCtorCallMethod()
    {
        MethodTakingGuid(new Guid(""));
    }

    static void ParameterlessCtorAssignToField()
    {
        field = new Guid();
    }

    static void ParameterlessCtorAssignToLocal()
    {
        Guid local = new Guid();
        // Force the value to be used
        local.ToString();
    }

    static void ParameterlessCtorCallMethod()
    {
        MethodTakingGuid(new Guid());
    }
}

以下是該類的IL,不包括不相關的位(如nops):

.class public auto ansi beforefieldinit Test extends [mscorlib]System.Object    
{
    // Removed Test's constructor, Main, and MethodTakingGuid.

    .method private hidebysig static void ParameterisedCtorAssignToField() cil managed
    {
        .maxstack 8
        L_0001: ldstr ""
        L_0006: newobj instance void [mscorlib]System.Guid::.ctor(string)
        L_000b: stsfld valuetype [mscorlib]System.Guid Test::field
        L_0010: ret     
    }

    .method private hidebysig static void ParameterisedCtorAssignToLocal() cil managed
    {
        .maxstack 2
        .locals init ([0] valuetype [mscorlib]System.Guid guid)    
        L_0001: ldloca.s guid    
        L_0003: ldstr ""    
        L_0008: call instance void [mscorlib]System.Guid::.ctor(string)    
        // Removed ToString() call
        L_001c: ret
    }

    .method private hidebysig static void ParameterisedCtorCallMethod() cil  managed    
    {   
        .maxstack 8
        L_0001: ldstr ""
        L_0006: newobj instance void [mscorlib]System.Guid::.ctor(string)
        L_000b: call void Test::MethodTakingGuid(valuetype [mscorlib]System.Guid)
        L_0011: ret     
    }

    .method private hidebysig static void ParameterlessCtorAssignToField() cil managed
    {
        .maxstack 8
        L_0001: ldsflda valuetype [mscorlib]System.Guid Test::field
        L_0006: initobj [mscorlib]System.Guid
        L_000c: ret 
    }

    .method private hidebysig static void ParameterlessCtorAssignToLocal() cil managed
    {
        .maxstack 1
        .locals init ([0] valuetype [mscorlib]System.Guid guid)
        L_0001: ldloca.s guid
        L_0003: initobj [mscorlib]System.Guid
        // Removed ToString() call
        L_0017: ret 
    }

    .method private hidebysig static void ParameterlessCtorCallMethod() cil managed
    {
        .maxstack 1
        .locals init ([0] valuetype [mscorlib]System.Guid guid)    
        L_0001: ldloca.s guid
        L_0003: initobj [mscorlib]System.Guid
        L_0009: ldloc.0 
        L_000a: call void Test::MethodTakingGuid(valuetype [mscorlib]System.Guid)
        L_0010: ret 
    }

    .field private static valuetype [mscorlib]System.Guid field
}

正如你所看到的,有很多不同的指令用於調用構造函數:

  • newobj :分配堆棧中的值,調用參數化的構造函數。 用於中間值,例如用於賦值給字段或用作方法參數。
  • call instance :使用已經分配的存儲位置(無論是否在堆棧上)。 這在上面的代碼中用於分配給局部變量。 如果同一個局部變量多次使用幾次new調用賦值,它只是初始化舊值頂部的數據 - 它不會每次分配更多的堆棧空間。
  • initobj :使用已分配的存儲位置並擦除數據。 這用於我們所有的無參數構造函數調用,包括那些分配給局部變量的構造函數。 對於方法調用,有效地引入了一個中間局部變量,其值由initobj

我希望這可以說明這個話題是多麼複雜,同時也讓我們看到了一些亮點。 在某些概念意義上,每一次new分配都會在堆棧中分配空間 - 但正如我們所看到的,即使在IL級別也不是真正發生的事情。 我想強調一個特例。 採取這種方法:

void HowManyStackAllocations()
{
    Guid guid = new Guid();
    // [...] Use guid
    guid = new Guid(someBytes);
    // [...] Use guid
    guid = new Guid(someString);
    // [...] Use guid
}

這個“邏輯上”有4個堆棧分配 - 一個用於變量,另一個用於三個new調用 - 但實際上(對於該特定代碼)堆棧只分配一次,然後重用相同的存儲位置。

編輯:只是要清楚,這只是在某些情況下是真的...特別是,如果Guid構造函數拋出異常, guid的值將不可見,這就是為什麼C#編譯器能夠重用相同的堆棧插槽。 有關更多詳細信息以及適用的情況,請參閱Eric Lippert 關於價值類型構建博文

我在寫這個答案的過程中學到了很多,如果有任何不清楚的地方,請澄清一下!


當你使用new時,對像被分配給堆。 它通常用於預計擴展。 當你聲明一個對像如,

Class var;

它被放置在堆棧上。

你將永遠不得不打電話銷毀你放在堆上的新物件。 這打開了內存洩漏的可能性。 放置在堆棧上的對像不容易洩漏內存!





c# .net memory-management