1. 程式人生 > >深入理解“靜態”和static關鍵字

深入理解“靜態”和static關鍵字

「深入理解」系列,本文介紹“靜態”的概念、在記憶體中的分佈和應用

關於“靜態”的誤解:重新認識static關鍵字

靜態(static)這個詞,翻譯過來就是“靜態的、靜止的”,至於為什麼叫這個名字估計這得問C語言的爸爸,這裡就不深究了(雖然在看了我的這篇文章後,你可以從“記憶體”的角度看但是不知道當時static被提出是不是這個意思)
一旦一個變數或方法被修飾為static,則表明該變數或方法為靜態變數或靜態方法,其儲存方式符合“靜態儲存方式”

這裡關於“static”關鍵字的最大誤解就是“人們往往認為只要某某被修飾為static就表明這個東西和物件是無關的——實際上這句話只對了一半,因為你在一些面向過程的語言(如C)中甚至用不到“物件”的概念——”


要真正的理解“靜態”和“static”關鍵字,必須要結合具體的語言型別才行

(或者在看完這篇文章後也可以說“static關鍵字的意義就是‘靜態儲存’,只不過在具體的語言上表現略有不同不過總的來說思想是一致的”)

面向物件的“靜態”

>>>以Java為例

面向物件的靜態變數和方法

這個估計是人們看到“static”後下意識立馬會想到的,也好理解:對於面向物件的語言而言,“靜態”等於“和物件無關”(至少90%以上就是這麼個意思);如果一個變數被修飾為static,該變數稱為靜態變數,表明該變數和具體物件無關;如果一個方法被修飾為static,該方法稱為靜態方法,表明該方法和具體物件無關

具體來說,靜態變數被所有的物件所共享,在記憶體中只有一個副本,它當且僅當在類初次載入時會被初始化;除了和物件無關,還可以從“靜態儲存”這一點來理解“static”(這一點在C語言的區域性靜態變數中體現的淋漓盡致,但是Java沒有所謂的“靜態區域性變數”,在Java的方法中宣告的區域性變數其實都是“普通區域性變數”即不滿足“靜態儲存”)
對於靜態方法,也是“和物件無關的”,實際上“static”表明其是伴隨著“類”(現在知道為什麼要分別討論“面向物件”和“面向過程”的“static”了吧)同時產生的,這意味著靜態變數和靜態方法都是先於物件存在於記憶體中的,所以我們我們得出“靜態方法”的幾個重要特點:


1. 靜態方法不可以訪問非靜態變數;
2. 靜態方法不可以訪問非靜態物件;
3. 推薦使用類名呼叫靜態方法
4. 不存在this
關於this關鍵字,這裡作簡單說明:每當new一個類的新的例項時,隨之產生的還有這個類的例項變數(和該變數繫結),但是例項方法不會與某個方法唯一繫結(否則記憶體開銷太大),為例區分出“每個例項會產生不同的動作”,Java使用this關鍵字(由編譯器自動加上,表示傳遞的是物件引用)

讓我們再深入記憶體看看:
對於靜態變數,它是隨著類的載入而載入隨著類的消亡而消亡的,如果一個類中有靜態變數的話,程式首先會把該靜態變數載入進記憶體中,也就是在堆中開闢一個區域專門存放;以後不管new多少個類的物件,該靜態變數永遠都是在那裡的;也就是說,靜態變數在類的初始化一次後,系統就不會為該變數開闢新的記憶體空間;而每new一個類的物件,系統就會重新在 堆記憶體中開闢一個新空間來存放該類的例項物件,並且棧中也會有一個新的引用變數去指向它;至於靜態方法也是類似的情況;
針對上述現象,Java

面向物件的“靜態儲存”

以Java為例,所有“靜態的”東西都被儲存在“方法區”,如圖:
這裡寫圖片描述
那個“Method Area”就是所謂的“方法區”,是儲存“靜態變數”和“靜態方法”的記憶體區域,其中的“執行常量池”用來存放在執行時產生的臨時常量,與之對應的“堆”也存放一些new出來的“常量”

“靜態儲存”和“普通儲存(堆、棧)的區別”:
1. 靜態儲存在類載入時隨之載入,這時候物件還沒有產生;
2. 靜態儲存值分配一塊儲存空間,且之後不管物件怎麼變靜態的東西都是不變的

面向過程的“靜態”

>>>以C語言為例

面向過程的靜態變數和方法

因為不存在“”類或“物件”的概念,面向過程語言如C語言的靜態變數主要用來劃分不同的“作用域”

典型的面向過程程式語言是沒有“類”的概念的,其組織形式就是一個個單獨的檔案(而面向物件可以以類為程式碼組織的基本單位),同樣的也可以在C語言中使用static關鍵字,表示“靜態儲存”
這裡不再說“和物件有關畢竟人家根本就沒有物件,但是作為物理載體的記憶體中的資料儲存方式總是不會變的,在某種意義上可以把“面向物件中static表示‘和物件無關’看成是‘滿足靜態儲存方式’的一種特殊表現形式罷了”
畢竟“靜態儲存”是物理是在,不管語言載體怎樣變化,物理本質是不會變的

在實踐開發中,static關鍵字在C語言的作用就是劃分檔案作用域:如果一個變數或函式被修飾為static,表明該變數或函式為該檔案所有,這意味著它只在定義它的原始檔內有效,其他原始檔無法訪問它,即使想要在標頭檔案中引用標頭檔案也會報錯,這樣的或函式叫“內部函式”,使用“內部函式”雖然限制了使用範圍但是也解決了潛在的“名稱空間的衝突”問題,即“允許統一資料夾下有兩個同名的靜態函式或者說同名內部函式”

通常我們使用include指令引入檔案中的函式,如果可以引入的話其預設的修飾符是extern(即時不寫編譯器也會幫你加上去)表示“在別的檔案中”,如:

//mylib.h
#ifndef _MYLIB_H_
#define _MYLIB_H_

static char *str;
int func(); //<==> extern func();

#endif
//mylib.c
#include "mylib.h"
#include<stdio.h>

int func() //<==> extern func()..
{
    printf("Hello world");
    return 0;
}

*str = "hello";
//main.c

int main(void)
{
    extern func();
}

簡而言之,extern的作用就是“取代include指令”,並且可以更精確的定位到某個具體的函式;extern和static是水火不相容的

值得一提的是,C語言允許“靜態區域性變數”,靜態區域性變數和作用域和區域性變數一樣,不過生存週期不一樣,區域性變數在定義區域性變數的函式呼叫完之後就從記憶體中釋放其值,而靜態區域性變數不釋放,等整個程式全部執行結束後才會從記憶體中釋放

面向過程的“靜態儲存”

上圖:
這裡寫圖片描述

這是開一個程序時C語言程式的記憶體區域劃分;其中.text段儲存程序所執行的程式二進位制檔案,.data段儲存程序所有的已初始化的全域性變數,.bss段儲存程序未初始化的全域性變數;在程序的整個生命週期中,.data段和.bss段內的資料時跟整個程序同生共死的,也就是在程序結束之後這些資料才會壽終就寢

你只要知道,面向過程的static關鍵字就是用來“限制訪問許可權”的

總結:還得從記憶體角度理解“靜態”

綜上所述,不論是面向物件還是面向過程,只要是“靜態”的,它就滿足“靜態儲存方式”,這才是“靜態”的本質!語言只是載體,“靜態儲存”的思想才是實質,其它的不過浮雲!