天天看點

【C 語言】記憶體管理 ( 動态記憶體配置設定 | 棧 | 堆 | 靜态存儲區 | 記憶體布局 | 野指針 )

文章目錄

  • ​​一. 動态記憶體配置設定​​
  • ​​1. 動态記憶體配置設定相關概念​​
  • ​​( 1 ) 動态記憶體配置設定 ( ① 變量 數組 -> 記憶體别名 | ② 變量 在 編譯階段 配置設定記憶體 | ③ 除了編譯器配置設定的記憶體 還需額外記憶體 -> 動态記憶體 )​​
  • ​​2. 動态記憶體配置設定 相關方法​​
  • ​​( 1 ) 相關 方法簡介 ( ① malloc calloc realloc 申請記憶體 | ② free 歸還記憶體 | ③ malloc 申請記憶體 , 不初始化值 | ④ calloc 申請記憶體 并 初始化 0 | ⑤ realloc 重置已經申請的記憶體 )​​
  • ​​( 2 ) malloc 函數 ( ① void *malloc(size_t size) ; size 位元組大小 | ② 傳回值 void* 需要強轉為指定類型 | ③ 系統實際配置設定記憶體比 malloc 稍大 | ④ 如果記憶體用完會傳回 NULL )​​
  • ​​( 3 ) free 函數 ( ① void free(void *ptr) | ② 作用 : 釋放 malloc 申請的動态空間 | ③ 參數 : void *ptr 指針指向要釋放的記憶體首位址 | ④ 傳回值 : 沒有傳回值 )​​
  • ​​( 4 ) calloc 函數 ( ① void *calloc(size_t nmemb, size_t size) | ② 作用 : 申請 指定元素個數 指定元素大小 的記憶體 , 并将每個元素初始化成 0 | ③ size_t nmemb 參數 : 元素個數 | ④ size_t size 參數 : 元素大小 )​​
  • ​​( 5 ) realloc 函數 ( ① void *realloc(void *ptr, size_t size) | ② 作用 : 重新配置設定一個已經配置設定并且未釋放的動态記憶體的大小 | ③ void *ptr 參數 : 指向 一塊已經存在的動态記憶體空間的首位址 | ④ size_t size 參數 : 需要重新配置設定記憶體大小 | ⑤ ptr 參數為 NULL , 函數與 malloc 作用一樣 | ⑥ 要使用新位址 舊位址 ptr 不能繼續使用了 )​​
  • ​​( 6 ) 代碼示例 ( 動态記憶體配置設定簡單示例)​​
  • ​​二. 棧 堆 靜态存儲區​​
  • ​​1. 棧​​
  • ​​( 1 ) 棧 相關概念​​
  • ​​(2) 代碼示例 ( 簡單的函數調用的棧記憶體分析 )​​
  • ​​( 3 ) 棧記憶體行為分析 ( 圖文分析版本 )​​
  • ​​2. 堆​​
  • ​​( 1 ) 标題3​​
  • ​​3. 靜态存儲區​​
  • ​​( 1 ) 标題3​​
  • ​​三. 程式記憶體布局​​
  • ​​1. 程式運作前的程式檔案的布局 ( 代碼段 | 資料段 | bss段 )​​
  • ​​(1) 相關概念簡介​​
  • ​​( 2 ) 分析程式檔案的記憶體布局​​
  • ​​2. 程式運作後的記憶體布局 ( 棧 | 堆 | 映射檔案資料 [ bss段 | data段 | text段 ] )​​
  • ​​( 1 ) 相關概念簡介​​
  • ​​3. 總結​​
  • ​​四. 野指針 ( 程式BUG根源 )​​
  • ​​1. 野指針相關概念​​
  • ​​( 1 ) 野指針簡介​​
  • ​​( 2 ) 野指針的三大來源​​
  • ​​2. 經典指針錯誤分析 (**本節所有代碼都是錯誤示例**)​​
  • ​​( 1 ) 非法記憶體操作​​
  • ​​( 2 ) 記憶體申請成功後未初始化​​
  • ​​( 3 ) 記憶體越界​​
  • ​​( 4 ) 記憶體洩露​​
  • ​​( 5 ) 指針多次釋放 (***誰申請誰釋放***)​​
  • ​​( 6 ) 使用已經釋放的指針​​
  • ​​3. C語言中避免指針錯誤的程式設計規範​​
  • ​​( 1 ) 申請記憶體後先判空​​
  • ​​( 2 ) 避免數組越界 注意數組長度​​
  • ​​( 3 ) 動态記憶體 誰申請 誰釋放​​
  • ​​( 4 ) 釋放後立即置NULL​​

一. 動态記憶體配置設定

1. 動态記憶體配置設定相關概念

( 1 ) 動态記憶體配置設定 ( ① 變量 數組 -> 記憶體别名 | ② 變量 在 編譯階段 配置設定記憶體 | ③ 除了編譯器配置設定的記憶體 還需額外記憶體 -> 動态記憶體 )

動态記憶體配置設定 :

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

2. 動态記憶體配置設定 相關方法

( 1 ) 相關 方法簡介 ( ① malloc calloc realloc 申請記憶體 | ② free 歸還記憶體 | ③ malloc 申請記憶體 , 不初始化值 | ④ calloc 申請記憶體 并 初始化 0 | ⑤ realloc 重置已經申請的記憶體 )

動态記憶體配置設定方法 :

  • 1.申請記憶體 : 使用 malloc 或 calloc 或 realloc 申請記憶體;
  • 2.歸還記憶體 : 使用 free 歸還 申請的記憶體 ;
  • 3.記憶體來源 : 系統專門預留一塊記憶體, 用來響應程式的動态記憶體配置設定請求 ;
  • 4.記憶體配置設定相關函數 :
  • ( 1 ) malloc:單純的申請指定位元組大小的動态記憶體, 記憶體中的值不管;
  • ( 2 ) calloc:申請 指定元素大小 和 元素個數的 記憶體, 并将每個元素初始化為 0;
  • ( 3 ) realloc:可以重置已經申請的記憶體大小;
#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);      

( 2 ) malloc 函數 ( ① voidmalloc(size_t size) ; size 位元組大小 | ② 傳回值 void需要強轉為指定類型 | ③ 系統實際配置設定記憶體比 malloc 稍大 | ④ 如果記憶體用完會傳回 NULL )

malloc 函數簡介 :

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

( 3 ) free 函數 ( ① void free(void *ptr) | ② 作用 : 釋放 malloc 申請的動态空間 | ③ 參數 : void *ptr 指針指向要釋放的記憶體首位址 | ④ 傳回值 : 沒有傳回值 )

free 函數簡介 :

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

( 4 ) calloc 函數 ( ① void *calloc(size_t nmemb, size_t size) | ② 作用 : 申請 指定元素個數 指定元素大小 的記憶體 , 并将每個元素初始化成 0 | ③ size_t nmemb 參數 : 元素個數 | ④ size_t size 參數 : 元素大小 )

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;

( 5 ) realloc 函數 ( ① void *realloc(void *ptr, size_t size) | ② 作用 : 重新配置設定一個已經配置設定并且未釋放的動态記憶體的大小 | ③ void *ptr 參數 : 指向 一塊已經存在的動态記憶體空間的首位址 | ④ size_t size 參數 : 需要重新配置設定記憶體大小 | ⑤ ptr 參數為 NULL , 函數與 malloc 作用一樣 | ⑥ 要使用新位址 舊位址 ptr 不能繼續使用了 )

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 一樣, 直接配置設定一塊新的動态記憶體, 并傳回一個指向其首位址的指針;

( 6 ) 代碼示例 ( 動态記憶體配置設定簡單示例)

代碼示例 :

  • 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.編譯運作結果:
    【C 語言】記憶體管理 ( 動态記憶體配置設定 | 棧 | 堆 | 靜态存儲區 | 記憶體布局 | 野指針 )

二. 棧 堆 靜态存儲區

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 指針指向目前的棧頂 ;
【C 語言】記憶體管理 ( 動态記憶體配置設定 | 棧 | 堆 | 靜态存儲區 | 記憶體布局 | 野指針 )

函數出棧流程 :

  • 1.esp 指針傳回 : 根據 ebp 指針 擷取 傳回位址, esp 直接指向這個傳回位址 ;
    • ebp 擷取 傳回地方方式 : ebp 指向傳回位址的下一個指針, ebp 指針回退一個指針 即可擷取 傳回位址 的指針, 然後擷取指針指向的内容 即傳回位址 ;
  • 2.ebp 指針傳回 : 擷取 ebp 指針指向的記憶體中的資料, 這個資料就是上一個ebp指向的記憶體位址值, ebp 指向這個位址值, 即完成操作 ;
  • 3.釋放棧空間 : 随着 esp 和 ebp 指針傳回, 棧空間也随之釋放了 ;
  • 4.繼續執行函數體 : 從函數2傳回函數1後, 繼續執行該函數1的函數體 ;
  • 【C 語言】記憶體管理 ( 動态記憶體配置設定 | 棧 | 堆 | 靜态存儲區 | 記憶體布局 | 野指針 )

(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 函數 參數 在棧底 ;
    【C 語言】記憶體管理 ( 動态記憶體配置設定 | 棧 | 堆 | 靜态存儲區 | 記憶體布局 | 野指針 )
  • ( 2 ) 傳回位址入棧: 然後将 傳回位址 放入棧中, 傳回位址是 棧底位址 ;
    【C 語言】記憶體管理 ( 動态記憶體配置設定 | 棧 | 堆 | 靜态存儲區 | 記憶體布局 | 野指針 )
  • ( 3 ) ebp 指針入棧: 将 old ebp 指針入棧, ebp 指針指向 old ebp 存放的位址 address1 , 這個 address1 是 棧底位址;
    【C 語言】記憶體管理 ( 動态記憶體配置設定 | 棧 | 堆 | 靜态存儲區 | 記憶體布局 | 野指針 )
  • ( 4 ) 資料入棧: ( 局部變量, 寄存器的值 等 ) ;
    【C 語言】記憶體管理 ( 動态記憶體配置設定 | 棧 | 堆 | 靜态存儲區 | 記憶體布局 | 野指針 )
  • ( 5 ) esp 指向棧頂: esp 指針指向 棧頂 (即資料後面的記憶體首位址), 此時棧頂資料 address2;
    【C 語言】記憶體管理 ( 動态記憶體配置設定 | 棧 | 堆 | 靜态存儲區 | 記憶體布局 | 野指針 )
  • ( 6 ) 資料總結: main 的棧中 棧底 到 棧頂 的資料 : main 參數 -> 傳回位址 -> old ebp -> 資料
  • ( 7 ) 執行函數體: 開始執行 main 函數的函數體, 執行 fun1 函數, 下面是 棧 中記憶體變化 :
int main()
{
  fun1(1);
  fun2(1);
  
  return 0;
}      
  • 2.調用 fun1 函數, 繼續将 fun1 函數内容入棧:
  • ( 1 ) 參數入棧 : 将 fun1 參數 入棧;
    【C 語言】記憶體管理 ( 動态記憶體配置設定 | 棧 | 堆 | 靜态存儲區 | 記憶體布局 | 野指針 )
  • ( 2 ) 傳回位址入棧: 存放一個傳回位址, 此時存放的是棧頂的值 address2 位址, 傳回的時候通過 ebp 指針回退一個讀取 ;
    【C 語言】記憶體管理 ( 動态記憶體配置設定 | 棧 | 堆 | 靜态存儲區 | 記憶體布局 | 野指針 )
  • ( 3 ) ebp 指針入棧: old ebp (上次 ebp 指針指向的位址) 指針指向的位址值入棧, 該指針指向 address1 位址, 即 ebp 指針上一次指向的位置,

    該棧記憶體中存放 ebp 指針上次指向的位址 address1, 這段存放 address1 的記憶體首位址為 address3,

    ebp 指針指向 address3 , 即 ebp 指針變量存儲 address3 的位址值, 棧記憶體中的 address3 存放 address1 位址 ;

    【C 語言】記憶體管理 ( 動态記憶體配置設定 | 棧 | 堆 | 靜态存儲區 | 記憶體布局 | 野指針 )
  • ( 4 ) 資料入棧: 存放資料 (局部變量) ;
    【C 語言】記憶體管理 ( 動态記憶體配置設定 | 棧 | 堆 | 靜态存儲區 | 記憶體布局 | 野指針 )
  • ( 5 ) esp 指向棧頂: esp 指向 棧頂 ;
    【C 語言】記憶體管理 ( 動态記憶體配置設定 | 棧 | 堆 | 靜态存儲區 | 記憶體布局 | 野指針 )
  • ( 6 ) 執行函數體: 開始執行 fun1 函數體内容, 執行結束後需要出棧 傳回 ;
void fun1(int i)
{
}      
  • 3.fun1 函數執行完畢, 開始 退棧 傳回 操作:
  • ( 1 ) 擷取傳回位址: 傳回位址存放在 ebp 的上一個指針位址, ebp 指向 傳回位址的尾位址,

    ebp 回退一個指針位置即可擷取傳回位址 , 此時的傳回位址是 address2 上面已經描述過了 ;

  • ( 2 ) esp 指針指向: esp 指向 address2, 即将 esp 指針變量的值 設定為 address2 即可 ;
    【C 語言】記憶體管理 ( 動态記憶體配置設定 | 棧 | 堆 | 靜态存儲區 | 記憶體布局 | 野指針 )
  • ( 3 ) ebp 指針指向:

    擷取上一個 ebp 指向的位址 : 目前 ebp 指向的記憶體中存儲了上一個 ebp 指向的記憶體位址, 擷取這個位址;

    ebp 指向這個剛擷取的位址 ;

    【C 語言】記憶體管理 ( 動态記憶體配置設定 | 棧 | 堆 | 靜态存儲區 | 記憶體布局 | 野指針 )
  • ( 4 ) 釋放棧空間: 将 esp 指針指向的目前位址 和 之後的位址 都釋放掉 ;
    【C 語言】記憶體管理 ( 動态記憶體配置設定 | 棧 | 堆 | 靜态存儲區 | 記憶體布局 | 野指針 )
  • ( 5 ) 執行 main 函數體: 繼續執行 main 函數 函數體 , 然後執行 fun2 函數;
int main()
{
  fun1(1);
  fun2(1);
  
  return 0;
}      
  • 4.執行 fun2 函數:
  • ( 1 ) 參數入棧: fun2 函數參數入棧;
    【C 語言】記憶體管理 ( 動态記憶體配置設定 | 棧 | 堆 | 靜态存儲區 | 記憶體布局 | 野指針 )
  • ( 2 ) 傳回位址 入棧: esp 指向的位址 存放到 傳回位址中 ;
    【C 語言】記憶體管理 ( 動态記憶體配置設定 | 棧 | 堆 | 靜态存儲區 | 記憶體布局 | 野指針 )
  • ( 3 ) ebp 位址入棧: 将 ebp 指向的位址存放到棧記憶體中, ebp 指向 該段記憶體的首位址 (即傳回位址的尾位址);
    【C 語言】記憶體管理 ( 動态記憶體配置設定 | 棧 | 堆 | 靜态存儲區 | 記憶體布局 | 野指針 )
  • ( 4 ) 資料入棧: 将資料 入棧 ;
    【C 語言】記憶體管理 ( 動态記憶體配置設定 | 棧 | 堆 | 靜态存儲區 | 記憶體布局 | 野指針 )
  • ( 5 ) esp 指向棧頂: esp 指向 資料 的末尾位址 ;
    【C 語言】記憶體管理 ( 動态記憶體配置設定 | 棧 | 堆 | 靜态存儲區 | 記憶體布局 | 野指針 )
  • ( 6 ) 執行函數體: 執行 fun2 函數體時, 發現 fun2 中居然調用了 fun1, 此時又要開始将 fun1 函數入棧 ;
int fun2(int i)
{
  fun1();
  return i;
}      
  • 5.fun1 函數入棧:
  • ( 1 ) 參數入棧: 将 fun1 參數入棧 ;
    【C 語言】記憶體管理 ( 動态記憶體配置設定 | 棧 | 堆 | 靜态存儲區 | 記憶體布局 | 野指針 )
  • ( 2 ) 傳回位址入棧: esp 指向的 傳回位址 存入棧記憶體 ;
    【C 語言】記憶體管理 ( 動态記憶體配置設定 | 棧 | 堆 | 靜态存儲區 | 記憶體布局 | 野指針 )
  • ( 3 ) ebp 位址入棧: 将 old ebp 位址 入棧, 并且 ebp 指針指向 該段 棧記憶體首位址 (即 傳回位址 的尾位址);
    【C 語言】記憶體管理 ( 動态記憶體配置設定 | 棧 | 堆 | 靜态存儲區 | 記憶體布局 | 野指針 )
  • ( 4 ) 資料入棧: 局部變量, 寄存器值 入棧 ;
    【C 語言】記憶體管理 ( 動态記憶體配置設定 | 棧 | 堆 | 靜态存儲區 | 記憶體布局 | 野指針 )
  • ( 5 ) esp 指針指向: esp 指針指向棧頂 ;
    【C 語言】記憶體管理 ( 動态記憶體配置設定 | 棧 | 堆 | 靜态存儲區 | 記憶體布局 | 野指針 )
  • ( 6 ) 執行函數體: 繼續執行函數體, 執行完 fun1 函數之後, 函數執行完畢, 開始出棧操作 ;
void fun1(int i)
{
}      
  • 6.fun1 函數 出棧:
  • ( 1 ) esp 指針傳回: 通過 ebp 讀取上一個指針, 擷取 傳回位址, esp 指向 傳回位址, 即上一個棧頂 ;
    【C 語言】記憶體管理 ( 動态記憶體配置設定 | 棧 | 堆 | 靜态存儲區 | 記憶體布局 | 野指針 )
  • ( 2 ) ebp 指針傳回: 讀取 ebp 指針指向的記憶體中的資料, 這個資料是上一個 ebp 指針指向的位址值, ebp 指向這個位址值;
    【C 語言】記憶體管理 ( 動态記憶體配置設定 | 棧 | 堆 | 靜态存儲區 | 記憶體布局 | 野指針 )
  • ( 3 ) 釋放棧空間: 執行完這兩個操作後, 棧空間就釋放了 ;
    【C 語言】記憶體管理 ( 動态記憶體配置設定 | 棧 | 堆 | 靜态存儲區 | 記憶體布局 | 野指針 )
  • ( 4 ) 執行函數體: 執行完 fun1 出棧後, 繼續執行 fun2 中的函數體, 發現 fun2 函數體也執行完了, 開始 fun2 出棧 ;
int fun2(int i)
{
  fun1();
  return i;
}      
  • 7.fun2 函數 出棧:
  • ( 1 ) esp 指針傳回: 通過 ebp 讀取上一個指針, 擷取 傳回位址, esp 指向 傳回位址, 即上一個棧頂 ;
    【C 語言】記憶體管理 ( 動态記憶體配置設定 | 棧 | 堆 | 靜态存儲區 | 記憶體布局 | 野指針 )
  • ( 2 ) ebp 指針傳回: 讀取 ebp 指針指向的記憶體中的資料, 這個資料是上一個 ebp 指針指向的位址值, ebp 指向這個位址值;
    【C 語言】記憶體管理 ( 動态記憶體配置設定 | 棧 | 堆 | 靜态存儲區 | 記憶體布局 | 野指針 )
  • ( 3 ) 釋放棧空間: 執行完這兩個操作後, 棧空間就釋放了 ;
    【C 語言】記憶體管理 ( 動态記憶體配置設定 | 棧 | 堆 | 靜态存儲區 | 記憶體布局 | 野指針 )
  • ( 4 ) 執行函數體: 執行完 fun2 出棧後, 繼續執行 main 中的函數體, 如果 main 函數執行完畢, esp 和 ebp 都指向 棧底 ;
    【C 語言】記憶體管理 ( 動态記憶體配置設定 | 棧 | 堆 | 靜态存儲區 | 記憶體布局 | 野指針 )

2. 堆

( 1 ) 标題3

堆 相關 概念 :

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

堆 管理 方法 :

  • 1.空閑連結清單法;
  • 2.位圖法;
  • 3.對象池法;

空閑連結清單法方案 :

  • 1.空閑連結清單圖示 : 表頭 -> 清單項 -> NULL ;
    【C 語言】記憶體管理 ( 動态記憶體配置設定 | 棧 | 堆 | 靜态存儲區 | 記憶體布局 | 野指針 )
  • 2.程式申請堆記憶體 : int* p = (int*)malloc(sizeof(int)) ; 申請一個 4 位元組的堆空間, 從空閑連結清單中查找能滿足要求的空間, 發現一個 5 位元組的空間, 滿足要求, 這裡直接将 5 位元組的空間, 配置設定給了程式 , 不一定要配置設定正好的記憶體給程式, 可能配置設定的記憶體比申請的要大一些 ;
    【C 語言】記憶體管理 ( 動态記憶體配置設定 | 棧 | 堆 | 靜态存儲區 | 記憶體布局 | 野指針 )
  • 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.代碼分析圖示:
    【C 語言】記憶體管理 ( 動态記憶體配置設定 | 棧 | 堆 | 靜态存儲區 | 記憶體布局 | 野指針 )

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;
}      
【C 語言】記憶體管理 ( 動态記憶體配置設定 | 棧 | 堆 | 靜态存儲區 | 記憶體布局 | 野指針 )
  • 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);
}      
【C 語言】記憶體管理 ( 動态記憶體配置設定 | 棧 | 堆 | 靜态存儲區 | 記憶體布局 | 野指針 )
  • 3.指針指向的變量在使用前被銷毀: 指針指向的變量如果被銷毀, 這個變量所在的空間也可能被配置設定了, 修改該空間内的内容, 後果無法估計;
#include <stdio.h>

//從函數中傳回的局部變量要注意一定要是值傳遞, 不能有位址傳遞
//局部變量在函數執行完就釋放掉了

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

int main()
{
  //從 fun() 函數中傳回的 str 的值是棧空間的值, 
  //該值在函數傳回後就釋放掉了, 
  //目前這個值是被已經銷毀了
  char * str = fun();
  
  //列印出來的值可能是正确的
  printf("%s\n", str);
}      
【C 語言】記憶體管理 ( 動态記憶體配置設定 | 棧 | 堆 | 靜态存儲區 | 記憶體布局 | 野指針 )

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)
  {
    //記憶體大于等于5以後才處理
    for(int i = 0; i < size; i ++)
    {
      p[i] = i;
      printf("%d\n", p[i]);
    }
  }
  
  //釋放記憶體
  free(p);
}

int main()
{
  fun(4);
  
  return 0;
}      

( 5 ) 指針多次釋放 (誰申請誰釋放)

指針被多次釋放 :

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

/*
  記憶體問題 : 多次釋放指針
  
  如果規避這種問題 : 動态記憶體 誰申請 誰釋放

*/
void fun(int* p, int size)
{
  int i = 0;
  
  for(i = 0; i < size; i ++)
  {
    p[i] = i;
    printf("%d\n", p[i]);
  }
  
  //釋放記憶體
  //  注意這裡 p 不是在本函數中申請的記憶體
  //  如果在其它位置再次釋放記憶體, 就可能會出錯
  free(p);
}

int main()
{
  //申請記憶體
  int* p = (int*)malloc(3 * sizeof(int));
  
  //使用記憶體, 并在函數中釋放記憶體
  fun(p, 3);
  
  //如果在此處釋放一個已經釋放的記憶體, 就會報錯
  free(p);
  
  return 0;
}      
【C 語言】記憶體管理 ( 動态記憶體配置設定 | 棧 | 堆 | 靜态存儲區 | 記憶體布局 | 野指針 )

( 6 ) 使用已經釋放的指針

使用已經釋放的指針 :

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

/*
  記憶體問題 : 使用已經釋放的指針
  
  如果規避這種問題 : 動态記憶體 誰申請 誰釋放

*/
void fun(int* p, int size)
{
  int i = 0;
  
  for(i = 0; i < size; i ++)
  {
    p[i] = i;
    printf("%d\n", p[i]);
  }
  
  //釋放記憶體
  //  注意這裡 p 不是在本函數中申請的記憶體
  //  如果在其它位置再次釋放記憶體, 就可能會出錯
  free(p);
}

int main()
{
  //申請記憶體
  int* p = (int*)malloc(3 * sizeof(int));
  int i = 0;
  
  //使用記憶體, 并在函數中釋放記憶體
  fun(p, 3);
  
  //使用已經釋放的指針
  //産生的後果無法估計
  for(i = 0; i <= 2; i ++)
  {
    p[i] = i;
  }
  
  return 0;
}      

3. C語言中避免指針錯誤的程式設計規範

( 1 ) 申請記憶體後先判空

申請空間後先判斷 : 使用 malloc 申請記憶體之後, 先檢查傳回值是否為 NULL, 防止使用 NULL 指針, 防止對 0 位址進行操作, 這樣會破壞作業系統的記憶體區; 作業系統檢測到程式使用 0 位址, 就會殺死本程式;

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

int main()
{
  //申請記憶體
  int* p = (int*)malloc(3 * sizeof(int));
  
  //申請完記憶體後, 先判斷是否申請成功, 在使用這段記憶體
  if(p != NULL){
    //執行相關操作
  }
  
  //釋放記憶體
  free(p);
  
  return 0;
}      

( 2 ) 避免數組越界 注意數組長度

避免數組越界 : 數組建立後, 一定要記住數組的長度, 防止數組越界, 推薦使用柔性數組;

( 3 ) 動态記憶體 誰申請 誰釋放

動态記憶體申請規範 : 動态記憶體的***申請操作*** 和 釋放操作 一一對應比對, 防止記憶體洩露和多次釋放; 誰申請 誰 釋放, 在哪個方法中申請, 就在哪個方法中釋放 ;

( 4 ) 釋放後立即置NULL

繼續閱讀