C++記憶體物件大會戰 禁止產生堆物件 禁止產生棧物件
如果一個人自稱為程式高手,卻對記憶體一無所知,那麼我可以告訴你,他一定在吹牛。用C或C++寫程式,需要更多地關注記憶體,這不僅僅是因為記憶體的分配是否合理直接影響著程式的效率和效能,更為主要的是,當我們操作記憶體的時候一不小心就會出現問題,而且很多時候,這些問題都是不易發覺的,比如記憶體洩漏,比如懸掛指標。筆者今天在這裡並不是要討論如何避免這些問題,而是想從另外一個角度來認識C++記憶體物件。
stack_object便是一個棧物件,它的生命期是從定義點開始,當所在函式返回時,生命結束。 另外,幾乎所有的臨時物件都是棧物件。比如,下面的函式定義:
這個函式至少產生兩個臨時物件,首先,引數是按值傳遞的,所以會呼叫拷貝建構函式生成一個臨時物件object_copy1 ,在函式內部使用的不是使用的不是object,而是object_copy1,自然,object_copy1是一個棧物件,它在函式返回時被釋放;還有這個函式是值返回的,在函式返回時,如果我們不考慮返回值優化(NRV),那麼也會產生一個臨時物件object_copy2,這個臨時物件會在函式返回後一段時間內被釋放。比如某個函式中有如下程式碼:
上面的第二個語句的執行情況是這樣的,首先函式fun返回時生成一個臨時物件object_copy2 ,然後再呼叫賦值運算子執行
看到了嗎?編譯器在我們毫無知覺的情況下,為我們生成了這麼多臨時物件,而生成這些臨時物件的時間和空間的開銷可能是很大的,所以,你也許明白了,為什麼對於“大”物件最好用const引用傳遞代替按值進行函式引數傳遞了。 接下來,看看堆。堆,又叫自由儲存區,它是在程式執行的過程中動態分配的,所以它最大的特性就是動態性。在C++中,所有堆物件的建立和銷燬都要由程式設計師負責,所以,如果處理不好,就會發生記憶體問題。如果分配了堆物件,卻忘記了釋放,就會產生記憶體洩漏;而如果已釋放了物件,卻沒有將相應的指標置為NULL,該指標就是所謂的“懸掛指標”,再度使用此指標時,就會出現非法訪問,嚴重時就導致程式崩潰。 那麼,C++中是怎樣分配堆物件的?唯一的方法就是用new(當然,用類malloc指令也可獲得C式堆記憶體),只要使用new,就會在堆中分配一塊記憶體,並且返回指向該堆物件的指標。 再來看看靜態儲存區。所有的靜態物件、全域性物件都於靜態儲存區分配。關於全域性物件,是在main()函式執行前就分配好了的。其實,在main()函式中的顯示程式碼執行之前,會呼叫一個由編譯器生成的_main()函式,而_main()函式會進行所有全域性物件的的構造及初始化工作。而在main()函式結束之前,會呼叫由編譯器生成的exit函式,來釋放所有的全域性物件。比如下面的程式碼:
實際上,被轉化成這樣:
所以,知道了這個之後,便可以由此引出一些技巧,如,假設我們要在main()函式執行之前做某些準備工作,那麼我們可以將這些準備工作寫到一個自定義的全域性物件的建構函式中,這樣,在main()函式的顯式程式碼執行之前,這個全域性物件的建構函式會被呼叫,執行預期的動作,這樣就達到了我們的目的。 剛才講的是靜態儲存區中的全域性物件,那麼,區域性靜態物件了?區域性靜態物件通常也是在函式中定義的,就像棧物件一樣,只不過,其前面多了個static關鍵字。區域性靜態物件的生命期是從其所在函式第一次被呼叫,更確切地說,是當第一次執行到該靜態物件的宣告程式碼時,產生該靜態區域性物件,直到整個程式結束時,才銷燬該物件。 還有一種靜態物件,那就是它作為class的靜態成員。考慮這種情況時,就牽涉了一些較複雜的問題。 第一個問題是class的靜態成員物件的生命期,class的靜態成員物件隨著第一個class object的產生而產生,在整個程式結束時消亡。也就是有這樣的情況存在,在程式中我們定義了一個class,該類中有一個靜態物件作為成員,但是在程式執行過程中,如果我們沒有建立任何一個該class object,那麼也就不會產生該class所包含的那個靜態物件。還有,如果建立了多個class object,那麼所有這些object都共享那個靜態物件成員。 第二個問題是,當出現下列情況時:
請注意上面標為黑體的三條語句,它們所訪問的s_object是同一個物件嗎?答案是肯定的,它們的確是指向同一個物件,這聽起來不像是真的,是嗎?但這是事實,你可以自己寫段簡單的程式碼驗證一下。我要做的是來解釋為什麼會這樣? 我們知道,當一個類比如Derived1,從另一個類比如Base繼承時,那麼,可以看作一個Derived1物件中含有一個Base型的物件,這就是一個subobject。一個Derived1物件的大致記憶體佈局如下: 讓我們想想,當我們將一個Derived1型的物件傳給一個接受非引用Base型引數的函式時會發生切割,那麼是怎麼切割的呢?相信現在你已經知道了,那就是僅僅取出了Derived1型的物件中的subobject,而忽略了所有Derived1自定義的其它資料成員,然後將這個subobject傳遞給函式(實際上,函式中使用的是這個subobject的拷貝)。 所有繼承Base類的派生類的物件都含有一個Base型的subobject(這是能用Base型指標指向一個Derived1物件的關鍵所在,自然也是多型的關鍵了),而所有的subobject和所有Base型的物件都共用同一個s_object物件,自然,從Base類派生的整個繼承體系中的類的例項都會共用同一個s_object物件了。上面提到的example、example1、example2的物件佈局如下圖所示: 二.三種記憶體物件的比較 棧物件的優勢是在適當的時候自動生成,又在適當的時候自動銷燬,不需要程式設計師操心;而且棧物件的建立速度一般較堆物件快,因為分配堆物件時,會呼叫operator new操作,operator new會採用某種記憶體空間搜尋演算法,而該搜尋過程可能是很費時間的,產生棧物件則沒有這麼麻煩,它僅僅需要移動棧頂指標就可以了。但是要注意的是,通常棧空間容量比較小,一般是1MB~2MB,所以體積比較大的物件不適合在棧中分配。特別要注意遞迴函式中最好不要使用棧物件,因為隨著遞迴呼叫深度的增加,所需的棧空間也會線性增加,當所需棧空間不夠時,便會導致棧溢位,這樣就會產生執行時錯誤。 堆物件,其產生時刻和銷燬時刻都要程式設計師精確定義,也就是說,程式設計師對堆物件的生命具有完全的控制權。我們常常需要這樣的物件,比如,我們需要建立一個物件,能夠被多個函式所訪問,但是又不想使其成為全域性的,那麼這個時候建立一個堆物件無疑是良好的選擇,然後在各個函式之間傳遞這個堆物件的指標,便可以實現對該物件的共享。另外,相比於棧空間,堆的容量要大得多。實際上,當實體記憶體不夠時,如果這時還需要生成新的堆物件,通常不會產生執行時錯誤,而是系統會使用虛擬記憶體來擴充套件實際的實體記憶體。 接下來看看static物件。 首先是全域性物件。全域性物件為類間通訊和函式間通訊提供了一種最簡單的方式,雖然這種方式並不優雅。一般而言,在完全的面嚮物件語言中,是不存在全域性物件的,比如C#,因為全域性物件意味著不安全和高耦合,在程式中過多地使用全域性物件將大大降低程式的健壯性、穩定性、可維護性和可複用性。C++也完全可以剔除全域性物件,但是最終沒有,我想原因之一是為了相容C。 其次是類的靜態成員,上面已經提到,基類及其派生類的所有物件都共享這個靜態成員物件,所以當需要在這些class之間或這些class objects之間進行資料共享或通訊時,這樣的靜態成員無疑是很好的選擇。 接著是靜態區域性物件,主要可用於儲存該物件所在函式被屢次呼叫期間的中間狀態,其中一個最顯著的例子就是遞迴函式,我們都知道遞迴函式是自己呼叫自己的函式,如果在遞迴函式中定義一個nonstatic區域性物件,那麼當遞迴次數相當大時,所產生的開銷也是巨大的。這是因為nonstatic區域性物件是棧物件,每遞迴呼叫一次,就會產生一個這樣的物件,每返回一次,就會釋放這個物件,而且,這樣的物件只侷限於當前呼叫層,對於更深入的巢狀層和更淺露的外層,都是不可見的。每個層都有自己的區域性物件和引數。 在遞迴函式設計中,可以使用static物件替代nonstatic區域性物件(即棧物件),這不僅可以減少每次遞迴呼叫和返回時產生和釋放nonstatic物件的開銷,而且static物件還可以儲存遞迴呼叫的中間狀態,並且可為各個呼叫層所訪問。 三.使用棧物件的意外收穫 前面已經介紹到,棧物件是在適當的時候建立,然後在適當的時候自動釋放的,也就是棧物件有自動管理功能。那麼棧物件會在什麼會自動釋放了?第一,在其生命期結束的時候;第二,在其所在的函式發生異常的時候。你也許說,這些都很正常啊,沒什麼大不了的。是的,沒什麼大不了的。但是隻要我們再深入一點點,也許就有意外的收穫了。 棧物件,自動釋放時,會呼叫它自己的解構函式。如果我們在棧物件中封裝資源,而且在棧物件的解構函式中執行釋放資源的動作,那麼就會使資源洩漏的概率大大降低,因為棧物件可以自動的釋放資源,即使在所在函式發生異常的時候。實際的過程是這樣的:函式丟擲異常時,會發生所謂的stack_unwinding(堆疊回滾),即堆疊會展開,由於是棧物件,自然存在於棧中,所以在堆疊回滾的過程中,棧物件的解構函式會被執行,從而釋放其所封裝的資源。除非,除非在解構函式執行的過程中再次丟擲異常――而這種可能性是很小的,所以用棧物件封裝資源是比較安全的。基於此認識,我們就可以建立一個自己的控制程式碼或代理來封裝資源了。智慧指標(auto_ptr)中就使用了這種技術。在有這種需要的時候,我們就希望我們的資源封裝類只能在棧中建立,也就是要限制在堆中建立該資源封裝類的例項。
四.禁止產生堆物件
|
相關文章
- 如何禁止JavaScript物件重寫?JavaScript物件
- String s = new String(" a ") 到底產生幾個物件?物件
- Jenkins踩坑之旅:使用Date物件產生RejectedAccessExceptionJenkins物件Exception
- 物件記憶體圖物件記憶體
- JVM-物件及物件記憶體佈局JVM物件記憶體
- 深度解讀《深度探索C++物件模型》之C++物件的記憶體佈局C++物件模型記憶體
- C++ 虛繼承 物件記憶體佈局C++繼承物件記憶體
- Java物件記憶體模型Java物件記憶體模型
- Java 物件記憶體分析Java物件記憶體
- JavaScript嚴格模式(三)- 物件的禁止操作JavaScript模式物件
- Java是否可以棧上分配物件記憶體? 為什麼?Java物件記憶體
- OC物件記憶體佈局物件記憶體
- Java物件記憶體佈局Java物件記憶體
- 資料庫物件比如表放入記憶體,行發生改變不會自動同步到記憶體的總結資料庫物件記憶體
- Objective-C記憶體管理:物件Object記憶體物件
- python物件的記憶體佔用Python物件記憶體
- Java物件的記憶體佈局Java物件記憶體
- JVM -- 物件的記憶體佈局JVM物件記憶體
- Phaser3 物件池隨機產生炸彈並銷燬物件隨機
- 吃人的那些 Java 名詞:物件、引用、堆、棧Java物件
- return new物件造成溢位記憶體物件記憶體
- 記憶體管理:判斷物件是否存活記憶體物件
- JS中的棧記憶體、堆記憶體JS記憶體
- 阿里為何禁止在物件中使用基本資料型別阿里物件資料型別
- Java記憶體溢位OutOfMemoryError的產生與排查Java記憶體溢位Error
- JVM記憶體結構、Java記憶體模型和Java物件模型JVM記憶體Java模型物件
- 一些轉儲和清除記憶體物件和物理物件的命令(轉)記憶體物件
- C++ 物件C++物件
- JS筆記—— 物件 (原型物件)JS筆記物件原型
- OC記憶體管理--物件的生成與銷燬記憶體物件
- 一個Java物件到底佔用多大記憶體?Java物件記憶體
- SAP ABAP 的兩種記憶體物件型別記憶體物件型別
- 圖文詳解Java物件記憶體佈局Java物件記憶體
- C++ 類 & 物件C++物件
- 記一次記憶體溢位導致的生產事故記憶體溢位
- 棧結構-物件形式物件
- C++物件導向三大特性C++物件
- [java設計模式]工廠設計模式,給物件一個合法的生產渠道。Java設計模式物件