1. 程式人生 > >【C 語言】記憶體管理 ( 動態記憶體分配 | 棧 | 堆 | 靜態儲存區 | 記憶體佈局 | 野指標 )

【C 語言】記憶體管理 ( 動態記憶體分配 | 棧 | 堆 | 靜態儲存區 | 記憶體佈局 | 野指標 )

一. 動態記憶體分配

1. 動態記憶體分配相關概念

動態記憶體分配 :

  • 1.C語言操作與記憶體關係密切 : C 語言中的所有操作都與記憶體相關 ;
  • 2.記憶體別名 : 變數 ( 指標變數 | 普通變數 ) 和 陣列 都是在 記憶體中的別名 ;
    • ( 1 ) 分配記憶體的時機 : 在編譯階段, 分配記憶體,
    • ( 2 ) 誰來分配記憶體 : 由編譯器來進行分配 ;
    • ( 3 ) 示例 : 如 定義陣列時必須指定陣列長度, 陣列長度在編譯的階段就必須指定 ;
  • 3.動態記憶體分配的由來 : 在程式執行時, 除了編譯器給分配的一些記憶體之外, 可能還需要一些額外記憶體才能實現程式的邏輯, 因此在程式中可以動態的分配記憶體 ;

2. 動態記憶體分配 相關方法 ( malloc | free | calloc | realloc )

(1) 相關 方法簡介

動態記憶體分配方法 :

  • 1.申請記憶體 : 使用 malloc 或 calloc 或 realloc 申請記憶體;
  • 2.歸還記憶體 : 使用 free 歸還 申請的記憶體 ;
  • 3.記憶體來源 : 系統專門預留一塊記憶體, 用來響應程式的動態記憶體分配請求 ;
  • 4.記憶體分配相關函式 :
       #include <stdlib.h>

       void *malloc(size_t size);
       void
free(void *ptr); void *calloc(size_t nmemb, size_t size); void *realloc(void *ptr, size_t size);
 - ***( 1 ) malloc*** : 單純的申請指定位元組大小的動態記憶體, 記憶體中的值不管 ; 
 - ***( 2 ) calloc*** : 申請 指定元素大小 和 元素個數的 記憶體, 並將每個元素初始化為 0 ;
 - ***( 3 ) realloc*** : 可以重置已經申請的記憶體大小 ; 

malloc 函式簡介 :

void *malloc
(size_t size);
  • 1.作用 : 分配一塊連續的記憶體, 單位 位元組, 該記憶體沒有具體的型別資訊 ;
  • 2.函式解析 :
    • ( 1 ) size_t size 引數 : 傳入一個位元組大小引數 , size 是要分配的記憶體的大小 ;
    • ( 2 ) void 返回值 : 返回一個 void 指標, 需要強制轉換為指定型別的指標 , 該指標指向記憶體的首地址 ;
  • 3.請求記憶體大小 : malloc 實際請求的記憶體大小可能會比 size 大一些, 大多少與編譯器和平臺先關 , 這點知道即可, 不要應用到程式設計中 ;
  • 4.申請失敗 : 系統為程式預留出一塊記憶體用於 在程式執行時 動態申請, 當這塊預留的記憶體用完以後, 在使用 malloc 申請, 就會返回 NULL ;

free 函式簡介 :

void free(void *ptr);
  • 1.作用 : 釋放 malloc 函式申請的 動態空間 ;
  • 2.函式解析 : 該函式沒有返回值 ;
    • ( 1 ) void *ptr 引數 : 要釋放的記憶體的首地址;
  • 3.傳入 NULL 引數 : 假如 free 方法傳入 NULL 引數, 則直接返回, 不會報錯 ;

calloc 函式簡介 :

void *calloc(size_t nmemb, size_t size);
  • 1.作用 : 比 malloc 先進一些, 可以申請 ① 指定元素個數 ② 指定元素大小 的記憶體 ;
  • 2.函式解析 :
    • ( 1 ) void 型別返回值 : 返回值是一個 void 型別, 需要轉換為實際的型別才可以使用 ;
    • ( 2 ) size_t nmemb 引數 : 申請記憶體的元素 個數 ;
    • ( 3 ) size_t size 引數 : 申請記憶體的元素 大小 ;
  • 3.記憶體中的值初始化 : calloc 分配動態記憶體後, 會將其中每個元素的值都初始化為 0 ;

realloc 函式簡介 :

void *realloc(void *ptr, size_t size);
  • 1.作用 : 重新分配一個已經分配並且未釋放的動態記憶體的大小 ;
  • 2.函式解析 :
    • ( 1 ) void 型別返回值* : 重新分配後的指標首地址, 與引數 ptr 指向的地址是相同的, 但是需要使用 返回的新地址 , 不能再使用老地址了 ;
    • ( 2 ) void *ptr 引數 : 指向 一塊已經存在的動態記憶體空間的首地址 ;
    • ( 3 ) size_t size 引數 : 需要分配的新記憶體大小 ;
  • 3.void *ptr 引數為 NULL : 如果傳入的 ptr 引數為 NULL, 那麼該函式執行效果與 malloc 一樣, 直接分配一塊新的動態記憶體, 並返回一個指向其首地址的指標 ;

(2) 程式碼示例 ( 動態記憶體分配簡單示例)

程式碼示例 :

  • 1.程式碼 :
#include <stdio.h>
#include <stdlib.h>

int main()
{
    //1. 使用 malloc 分配 20 個位元組的記憶體, 這些記憶體中的資料保持原樣
    int* p1 = (int*)malloc(sizeof(int) * 5);

    //2. 使用 calloc 分配 5 個 int 型別元素的 記憶體, 初始化 5 個元素的值為 0
    int* p2 = (int*)calloc(5, sizeof(int));

    //3. 以 int 型別 列印 p1 和 p2 指向的記憶體中的資料值
    int i = 0; 
    for(i = 0; i < 5; i ++)
    {
        printf("p1[%d] = %d, p2[%d] = %d\n", i, p1[i], i, p2[i]);
    }

    //4. 重新分配 p1 指向的記憶體, 在多分配 10 個數據;
    p1 = (int*) realloc(p1, 15);
    for(i = 0; i < 15; i ++)
    {
        printf("p1[%d] = %d\n", i, p1[i]);
    }

    return 0;
}
  • 2.編譯執行結果 :
    這裡寫圖片描述

二. 棧 堆 靜態儲存區

1. 棧

(1) 棧 相關概念

棧 簡介 :

  • 1.主要作用 : 維護 程式的 上下文 資訊, 主要是 區域性變數, 函式 的儲存 ;
  • 2.儲存策略 : 後進先出 ;

棧對函式的作用 :

  • 1.函式依賴於棧 : 棧記憶體中儲存了函式呼叫需要所有資訊 :
    • ( 1 ) 棧 儲存 函式引數 : 函式的引數都會依次入棧, 儲存在棧記憶體中 ;
    • ( 2 ) 棧 儲存 函式返回地址 : ebp 指標指向 返回地址, 函式執行完畢後跳轉到該返回地址 繼續執行下面的語句 ;
    • ( 3 ) 棧 儲存 資料 : 區域性變數儲存在棧記憶體中 ;
    • ( 4 ) 棧 儲存 函式呼叫的上下文 : 棧中儲存幾個地址, 包括 返回地址, old ebp 地址, esp指向棧頂地址 ;
  • 2.棧是高階語言必須的 : 如果沒有棧, 那麼就沒有函式, 程式則回退到彙編程式碼的樣子, 程式從頭執行到尾 ;

函式 棧記憶體 的幾個相關概念 :

  • 1.esp 指標 : esp 指標變數所在的地址不重要, 講解的全程沒有涉及到過, 重要的是 esp 指向的值, 這個值隨著 函式 入棧 出棧 一直的變 ;
    • ( 1 ) 入棧 : esp 上次指向的地址 放入 返回地址 中, 然後 esp 指向新的棧頂 ;
    • ( 2 ) 出棧 : 獲取 返回地址 中的地址, esp 指向 該獲取的地址 (獲取方式 通過 ebp 指標獲取);
  • 2.ebp 指標 : ebp 指標變數所在的地址不重要, 講解全過程中沒有涉及到, 重要的是 ebp 指向的值, 這個是隨著 函式 入棧 出棧 一直在變 ;
    • ( 1 ) 入棧 : 將 ebp 指標指向的地址 入棧, 並且 ebp 指向新的棧記憶體地址 ;
    • ( 2 ) 出棧 : ebp 回退一個指標即可獲取 返回地址 (這個返回地址供 esp 指標使用), 然後 ebp 獲取記憶體中的地址, 然後ebp 直接指向這個地址, 即回退到上一個函式的ebp地址;
  • 3.返回地址作用 : 指引 esp 指標回退到上一個函式的棧頂 ;
  • 4.ebp 地址作用 : 指引 ebp 指標會退到上一個函式的 ebp 地址, 獲取 esp 的返回地址 ;
  • 5.初始地址 : 最初的 返回地址 和 old ebp 地址值 是 棧底地址 ;

函式入棧流程 :

  • 1.引數入棧 : 函式的引數 存放到棧記憶體中 ;
  • 2.返回地址 入棧 : 每個函式都有一個返回地址, 這個返回地址是當前 esp 指標指向的地址, 即上一個函式的棧頂, 當出棧時 esp 還要指向這個地址用於釋放被彈出的函式佔用的棧空間 ;
  • 3.old esp 入棧 : old esp 是上一個 esp 指標指向的地址, 將這個地址存入棧記憶體中, 並且 esp 指標指向這個棧記憶體的首地址 ( 這個棧記憶體是存放 old esp 的棧記憶體 ) ;
  • 4.資料入棧 : 暫存器 和 區域性變數資料 入棧 ;
  • 5.esp指向棧頂 : esp 指標指向當前的棧頂 ;
    這裡寫圖片描述

函數出棧流程 :

  • 1.esp 指標返回 : 根據 ebp 指標 獲取 返回地址, esp 直接指向這個返回地址 ;
    • ebp 獲取 返回地方方式 : ebp 指向返回地址的下一個指標, ebp 指標回退一個指標 即可獲取 返回地址 的指標, 然後獲取指標指向的內容 即返回地址 ;
  • 2.ebp 指標返回 : 獲取 ebp 指標指向的記憶體中的資料, 這個資料就是上一個ebp指向的記憶體地址值, ebp 指向這個地址值, 即完成操作 ;
  • 3.釋放棧空間 : 隨著 esp 和 ebp 指標返回, 棧空間也隨之釋放了 ;
  • 4.繼續執行函式體 : 從函式2返回函式1後, 繼續執行該函式1的函式體 ;
    這裡寫圖片描述

(2) 程式碼示例 ( 簡單的函式呼叫的棧記憶體分析 )

程式碼示例 :

  • 1.程式碼 :
#include <stdio.h>

void fun1(int i)
{
}

int fun2(int i)
{
    fun1();
    return i;
}

/*
    分析棧記憶體 入棧 出棧 esp ebp 指標操作; 

    程式開始執行, 目前 棧 中是空的, 棧底沒有資料 ; 

    注意點 : 
    1. esp 指標 : esp 指標變數所在的地址不重要, 講解的全程沒有涉及到過, 重要的是 esp 指向的值, 這個值隨著 函式 入棧 出棧 一直的變 ; 
        ( 1 ) 入棧 : esp 上次指向的地址 放入 返回地址 中, 然後 esp 指向新的棧頂 ; 
        ( 2 ) 出棧 : 獲取 返回地址 中的地址, esp 指向 該獲取的地址 (獲取方式 通過 ebp 指標獲取); 
    2. ebp 指標 : ebp 指標變數所在的地址不重要, 講解全過程中沒有涉及到, 重要的是 ebp 指向的值, 這個是隨著 函式 入棧 出棧 一直在變 ;
        ( 1 ) 入棧 : 將 ebp 指標指向的地址 入棧, 並且 ebp 指向新的棧記憶體地址 ; 
        ( 2 ) 出棧 : ebp 回退一個指標即可獲取 返回地址 (這個返回地址供 esp 指標使用), 然後 ebp 獲取記憶體中的地址, 然後ebp 直接指向這個地址, 即回退到上一個函式的ebp地址;             
    3. 返回地址作用 : 指引 esp 指標回退到上一個函式的棧頂 ; 
    4. ebp 地址作用 : 指引 ebp 指標會退到上一個函式的 ebp 地址, 獲取 esp 的返回地址 ;  
    5. 初始地址 : 最初的 返回地址 和 old ebp 地址值 是 棧底地址 ; 

    1. main 函式執行
        ( 1 ) 引數入棧 : 將 引數 放入棧中, 此時 main 函式 引數 在棧底 ;
        ( 2 ) 返回地址入棧 : 然後將 返回地址 放入棧中, 返回地址是 棧底地址 ;
        ( 3 ) ebp 指標入棧 : 將 old ebp 指標入棧, ebp 指標指向 old ebp 存放的地址 address1 , 這個 address1 是 棧底地址; 
        ( 3 ) 資料入棧 :  ( 區域性變數, 暫存器的值 等 ) ; 
        ( 4 ) esp 指向棧頂 : esp 指標指向 棧頂 (即資料後面的記憶體首地址), 此時棧頂資料 address2;
        ( 5 ) 資料總結 : main 的棧中 棧底 到 棧頂 的資料 : main 引數 -> 返回地址 -> old ebp -> 資料
        ( 6 ) 執行函式體 : 開始執行 main 函式的函式體, 執行 fun1 函式, 下面是 棧 中記憶體變化 : 

    2. 呼叫 fun1 函式, 繼續將 fun1 函式內容入棧 : 
        ( 1 ) 引數入棧 : 將 fun1 引數 入棧 
        ( 2 ) 返回地址入棧 : 存放一個返回地址, 此時存放的是棧頂的值 address2 地址, 返回的時候通過 ebp 指標回退一個讀取 ;
        ( 3 ) ebp 指標入棧 : old ebp (上次 ebp 指標指向的地址) 指標指向的地址值入棧, 該指標指向 address1 地址, 即 ebp 指標上一次指向的位置, 
                該棧記憶體中存放 ebp 指標上次指向的地址 address1, 這段存放 address1 的記憶體首地址為 address3,
                ebp 指標指向 address3 , 即 ebp 指標變數儲存 address3 的地址值, 棧記憶體中的 address3 存放 address1 地址 ; 
        ( 3 ) 資料入棧 : 存放資料 (區域性變數) 
        ( 4 ) esp 指向棧頂 : esp 指向 棧頂 
        ( 5 ) 執行函式體 : 開始執行 fun1 函式體內容, 執行結束後需要出棧 返回 ;

    3. fun1 函式執行完畢, 開始 退棧 返回 操作 : 
        ( 1 ) 獲取返回地址 : 返回地址存放在 ebp 的上一個指標地址, ebp 指向 返回地址的尾地址, 
                ebp 回退一個指標位置即可獲取返回地址 , 此時的返回地址是 address2 上面已經描述過了 ;
        ( 2 ) esp 指標指向 : esp 指向 address2, 即將 esp 指標變數的值 設定為 address2 即可 ;
        ( 3 ) ebp 指標指向 : 
                    獲取上一個 ebp 指向的地址 : 當前 ebp 指向的記憶體中儲存了上一個 ebp 指向的記憶體地址, 獲取這個地址;
                    ebp 指向這個剛獲取的地址 ; 
        ( 4 ) 釋放棧空間 : 將 esp 指標指向的當前地址 和 之後的地址 都釋放掉 ; 

        ( 5 ) 執行 main 函式體 : 繼續執行 main 函式 函式體 , 然後執行 fun2 函式; 


    4. 執行 fun2 函式
        ( 1 ) 引數入棧 : fun2 函式引數入棧; 
        ( 2 ) 返回地址 入棧 : esp 指向的地址 存放到 返回地址中 ; 
        ( 3 ) ebp 地址入棧 : 將 ebp 指向的地址存放到棧記憶體中, ebp 指向 該段記憶體的首地址 (即返回地址的尾地址);
        ( 4 ) 資料入棧 : 將資料 入棧
        ( 5 ) esp 指向棧頂 : esp 指向 資料 的末尾地址 ; 

        ( 6 ) 執行函式體 : 執行 fun2 函式體時, 發現 fun2 中居然呼叫了 fun1, 此時又要開始將 fun1 函式入棧 ; 


    5. fun1 函式入棧
        ( 1 ) 引數入棧 : 將 fun1 引數入棧
        ( 2 ) 返回地址入棧 : esp 指向的 返回地址 存入棧記憶體 ; 
        ( 3 ) ebp 地址入棧 : 將 old ebp 地址 入棧, 並且 ebp 指標指向 該段 棧記憶體首地址 (即 返回地址 的尾地址);
        ( 4 ) 資料入棧 : 區域性變數, 暫存器值 入棧 ;  
        ( 5 ) esp 指標指向 : esp 指標指向棧頂 ; 

        ( 6 ) 執行函式體 : 繼續執行函式體, 執行完 fun1 函式之後, 函式執行完畢, 開始出棧操作 ; 

    6. fun1 函式 出棧
        ( 1 ) esp 指標返回 : 通過 ebp 讀取上一個指標, 獲取 返回地址, esp 指向 返回地址, 即上一個棧頂 ; 
        ( 2 ) ebp 指標返回 : 讀取 ebp 指標指向的記憶體中的資料, 這個資料是上一個 ebp 指標指向的地址值, ebp 指向這個地址值; 
        ( 3 ) 釋放棧空間 : 執行完這兩個操作後, 棧空間就釋放了 ; 

        ( 4 ) 執行函式體 : 執行完 fun1 出棧後, 繼續執行 fun2 中的函式體, 發現 fun2 函式體也執行完了, 開始 fun2 出棧 ; 

    7. fun2 函式 出棧 
        ( 1 ) esp 指標返回 : 通過 ebp 讀取上一個指標, 獲取 返回地址, esp 指向 返回地址, 即上一個棧頂 ; 
        ( 2 ) ebp 指標返回 : 讀取 ebp 指標指向的記憶體中的資料, 這個資料是上一個 ebp 指標指向的地址值, ebp 指向這個地址值; 
        ( 3 ) 釋放棧空間 : 執行完這兩個操作後, 棧空間就釋放了 ; 

        ( 4 ) 執行函式體 : 執行完 fun2 出棧後, 繼續執行 main 中的函式體, 如果 main 函式執行完畢, esp 和 ebp 都指向 棧底 ; 

*/
int main()
{
    fun1(1);
    fun2(1);

    return 0;
}
  • 2.編譯執行結果 : 沒有輸出結果, 編譯通過 ;

(3) 棧記憶體行為分析 ( 圖文分析版本 )

分析的程式碼內容 :

#include <stdio.h>

void fun1(int i)
{
}

int fun2(int i)
{
    fun1();
    return i;
}

int main()
{
    fun1(1);
    fun2(1);

    return 0;
}

程式碼 棧記憶體 行為操作 圖示分析 :

  • 1.main 函式執行 :
    • ( 1 ) 引數入棧 : 將 引數 放入棧中, 此時 main 函式 引數 在棧底 ;
      這裡寫圖片描述
    • ( 2 ) 返回地址入棧 : 然後將 返回地址 放入棧中, 返回地址是 棧底地址 ;
      這裡寫圖片描述
    • ( 3 ) ebp 指標入棧 : 將 old ebp 指標入棧, ebp 指標指向 old ebp 存放的地址 address1 , 這個 address1 是 棧底地址;
      這裡寫圖片描述
    • ( 4 ) 資料入棧 : ( 區域性變數, 暫存器的值 等 ) ;
      這裡寫圖片描述
    • ( 5 ) esp 指向棧頂 : esp 指標指向 棧頂 (即資料後面的記憶體首地址), 此時棧頂資料 address2;
      這裡寫圖片描述
    • ( 6 ) 資料總結 : main 的棧中 棧底 到 棧頂 的資料 : main 引數 -> 返回地址 -> old ebp -> 資料
    • ( 7 ) 執行函式體 : 開始執行 main 函式的函式體, 執行 fun1 函式, 下面是 棧 中記憶體變化 :
int main()
{
    fun1(1);
    fun2(1);

    return 0;
}
  • 2.呼叫 fun1 函式, 繼續將 fun1 函式內容入棧 :
    • ( 1 ) 引數入棧 : 將 fun1 引數 入棧 ;
      這裡寫圖片描述
    • ( 2 ) 返回地址入棧 : 存放一個返回地址, 此時存放的是棧頂的值 address2 地址, 返回的時候通過 ebp 指標回退一個讀取 ;
      這裡寫圖片描述
    • ( 3 ) ebp 指標入棧 : old ebp (上次 ebp 指標指向的地址) 指標指向的地址值入棧, 該指標指向 address1 地址, 即 ebp 指標上一次指向的位置,
      該棧記憶體中存放 ebp 指標上次指向的地址 address1, 這段存放 address1 的記憶體首地址為 address3,
      ebp 指標指向 address3 , 即 ebp 指標變數儲存 address3 的地址值, 棧記憶體中的 address3 存放 address1 地址 ;
      這裡寫圖片描述
    • ( 4 ) 資料入棧 : 存放資料 (區域性變數) ;
      這裡寫圖片描述
    • ( 5 ) esp 指向棧頂 : esp 指向 棧頂 ;
      這裡寫圖片描述
    • ( 6 ) 執行函式體 : 開始執行 fun1 函式體內容, 執行結束後需要出棧 返回 ;
void fun1(int i)
{
}
  • 3.fun1 函式執行完畢, 開始 退棧 返回 操作 :
    • ( 1 ) 獲取返回地址 : 返回地址存放在 ebp 的上一個指標地址, ebp 指向 返回地址的尾地址,
      ebp 回退一個指標位置即可獲取返回地址 , 此時的返回地址是 address2 上面已經描述過了 ;
    • ( 2 ) esp 指標指向 : esp 指向 address2, 即將 esp 指標變數的值 設定為 address2 即可 ;
      這裡寫圖片描述
    • ( 3 ) ebp 指標指向 :
      獲取上一個 ebp 指向的地址 : 當前 ebp 指向的記憶體中儲存了上一個 ebp 指向的記憶體地址, 獲取這個地址;
      ebp 指向這個剛獲取的地址 ;
      這裡寫圖片描述
    • ( 4 ) 釋放棧空間 : 將 esp 指標指向的當前地址 和 之後的地址 都釋放掉 ;
      這裡寫圖片描述
    • ( 5 ) 執行 main 函式體 : 繼續執行 main 函式 函式體 , 然後執行 fun2 函式;
int main()
{
    fun1(1);
    fun2(1);

    return 0;
}
  • 4.執行 fun2 函式 :
    • ( 1 ) 引數入棧 : fun2 函式引數入棧;
      這裡寫圖片描述
    • ( 2 ) 返回地址 入棧 : esp 指向的地址 存放到 返回地址中 ;
      這裡寫圖片描述
    • ( 3 ) ebp 地址入棧 : 將 ebp 指向的地址存放到棧記憶體中, ebp 指向 該段記憶體的首地址 (即返回地址的尾地址);
      這裡寫圖片描述
    • ( 4 ) 資料入棧 : 將資料 入棧 ;
      這裡寫圖片描述
    • ( 5 ) esp 指向棧頂 : esp 指向 資料 的末尾地址 ;
      這裡寫圖片描述
    • ( 6 ) 執行函式體 : 執行 fun2 函式體時, 發現 fun2 中居然呼叫了 fun1, 此時又要開始將 fun1 函式入棧 ;
int fun2(int i)
{
    fun1();
    return i;
}
  • 5.fun1 函式入棧 :
    • ( 1 ) 引數入棧 : 將 fun1 引數入棧 ;
      這裡寫圖片描述
    • ( 2 ) 返回地址入棧 : esp 指向的 返回地址 存入棧記憶體 ;
      這裡寫圖片描述
    • ( 3 ) ebp 地址入棧 : 將 old ebp 地址 入棧, 並且 ebp 指標指向 該段 棧記憶體首地址 (即 返回地址 的尾地址);
      這裡寫圖片描述
    • ( 4 ) 資料入棧 : 區域性變數, 暫存器值 入棧 ;
      這裡寫圖片描述
    • ( 5 ) esp 指標指向 : esp 指標指向棧頂 ;
      這裡寫圖片描述
    • ( 6 ) 執行函式體 : 繼續執行函式體, 執行完 fun1 函式之後, 函式執行完畢, 開始出棧操作 ;
void fun1(int i)
{
}
  • 6.fun1 函式 出棧 :
    • ( 1 ) esp 指標返回 : 通過 ebp 讀取上一個指標, 獲取 返回地址, esp 指向 返回地址, 即上一個棧頂 ;
      這裡寫圖片描述
    • ( 2 ) ebp 指標返回 : 讀取 ebp 指標指向的記憶體中的資料, 這個資料是上一個 ebp 指標指向的地址值, ebp 指向這個地址值;
      這裡寫圖片描述
    • ( 3 ) 釋放棧空間 : 執行完這兩個操作後, 棧空間就釋放了 ;
      這裡寫圖片描述
    • ( 4 ) 執行函式體 : 執行完 fun1 出棧後, 繼續執行 fun2 中的函式體, 發現 fun2 函式體也執行完了, 開始 fun2 出棧 ;
int fun2(int i)
{
    fun1();
    return i;
}
  • 7.fun2 函式 出棧 :
    • ( 1 ) esp 指標返回 : 通過 ebp 讀取上一個指標, 獲取 返回地址, esp 指向 返回地址, 即上一個棧頂 ;
      這裡寫圖片描述
    • ( 2 ) ebp 指標返回 : 讀取 ebp 指標指向的記憶體中的資料, 這個資料是上一個 ebp 指標指向的地址值, ebp 指向這個地址值;
      這裡寫圖片描述
    • ( 3 ) 釋放棧空間 : 執行完這兩個操作後, 棧空間就釋放了 ;
      這裡寫圖片描述
    • ( 4 ) 執行函式體 : 執行完 fun2 出棧後, 繼續執行 main 中的函式體, 如果 main 函式執行完畢, esp 和 ebp 都指向 棧底 ;
      這裡寫圖片描述

2. 堆

(1) 標題3

堆 相關 概念 :

  • 1.棧的特性 : 函式執行開始時入棧, 在函式執行完畢後, 函式棧要釋放掉, 因此函式棧內的部分型別資料無法傳遞到函式外部 ;
  • 2.堆 空間 : malloc 動態申請記憶體空間, 申請的空間是作業系統預留的一塊記憶體, 這塊記憶體就是堆 , 程式可以自由使用這塊記憶體 ;
  • 3.堆 有效期 : 堆空間 從申請獲得開始生效, 在程式主動釋放前都是有效的, 程式釋放後, 堆空間不可用 ;

堆 管理 方法 :

  • 1.空閒連結串列法 ;
  • 2.點陣圖法 ;
  • 3.物件池法 ;

空閒連結串列法方案 :

  • 1.空閒連結串列圖示 : 表頭 -> 列表項 -> NULL ;
    這裡寫圖片描述
  • 2.程式申請堆記憶體 : int* p = (int*)malloc(sizeof(int)) ; 申請一個 4 位元組的堆空間, 從空閒連結串列中查詢能滿足要求的空間, 發現一個 5 位元組的空間, 滿足要求, 這裡直接將 5 位元組的空間, 分配給了程式 , 不一定要分配正好的記憶體給程式, 可能分配的記憶體比申請的要大一些 ;
    這裡寫圖片描述

  • 3.程式釋放堆記憶體 : 將 p 指向的記憶體插入到空閒連結串列中 ;

3. 靜態儲存區

(1) 標題3

靜態儲存區 相關概念 :

  • 1.靜態儲存區 內容 : 靜態儲存區用於儲存程式的靜態區域性變數 和 全域性變數 ;
  • 2.靜態儲存區大小 : 在程式編譯階段就可以確定靜態儲存區大小了, 將靜態區域性變數和全部變數 的大小相加即可 ;
  • 3.靜態儲存區 生命週期 : 程式開始執行時分配靜態儲存區, 程式執行結束後釋放靜態儲存區 ;
  • 4.靜態區域性變數 : 靜態區域性變數在程式執行過程中, 會一直儲存著 ;

總結 :
1.棧記憶體 : 主要儲存函式呼叫相關資訊 ;
2.堆記憶體 : 用於程式申請動態記憶體, 歸還動態記憶體使用 ;
3.靜態儲存區 : 用於儲存程式中的 全域性變數 和 靜態區域性變數 ;

三. 程式記憶體佈局

1. 程式執行前的程式檔案的佈局 ( 程式碼段 | 資料段 | bss段 )

(1) 相關概念簡介

可執行程式檔案的內容 : 三個段 是程式檔案的資訊, 編譯後確定 ;

  • 1.文字段 ( .text section ) : 存放程式碼內容, 編譯時就確定了, 只能讀, 不能寫 ;
  • 2.資料段 ( .data section ) : 存放 已經初始化的 靜態區域性變數 和 全域性變數, 編譯階段確定, 可讀寫 ;
  • 3.BSS段 ( .bss section ) : 存放 沒有初始化的 靜態區域性變數 和 全域性變數, 可讀寫 , 程式開始執行的時候 初始化為 0 ;

(2) 分析程式檔案的記憶體佈局

分析簡單程式的 程式檔案佈局 :

  • 1.示例程式碼 :
#include <stdio.h>

//1. 全域性的 int 型別變數, 並且進行了初始化, 存放在 資料段
int global_int = 666;
//2. 全域性 char 型別變數, 沒有進行初始化, 存放在 bss段
char global_char;

//3. fun1 和 fun2 函式存放在文字段
void fun1(int i)
{
}

int fun2(int i)
{
    fun1();
    return i;
}

int main()
{
    //4. 靜態區域性變數, 並且已經初始化過, 存放在 資料段;
    static int static_part_int = 888;
    //5. 靜態區域性變數, 沒有進行初始化, 存放在 bss段;
    static char static_part_char;

    //6. 區域性變數存放到文字段
    int part_int = 999;
    char part_char;

    //7. 函式語句等內容存放在文字段
    fun1(1);
    fun2(1);

    return 0;
}
  • 2.程式碼分析圖示 :
    這裡寫圖片描述

2. 程式執行後的記憶體佈局 ( 棧 | 堆 | 對映檔案資料 [ bss段 | data段 | text段 ] )

(1) 相關概念簡介

程式執行後的記憶體佈局 : 從高地址 到 低地址 介紹, 順序為 棧 -> 堆 -> bss段 -> data 段 -> text段 ;

  • 1.棧 : 程式執行後才分配棧記憶體, 存放程式的函式資訊 ;
  • 2.堆 : 分配完棧記憶體後分配堆記憶體, 用於響應程式的動態記憶體申請 ;
  • 3.bss 段 : 從程式檔案對映到記憶體空間中 , 存放 沒有初始化的 靜態區域性變數 和 全域性變數, 其值自動初始化為 0 ;
  • 4.data 段 : 從程式檔案對映到記憶體空間中 , 存放 已經初始化過的 靜態區域性變數 和 全域性變數 ;
  • 5.text 段 : 從程式檔案對映到記憶體空間中 , 存放編寫的程式程式碼 ;
  • 6.rodata 段 : 存放程式中的常量資訊 , 只能讀取, 不能修改, 如 字串常量, 整形常量 等資訊 , 如果強行修改該段的值, 在執行時會報段錯誤 ;

3. 總結

程式記憶體總結 :

  • 1.靜態儲存區 : .bss 段 和 .data 段 是靜態儲存區 ;
  • 2.只讀儲存區 : .rodata 段存放常量, 是隻讀儲存區 ;
  • 3.棧記憶體 : 區域性變數存放在棧記憶體中 ;
  • 4.堆記憶體 : 使用 malloc 動態申請 堆記憶體 ;
  • 5.程式碼段 : 程式碼存放在 .text 段 中 , 函式的地址 是程式碼段中的地址 ;

函式呼叫過程 :

  • 1.函式地址 : 函式地址對應著程式記憶體空間中程式碼段的位置 ;
  • 2.函式棧 : 函式呼叫時, 會在棧記憶體中建立 函式呼叫的 活動記錄, 如 引數 返回地址 old ebp地址 資料 等 ;
  • 3.相關資源訪問 : 函式呼叫時, 在程式碼段的函式存放記憶體操作資訊, 執行函式時, 會根據 esp 棧頂指標 查詢函式的 區域性變數等資訊, 需要靜態變數會從 bss 段 或 data段 查詢資訊, 需要常量值時 去 rodata 段去查詢資訊 ;

四. 野指標 ( 程式BUG根源 )

1. 野指標相關概念

(1) 野指標簡介

野指標相關概念 :

  • 1.野指標定義 : 野指標的 指標變數 儲存的地址值值 不合法 ;
  • 2.指標合法指向 : 指標只能指向 棧 和 堆 中的地址, 除了這兩種情況, 指標指向的其它地址都是不合法的 ;
  • 3.空指標 與 野指標 : 空指標不容易出錯, 因為可以判斷出來, 其指標地址為 0 ; 野指標指標地址 不為 0 , 但是其指向的記憶體不可用 ;
  • 4.野指標不可判定 : 目前 C 語言中 無法判斷 指標 是否 為野指標 ;

(2) 野指標的三大來源

野指標來源 :

  • 1.區域性變數指標未初始化 : 區域性指標變數, 定以後, 沒有進行初始化 ;
#include <stdio.h>
#include <string.h>

//1. 定義一個結構體, 其中包含 字串 和 int 型別元素
struct Student
{
    char* name;
    int age;
};

int main()
{
    //2. 宣告一個 Student 結構體變數但是沒有進行初始化, 
    //  結構體中的兩個元素都是隨機值
    //  需要 malloc 初始化該區域性變數
    struct Student stu;

    //3. 向 stu.name 指標指向的地址 寫入 "Bill Gates" 字串, 
    //      要出事, stu.name 沒有進行初始化, 其地址是隨機值, 
    //      向一個隨機地址中寫入資料, 會出現任意情況, 嚴重會直接讓系統故障
    //4. 此時 stu.name 就是一個野指標
    strcpy(stu.name, "Bill Gates");

    stu.age = 63;
    return 0;
}

這裡寫圖片描述
- 2.使用已經釋放的指標 : 指標指向的記憶體控制元件, 已經被 free 釋放了, 之後在使用就變成了野指標 ; 如果該指標沒有分配, 寫入無所謂; 如果該地址被分配給程式了, 隨意修改該值會造成無法估計的後果;

#include <stdio.h>
#include <malloc.h>
#include <string.h>

int main()
{
    //1. 建立一個字串, 併為其分配空間
    char* str = (char *)malloc(3);
    //2. 給字串賦值, 申請了 3 個位元組, 但是放入了 11 個字元
    //   有記憶體越界的風險
    strcpy(str, "HanShuliang");
    //3. 列印字串
    printf("%s\n", str);
    //4. 釋放字串空間
    free(str);
    //5. 再次列印, 為空
    printf("%s\n", str);
}

這裡寫圖片描述
- 3.指標指向的變數在使用前被銷燬 : 指標指向的變數如果被銷燬, 這個變數所在的空間也可能被分配了, 修改該空間內的內容, 後果無法估計;

#include <stdio.h>

//從函式中返回的區域性變數要注意一定要是值傳遞, 不能有地址傳遞
//區域性變數在函式執行完就釋放掉了

char * fun()
{
    //注意該變數是區域性變數, 
    //函式執行完畢後該變數所在的棧空間就會被銷燬
    char* str = "Hanshuliang";
    return str;
}

int main()
{
    //從 fun() 函式中返回的 str 的值是棧空間的值, 
    //該值在函式返回後就釋放掉了, 
    //當前這個值是被已經銷燬了
    char * str = fun();

    //打印出來的值可能是正確的
    printf("%s\n", str);
}

這裡寫圖片描述

2. 經典指標錯誤分析 (本節所有程式碼都是錯誤示例)

(1) 非法記憶體操作

非法記憶體操作 : 主要是結構體的指標成員出現的問題, 如結 ① 構體指標未進行初始化(分配動態記憶體, 或者分配一個變數地址), 或者② 進行了初始化, 但是超出範圍使用;

  • 1.結構體成員指標未初始化 : 結構體的成員中 如果有指標, 那麼這個指標在使用時需要進行初始化, 結構體變數聲明後, 其成員變數值是隨機值, 如果指標值是隨機值得話, 那麼對該指標操作會產生未知後果; 錯誤示例 :
#include <stdio.h>

//在結構體中定義指標成員, 當結構體為區域性變數時, 該指標成員 int* ages 需要手動初始化操作
struct Students
{
    int* ages;
};

int main()
{
    //1. 在函式中宣告一個結構體區域性變數, 結構體成員不會自動初始化, 此時其中是隨機值 
    struct Students stu1;

    //2. 遍歷結構體的指標成員, 併為其賦值, 但是該指標未進行初始化, 對一個隨機空間進行操作會造成未知錯誤
    int i = 0;
    for(i = 0; i < 10; i ++)
    {
        stu1.ages[i] = 0;
    }

    return 0;
}
  • 2.結構體成員初始化記憶體不足 : 給結構體初始化時為其成員分配了空間, 但是使用的指標操作超出了分配的空間, 那麼對於超出的空間的使用會造成無法估計的錯誤; 錯誤示例 :
#include <stdio.h>
#include <stdlib.h>

//在結構體中定義指標成員, 當結構體為區域性變數時, 該指標成員 int* ages 需要手動初始化操作
struct Students
{
    int* ages;
};

int main()
{
    //1. 在函式中宣告一個結構體區域性變數, 結構體成員不會自動初始化, 此時其中是隨機值 
    struct Students stu1;

    //2. 為結構體變數中的 ages 指標分配記憶體空間, 並進行初始化;
    stu1.ages = (int *)calloc(2, sizeof(int));

    //3. 遍歷結構體的指標成員, 併為其賦值, 此處超出了其 2 * 4 位元組的範圍, 8 ~ 11 位元組可能分配給了其他應用
    int i = 0;
    for(i = 0; i < 3; i ++)
    {
        stu1.ages[i] = 0;
    }

    free(stu1.ages);

    return 0;
}

(2) 記憶體申請成功後未初始化

記憶體分配成功, 沒有進行初始化 : 記憶體中的是隨機值, 如果對這個隨機值進行操作, 也會產生未知後果;

#include <stdio.h>
#include <stdlib.h>

//記憶體分配成功, 需要先進行初始化, 在使用這塊記憶體

int main()
{
    //1. 定義一個字串, 為其分配一個 20 位元組空間
    char* str = (char*)malloc(20);

    //2. 列印字串, 這裡可能會出現錯誤, 因為記憶體沒有初始化
    //   此時其中的資料都是隨機值, 不確定在哪個地方有 '\0' 即字串結尾
    //   列印一個位置長度的 str, 顯然不符合我們的需求
    printf(str);

    //3. 釋放記憶體
    free(str);

    return 0;
}

(3) 記憶體越界

記憶體越界分析 :

#include <stdio.h>

//陣列退化 : 方法中的陣列引數會退化為指標, 即這個方法可以傳入任意 int* 型別的資料
//不能確定陣列大小 : 只有一個 int* 指標變數, 無法確定這個陣列的大小 
//可能出錯 : 這裡按照10個位元組處理陣列, 如果傳入一個 小於 10位元組的陣列, 可能出現錯誤 
void fun(int array[10])
{
    int i = 0;

    for(i = 0; i < 10; i ++)
    {
        array[i] = i;
        printf("%d\n", array[i]);
    }
}

int main()
{
    //1. 定義一個大小為 5 的int 型別陣列, 稍後將該陣列傳入fun方法中
    int array[5];

    //2. 將大小為5的int型別陣列傳入fun函式, 此時fun函式按照int[10]型別超出範圍為陣列賦值
    //   如果為一個未知地址賦值會出現無法估計的後果
    fun(array);

    return 0;
}

(4) 記憶體洩露

記憶體洩露 :

  • 1.錯誤示例 :
#include <stdio.h>

/*
    記憶體問題 : 該函式有一個入口, 兩個出口
               正常出口 : 處理的比較完善, 記憶體會釋放;
               異常出口 : 臨時機制, 出現某種狀態, 沒有處理完善, 出現了記憶體洩露

*/
void fun(unsigned int size)
{
    //申請一塊記憶體空間
    int* p = (int*)malloc(size * sizeof(int));
    int i = 0;

    //如果size小於5, 就不處理, 直接返回
    //注意 : 在這個位置, 善後沒有處理好, 沒有釋放記憶體
    //       如果size小於5, 臨時退出函式, 而 p 指標指向的記憶體沒有釋放
    //       p 指標是一個區域性變數, 函式執行完之後, 該區域性變數就消失了, 之後就無法釋放該記憶體了
    if(size < 5)
    {
        return;
    }

    //記憶體大於等於5以後才處理
    for(int i = 0; i < size; i ++)
    {
        p[i] = i;
        printf("%d\n", p[i]);
    }

    //釋放記憶體
    free(p);
}

int main()
{
    fun(4);

    return 0;
}
  • 2.正確示例 :
#include <stdio.h>

/*
    記憶體問題 : 該函式有一個入口, 兩個出口
               正常出口 : 處理的比較完善, 記憶體會釋放;
               異常出口 : 臨時機制, 出現某種狀態, 沒有處理完善, 出現了記憶體洩露

*/
void fun(unsigned int size)
{
    //申請一塊記憶體空間
    int* p = (int*)malloc(size * sizeof(int));
    int i = 0;

    //將錯誤示例中的此處的出口取消即可解決記憶體洩露的問題
    if(size >= 5)
    {
        
            
           

相關推薦

C 語言記憶體管理 ( 動態記憶體分配 | | | 靜態儲存區 | 記憶體佈局 | 指標 )

一. 動態記憶體分配 1. 動態記憶體分配相關概念 動態記憶體分配 : 1.C語言操作與記憶體關係密切 : C 語言中的所有操作都與記憶體相關 ; 2.記憶體別名 : 變數 ( 指標變數 | 普通變數 ) 和 陣

C語言動態記憶體分配小結

為什麼存在動態記憶體分配? 我們已經掌握的記憶體開闢方式有: int val = 20;//在棧空間上開闢四個位元組 char arr[10];//在棧空間上開闢10個位元組的連續空間 但是上面開闢空間的方式有兩個特點: 1.空間開闢的大小是固定的 2.陣列在申明的時

C語言動態記憶體分配(malloc,realloc,calloc,free)的基本理解和區別

#include<Windows.h> #include<stdio.h> #include<malloc.h> int main() { int* p = NULL; printf("%x\n", p); p = (int*)malloc(sizeof(int)*

C語言程式碼規範 記憶體管理

總結查詢資料 總結記憶體申請釋放相關知識點如下。 參考:http://blog.csdn.net/chenyiming_1990/article/details/9476181 一、程式記憶體的組成: 1. 一共由3個部分組成: BSS段 : 不在可執行檔案中,由系統初始

C語言記憶體分配函式malloc/ calloc/ realloc及記憶體釋放free

前言: 記憶體區域劃分與分配: 1、棧區(stack)——程式執行時由編譯器自動分配,存放函式的引數值,區域性變數的值等,程式結束時由編譯器自動釋放。 2、堆區(heap) —— 在記憶體開闢另一塊儲存區域。一般由程式設計師分配釋放, 若程式設計師不釋放,程式結束時可

C語言結構體、聯合,記憶體對齊規則總結

一、結構體 1.1什麼是結構體       在C語言中,結構體是一種資料結構,是C提供的聚合型別(C提供了兩種聚合型別:陣列和結構)的一種。結構體與陣列的區別是:陣列是相同型別的集合,而結構體可能具有不同的型別。 結構體也可以被宣告為變數,陣列或者指標等,用以實現較複雜的

C語言malloc()和free()函式的講解以及相關記憶體洩漏問題

1、函式原型及說明: void *malloc(long NumBytes):該函式分配了NumBytes個位元組,並返回了指向這塊記憶體的指標。如果分配失敗,則返回一個空指標(NULL)。 關於分配失敗的原因,應該有多種,比如說空間不足就是一種。 void free(void *FirstByte): 該

C語言如何計算變數或型別佔記憶體的大小

一般形式 語法形式 執行結果 sizeof(型別) 型別佔用的記憶體位元組數 sizeof(變數或表示式) 變數或表示式所屬型別佔的記憶體位元組數

C語言建立動態陣列

#include <iostream> #include <malloc.h> using namespace std; int main() { int *arr; int len; cout << "輸入需要建立的陣列的長度:"; cin

作業系統C語言模擬作業系統實現動態分割槽分配演算法

#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<string.h> #include<stdlib.h> #include<math.h> #defi

C語言動態連結串列和靜態連結串列的建立

動態連結串列和靜態連結串列 #include<stdio.h> #include<stdlib.h> #include<malloc.h> struct wep{

C語言unix c如何察看可執行檔案依賴哪些動態

5、如何察看可執行檔案依賴哪些動態庫: 【ldd 可執行檔案的名字】:列印程式或庫檔案所依賴的共享庫列表 舉例: tarena@ubuntu:~/day/day24$

c語言動態開闢一個二維陣列

// 動態開闢一個二維陣列 #include <stdio.h> #include <stdlib.h> int main() { int i = 0; int j = 0; int line = 0; int row =

C語言統計數字在排序數組中出現的次數

語言 個數 統計 ret r+ () class tdi times //數字在排序數組中出現的次數。 //統計一個數字在排序數組中出現的次數。比如:排序數組{1,2,3,3,3,3,4,5}和數字3,因為3出現了4次,因此輸出4. #include <stdio

C語言推斷一個數是否為2的n次方

post data- popu scanf scan ng- 輸入 ont print //推斷一個數是否為2的n次方 #include <stdio.h> int is_two_n(int num) { if ((num&(num - 1))

C語言 二叉樹的基本運算

IT btree AS CA style pri != -- str • 二叉樹節點類型BTNode: 1 typedef struct node 2 { 3 char data; 4 struct node *lchild, *rch

C語言類型限定詞

變量 可變 oct 包含 一個數 sta ans eof 方式 ANSI C 的類型限定詞有const、volatile以及restrict三個,以下分別介紹三個限定詞: 1、類型限定詞const (1)、如果變量中帶有const關鍵字,則該變量無法進行賦值、增量及減量運算

C語言平衡二叉樹

avl 簡介 二叉搜索樹 沒有 TP 假設 它的 left 操作 AVL樹簡介 AVL樹的名字來源於它的發明作者G.M. Adelson-Velsky 和 E.M. Landis。AVL樹是最先發明的自平衡二叉查找樹(Self-Balancing Binary Searc

C語言輸入一個整數,求它的原碼,反碼,補碼值

補碼 while src info idt IV com scan -- 1 #include<stdio.h> 2 #include<math.h> 3 int main() 4 { 5 int m,n,a[10],i=0,y[

C語言數據對其(內存對齊)

brush size return () def ont http 之間 sign 數據對齊 結構體之間的對齊是有很多種方法的,也是根據你所用的系統位數有關。下面都是以32位系統來講的,32位系統一般以字對齊,字就是系統位數,32位系統則是32位對齊,也就是4字節(in