第一章 關鍵字的深度理解 number-1
c語言有32個關鍵字,每個關鍵字你都理解嗎?
今天出場的是:
auto , register, static, extern
為什麼他們會一起呢,說到這裡不得不談到c語言對變數的描述。
c給每個變數3個基本屬性:
1. 作用域
2. 生命期
3. 儲存型別
一些基本概念定義:
塊的定義:
在c語言裡 : 是以{}定義的 很顯然函式是其中一個。
宣告和定義:
1 宣告是說有這個東西,但並不是馬上要產生出來,比如你跟別人說你會玩dota,但你現在並沒有玩。理解這個就好理解定義了。
注意: 宣告可以在多個地方, 而這些地方就是所謂的名稱空間(c語言沒有這個概念,只是比喻),如果要引用全域性變數,而又不在全域性變數的作用範圍內,可以用extern關鍵字,表示我要引用這個變數,他已經在某個檔案裡或其他全域性區域定義了,還有就是沒有定義,我也可以用,但link時候如果沒有定義就會報錯,無法解析(這個問題要牽扯到編譯器的連結原理,在以後會有分析)。
2 定義就是產生它,一般我們都是宣告和定義一起的,全域性變數都是定義在檔案的最上面,所以沒有將宣告和定義分開,分開的情況是在,多個檔案編譯中,如果其中一個(如果是標頭檔案)檔案定義了一個全域性變數a,那麼你引用這個標頭檔案時(所謂的標頭檔案包含,就是把標頭檔案的東西copy到你的.c檔案上面,只是方便瀏覽原始碼才有標頭檔案概念,一次定義,可以多處使用),很顯然,你的.c檔案再定義一個就是重複定義了。還有一種情況是: 在一個.c檔案裡定義了一個全域性變數int a,如果你又在其他.c檔案裡定義一個全域性變數int a,link 時( 連結)就會出錯,如果是同名但不通型別,連結正常(編譯器可以區分他們,這個問題要牽扯到編譯器的連結原理,在以後會有分析)。
注意: 定義只能定義一次,不能多次,如果要問為什麼,那是因為定義就是給變數分配記憶體空間(按變數的型別分配大小),所以只能定義一次。
作用域: 一個變數,就像當官一樣,也有自己的領地,比如全域性變數(就是從定義地方到檔案結束),但是在某個塊的內部,如果也定義了一個同名的變數(只可以定義一個),那這個地方就歸他管了,你沒有權利,他就是這個地方的皇帝,然而如果在其他塊中沒有與你同名的變數,那這些地方就由你管。看下面的圖知道, 如果不是在函式內定義的變數,都是全域性變數,其實全域性變數區,就是各個函式之間的區域。全域性是向下擴充套件的,並不是整個檔案,就是說,在某個定義全域性變數的上面,已經沒有他的存在。如果上面的要訪問他,可以用extern關鍵字(宣告),這些對於區域性變數行不通,對於區域性變數在c語言裡都必須定義在最前面。
要理解生命期先知道下面的。
看一下原始碼檔案的結構:
圖 1
上面這個圖配合下面概念講的。
全域性變數和區域性變數, 你真的理解他們嗎?其實談到這個,又得講一下c語言記憶體分配:
系統核心為高2GB,使用者為低2GB(在32位的記憶體中)
圖 2
BSS: 是“Block Started by Symbol”的縮寫,意為“以符號開始的塊”,在程式開始之前,核心將此段初始化為0。既這片記憶體在系統啟動之後本身初值為0。
在c語言裡 記憶體可分為靜態儲存區和動態儲存區。
靜態儲存區: 就是程式在編譯時就已經分配了,比如,全域性變數(全域性靜態變數),靜態區域性變數等,且初始化是在程式啟動開始時就已經初始化好了,對於這點,舉個例:
void Fun()
{
static int a = 10; /*其實這個語句在函式執行時並沒有執行,它是提供給系統(編譯 器)一個初值10,然後再程式啟動之前,把此變數初始化。*/
}
動態儲存區: 程式在執行時,根據需要才分配的,比如,使用者用malloc等動態申請的記憶體(系統堆,但使用者控制),系統控制的函式呼叫棧。
所以,
全域性變數: 就是定義在函式外的,且記憶體在靜態儲存區,作用範圍:定義他的位置到檔案結束。
區域性變數:定義在塊內(函式是塊的最高境界),分為普通的區域性變數(就是在棧中),和靜態區域性變數(在靜態儲存區),作用範圍:定義它的位置到塊結束。
注意: 很多人會不知道,什麼時候用區域性變數,什麼時候用全域性變數,少用全域性變數,多用區域性變數。全域性變數是放在靜態儲存區中,定義了全域性變數,系統就少了一個可以利用資料儲存空間,太多全域性變數,會導致編譯器無足夠記憶體分配;而區域性變數,可以在不同的模組中重用(合理分配),其次從模組的耦合性來說,全域性變數使模組的耦合性增加,模組獨立性不高,還有在多執行緒程式裡面,一般都建議不要用全域性變數,這樣可能造成執行緒訪問衝突,加鎖等很多問題,但這個東西是相對的,有時用全域性變數已有好處,比如全域性變數的空間範圍一般比函式棧空間要大(和編譯器有關),有時可能棧要溢位。如果函式要反覆經常呼叫,函式裡面有很多的區域性變數,這些變數就可能要求棧反覆地申請和釋放,這樣也耗時,所以這個問題要自己去體會,根據實際情況分析。
生命期: 就是一個變數記憶體分配到記憶體釋放(我們稱為死亡)的時間段。全域性變數和區域性靜態變數一般是和程式共存亡,普通的區域性變數一般是和塊共存亡。
注意:雖然區域性靜態變數離開他的塊時是不能訪問的,但他並沒有死亡,我們可以通過其他手段來訪問它(嘿嘿)。
儲存型別: 這個後面會有詳細的講解,現在只是說一下,其實在32系統中(其他可以類推),系統記憶體就是一個數組,每個地址對應一個位元組。然後,為了讓變數掌握的位元組個數不同(提供更多給介面方便我們使用),系統為了區分它們,就給他們取了不同的名字(char,short,int,long等),然後在用的時候,就從它們的起始地址取出所佔的位元組數,然後再按一定語意來解析它們(之前協商好的協議(自己的理解))。
好,現在來具體講哈
auto , register, static, extern
4個關鍵字
auto: 從名字上,我們稱為自動變數,一般都叫區域性變數,變數的預設屬性就是auto,所以一般都沒有用。
register: 就是定義暫存器變數,此變數有個例外,就是如果他真的稱為暫存器變數,那麼他存在於暫存器中,就不是在記憶體中,所以沒有地址。此關鍵字會讓系統儘量合適地把此變數作為暫存器變數(為什麼不是一定、而是儘量,原因有很多,比如暫存器有限等),目的是提高訪問速度。當一個變數被頻繁讀/寫時,需要反覆訪問記憶體,花費大量存取時間。為了提高訪問效率,可以使用CPU暫存器變數,不需要訪問記憶體,直接進行讀/寫,這個關鍵字在嵌入式程式設計中經常會用到。
注意:
1) 只有區域性自動變數和形參才可以定義為暫存器變數。因為暫存器變數屬於動態儲存方式,因此凡需要採用靜態儲存方式變數都不能定義為暫存器變數。
但是使用register修飾符有幾點限制
2) register變數必須是能被CPU所接受的型別。
這通常意味著register變數必須是一個單個的值,並且長度應該小於或者等於整型的長度。不過,有些機器的暫存器也能存放浮點數。
3)因為register變數可能不存放在記憶體中,所以不能用“&”來獲取register變數的地址。
在呼叫一個函式時佔用一些暫存器以存放暫存器變數的值,函式呼叫結束後釋放暫存器。此後,在呼叫另外一個函式時又可以利用這些暫存器來存放該函式的暫存器變數。
4)由於暫存器的數量有限(不同的型別cpu(Intel系列,ARM系列,PowerPC系列等)暫存器數目不一),不能定義任意多個暫存器變數,而且某些暫存器只能接受特定型別的資料(如指標和浮點數),因此真正起作用的register修飾符的數目和型別都依賴於執行程式的機器,而任何多餘的register修飾符都將被編譯程式所忽略。
5) 早期的C編譯程式不會把變數儲存在暫存器中,除非你命令它這樣做,這時register修飾符是C語言的一種很有價值的補充(由於歷史原因)。然而,隨著編譯程式設計技術的進步,在決定哪些變數應該被存到暫存器中時,現在的C編譯環境能比程式設計師做出更好的決定。實際上,許多編譯程式都會忽略register修飾符,因為儘管它完全合法,但它僅僅是暗示而不是命令。
static: 此關鍵字很重要
對於變數:
1 static的變數儲存在靜態區,所以變數的值,有永續性,不會隨著塊的離開而消失;
2 變數被隱藏,如果是全域性變數(此針對於多個檔案)可見性就是定義他那個檔案,其他檔案不可訪問,不可以用extern來引用,反過來就是說,如果沒有static關鍵字,其他檔案就可以通過extern來引用;
3 變數沒有定義初值,系統(編譯器)會預設設定初值為0;
對於函式: 沒有static修飾的函式,在多個檔案編譯時,具有全域性可見性(這個讀者自己可以試試)而有static修飾的函式,只是在本檔案可見,對其他檔案隱藏。
extern : 上面很多地方提到, 對於全域性變數如果不在作用域內,extern 可以擴大他的作用域。例如,在單個檔案中,在全域性變數之上地方可以用extern來引用下面定義的全域性變數,而在多個檔案中,extern可以引用其他檔案的全域性變數。
注意: extern能否引用其他檔案的全域性變數,要看其他檔案的全域性變數是否隱藏,很顯然區域性變數是不能引用的。
宣告:
本文完全是為了學習而誕生的, 我們只有不斷理解,不斷地從錯誤的認識中清醒過來,才可能更好使用它,希望對你們有用,要成為頂級的c程式設計師這些只是,記住只是基礎中的基礎,還有很多的東西有待我們去研究實踐,這路還很長。
思考小問題:
c語言裡的一個遞迴問題,怎麼從第20層直接返回到第17層,保證程式正常執行?