C 中的陣列和區域性靜態物件

2021-04-02 18:08:32 字數 4308 閱讀 3010

像以前我說過的,我已經不下五次「以為」我理解陣列了,然而今天又一次發現自己無知。

初學c的時候我把陣列當成指標看。明白了一些實現機制後,我開始把陣列當成乙個特殊的變數; 我開始察看彙編剖析原理,理解逐步深入。 可是,明白得越多,就會發現越多的特例。 最終 —— 我陷入了看似任意且繁雜的規則的迷宮。

還是的回到定義上來。c/c++的陣列保證兩件事:

1 陣列是由同型別成員組成的構造物件,占有一塊連續的記憶體。

2 它的名稱表示它的首位址。

僅此而已,除此之外 —— 任何內容都不屬於陣列的定義。

所以,我們知道,當我們需要陣列的時候,我們定義就是了。需要靜態陣列? c在靜態區給你乙個陣列。 需要自動陣列? c在棧上給你乙個陣列。它還會自動給你初始化:

char str = "it's convenient to use arrays";

於是棧上有了我們需要的、不折不扣剛好30位元組的空間,當我們用str索引他的時候,他已經有需要的內容了。 這就是c/c++。他選取了最精確的定義,而隱藏繁雜的後台。一切都有可能改變,唯有概念不變。

ansi c++98太厚了, 一時還沒法學會在裡面找到自己要東西。下面是我的一些總結:

1 首先談談函式內定義的普通陣列,即auto陣列。 auto 陣列的的確確實在棧上建立的,且(可以認為)在定義的位置初始化。 所以,若你希望用乙個陣列實現乙個頗大的正弦速查表,推薦還是改用指標 —— 系統切切實實會在棧上初始化乙個結結實實的巨大 auto 陣列。

auto陣列在離開作用域後,其記憶體就被**了 —— 按照c的說法,你不應該繼續用它了。這的確就是我們編寫程式需要用到的所有知識 —— 不過,若想刨根問底,稍候,我也會談到這是乙個怎麼樣的內部過程。

2 static 陣列是怎麼回事呢?

c和c++中有很多 static 。 可以說,每種 static 都不是一回事。

static 陣列都儲存在靜態資料域中。這裡要細談的是區域性 static 陣列的初始化:

區域性static物件當你第一次用的時候初始化;主程式結束時,所有初始化過的區域性static 物件按和構造相反的順序析構  —— 然後全域性物件才開始析構。

面是乙個我曾研究過很長時間的乙個經典問題,在此剖析一下:

char* gethello()

int main()

這是乙個錯誤的程式,他試圖返回 auto 陣列,結果是未定義的。雖然在vc7 release 和devc4.9下都沒問題,但是在vc7 debug模式下就會輸出亂碼。

那麼返回auto陣列問題到底在**呢? 微觀的看看整個指令流程罷:

我希望大家都明白 a**/c/c++ 中函式呼叫是怎麼一回事。函式使用乙個統一的棧儲存返回位址、引數和 auto 變數。 呼叫時往棧裡堆東西, 返回時則宣布自己的東西都不要了。  正因為 auto 變數是棧上臨時分配的, 所以 c/c++ 才支援遞迴呼叫 —— 若每個變數都在資料區靜態引用, 那麼每遞迴一層,原來的東西就刷掉啦~~~

首先, gethello 已經知道自己要分配12位元組的棧空間給 str。所以gethello 函式開始就把空間留好了 —— 他可能這麼做:儲存當前棧頂 esp 到 ebp, 並讓棧頂 esp 減12(堆疊一般是向「下」增長的)。 str知道自己相對 ebp的偏移, 這都是編譯期就算好了的。假如 str 不需要初始化,那麼實際上定義它不會有任何額外開銷。

當執行 char str = "hello world" 的時候, 函式必須把棧上一片區域正確初始化。於是編譯器(如vc)可以這麼實現 —— 他預先在資料區放置字串 hello world/0, 然後把它拷到棧 ebp + 對應偏移的位置上。

最後,函式返回 str 的首位址 —— 這是棧上的位址 —— 並且恢復棧頂 esp 到呼叫前的位置 —— 於是這段字串就在棧頂之外了。

現在的問題是,為什麼會出現亂碼呢? 雖然棧頂 恢復了,但棧上的 "hello world" 仍然沒有被刪除阿!

這有點像物理學的測不准原理: 你為了測量乙個量,就必須改變它。道理類似 —— 為了輸出這個 "hello world", 我們呼叫了 ostream:: operator<<。 這個函式同樣可能(也可能不)使用堆疊。 假如需要的臨時變數覆蓋了 "hello world" ,就只能看見亂碼了。

用下面的做法,在呼叫函式前複製那個懸著的字串,就可以看見正確內容了:

int main ()

更好的做法是, 直接用乙個 const char 引用那個資料區的字串並返回:

const char* gethello()

另外說一句, c/c++規定,字串 "hello world" 的型別是 char 陣列,儲存型別為 static 唯讀。 為了與c相容, 你可以 char* str = "hello world", 但是這不安全。修改其內容會導致未定義行為 —— 事實上,除了古老的在保護模式下執行的 tc 以外, vc和 gcc 中下面行為都會最晚在下一次訪問的時候執行時崩潰:

char * str = "hello world";

str[0] = 'h';   // 未定義行為!如果是vc debug模式,在這裡就已經掛了

cout<< str << endl;  // 一般來說會在這裡掛掉

還有乙個常見問題是,兩個字串常量是否會合併:

char * str1 = "hello world";

char * str2 = "hello world";

cout<< boolalpha << ( str1 == str2 ) << endl;// 是否為 true 與實現相關

c++規定,上面兩個相同的字串是否合併取決於實現。具體說,我的簡單測試**在vc 7.1 環境下,選擇「完全優化」、「不優化」時他們沒有合併,而「最大化速度」和「最小化大小」時則合併了。 gcc 3.4.2 下則都合併了。實際上,當多個obj 參與連線的情況下,合併的難度會更大。

資料區直接拷貝記憶體映象的確是一種高效的初始化技巧 —— 如果你用 static 修飾陣列,甚至可以直接用資料區的原始資料。 然而事情並不總是這麼順人心。在oop中,我們面臨乙個麻煩:有些物件必須呼叫建構函式。

如下面乙個例子:

class a;

// 當然,去掉建構函式a::a( int ) 前的 explicit就可以寫成:

// a a = ;

}結果並不是三個拷貝建構函式,而是三個普通的建構函式。察看彙編發現,系統以 a[0] a[1] a[2] 位址為 this 分別呼叫三次建構函式,而和靜態區沒有任何關係。(不過若是用a a = ; 則會從靜態區索引0 / 1/ 2作為引數 )

假如我們有乙個區域性靜態陣列, 其中包含的是有建構函式的物件:

static a a = ;

那麼它和一般物件陣列在實現上有絕對的區別:

static int nums = ;

後者其實什麼都沒有作。nums直接表示資料區串 0 1 2 的首位址。而前者,他的資料被分配在資料區,必須以成員對應位址為 this 呼叫三次 a( int )。

區域性靜態物件還有更多奇特的屬性。

們知道, c++有這麼一句話: 物件的析構遵從和構造相反的順序。

全域性物件在呼叫 main 之前初始化, 在退出main之後析構。 區域性靜態物件則在「某個時刻」初始化一次且僅一次;若他初始化過,就必須且只能在退出時析構。

那麼在何時初始化呢? 唯一的答案是:「在第一次執行到其定義的時候」。 因為建構函式往往有引數 —— 系統不可能在 main 開始之前就確定所有引數。

這種「執行時」在區域性構造,最後又要求在全域性析構的模式讓我開始好奇 —— 系統如何知道他構造了,它又是如何析構的呢?

下面的例子測試兩個全域性變數何時構造,又是按什麼順序析構的:

void func()

int main(){

func();

func();

cout<<"-------------"<這個測試中, 流程是否經過 a( 0 ) 和 a( 1 ) 取決於兩次呼叫 func() 的使用者四次輸入。

1 輸入 0   0  0  0 , 結果:

沒有任何物件被構造,也沒有任何物件被析構。

2 當輸入 1 1 1 1 時,結果:

1a(0);

1a(1);11

-------------

~a(1);

~a(0);

每個靜態區域性物件只構造了一次,並且在退出後逆序析構。 你可以試試 0 1 1 0 這樣的輸入,c++仍然知道如何正確逆序析構。

簡直像魔術一樣! c++ 到底如何做到這個的呢? 察看彙編可以看見vc的實現:

1 他對每個區域性 static 物件都定義了乙個相關的靜態變數, 監視其是否初始化過。

2 他把初始化過的 static 物件丟到乙個全域性棧中, 以便程式結束時析構。

C 之全域性物件,區域性物件,靜態區域性物件

先說兩個概念 作用域 scope 和生命週期 lifetime 作用域 名字的作用域指的是知道該名字的程式文字區域 生命週期 物件的生命週期指在程式執行過程中物件存在的時間 全域性物件,顧名思義是全域性的物件,其作用域是整個程式文字,其物件的宣告週期是整個程式的執行過程 區域性物件 一般說的區域性變...

C 之區域性物件(自動物件和靜態區域性物件)

1 自動物件 預設情況下,區域性變數的生命期侷限於所在函式的每次執行期間。只有當定義它的函式被呼叫時才存在的物件稱為自動物件。自動物件在每次呼叫函式時建立和撤銷。該型別區域性變數儲存在棧上,在動態儲存區。區域性變數所對應的自動物件在函式控制經過變數定義語句時建立。如果在定義時提供了初始化,那麼每次建...

C 中的巢狀類和區域性類

最近趁著春節假期空閒,找了本c primer 學了幾章,發現 c 中的許多特性自己都不知道。其中巢狀類和區域性類感覺還是蠻有用的,簡單的寫寫他們的用法。其實在c 語言中也有類似的用法,在乙個結構體中巢狀另乙個結構體,或者在乙個結構體中巢狀乙個 union 我們還知道,c 語言中被巢狀的結構體或 un...