文章目錄
- 一. 動态記憶體配置設定
- 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.編譯運作結果:
二. 棧 堆 靜态存儲區
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)
{
//記憶體大于等于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 ) 動态記憶體 誰申請 誰釋放
動态記憶體申請規範 : 動态記憶體的***申請操作*** 和 釋放操作 一一對應比對, 防止記憶體洩露和多次釋放; 誰申請 誰 釋放, 在哪個方法中申請, 就在哪個方法中釋放 ;