一文讀懂堆棧與堆,代碼世界的“內存雙雄”
在代碼的奇妙世界里,有一對至關重要卻又常讓人混淆的 “孿生兄弟”—— 堆棧與堆,它們堪稱代碼世界的 “內存雙雄”。理解它們,就如同掌握了打開高效代碼之門的鑰匙,無論是初涉編程領域的新手,還是久經沙場的編程老將,都能從中受益匪淺。當程序在計算機中運行,內存便如同一個龐大的倉庫,存儲著程序運行所需的各種數據和指令。而堆棧與堆,就是倉庫中兩種截然不同的存儲方式。
堆棧,猶如一座秩序井然的小閣樓,有著嚴格的進出規則,數據按照 “后進先出” 的原則存放。函數調用時,局部變量、函數參數以及返回地址等信息,都會被有序地放置在這里,它的管理高效且自動,就像有一位不知疲倦的管理員時刻維護著秩序。反觀堆,更像是一片廣闊無垠的大廣場,充滿了靈活性。在這里,程序員可以根據實際需求,隨時申請或釋放內存空間,以滿足那些大小不定、生命周期復雜的數據結構和對象的存儲需求。但這片廣場也需要程序員精心打理,否則容易出現 “雜物堆積”,也就是內存泄漏等問題。接下來,讓我們一同深入探索堆棧與堆的奧秘,揭開它們神秘的面紗,看看這對 “內存雙雄” 在代碼世界里究竟是如何施展各自的 “魔力”,影響著程序的性能與效率的 。
Part1堆棧與堆概念
堆棧(Stack)與堆(Heap)宛如兩座風格迥異的倉庫,靜靜佇立在程序運行的內存空間里。堆棧遵循 “后進先出” 的規則,數據進出有序,如同按順序排列的一摞盤子,后放上去的先被拿走。它主要負責存儲函數調用時的局部變量、函數參數以及返回地址等信息,這些數據的生命周期往往隨著函數調用的開始而誕生,隨著函數調用的結束而消逝。其內存由操作系統自動分配和管理,這使得堆棧的操作極為高效,速度如同閃電一般。不過,堆棧的空間相對有限,好似一座面積不大的倉庫,能容納的數據量較為受限。
而堆則截然不同,它是一片更為靈活的內存區域,猶如一個大型的開放式倉庫,數據的存儲順序并不固定。當程序需要動態分配內存,例如創建大小在編譯時無法確定的對象或數據結構時,堆便派上了用場。
在這里,程序員擁有更大的控制權,可以按需申請和釋放內存空間。但這也意味著需要手動管理內存,一旦操作不當,就容易引發內存泄漏等問題,就像在大倉庫中隨意堆放物品,卻不加以整理,可能導致空間混亂無序。盡管堆內存的分配過程相對復雜,速度也不及堆棧,但它能夠提供更大的內存空間,滿足程序對大規模數據存儲的需求 。

當一個程序啟動運行時,操作系統會為其精心規劃一片內存空間,這片空間被巧妙地劃分為五個獨特的區域,各區域各司其職,共同保障程序的順暢運作
1、棧區(stack):由編譯器自動分配釋放 ,存放為運行函數而分配的局部變量、函數參數、返回數據、返回地址等。其操作方式類似于數據結構中的棧。
2、堆區(heap):一般由程序員分配釋放, 若程序員不釋放,程序結束時可能由OS回收 。分配方式類似于鏈表。
3、靜態區,亦稱全局區。全局變量、靜態變量和常量都是存儲在該區的,程序結束后該區的變量由系統釋放。該區具體又可以三部分:①已初始化的全局變量和靜態變量;②未初始化的全局變量和靜態變量;③常量數據區。
4、文字常量區:程序代碼中使用的常量字符串,程序結束后由系統釋放。
5、程序代碼區:存放函數體的二進制代碼。
int a = 0; //全局初始化區
char *p1; //全局未初始化區
int main()
{
int b; //局部變量,棧區
char s[] = "abc"; //局部變量,棧區, abc在常量區
char *p2; //局部變量,棧區
char *p3 = "123456"; //123456在常量區,p3棧區。
static int c =0; //靜態變量,全局初始化區
//動態函數分配的區域就在堆區。
p1 = (char *)malloc( sizeof(char) * 10 );
p2 = (char *)malloc( sizeof(char) * 20 );
strcpy(p1, "123456"); //123456放在常量區
return 0;
}1.1堆棧:遵循 “后進先出” 原則的臨時倉庫
堆棧是一種特殊的數據結構,遵循 “后進先出”(Last In First Out,LIFO)的原則,就像一摞盤子,最后放上去的盤子最先被拿走。在程序執行過程中,堆棧主要用于存儲臨時數據,比如函數調用時的局部變量、函數參數和返回地址等。
當一個函數被調用時,系統會在堆棧的頂部為其分配一塊內存空間,用于存儲該函數的局部變量和相關信息,這個過程稱為 “壓棧”(Push)。當函數執行結束后,這塊內存空間會被自動釋放,數據從堆棧中移除,這個過程稱為 “出棧”(Pop)。由于堆棧的操作非常簡單,只需要在棧頂進行壓棧和出棧操作,所以它的速度非常快,就像在一摞盤子的頂部快速地放上或拿走盤子一樣。
1.2堆:用于動態分配內存的自由市場
堆是另一種用于存儲數據的內存區域,它主要用于動態分配內存。與堆棧不同,堆中的數據存儲順序沒有特定的規則,也不遵循 “后進先出” 的原則,更像是一個自由市場,你可以在任何時候根據需要申請或釋放大小不同的內存塊。
在程序運行時,如果我們需要創建一個對象或者分配一塊內存空間,就可以從堆中申請。例如,在 C++ 中,我們使用new關鍵字來從堆中分配內存;在 Java 中,使用new操作符創建對象時,對象也會被分配到堆中。堆的優點是可以靈活地分配和釋放內存,適合存儲生命周期較長、大小不確定的數據。但是,由于堆的內存管理相對復雜,需要進行內存的分配和釋放操作,所以它的速度相對較慢。
1.3堆棧與堆的結構差異示意圖
為了更直觀地理解堆棧與堆的區別,我們來看下面這張示意圖:

通過上面的介紹,相信你已經對堆棧與堆的概念有了初步的了解。堆棧就像是一個高效的臨時倉庫,用于存儲函數調用時的臨時數據;而堆則像是一個自由市場,提供了更靈活的內存分配方式。它們在程序運行中都扮演著重要的角色,缺一不可。
Part2內存分配:堆棧與堆的 “分配之道”
在程序運行過程中,內存分配是一個至關重要的環節。堆棧和堆在內存分配方式上有著顯著的區別,這些區別直接影響著程序的性能和內存使用效率。下面,讓我們深入探討一下堆棧與堆的內存分配機制。
2.1棧內存分配:有序存儲
堆棧的內存分配是由操作系統自動管理的,就像一個自動整理的倉庫,所有的操作都由倉庫管理員(操作系統)自動完成。當一個函數被調用時,系統會在堆棧的頂部為其分配一塊連續的內存空間,用于存儲該函數的局部變量、函數參數和返回地址等信息。這就好比在倉庫的頂層專門為某個訂單預留了一塊連續的空間,用來存放與該訂單相關的所有物品。
例如,我們來看下面這段 C++ 代碼:
#include <iostream>
void function(int a, int b) {
int c = a + b;
std::cout << "The result is: " << c << std::endl;
}
int main() {
int x = 5;
int y = 3;
function(x, y);
return 0;
}在這個例子中,當main函數調用function函數時,系統會在堆棧中為function函數分配一塊內存空間,用于存儲參數a、b和局部變量c。同時,系統還會將function函數調用結束后要返回的地址壓入堆棧。這個過程可以用下面的堆棧幀變化示意圖來表示:


從圖中可以看出,堆棧的內存分配是連續的,新分配的內存空間總是在堆棧的頂部,緊挨著上一次分配的空間。這種連續的內存分配方式使得堆棧的分配和釋放速度非常快,因為操作系統只需要簡單地移動堆棧指針(類似于倉庫管理員移動記錄貨物位置的指針),就可以完成內存的分配和釋放操作。
然而,堆棧的空間是有限的,就像倉庫的頂層空間是有限的一樣。如果在程序中不斷地調用函數,或者在函數中定義大量的局部變量,就可能導致堆棧空間不足,從而引發棧溢出(Stack Overflow)錯誤。這就好比倉庫的頂層空間被占滿了,無法再為新的訂單預留空間。
2.2堆內存分配:動態存儲
堆的內存分配則需要程序員手動管理,這就像一個自由市場,你需要自己去尋找合適的攤位(內存空間),并在使用完后自己清理攤位(釋放內存)。在 C++ 中,我們使用new關鍵字來從堆中分配內存,使用delete關鍵字來釋放內存;在 Java 中,使用new操作符創建對象時,對象會被分配到堆中,內存的釋放由垃圾回收器(Garbage Collector)自動完成,但程序員無法精確控制內存釋放的時機。
例如,下面是一段在 C++ 中從堆中分配內存的代碼:
#include <iostream>
int main() {
int* ptr = new int;
*ptr = 10;
std::cout << "The value is: " << *ptr << std::endl;
delete ptr;
return 0;
}在這段代碼中,我們使用new int從堆中分配了一塊內存空間,并將其地址賦值給指針ptr。然后,我們通過指針ptr對這塊內存進行操作,將值 10 存儲到這塊內存中。最后,使用delete ptr釋放了這塊內存。
再看一個 Java 中創建對象并分配到堆中的例子:
public class Main {
public static void main(String[] args) {
String str = new String("Hello, World!");
System.out.println(str);
}
}在這個例子中,使用new String("Hello, World!")創建了一個String對象,并將其分配到堆中。str是一個引用變量,存儲在堆棧中,它指向堆中的String對象。
堆中的內存分配是不連續的,每次分配的內存空間可能位于堆的不同位置。這是因為堆的內存管理是通過鏈表等數據結構來實現的,系統會在空閑內存列表中尋找合適大小的內存塊進行分配。這種分配方式使得堆的內存分配非常靈活,可以根據需要分配任意大小的內存空間,適合存儲生命周期較長、大小不確定的數據。
但是,由于堆的內存分配需要遍歷空閑內存列表,尋找合適的內存塊,并且在釋放內存時可能會產生內存碎片(就像自由市場中攤位被拆除后留下的零散空地),導致后續的內存分配效率降低。例如,如果我們頻繁地分配和釋放大小不同的內存塊,堆中就會出現許多小塊的空閑內存,這些空閑內存由于太小,無法滿足后續較大內存塊的分配需求,從而造成內存浪費。
Part3生命周期管理:兩者的 “生存之道”
在程序運行過程中,堆棧和堆中的數據有著不同的生命周期管理方式,這也是它們的重要區別之一。了解這些區別,有助于我們更好地編寫高效、穩定的代碼,避免內存泄漏等問題。
3.1堆棧的生命周期
堆棧中數據的生命周期與函數的調用密切相關,就像演員在舞臺上的表演時間與節目流程緊密相連。當一個函數被調用時,系統會在堆棧的頂部為其分配一塊內存空間,用于存儲該函數的局部變量、函數參數和返回地址等信息,這個過程就像是為演員準備了一個專屬的舞臺區域。
例如,我們來看下面這段 Python 代碼:
def add_numbers(a, b):
result = a + b
return result
x = 5
y = 3
sum_result = add_numbers(x, y)
print(sum_result)在這個例子中,當add_numbers函數被調用時,系統會在堆棧中為其分配一塊內存空間,用于存儲參數a、b和局部變量result;當add_numbers函數執行結束后,它在堆棧中占用的內存空間會被自動釋放,就像演員表演結束后離開舞臺,舞臺區域被清理。
這種自動分配和釋放內存的方式使得堆棧的生命周期管理非常簡單高效,就像舞臺工作人員能夠快速地為演員準備舞臺和清理舞臺,不需要過多的人工干預。但是,由于堆棧的空間是有限的,如果在程序中不斷地調用函數,或者在函數中定義大量的局部變量,就可能導致堆棧空間不足,從而引發棧溢出(Stack Overflow)錯誤,就像舞臺上演員太多,超出了舞臺的承載能力。
3.2堆的生命周期
堆中數據的生命周期則由程序員手動控制,這就像你在一個大型倉庫中租用了一個攤位,你需要自己決定什么時候租用(分配內存),什么時候歸還(釋放內存)。在 C++ 中,我們使用new關鍵字來從堆中分配內存,使用delete關鍵字來釋放內存;在 Java 中,使用new操作符創建對象時,對象會被分配到堆中,內存的釋放由垃圾回收器(Garbage Collector)自動完成,但程序員無法精確控制內存釋放的時機。
例如,下面是一段在 C++ 中從堆中分配內存的代碼:
#include <iostream>
int main() {
int* ptr = new int;
*ptr = 10;
std::cout << "The value is: " << *ptr << std::endl;
// 這里如果忘記釋放內存,就會導致內存泄漏
// delete ptr;
return 0;
}在這段代碼中,我們使用new int從堆中分配了一塊內存空間,并將其地址賦值給指針ptr。然后,我們通過指針ptr對這塊內存進行操作,將值 10 存儲到這塊內存中。如果我們在程序結束時忘記使用delete ptr釋放這塊內存,那么這塊內存就會一直被占用,無法被其他程序使用,從而導致內存泄漏,就像你租用了倉庫的攤位,但是使用完后卻不歸還,導致其他商家無法使用這個攤位。
再看一個 Java 中創建對象并分配到堆中的例子:
public class Main {
public static void main(String[] args) {
String str = new String("Hello, World!");
System.out.println(str);
// 這里雖然不需要手動釋放內存,但是在對象不再使用時,垃圾回收器可能不會立即回收
}
}在這個例子中,使用new String("Hello, World!")創建了一個String對象,并將其分配到堆中。str是一個引用變量,存儲在堆棧中,它指向堆中的String對象。雖然 Java 的垃圾回收器會自動回收不再使用的對象,但在對象不再使用時,垃圾回收器可能不會立即回收,這就可能導致內存占用時間過長,影響程序的性能,就像倉庫中的一些攤位雖然不再使用,但是管理員可能不會立即收回,導致倉庫空間利用率降低。
因此,在使用堆內存時,程序員需要特別注意內存的分配和釋放,避免內存泄漏和內存占用時間過長等問題。可以通過及時釋放不再使用的內存,或者在程序設計中合理規劃對象的生命周期,來提高內存的使用效率。
Part4應用場景:堆棧與堆的 “用武之地”
在編程的世界里,堆棧與堆各自有著獨特的應用場景,它們就像編程舞臺上的兩位主角,在不同的場景中發揮著關鍵作用。下面,我們來詳細了解一下它們的常見應用。
4.1堆棧的常見應用
①函數調用管理:堆棧在函數調用中扮演著至關重要的角色,它就像是一個有序的倉庫,負責存儲函數調用時的各種信息。當一個函數被調用時,系統會將該函數的返回地址、參數以及局部變量等信息壓入堆棧中。例如,我們來看下面這段 Python 代碼:
def add(a, b):
result = a + b
return result
def main():
x = 5
y = 3
sum_result = add(x, y)
print(sum_result)
main()在這個例子中,當main函數調用add函數時,系統會將add函數的返回地址、參數x和y以及局部變量result壓入堆棧。當add函數執行完畢后,這些信息會從堆棧中彈出,程序繼續執行main函數的后續代碼。這種方式確保了函數調用的正確性和程序執行的有序性,就像倉庫管理員按照順序存放和取出貨物一樣。
②算法回溯:在許多算法中,如深度優先搜索(DFS)算法,堆棧被用于記錄路徑或狀態,允許算法回退到之前的決策點。以一個簡單的迷宮搜索問題為例,假設我們有一個迷宮,用二維數組表示,其中 0 表示通路,1 表示墻壁。我們使用深度優先搜索算法來尋找從起點到終點的路徑。
maze = [
[0, 1, 0, 0, 0],
[0, 1, 0, 1, 0],
[0, 0, 0, 0, 0],
[0, 1, 1, 1, 0],
[0, 0, 0, 1, 0]
]
start = (0, 0)
end = (4, 4)
def dfs(maze, start, end):
stack = [(start, [start])]
visited = set([start])
while stack:
(x, y), path = stack.pop()
if (x, y) == end:
return path
for dx, dy in [(0, 1), (1, 0), (0, -1), (-1, 0)]:
new_x, new_y = x + dx, y + dy
if 0 <= new_x < len(maze) and 0 <= new_y < len(maze[0]) and maze[new_x][new_y] == 0 and (new_x, new_y) not in visited:
stack.append(((new_x, new_y), path + [(new_x, new_y)]))
visited.add((new_x, new_y))
return None
path = dfs(maze, start, end)
if path:
print("找到路徑:", path)
else:
print("沒有找到路徑")在這個算法中,我們使用堆棧stack來存儲當前位置和從起點到當前位置的路徑。每次從堆棧中彈出一個位置和路徑,如果該位置是終點,則找到了路徑。否則,遍歷該位置的四個方向,如果是通路且未訪問過,則將新位置和更新后的路徑壓入堆棧。如果堆棧為空,表示所有可能的路徑都已嘗試,仍未找到終點,則沒有找到路徑。通過這種方式,堆棧幫助我們實現了回溯功能,就像在迷宮中標記走過的路,當遇到死胡同時能夠回到之前的路口嘗試其他方向。
③撤銷操作:許多應用程序,如文本編輯器、繪圖軟件等,使用堆棧來實現 “撤銷” 功能。每次用戶進行操作時,當前狀態被壓入堆棧中,當用戶點擊撤銷時,彈出棧頂元素,恢復到之前的狀態。例如,在一個簡單的文本編輯器中,用戶輸入了一系列字符,每次輸入操作都被記錄在堆棧中。當用戶點擊撤銷按鈕時,最近的一次輸入操作被從堆棧中彈出,文本恢復到之前的狀態。這種方式使得用戶能夠方便地撤銷錯誤操作,提高了用戶體驗,就像時光倒流,回到之前的操作狀態。
4.2堆的常見應用
①動態內存分配:堆適用于需要在運行時動態分配內存的場景。比如,當我們需要創建一個大小不確定的數組時,就可以使用堆內存。在 C++ 中,可以使用new關鍵字來分配堆內存,如下所示:
#include <iostream>
int main() {
int size;
std::cout << "請輸入數組的大小: ";
std::cin >> size;
int* array = new int[size];
for (int i = 0; i < size; i++) {
array[i] = i + 1;
}
for (int i = 0; i < size; i++) {
std::cout << array[i] << " ";
}
delete[] array;
return 0;
}在這段代碼中,我們首先從用戶處獲取數組的大小,然后使用new int[size]在堆中分配一塊大小為size的整數數組內存空間。在使用完數組后,使用delete[] array釋放這塊內存。這種動態內存分配方式使得我們可以根據實際需求靈活地分配內存,避免了靜態數組大小固定的限制,就像根據實際需要在倉庫中租用不同大小的存儲空間。
②對象管理:在面向對象編程中,堆常用于存儲實例化的對象,因為對象的生命周期可能超出函數調用的范圍。例如,在 Java 中創建一個Person類的對象:
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public void introduce() {
System.out.println("我叫" + name + ",今年" + age + "歲。");
}
}
public class Main {
public static void main(String[] args) {
Person person = new Person("張三", 20);
person.introduce();
}
}在這個例子中,使用new Person("張三", 20)創建了一個Person對象,并將其存儲在堆中。person是一個引用變量,存儲在堆棧中,它指向堆中的Person對象。由于對象的生命周期可能在程序的多個部分使用,使用堆內存可以確保對象在需要時一直存在,就像為對象在倉庫中找到了一個長期的存儲位置。
③大型數據存儲:當需要存儲大型數據,如大型圖像、文件數據等,堆的內存空間相對較大,適合處理這些大塊數據。以存儲一個大型圖像數據為例,假設我們有一個高分辨率的圖像,其像素數據量非常大。我們可以在堆中分配足夠的內存來存儲這些像素數據,如下是一個簡化的 Python 示例,使用numpy庫來處理圖像數據(實際圖像存儲會更復雜):
import numpy as np
# 假設圖像的寬度和高度
width = 1920
height = 1080
# 在堆中分配內存存儲圖像數據(這里簡單假設為三維數組表示RGB圖像)
image_data = np.zeros((height, width, 3), dtype=np.uint8)
# 模擬對圖像數據進行處理
for i in range(height):
for j in range(width):
image_data[i][j][0] = 255 # 紅色通道全為255,模擬紅色圖像
# 這里省略圖像保存等操作在這個示例中,我們使用numpy庫在堆中分配了一個三維數組來存儲圖像的像素數據。由于圖像數據量較大,使用堆內存可以滿足存儲需求,并且方便對數據進行處理,就像使用一個大型倉庫來存放大型貨物并進行加工處理。
Part5性能對比:誰才是 “速度之王”
在計算機編程的世界里,性能是衡量程序優劣的重要指標之一。堆棧和堆在性能方面有著顯著的差異,這些差異決定了它們在不同場景下的適用性。接下來,我們就來深入探討一下堆棧和堆的性能特點。
5.1堆棧:高效的 “短跑健將”
堆棧的操作速度猶如閃電,這得益于它簡單而高效的內存管理方式。堆棧遵循 “后進先出”(LIFO)的原則,數據的入棧和出棧操作只需要在棧頂進行,就像在一摞盤子的頂部快速地放上或拿走盤子一樣。這種操作方式使得堆棧的時間復雜度幾乎為 O (1),也就是說,無論堆棧中已經存儲了多少數據,進行一次入棧或出棧操作所需的時間基本是固定的,不受數據量的影響。
例如,在一個函數調用過程中,當函數被調用時,系統會迅速在堆棧的頂部為其分配一塊內存空間,用于存儲函數的局部變量、參數和返回地址等信息,這個過程就是入棧操作。當函數執行結束后,這些信息會從堆棧的頂部被快速移除,即出棧操作。整個過程非常迅速,幾乎不需要額外的計算開銷。
5.2堆:靈活但稍顯 “遲緩” 的 “長跑選手”
堆的內存管理相對復雜,這使得它在性能上與堆棧相比稍遜一籌。堆的內存分配和釋放需要進行一系列的操作,包括查找合適的空閑內存塊、分割內存塊、合并內存塊等。這些操作涉及到對內存鏈表的遍歷和更新,因此堆的操作時間復雜度較高,通常為 O (n) 或 O (log n),其中 n 表示堆中元素的數量。
以從堆中分配一塊內存為例,系統需要在堆的空閑內存鏈表中搜索一塊大小合適的內存塊。如果沒有找到完全匹配的內存塊,可能還需要對找到的內存塊進行分割,以滿足分配需求。這個過程需要遍歷鏈表中的多個節點,隨著堆中元素數量的增加,搜索時間也會相應增加。
在釋放內存時,堆也需要進行一些額外的操作。如果釋放的內存塊與相鄰的空閑內存塊相鄰,系統需要將它們合并成一個更大的空閑內存塊,以減少內存碎片的產生。這個合并過程同樣需要對內存鏈表進行更新和調整,也會消耗一定的時間。
5.3性能對比圖表
為了更直觀地展示堆棧和堆在性能上的差異,我們來看下面這張圖表:
操作類型 | 堆棧時間復雜度 | 堆時間復雜度 |
入棧 / 出棧 | O(1) | - |
分配內存 | - | O (n) 或 O (log n) |
釋放內存 | - | O (n) 或 O (log n) |
從圖表中可以清晰地看出,堆棧在入棧和出棧操作上具有明顯的性能優勢,而堆在內存分配和釋放操作上的時間復雜度相對較高。
堆棧就像是一位敏捷的短跑健將,在處理函數調用和臨時數據存儲等場景時,能夠以極快的速度完成操作;而堆則更像是一位耐力十足的長跑選手,雖然在速度上不及堆棧,但它的靈活性使得它在需要動態分配內存和存儲長生命周期數據的場景中發揮著重要作用。在實際編程中,我們需要根據具體的需求和場景,合理地選擇使用堆棧或堆,以實現程序性能的最優化。
最后,給大家留一個思考問題:在一個大型項目中,如何結合堆棧和堆的特點,設計出更合理的內存管理方案,以提高項目的整體性能和穩定性呢?
































