【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 : 可以重置已經申請的內存大小 ;
( 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.代碼 :
- 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.釋放??臻g : 隨著 esp 和 ebp 指針返回, ??臻g也隨之釋放了 ;
- 4.繼續執行函數體 : 從函數2返回函數1后, 繼續執行該函數1的函數體 ;
(2) 代碼示例 ( 簡單的函數調用的棧內存分析 )
代碼示例 :
- 1.代碼 :
- 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 函數, 下面是 棧 中內存變化 :
- ( 1 ) 參數入棧 : 將 參數 放入棧中, 此時 main 函數 參數 在棧底 ;
- 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 函數體內容, 執行結束后需要出棧 返回 ;
- ( 1 ) 參數入棧 : 將 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 函數;
- ( 1 ) 獲取返回地址 : 返回地址存放在 ebp 的上一個指針地址, ebp 指向 返回地址的尾地址,
- 4.執行 fun2 函數 :
- ( 1 ) 參數入棧 : fun2 函數參數入棧;
- ( 2 ) 返回地址 入棧 : esp 指向的地址 存放到 返回地址中 ;
- ( 3 ) ebp 地址入棧 : 將 ebp 指向的地址存放到棧內存中, ebp 指向 該段內存的首地址 (即返回地址的尾地址);
- ( 4 ) 數據入棧 : 將數據 入棧 ;
- ( 5 ) esp 指向棧頂 : esp 指向 數據 的末尾地址 ;
- ( 6 ) 執行函數體 : 執行 fun2 函數體時, 發現 fun2 中居然調用了 fun1, 此時又要開始將 fun1 函數入棧 ;
- ( 1 ) 參數入棧 : fun2 函數參數入棧;
- 5.fun1 函數入棧 :
- ( 1 ) 參數入棧 : 將 fun1 參數入棧 ;
- ( 2 ) 返回地址入棧 : esp 指向的 返回地址 存入棧內存 ;
- ( 3 ) ebp 地址入棧 : 將 old ebp 地址 入棧, 并且 ebp 指針指向 該段 棧內存首地址 (即 返回地址 的尾地址);
- ( 4 ) 數據入棧 : 局部變量, 寄存器值 入棧 ;
- ( 5 ) esp 指針指向 : esp 指針指向棧頂 ;
- ( 6 ) 執行函數體 : 繼續執行函數體, 執行完 fun1 函數之后, 函數執行完畢, 開始出棧操作 ;
- ( 1 ) 參數入棧 : 將 fun1 參數入棧 ;
- 6.fun1 函數 出棧 :
- ( 1 ) esp 指針返回 : 通過 ebp 讀取上一個指針, 獲取 返回地址, esp 指向 返回地址, 即上一個棧頂 ;
- ( 2 ) ebp 指針返回 : 讀取 ebp 指針指向的內存中的數據, 這個數據是上一個 ebp 指針指向的地址值, ebp 指向這個地址值;
- ( 3 ) 釋放棧空間 : 執行完這兩個操作后, ??臻g就釋放了 ;
- ( 4 ) 執行函數體 : 執行完 fun1 出棧后, 繼續執行 fun2 中的函數體, 發現 fun2 函數體也執行完了, 開始 fun2 出棧 ;
- ( 1 ) esp 指針返回 : 通過 ebp 讀取上一個指針, 獲取 返回地址, esp 指向 返回地址, 即上一個棧頂 ;
- 7.fun2 函數 出棧 :
- ( 1 ) esp 指針返回 : 通過 ebp 讀取上一個指針, 獲取 返回地址, esp 指向 返回地址, 即上一個棧頂 ;
- ( 2 ) ebp 指針返回 : 讀取 ebp 指針指向的內存中的數據, 這個數據是上一個 ebp 指針指向的地址值, ebp 指向這個地址值;
- ( 3 ) 釋放??臻g : 執行完這兩個操作后, ??臻g就釋放了 ;
- ( 4 ) 執行函數體 : 執行完 fun2 出棧后, 繼續執行 main 中的函數體, 如果 main 函數執行完畢, esp 和 ebp 都指向 棧底 ;
- ( 1 ) esp 指針返回 : 通過 ebp 讀取上一個指針, 獲取 返回地址, esp 指向 返回地址, 即上一個棧頂 ;
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.示例代碼 :
- 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.局部變量指針未初始化 : 局部指針變量, 定以后, 沒有進行初始化 ;
- 2.使用已經釋放的指針 : 指針指向的內存控件, 已經被 free 釋放了, 之后在使用就變成了野指針 ; 如果該指針沒有分配, 寫入無所謂; 如果該地址被分配給程序了, 隨意修改該值會造成無法估計的后果;
- 3.指針指向的變量在使用前被銷毀 : 指針指向的變量如果被銷毀, 這個變量所在的空間也可能被分配了, 修改該空間內的內容, 后果無法估計;
2. 經典指針錯誤分析 (本節所有代碼都是錯誤示例)
( 1 ) 非法內存操作
非法內存操作 : 主要是**結構體的指針成員出現的問題, 如結 ① 構體指針未進行初始化(分配動態內存, 或者分配一個變量地址), 或者***② 進行了初始化, 但是超出范圍使用***;
- 1.結構體成員指針未初始化 : 結構體的成員中 如果有指針, 那么這個指針在使用時需要進行初始化, 結構體變量聲明后, 其成員變量值是隨機值, 如果指針值是隨機值得話, 那么對該指針操作會產生未知后果; 錯誤示例 :
- 2.結構體成員初始化內存不足 : 給結構體初始化時為其成員分配了空間, 但是使用的指針操作超出了分配的空間, 那么對于超出的空間的使用會造成無法估計的錯誤; 錯誤示例 :
( 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.錯誤示例 :
- 2.正確示例 :
( 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 语言】内存管理 ( 动态内存分配 | 栈 | 堆 | 静态存储区 | 内存布局 | 野指针 )的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【C 语言】指针 与 数组 ( 指针 |
- 下一篇: 【C 语言】C 语言 函数 详解 ( 函