Stack、Heap、Static

  • 3064
  • 0
  • C#
  • 2020-06-19

上週提到一些C#的架構與規則,這篇再補充一些概念。直接用static在google搜尋,會找到許多中文的網頁,但發現蠻多都只針對static的特性做總結,但筆者希望能找到更多的資訊,就開始嘗試找英文的說明,除了練習英文之外,也順便訓練一下自己找答案的能力!查詢Static的過程中,額外發現跟Stack及Heap也有些關係,也一併提出來聊聊。

 

 

像隱形人般的記憶體


執行程式的過程中,記憶體的動作是甚麼?
寫程式的前面幾行一定會設定變數,變數可以儲存Numeric Type、Bool Type等基礎型別,也可以儲存較複雜的結構,例如Method、Object等。記憶體在過程中,將需要用到的資料暫時儲存起來,等到不需要時再釋放空間。一般來說,執行程式會依程式碼,由上至下依序執行,但因為程式中會使用物件導向設計或呼叫方法,程式本身亦有著不同資料型別,所以記憶體的儲存機制,並不是單純的一路紀錄再統一釋放。對於記憶體儲存的機制有更深入的了解,也能幫助我們更準確的解釋程式碼的行為。

 

Stack與Heap的概念
在工廠中會依工作性質把區域分開,為了應對上述不同的工作需求,記憶體的管理上也會特別分出幾塊區域,分別用來儲存不同性質的資料。其中較重要的有StackHeap的概念,Stack比較穩重,會特別依程式執行步驟儲存,且強調先進後出,就像一堆折疊好的衣服,要拿某一件衣服必須從上面開始,代表著當下線程的狀態。而Heap比較活潑,所以不會特別做排序,只將資料統一集中在大本營,就像一堆零散沒折好的衣服,但可以直接找到想找的衣服。

 

「實值型別、參考型別」 與 「Stack、Heap」的關係
在C#中,實值型別存放在Stack,參考型別的話,Pointer會存在Stack,裡面可以是null或資料存放的記憶體位址,整個物件則存放在Heap中。關於實值型別與參考型別,已有前人將資料整理成圖片。

參考型別中,String是比較特別的存在,當我們要使用Object一定會使用new,目的就是為了在Heap中指定一塊新的空間存放資料,並且把位址存在Stack。但String並沒有做這件事啊?? 為什麼是參考型別?? 答案就在string pool裡,透過pool也可有效的減少不停的生成物件,減少記憶體的分配與釋放,跟有興趣的就自己google吧。

Stack會在線程結束後,自動回收記憶體,例如在Main()中呼叫addFive(),執行完addFive()裡最後一行程式碼後,就會開始自動回收跟addFive()相關的Stack記憶體空間,等到整個程式跑完,才會開始去回收跟Main()有關的記憶體空間,這裡又再次強調了後進先出的概念。

當存在Stack的指針,隨著線程結束後被刪除,與Heap中物件的連接就沒了,但Heap中的資料並不會馬上就被刪除,因為Heap存在長短期的資料,故很難預測順序,故程式並不會依照線程而進行回收。必須達到一定的條件後,系統才會啟動Garbage Collection(GC)的機制,判斷有哪些空間可以收回,或是透過程式人員手動控制GC。

圖片來源及參考資料

  1. https://www.c-sharpcorner.com/article/C-Sharp-heaping-vs-stacking-in-net-part-i/
  2. https://www.codeproject.com/Articles/76153/Six-important-NET-concepts-Stack-heap-value-types#Stack%20and%20Heap

 


像調味料的關鍵字 - static


static關鍵字,中文會被翻譯成靜態,可用於修飾class、method、variable等,中文就是靜態類型、靜態方法、靜態變數等。因為區域變數的特性,即使傳遞變數給方法,內外變數並不能互通,要取出結果則需要return,若直接用static修飾變數,會成為該class的全域變數的概念

namespace ConsoleApp1
{
    class Program
    {
        static int x = 0;
        class Test
        {
            public void display()
            {
                x += 1;
            }
        }

        static void Main(string[] args)
        {
            Test test = new Test();
            test.display();
            test.display();
            Console.WriteLine(x);
            Console.ReadLine();
        }
    }
}

//結果為2

到這裡可以發現,靜態變數可以不斷被不同方法取用或修改值。靜態變數名稱只能只能出現一次,且直到程式結束才會將記憶體空間釋放出來,這已經不同於區域變數的記憶體管理了,查了一些英文資料,看到的說法都是,任何的實值型別,加上static後都改為放在heap

static也可以用來修飾方法跟類別,非靜態類別可以同時擁有靜態方法及非靜態的方法經過初始化後,實際上能呼叫的僅有非靜態方法,可能是靜態方法被改存到不同位址,或是內定會移除靜態方法,這個看有沒有高手能回答了。

靜態類別就嚴格了,底下只能有靜態成員。靜態類別不能使用new,因為在一開始就已經指定好記憶體位置了。但靜態類別內若放非靜態成員,非靜態成員須透過new指定記憶體空間後才能使用,如此一來就會像下圖一樣報錯了,而且屬性也只會顯示出靜態的方法。

static就像是調味料,少了不好吃,但太多絕對會是麻煩。透過static可免去初始化的動作,若很多檔案有共通,且經常使用的方法或變數,會非常方便。但這是把雙面刃,因為靜態類別僅能包含靜態成員,會被限制一定要用static的連環問題。而靜態成員會存活到整隻程式結束,一直滾雪球的情況下,專案一大,可能會引發記憶體不足的問題。太多檔案共用,也會造成耦合度過高但又難以分離。最後靜態類別不能被繼承會失去物件導向的特性與優勢。