動態內存管理詳解
- 前言:
- 一、為什么存在動態內存分配
- 二、動態內存函數
- 2.1malloc函數
- 2.2calloc函數
- 2.3realloc函數
- 2.4free函數
- 三、常見的動態內存錯誤
- 3.1 對NULL指針解引用操作
- 3.2 對動態開辟空間的越界訪問
- 3.3 對非動態開辟內存使用free釋放
- 3.4 使用free釋放動態開辟內存的一部分
- 3.5 對同一塊內存多次釋放
- 3.6 動態開辟內存忘記釋放(內存泄漏)
- 四、幾個經典的例子
- 五、c/c++程序的內存分配
- 六、柔性數組
- 6.1柔型數組的特點
- 6.2 柔性數組的使用
- 6.3柔性數組的優勢
前言:
我們一般開辟內存是直接開辟空間,開辟了空間就不會改變了,為了更節約空間,避免浪費空間,我們可以動態的開辟空間。這樣,空間用完了,我們可以擴充空間。
一、為什么存在動態內存分配
我們已經掌握的內存開辟方式:
int vai=10; / /在棧空間上開辟四個字節。
int arr[10]={0}; / /在棧空間上開辟40個字節的連續空間。
特點:
- 空間開辟大小是固定的。
- 數組在申明的時候,必須指定長度,才能在編譯的時候分配空間。
對于空間的需求,不僅僅是上述的情況,有時候我們需要的空間大小只有在程序運行的時候才會知道,數組在編譯時開辟空間的方式就不能滿足了。
二、動態內存函數
關于動態內存函數的知識,可以參考我的另一篇文章動態內存函數詳解
2.1malloc函數
void* malloc(size_t size);
malloc函數向內存申請一塊連續可用的空間,返回指向這個空間的指針。
malloc函數申請到空間后直接返回這塊空間的起始地址,不會初始化空間的內容。
malloc申請的空間,當程序退出時,還給操作系統,當程序不退出,動態申請的內存,不會主動釋放,需要用free函數來釋放
2.2calloc函數
void* calloc(size_t num , size_t size) ;
calloc函數也可以申請動態內存空間。并且給空間初始化為0。
2.3realloc函數
void* realloc( void* ptr , size_t size);
realloc函數可以申請動態內存空間,使動態內存空間管理更靈活。
2.4free函數
void* free (void* ptr ) ;
free函數只能釋放動態開辟的內存。
三、常見的動態內存錯誤
3.1 對NULL指針解引用操作
可能運行成功
#include <stdio.h>
#include <stdlib.h>
void test()
{int* p = (int*)malloc(10 * sizeof(int));*p = 20;//如果p的值是NULL就會有問題free(p);
}
int main()
{test();return 0;
}
解決方法:需要對空指針進行判定(空指針不能被賦值,就是p為空不能解引用)
#include <stdio.h>
#include <stdlib.h>
void test()
{int* p = (int*)malloc(10 * sizeof(int));if (p == NULL){perror("malloc");return;}*p = 20;//如果p的值是NULL就會有問題free(p);
}
int main()
{test();return 0;
}
3.2 對動態開辟空間的越界訪問
越界訪問系統會崩潰
#include <stdio.h>
#include <stdlib.h>
void test()
{int i = 0;int* p = (int*)malloc(10 * sizeof(int));if (p == NULL){perror("malloc");return;}for (i = 0; i <= 10; i++){*(p + i) = i + 1;//當i=10時,越界訪問}free(p);
}
int main()
{test();return 0;
}
解決:因為要訪問的空間存在越界問題,那么我們就讓它不越界訪問
#include <stdio.h>
#include <stdlib.h>
void test()
{int i = 0;int* p = (int*)malloc(10 * sizeof(int));if (p == NULL){perror("malloc");return;}for (i = 0; i < 10; i++)//改為i < 10{*(p + i) = i + 1;}free(p);
}
int main()
{test();return 0;
}
3.3 對非動態開辟內存使用free釋放
程序崩潰
#include <stdio.h>
#include <stdlib.h>
void test()
{int a = 10;int* p = &a;//釋放非動態開辟內存空間有問題free(p);//不行,運行程序系統崩潰
}
int main()
{test();return 0;
}
解決方法:非動態開辟的內存不要用free函數釋放
3.4 使用free釋放動態開辟內存的一部分
系統崩潰
#include <stdio.h>
#include <stdlib.h>
void test()
{int* p = (int*)malloc(10 * sizeof(int));if (p == NULL){perror("malloc");return;}int i = 0;for (i = 0; i < 5; i++){*p = i;p++;}//釋放free(p);//p不在指向動態內存的起始位置p = NULL;
}
int main()
{test();return 0;
}
解決問題:動態開辟內存起始位置不能亂使用,釋放的時候要從起始位置全部釋放(開辟的一塊連續的空間)
p++;(這里p不是指向開辟空間的起始位置,釋放的是后面的一部分,前面沒有釋放,程序崩潰了。)
3.5 對同一塊內存多次釋放
程序崩潰了,不允許這么操作。
#include <stdio.h>
#include <stdlib.h>
void test()
{int* p = (int*)malloc(10 * sizeof(int));if (p == NULL){perror("malloc");return;}//使用//釋放free(p);//釋放free(p);//重復釋放,程序就崩了
}
int main()
{test();return 0;
}
解決方法:第一次釋放結束,把p置NULL,下一次釋放,釋放空指針,啥事都不干。
#include <stdio.h>
#include <stdlib.h>
void test()
{int* p = (int*)malloc(10 * sizeof(int));if (p == NULL){perror("malloc");return;}//使用//釋放free(p);//釋放結束,把p置空p = NULL;//釋放free(p);
}
int main()
{test();return 0;
}
3.6 動態開辟內存忘記釋放(內存泄漏)
找不到p,沒有釋放技術內存泄漏
#include <stdio.h>
#include <stdlib.h>
void test()
{int* p = (int*)malloc(100);if (p == NULL){perror("malloc");return;}*p = 20;
}
int main()
{test();//回來之后,p被銷毀while (1);return 0;
}
- 把100個字節空間的起始地址賦值給p。
- p是函數里的局部變量。
- 出了這個函數p被銷毀,找不到這100個字節的空間了,但是這100個字節的空間還在。
解決方法:只有兩種方式可以銷毀。
- free函數釋放。
- 程序結束(退出)
注意:
動態申請的內存空間不會因為出了作用域就自動銷毀,也·不會把內存還給操作系統。
四、幾個經典的例子
題目1:
存在兩個問題
#include <stdio.h>
#include <stdlib.h>
void Getmemory(char* p)
{p = (char*)malloc(100);//沒有釋放空間,存在內存泄漏的問題
}
void test()
{char* str = NULL;Getmemory(str);strcpy(str, "hello world");//不能對空指針進行解引用操作,程序會崩潰。printf(str);
}
int main()
{test();return 0;
}
題目2:
存在一個問題
#include <stdio.h>
#include <string.h>
char* Getmemory()
{char p[] = "hello world";//函數結束,內存釋放,空間還給操作系統。
}
void test(void)
{char* str = NULL;str=Getmemory();//記住了地址,卻找不到數據(這是個野指針,它非法訪問)strcpy(str, "hello world");printf(str);
}
int main()
{test();return 0;
}
題目3:
存在一個問題
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void Getmemory(char** p, int num)
{*p = (char*)malloc(num);
}
void test(void)
{char* str = NULL;Getmemory(&str,100);strcpy(str, "hello world");printf(str);//內存泄漏//free(str);//str=NULL;
}
int main()
{test();return 0;
}
題目4:
存在一個問題
#include <stdio.h>
#include <string.h>
#include <stdlib.h>void test(void)
{char* str =(char*) malloc(100);strcpy(str, "hello");free(str);//內存被釋放,str成為野指針。if (str != NULL)//肯定成立{strcpy(str, "world");//非法訪問printf(str);}printf(str);
}
int main()
{test();return 0;
}
五、c/c++程序的內存分配
初步了解c/c++中內存區域的劃分:
1.棧區(stack) :在執行函數時, 函數內局部變量的存儲單元都可以在棧上創建,函數執行結束時這些存儲單元自動被釋放。棧內存分配運算內置于處理器的指令集中,效率很高,但是分配的內存容量有限。棧區主要存放運行函數而分配的局部變量、 函數參數、返回數據、返回地址等。
2.堆區(heap) :一般由程序員分配釋放, 若程序員不釋放, 程序結束時可能由OS回收,分配方式類似于鏈表,
3.數據段(靜態區) (static): 存放全局變量,靜態數據。 程序結束后由系統釋放。
4.代碼段:存放函數體(類成員函數和全局函數)的二進制代碼。
注意:
實際上普通的局部變量是在棧區分配空間的,棧區的特點是在上面創建的變量出了作用域就銷毀。但是被static修飾的變量存放在數據段(靜態區),數據段的特點是在上面創建的變量,直到程序結束才銷毀。
所以static修飾的變量生命周期變長了。
六、柔性數組
結構體中,最后一個成員是數組或者是指針,他們是有區別的。
c99中,結構體的最后一個成員可以是未知大小的數組,這叫柔性數組的成員。
結構體:
typedef struct st
{int i;int arr[];
}ts;
6.1柔型數組的特點
結構體中柔性數組前面必須至少有一個其他成員。
sizeof返回的這種結構的大小不包括柔性數組的內存。(只計算柔性數組前面成員的大小)
包含柔性數組成員的結構用malloc()函數進行內存的動態分配,并且分配的內存應該大于結構的大小,以適應柔性數組的預期大小。
如:
ts* ps = (ts*)malloc(sizeof(ts) + 40);
6.2 柔性數組的使用
int main()
{
int i = 0;
ts* p = (ts*)malloc(sizeof(ts) + 100 * sizeof(int));
if (p == NULL)
{perror("malloc");return 0 ;
}p->i = 100;
for (i = 0; i < 100; i++)
{p->arr[i] = i + 1;
}
free(p);
p = NULL;
return 0;
}
柔性數組成員a,相當于獲得了100個整型元素的連續空間
6.3柔性數組的優勢
代碼1:
int i = 0;
ts* p = (ts*)malloc(sizeof(ts) + 100 * sizeof(int));
if (p == NULL)
{perror("malloc");return 0 ;
}p->i = 100;
for (i = 0; i < 100; i++)
{p->arr[i] = i + 1;
}
free(p);
p = NULL;
代碼2:
typedef struct st
{int i;int* p;
}ts;
ts* ps = (ts*)malloc(sizeof(ts));
ps->i = 100;
ps->p = (int*)malloc(ps->i * sizeof(int));
for (i = 0; i < 100; i++)
{ps->p[i] = i + 1;
}
//釋放空間
free(ps->p);//先釋放里面
ps->p = NULL;
free(ps);//后釋放外面
ps = NULL;
上面代碼1和代碼2可以完成相同的功能,但是代碼1的實現有兩種好處。
第一個好處:方便內存釋放
如果我們的代碼是在一個給別人用的函數中, 你在里面做了二次內存分配并把整個結構體返回給用戶。用戶調用free可以釋放結構體,但是用戶并不知道這個結構體內的成員也需要free,所以你不能指望用戶來發現這個事。所以,如果我們把結構體的內存以及其成員要的內存一次性分配好了,并返回給用戶一個結構體指針,用戶做一次free就可以把所有的內存也給釋放掉。
第二個好處:這樣有利于訪問速度
連續的內存有益于提高訪問速度,也有益于減少內存碎片。