C++記憶體分區模型

来源:https://www.cnblogs.com/sarexpine/archive/2023/07/23/17576037.html
-Advertisement-
Play Games

當涉及C++記憶體分區模型時,我們必須理解棧、堆和全局/靜態存儲區的概念。棧用於存儲函數調用和局部變數,堆用於動態記憶體分配,而全局/靜態存儲區用於全局變數和靜態變數。同時,我們還探討了棧幀重用現象,它可能在函數調用時導致局部變數地址重疊。瞭解這些記憶體分區的特點和優化行為,可以幫助我們編寫高效、可靠的C... ...


C++記憶體分區模型

在執行C++程式的過程中,記憶體大致分為四個區域:

  1. 棧區(Stack):用於實現函數調用。由編譯器自動分配釋放,存放函數的參數值和局部變數等

  2. 堆區(Heap):用於存放動態分配的變數。由程式員動態分配和釋放,使用new和delete操作符

  3. 全局/靜態存儲區(Data Segment & BSS Segment):存放全局變數和靜態變數,程式結束時釋放

    數據段 Data Segment (全局/靜態存儲區) : 存放初始化了的全局變數和靜態變數

    BSS段 BSS Segment : 用於存放未初始化的全局變數和靜態變數,節省空間,實際上不占用磁碟空間

  4. 代碼區(Text Segment):通常也被稱為文本區或只讀區。存放程式的二進位代碼和常量,代碼段是只讀的,可以被多個進程共用

也有人認為 常量存儲區 在記憶體中是獨立的,C++標準並沒有明確將常量存儲區單獨列為記憶體分區模型的一部分。因此,記憶體分區模型的確切細節可以根據不同的觀點和上下文而有所不同

註意:不同的操作系統對程式記憶體的管理和劃分會有所不同。上述的C++記憶體區域劃分主要是針對通用的情況,並不限定在某個特定操作系統上

1. 代碼區

代碼區 (Code Segment) 也被稱為文本段 (Text Segment) 或者只讀區,主要包括可以執行的文件 ELF (Executable and Linkable Format) 和常量

代碼區(Code Segment)也被稱為文本段(Text Segment)或只讀區,它有以下幾個主要特征:

  1. 代碼區是程式的靜態存儲區域,存放程式執行所需的機器指令和常量數據,如字元串字面量和 const 變數的初始化值
  2. 代碼區的內容為只讀屬性,不允許修改程式中的代碼,保障程式的安全性和穩定性
  3. 程式運行前,代碼區的記憶體大小已在可執行文件中確定,載入時系統會自動分配適當的記憶體
  4. 多個進程可以共用相同的代碼區,節省記憶體空間
  5. 為提高效率,一些字元串字面量 (例如 "Hello") 可能會存放在共用的只讀數據區而不是代碼區
  6. 若代碼區中的常量初始化需要運行時計算,會將其放在數據區而不是代碼區
  7. 代碼區也包含只讀數據,如跳轉表和常量表等
  8. 程式運行時,代碼區通常不會改變大小,若需擴展,依賴操作系統提供的機制

2. 全局/靜態存儲區

全局/靜態存儲區主要包括以下兩個部分:

  1. 數據段(Initialized Data Segment)

    • 用於存放初始化了的全局變數和靜態變數

    • 存儲在此段的數據在程式運行前分配,運行結束後釋放

    • 有初始化值的全局變數和靜態變數存放在此

    • 數據段屬於可讀可寫區域

  2. BSS段(Block Started by Symbol)

    • 用於存放未初始化的全局變數和靜態變數

    • 不占用實際的磁碟空間,只在編譯時預留記憶體空間

    • 無初始化值的全局變數和靜態變數存放在此

    • 程式啟動時會自動初始化為預設值

    • 屬於可讀寫區域

兩者的主要區別在於初始化狀況。全局變數和靜態變數可以顯式初始化,如果沒有顯式初始化,它們會被自動初始化為預設值(0 或 nullptr,取決於變數的類型),該區域的數據在程式整個運行周期中一直存在

3. 棧區

棧區是用於實現函數調用和局部變數存儲的一種記憶體區域。在 C++ 中,每當調用一個函數時,系統會自動在棧區為該函數分配一塊記憶體,稱為棧幀(Stack Frame)。棧幀用於存儲函數的參數值、局部變數以及函數執行期間的一些控制信息 ^f6a053

以下是棧區的一些關鍵特點:

  1. LIFO(Last-In-First-Out)原則:棧區採用後進先出的原則,即最後壓入棧的數據會最先彈出。這是因為每次調用函數時,會將函數的棧幀壓入棧頂,函數執行結束後,棧幀會從棧頂彈出
  2. 自動分配和釋放:棧區的記憶體分配和釋放是由編譯器自動管理的,當進入函數時,會為該函數分配一塊連續的記憶體區域,併在函數返回時自動釋放這塊記憶體。這樣的自動分配和釋放使得棧區的記憶體管理相對高效,但也意味著棧上的數據生命周期必須在函數調用內部
  3. 局部變數存儲:棧區主要用於存儲函數的局部變數,這些變數的生命周期與函數的調用和返回相對應。當函數調用結束,棧幀會被銷毀,其中的局部變數也會被銷毀,因此在函數外部無法訪問這些局部變數
  4. 函數調用:當調用函數時,函數的參數值和返回地址會被壓入棧幀中,函數執行過程中的其他局部變數也會存儲在棧幀中。函數返回時,棧幀會從棧頂彈出,恢復調用函數的現場
  5. 棧溢出:棧區的大小是有限的,如果遞歸調用過深或者函數中使用了大量的局部變數,可能導致棧溢出(Stack Overflow)錯誤,即棧區的記憶體已被耗盡
  6. 線程私有:每個線程都有自己的棧區,棧區的記憶體是線程私有的,不同線程之間的棧區不共用

3.1 棧溢出

棧溢出(Stack Overflow)是指程式在運行過程中,不斷調用函數,導致棧空間被占滿,無法再分配新的棧幀。棧是一種先進後出的數據結構,用於存儲局部變數、參數、返回地址等信息。棧的大小是有限的,一般為8~10MB。棧溢出通常發生在遞歸調用過深或死迴圈的情況下

以下是一個程式在棧區的示例圖。在主函數調用自定義的函數,函數內部形成遞歸

img

當程式一直運行時,遞歸的函數會不斷占用棧區的記憶體空間,從而導致 棧溢出

img

3.2 緩衝區溢出

在C++中,緩衝區溢出通常發生在棧區。棧區用於存儲函數調用時的局部變數和函數調用的返回地址

當函數調用時,函數的棧幀(包含局部變數和其他控制信息)被壓入棧中。如果函數中使用了緩衝區(數組)並且沒有進行足夠的邊界檢查,很容易導致寫入超出緩衝區邊界的數據,從而覆蓋棧上其他變數和控制信息,這就是緩衝區溢出

例如以下代碼就是一個緩衝區溢出的案例:

#include <iostream>

int main()
{
    //創建並初始化數組
    char arr[3] = {'a', 'a', 'a'};

    //向數組寫入數據,但超出了數組的邊界
    for (int i = 0; i <= 4; i++)
    {
        arr[i] = 'b';
    }
}

我們在合適的位置添加調試斷點,並對數組arr添加監視,觀察記憶體變化

img

找到數組對應的地址,觀察記憶體中的變化

img

按下F11逐語句調試,發現數組越界後仍然向未被允許訪問的記憶體中寫入數據

img

顯示數組所申請的記憶體附近堆棧被損壞,這就是緩衝區溢出的危害

img

參考:緩衝區溢出原理——作者:程式員老馬

3.3 數據不穩定

棧區中的局部變數生命周期與函數的調用和返回相關。當函數返回後,棧幀會被銷毀,其中的局部變數也會被銷毀。如果在函數內部使用了指向棧區局部變數的指針,併在函數返回後繼續使用這些指針,會導致懸空指針(Dangling Pointers)也就是野指針的問題,訪問已被銷毀的局部變數的記憶體區域,可能引發未定義的行為

下麵是示例代碼,證明棧幀銷毀後局部變數也被銷毀,造成懸掛指針的問題:

#include <iostream>

int* dangPtr;

void test() {

	int x = 10;
	dangPtr = &x;

	std::cout << "dangPtr in test: " << *dangPtr << std::endl;
}

int main() {

	test();
	std::cout << "dangPtr in main: " << *dangPtr << std::endl;

	return 0;
}

在函數test調用完成之後,此時test函數中的所有局部變數已經被銷毀了。這時候指針dangPtr指向的變數x的記憶體地址被系統釋放,指針變為懸空指針 (Dangling Pointers) 也就是野指針,這在記憶體中是非法的,引發程式中斷

img

此時函數還未結束,局部變數x還存在沒有被系統銷毀

img

此時test函數執行結束,局部變數被系統銷毀,同樣的變數x所存的值也被銷毀

這時候指針解引用訪問地址後的值就變成了野指針,非法訪問未申請的記憶體

img

3.4 棧幀重用(Stack Frame Reuse)

棧幀重用(Stack Frame Reuse) 是一種編譯器優化技術,旨在減少函數調用時的棧記憶體分配和釋放開銷。它是通過在不同函數調用中復用棧空間來實現的,使得多個函數調用可以共用同一塊棧空間

除了"棧幀重用",這種優化技術也可能被稱為其他類似的術語,例如:

  1. 棧記憶體優化(Stack Memory Optimization):指代對棧記憶體的優化措施,其中棧重用是可能的一種優化方式
  2. 棧空間復用(Stack Space Reuse):強調的是復用棧空間,使得多個函數調用可以共用同一塊棧空間

棧幀重用是一種現象,出現在自動變數(由編譯器自動分配並回收棧上生命周期僅在函數內的變數)位於函數的棧幀上時。在函數執行過程中,每個函數都會為其局部變數在棧上分配記憶體空間,併在函數結束時釋放這些棧記憶體。當後續函數再次向棧上請求記憶體時,有可能會獲得先前函數釋放的棧記憶體。如果後續函數中出現了和先前函數相同名稱和類型的局部變數,那麼這些變數的地址可能會重疊,即它們在同一片棧記憶體區域中

棧區採用自動管理機制,編譯器可能出於效率考慮而選擇復用先前函數釋放的棧空間。這種自動的棧幀重用特性對於那些只關心變數生命周期而不依賴於其地址的代碼來說,並不會產生影響

#include <iostream>

void funcA() {
    int a = 10;
    std::cout << "Address of variable 'a' in funcA: " << &a << std::endl;
}

void funcB() {
    int b = 20;
    std::cout << "Address of variable 'b' in funcB: " << &b << std::endl;
}

int main() {
    funcA();
    funcB();
    return 0;
}

輸出結果:

Address of variable 'a' in funcA: 0000009F4A6FF884
Address of variable 'b' in funcB: 0000009F4A6FF884

img

如果確實需要保證變數地址不變,可以採用以下方法:

  1. 使用 static 關鍵字:在函數內部聲明 static 局部變數,這樣變數的生命周期將持續到程式的整個執行過程,而不是只在函數執行時存在
  2. 將變數置於堆或全局區中:通過使用動態記憶體分配(例如 newmalloc 等)在堆上分配記憶體,或者將變數聲明為全局變數,可以保證其地址在函數調用之間不變

需要註意的是,棧幀重用是編譯器的一種優化行為,開發者不需要顯式地實現它。瞭解棧幀重用的概念有助於理解編譯器的優化機制,但在實際編程中,重要的是編寫簡潔、易讀和正確的代碼,而非過度關註微觀優化。優化應該基於實際的性能分析和需求。

4. 堆區

當涉及堆區時,需要理解動態記憶體分配的概念。堆區是程式運行時用於存儲動態分配的數據的一部分記憶體,它的管理與棧區不同

  1. 動態記憶體分配:在堆區進行動態記憶體分配意味著程式員可以在運行時請求額外的記憶體空間來存儲數據。與棧區不同,棧區的記憶體是在編譯時自動分配和釋放的,而堆區的記憶體是在運行時手動申請併在不再使用時手動釋放

  2. 操作符new和delete:在C++中,使用new操作符可以在堆區動態地分配記憶體。new返回所分配記憶體的指針,該指針指向存儲分配的數據的堆區記憶體。而使用delete操作符可以釋放堆區記憶體並將其返回給操作系統,以便其他程式可以使用。註意,堆區記憶體的釋放是程式員的責任,避免記憶體泄漏

    cppCopy codeint* ptr = new int; // 分配一個int大小的堆區記憶體
    *ptr = 10; // 將值10存儲在堆區記憶體中
    delete ptr; // 釋放堆區記憶體
    
  3. 生命周期:堆區的生命周期由程式員控制。在使用new分配記憶體後,記憶體將一直存在,直到使用delete釋放記憶體或程式結束。如果忘記釋放記憶體,將導致[[^記憶體泄漏]],堆區的記憶體將永遠無法回收,直到程式結束

  4. 不穩定性:由於堆區的記憶體是手動管理的,如果程式員使用指針錯誤地引用已釋放的堆區記憶體(懸空指針),或者釋放後繼續訪問已釋放的記憶體(野指針),會導致不穩定性和未定義行為

  5. 堆區碎片:隨著時間的推移,動態記憶體的頻繁分配和釋放可能導致堆區出現碎片。堆區碎片是指堆中剩餘的不連續、無法利用的小塊記憶體。雖然這不會直接影響程式的正確性,但在某些情況下,可能會降低記憶體的利用率

  6. 線程共用:堆區記憶體可以線上程之間共用,多個線程可以訪問和使用堆區的相同記憶體。這使得堆區在多線程編程中非常有用,但也需要註意同步和避免競爭條件

  7. 異常安全性:由於堆區記憶體的手動管理,需要特別註意異常安全性。如果在使用new分配記憶體後,出現異常而未能釋放記憶體,可能導致記憶體泄漏。為了確保異常安全性,可以使用智能指針等資源管理工具來管理動態記憶體,避免手動釋放記憶體的繁瑣工作

4.1 記憶體溢出

記憶體溢出(Out of Memory)是指程式在運行過程中,創建了大量的對象或線程,導致堆空間或方法區空間被占滿,無法再分配新的記憶體

堆和方法區是兩種動態分配的記憶體區域,用於存儲對象實例、類信息、常量等信息。堆和方法區的大小是可配置的,一般為幾百MB到幾GB。記憶體溢出通常發生在對象或線程泄漏、記憶體分配過大、GC效率低下的情況下

#include <iostream>

int main() {
    // 請求分配一個非常大的整數數組(堆區記憶體)
    // 這裡如果開闢單個數組沒辦法報錯
    // 所以為了模擬更確切的情況,假設就在程式中一次性開闢很大的空間
    int* hugeArray0 = new int[999999999];
    int* hugeArray1 = new int[999999999];
    int* hugeArray2 = new int[999999999];
    int* hugeArray3 = new int[999999999];

    // 使用堆區記憶體,可能導致記憶體溢出
    for (int i = 0; i < 999999999; i++) {
        hugeArray0[i] = i;
    }

    // 不要忘記釋放堆區記憶體
    delete[] hugeArray0;

    system("pause");
    return 0;
}

img

img

記憶體分配失敗,拋出 std::bad_alloc 異常並終止執行,而且可以看到右側進程記憶體已經快達到極限了

img

4.2 記憶體泄漏

堆區的記憶體泄漏是指在程式中動態分配了記憶體(使用 newmalloc 等操作符),但在不再需要這些記憶體時未及時釋放,導致程式無法再訪問這些記憶體,從而造成了資源浪費

記憶體泄漏是一種常見的編程錯誤,特別是在長時間運行的程式中,如果頻繁地分配記憶體而不釋放,最終可能會耗盡可用記憶體,導致程式崩潰或系統變慢

#include <iostream>

int main() {
    while (true) {
        // 在堆區動態分配一個整數數組,但未釋放記憶體
        int* ptr = new int[1000];
        // 未釋放記憶體,重覆分配,導致記憶體泄漏
    }

    return 0;
}

img

4.3 多重釋放與非法指針

多重釋放:在堆區釋放了記憶體後,如果再次嘗試釋放相同的記憶體,就會導致多重釋放問題。這會破壞堆的記憶體管理結構,可能導致程式崩潰

#include <iostream>

int main()
{
	int* arr = new int[10];

	delete[] arr;
	delete[] arr;

	system("pause");
	return 0;
}

img

img

非法指針:在堆區釋放了記憶體後,如果仍然保留指向該記憶體的指針,就會導致懸空指針。而指向未初始化或已釋放的記憶體的指針稱為野指針。使用懸空指針或野指針可能導致程式崩潰或產生不可預測的行為

#include <iostream>

int main()
{
	char* ch = new char[10];
	for (int i = 0; i < 10; i++)
	{
		ch[i] = 'a';
	}
	char* ptr = ch;

	std::cout << ptr << std::endl;
	delete[] ch;
	std::cout << *ptr << std::endl;
	std::cout << *ptr << std::endl;

	return 0;
}

img

img

4.4 產生堆記憶體碎片

記憶體塊碎片化: 多次動態分配和釋放堆區記憶體可能導致記憶體塊碎片化,使得分配大塊連續記憶體變得困難,從而降低堆區記憶體的效率

#include <iostream>

int main() {
    const int N = 1000;
    const int M = 100;

    // 創建一個數組用於存儲指向動態分配的記憶體塊的指針
    int* ptrArray[N];

    // 模擬多次動態分配和釋放記憶體
    for (int i = 0; i < N; i++) {
        // 每次動態分配 M 個 int 大小的記憶體塊
        ptrArray[i] = new int[M];

        // 將記憶體塊清零,以模擬實際使用場景
        for (int j = 0; j < M; j++) {
            ptrArray[i][j] = 0;
        }
    }

    // 輸出每個記憶體塊的首地址,用於觀察地址分佈情況
    for (int i = 0; i < N; i++) {
        std::cout << "Block " << i + 1 << ": " << ptrArray[i] << std::endl;
    }

    // 釋放動態分配的記憶體
    for (int i = 0; i < N; i++) {
        delete[] ptrArray[i];
    }

    return 0;
}

img

參考:C 程式記憶體管理中,容易使記憶體碎片化的 <堆>——作者:考鼎錄


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 首先來看一下需要操作的函數,以及配置的步驟: 圖1 圖2 Code: usart.c #include "usart.h"void ustart_Init(void ){ GPIO_InitTypeDef GPIO_Init_Ustar ; // 定義輸出埠TX的結構體對象 USART_InitT ...
  • ## 一、mysql安裝 在配置Hive之前一般都需要安裝和配置MySQL,因為Hive為了能操作HDFS上的數據集,那麼他需要知道數據的切分格式,如行列分隔符,存儲類型,是否壓縮,數據的存儲地址等信息。 為了方便以後操作所以他需要將這些信息通過一張表存儲起來,然後將這張表(元數據)存儲到mysql ...
  • 【JavaScript寫法】數組去重 在進行項目開發的時候,有時候需要把一些前端的數組進行去重處理,得到一個去重後的數據,然後再進行相關的操作,這也是在前端面試中經常出現的問題 ...
  • - Vue 初始化 - 模板渲染 - 組件渲染 為了便於理解,本文將從以下兩個方面進行探索: - 從 Vue 初始化,到首次渲染生成 DOM 的流程。 - 從 Vue 數據修改,到頁面更新 DOM 的流程。 # Vue 初始化 先從最簡單的一段 Vue 代碼開始: """ {{ message }} ...
  • 博客推行版本更新,成果積累制度,已經寫過的博客還會再次更新,不斷地琢磨,高質量高數量都是要追求的,工匠精神是學習必不可少的精神。因此,大家有何建議歡迎在評論區踴躍發言,你們的支持是我最大的動力,你們敢投,我就敢肝 ...
  • 一、前言 常見的DDD實現架構有很多種,如經典四層架構、六邊形(適配器埠)架構、整潔架構(Clean Architecture)、CQRS架構等。架構無優劣高下之分,只要熟練掌握就都是合適的架構。本文不會逐個去講解這些架構,感興趣的讀者可以自行去瞭解。 本文將帶領大家從日常的三層架構出發,精煉推導 ...
  • ## 介紹 ### 快速概覽 `settings.xml`文件中的 `settings` 元素包含用於定義以各種方式配置Maven執行的值的元素,如`pom.xml`,但不應綁定到任何特定項目或分發給受眾。這些值包括本地倉庫位置、備用遠程倉庫伺服器和身份驗證信息。 `settings.xml`文件可 ...
  • 搭建多Master多Slave模式(同步)集群時的java.lang.NullPointerException異常 一、運行環境等基本描述(問題產生原因是許可權問題,即許可權不夠導致無法啟動broker,甚至broker線程無法通過jps命令查出。下麵闡述分析思路) 1.1)操作系統:Linux 虛擬機 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...