日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

【C 语言】内存管理 ( 动态内存分配 | 栈 | 堆 | 静态存储区 | 内存布局 | 野指针 )

發布時間:2025/6/17 编程问答 26 豆豆
生活随笔 收集整理的這篇文章主要介紹了 【C 语言】内存管理 ( 动态内存分配 | 栈 | 堆 | 静态存储区 | 内存布局 | 野指针 ) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

相關文章鏈接 :
1.【嵌入式開發】C語言 指針數組 多維數組
2.【嵌入式開發】C語言 命令行參數 函數指針 gdb調試
3.【嵌入式開發】C語言 結構體相關 的 函數 指針 數組
4.【嵌入式開發】gcc 學習筆記(一) - 編譯C程序 及 編譯過程
5.【C語言】 C 語言 關鍵字分析 ( 屬性關鍵字 | 常量關鍵字 | 結構體關鍵字 | 聯合體關鍵字 | 枚舉關鍵字 | 命名關鍵字 | 雜項關鍵字)
6.【C 語言】編譯過程 分析 ( 預處理 | 編譯 | 匯編 | 鏈接 | 宏定義 | 條件編譯 | 編譯器指示字 )
7.【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 函數 ( ① void malloc(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 個元素的值為 0int* 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 還要指向這個地址用于釋放被彈出的函數占用的??臻g ;
  • 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.釋放??臻g : 隨著 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 ) 釋放??臻g : 將 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 ) 釋放??臻g : 執行完這兩個操作后, 棧空間就釋放了 ; ( 4 ) 執行函數體 : 執行完 fun1 出棧后, 繼續執行 fun2 中的函數體, 發現 fun2 函數體也執行完了, 開始 fun2 出棧 ; 7. fun2 函數 出棧 ( 1 ) esp 指針返回 : 通過 ebp 讀取上一個指針, 獲取 返回地址, esp 指向 返回地址, 即上一個棧頂 ; ( 2 ) ebp 指針返回 : 讀取 ebp 指針指向的內存中的數據, 這個數據是上一個 ebp 指針指向的地址值, ebp 指向這個地址值; ( 3 ) 釋放??臻g : 執行完這兩個操作后, ??臻g就釋放了 ; ( 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 ) 釋放??臻g : 將 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 ) 釋放棧空間 : 執行完這兩個操作后, ??臻g就釋放了 ;
    • ( 4 ) 執行函數體 : 執行完 fun1 出棧后, 繼續執行 fun2 中的函數體, 發現 fun2 函數體也執行完了, 開始 fun2 出棧 ;
int fun2(int i) {fun1();return i; }
  • 7.fun2 函數 出棧 :
    • ( 1 ) esp 指針返回 : 通過 ebp 讀取上一個指針, 獲取 返回地址, esp 指向 返回地址, 即上一個棧頂 ;
    • ( 2 ) ebp 指針返回 : 讀取 ebp 指針指向的內存中的數據, 這個數據是上一個 ebp 指針指向的地址值, ebp 指向這個地址值;
    • ( 3 ) 釋放??臻g : 執行完這兩個操作后, 棧空間就釋放了 ;
    • ( 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() {//注意該變量是局部變量, //函數執行完畢后該變量所在的??臻g就會被銷毀char* str = "Hanshuliang";return str; }int main() {//從 fun() 函數中返回的 str 的值是??臻g的值, //該值在函數返回后就釋放掉了, //當前這個值是被已經銷毀了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){//內存大于等于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; }




( 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


指針釋放后立即設置NULL : 在一個指針被 free() 掉以后, 馬上將該指針設置為 NULL, 防止重復使用該指針;


總結

以上是生活随笔為你收集整理的【C 语言】内存管理 ( 动态内存分配 | 栈 | 堆 | 静态存储区 | 内存布局 | 野指针 )的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。