C 應該更多使用堆還是棧?

2022-07-04 16:24:13 字數 4101 閱讀 9766

問題取自知乎:c++可以通過new建立物件,也可以通過type o(...)建立物件,前者在傳遞物件給函式時只需傳遞指標,不存在很大開銷,後者可通過move操作傳遞物件,工程中應當更多使用哪個呢?

先複習基礎知識和明確問題:

實際工程中使用棧還是堆,是多角度、多判定標準綜合考慮的。只有聚焦具體的應用場景,結論才有意義。以下是常見的判定標準:^ab

cd《effective stl》(2013),scott meyers 著。第10條 了解分配子 allocator 的約定和限制;第11條 理解自定義分配子的合理用法;第13條 vector 和 string 優先於動態分配的陣列;第14條 使用 reserve 來避免不必要的重新分配;第17條 使用 swap 技巧除去多餘的容量 

^find memory leaks with the crt library (_crtdumpmemoryleaks()) 

^msvc compiler option: /f (set stack size) 

^msvc linker option: /stack (stack allocations) (also editbin option) 

thread stack size 

^c/c++ maximum stack size of program 

^《effective c++》第3版 (2011),scott meyers 著。第8章 定製 new 和 delete 

這個問題是乙個非常好的問題!

它反應了堆與棧各有利弊。

,是不需要涉及記憶體分配的,你可以把它看成乙個很長的連續記憶體,用來執行函式。自動以先進後出的方式使用。具體的進出在c++裡你可以假設是不能操縱這個棧的,實際上它存在。

main函式是第乙個進棧的函式(有人指出了這裡不嚴謹,的確發生了其他的事情,為了理解方便可以假設這句話是「對的」,不會妨礙我們理解)中間執行了其他程式,就會有其他的函式入棧,執行完乙個函式,這個函式就會從棧裡彈出來。main函式是最後乙個退出棧的函式(同理,這裡也是不嚴謹的,比如靜態物件的析構函式在main退出以後執行)。

但是棧的弊端也在這裡,函式在不斷的發生呼叫,棧是乙個臨時的執行產所,我們不能把資料持久的儲存在棧上,因為函式執行完,那個資料就等於是不再可以使用了,生命週期只有函式執行開始和結束之間的那一會兒。

有些資料,比如我們儲存的乙個班級的學生資料在記憶體裡,我們希望可以對這個資料集合進行不斷的增刪改查,會用不同的函式來執行這些操作。那就要求這些資料集合儲存的資料要能根據我們的需要產生和釋放,所以集合動態變化的那部分資料就只能放到另乙個叫堆的記憶體空間裡。

堆,堆就是用來彌補棧的資料不能持久儲存的能力的,在堆上分配的記憶體,只有我們主動釋放,才會被**,否則就一直為我們使用。如果忘記釋放,就等於縮小了可以使用記憶體的大小(又叫記憶體洩露,乙個日夜執行的服務端程式,某個函式執行一次洩露乙個位元組,隨著客戶端大量頻繁的呼叫這個服務,可能都會導致伺服器記憶體爆掉)。堆的好處就是可以持久的儲存資料。

由於堆可以手動的釋放和申請,所以像vector,string這種動態可變大小的容器,他們儲存的資料都是放在堆上面的。但是vectora;這裡的a是乙個棧物件,這一點也不矛盾。意思就是我知道a只是臨時的函式內物件,但是我也希望在執行函式的過程中,a具有擴容的能力。這是很自然的事情。

假如這裡的a是在main函式內定義的,由於main函式的執行伴隨著整個程式,所以a的使用就更能夠達到被各個函式用於不斷增刪改查的目的。

現在回答的你問題,指標由於只需要傳遞位址,所以效率高(指標變數就像乙個整形變數一樣,儲存了乙個整數,只不過這個整數是乙個位址而已,而不需要拷貝整個指標指向的物件)。這是c語言的邏輯。c語言都是這麼做的。通過傳遞指標來做絕大多數的事情。但是弊端就是指標很難被管理,很容易忘記釋放,或者重複釋放,甚至不太知道**才是釋放的時機。c++ 在一開始就是為了解決這個問題才產生的。所以,如果你用c++語言還傳遞指標,這是不建議的,違背了c++的初衷。在c++裡建議的方式是傳遞引用。

如果是傳遞不需要被修改的物件,c++裡更好的方式是傳遞const t&,也就是常量引用

const引用引數有四個好處:1 避免了堆記憶體分配;2 避免了棧物件拷貝;3 函式內不小心修改了該物件編譯器報錯;4 **直觀易於理解。

所以,《effective c++ 》有一條指出應該最大限度的使用const引用引數。

既然是傳遞引用,就應該保證引用引數還在,不能這邊函式要使用乙個引用物件,在外面其他地方因為超出作用域已經被釋放了。這就要求你要知道你的引數的生命週期(也就是業務需求),這一點實際上很容易做到,就像上文說的在main函式中定義的用來儲存班級學生資訊的容器物件,就可以不斷的傳遞自身的引用給各個操作它的函式。

你說的move同樣是乙個很好的問題

考慮到從函式直接返回乙個vector,比如 vectorfun(),**看起來像這樣: a = f();

而不是這樣:

vector a;

f(a);//這裡得到a,看起來就不直觀

既然要能夠從函式返回vector,那麼我們也知道vector裡面儲存了動態記憶體資料,這時候的vector是乙個非平凡的類,也就是值拷貝返回這種物件會導致其管理的動態記憶體有多個物件同時在指向的問題(加上拷貝得到的物件也在指向)。由誰釋放就是乙個大問題。所以c++誕生了複製控制

賦值控制也好,複製控制也罷,都是乙個意思,就是解決vector這種型別的物件的拷貝問題。

還是剛才的那個從函式返回vector的問題,賦值控制解決了記憶體不會混亂的問題,c++沿用了c語言的值語義(賦值總是拷貝,如果是vector這樣的容器就連動態記憶體也一起拷貝),但是總是把乙個物件的動態記憶體拷貝乙份過來,另乙個物件可能立刻又要釋放,這個物件也涉及分配一次記憶體,實在是代價太高。本來動態記憶體的分配就已經需要成本了,現在乙個賦值語句,居然要釋放一次,再開闢一次記憶體,還要拷貝一次,這個代價太高了。

所以我們希望像函式返回vector這種場景的效率能更高一些,這就產生了移動語義

移動語義就是偷梁換柱,將兩個物件的動態記憶體互換:

a = f();

f函式裡面的那個vector使用移動語義把自己的動態記憶體交給a之後就析構了。這樣就避免了一次記憶體釋放和記憶體開闢。豈不是很完美。

所以你說的move這種情形,在c++裡並不常見,只有在函式返回乙個vector這種物件的時候才會發生。其餘場景都被引用引數完成了

c++ 原始指標、shared_ptr、unique_ptr分別在什麼場景下使用​blog.csdn.net

但是也有一些庫比如rapidjson,為了快,統統使用移動語義。這種庫要求你賦值之後,物件的動態記憶體部分就被轉移走了。這種場景下很少發生不必要的動態記憶體的開闢和釋放。而且就算發生了動態記憶體的開闢和釋放,這個庫自己也在一開始就開闢了很長一段記憶體給自己使用,類似於記憶體池。用完了再來一段。最大限度的避免了記憶體碎片。

這種庫的作者可以駕馭這種記憶體分配 方式,對於普通開發者實在沒有必要這麼幹,我們會用就足矣。

但是,很多新人不好好打基礎,大學沒畢業就要實現乙個記憶體池。這就是不務正業(對,瞅啥呢,說的就是你)。

就像vector的移動函式,我們知道有這麼回事,知道會用,就足以。絕大多數情況下,至少我工作7年以來,從來沒有在工作中需要寫移動函式的。因為各種庫非常完善,stl裡面的容器早就寫好了,你用就行了。工作中極少自己寫乙個容器,都是基於stl的模板容器開發(直接使用,而不創造新的)。

所以,結論就是,指標幾乎不會在c++裡作為引數傳遞(除非有編碼規範)。引數傳遞時move也不會發生,因為被引用引數取代。函式返回vector這種物件需要move,但是標準庫早就替你寫好了,所以你也不必擔心。

C 堆還是棧上建立物件

如果需要在堆上建立物件,要麼使用new運算子,要麼使用malloc系列函式。這點沒有異議。真正有異議的是下面的 c 1 object obj 此時,obj是在棧上分配的嗎?要回答這個問題,我們首先要理解這個語句是什麼意思。這個語句就是代表著,在棧上建立物件嗎?其實,這行語句的含義是,使物件obj具有...

堆記憶體還是棧記憶體?

劍指offer 裡面有一道題目,把一字元陣列中的空格用字串 20 代替 看了書上的思路,然後我寫出來的程式當輸入的空格太多時,會出現錯誤 memory clobbered before allocated block 其原因是沒搞清楚棧記憶體,堆記憶體的分配和區別。錯誤 如下 include inc...

SSH應該使用金鑰還是密碼?

關於ssh,幾乎每個人都同意金鑰要優於密碼,更安全,並且更先進,但我並不同意這個觀點。雖然金鑰的確可以更好,但它有著還沒被意識到的嚴重風險,並且我認為比得到妥善管理的密碼更不安全。通常金鑰更好的理由是多數人使用了弱密碼,並且系統之間共享密碼,所以一旦發生密碼洩漏就會同時危害到多個系統。既然金鑰可以設...