動態內存的開辟在C語言中相當重要的知識
1、為什么會存在動態內存分配
內存的開辟方式:
int a=20;//在棧空間上開辟4個字節
int arr[10];//在棧空間上開辟40個字節的連續空間
這種開辟空間的方式有兩個特點:
1、開辟的空間大小是固定的
2、數組在聲明的時候,必須指定數組長度,它需要的內存在編譯時分配。
但是這種內存開辟的方式存在缺陷,比如我們在寫通訊錄管理系統時指定了100個元素,但當我們填入元素過多時空間會不夠用,當聯系人較少時,又會產生空間的浪費,所以我們可以用一種靈活的方式,用多少提供多少,這種方式即動態內存管理。
2、動態內存函數的介紹
2.1malloc和free
void * malloc(size_t? size);
·?如果分配成功則返回指向被分配內存的指針,否則返回空指針NULL。
·?返回類型為void*,所以malloc函數并不知道開辟空間的類型,具體在使用時侯使用者自己來決定。
·?如果參數size為0,malloc的行為是標準為定義的,取決于編譯器。
free是專門用來做動態內存的釋放和回收的:
void free(void* ptr)
·?如果參數ptr指向的空間不是動態開辟的,那么free的行為是未定義的
·?如果參數ptr是NULL指針,則函數什么事都不做
malloc函數與free函數的聲明都在stdlib.h中
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
//malloc函數的使用
int main()
{int arr[10] = { 0 };//動態內存分配int* p = (int*)malloc(40);if (p == NULL){printf("%s\n", strerror(errno));return 1;}//說明開辟成功,使用內存int i = 0;for (i = 0; i < 10; i++){*(p + i) = i;}for (i = 0; i < 10; i++){printf("%d ", *(p + i));}//free(p);//p=NULL;return 0;
}
//沒有free,并不是說內存空間就不回收了,當程序退出的時候,系統會自動回收內存空間的
注意:在使用malloc開辟空間時,使用完成一定要釋放空間,否則可能導致內存泄漏。?
內存泄漏:是指程序動態分配內存后,未能及時釋放這些內存,導致系統無法再為其他對象分配內存,或者可能導致系統內存耗盡的現象。
?2.2calloc
calloc函數也用來動態內存分配
void * calloc(size_t num,size_t size);
·????????函數的功能是為num個大小為size的元素開辟一塊空間,并且把空間的每個字節初始化為0.
·????????與函數malloc的區別在于calloc會在返回地址之前把申請的空間的每個字節初始化為全0。
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
int main()
{int* p = (int*)calloc(10, sizeof(int));if (p == NULL){printf("%s\n", strerror(errno));return 1;}int i = 0;for (i = 0; i < 10; i++){printf("%d ", *(p + i));}free(p);p = NULL;return 0;
}//calloc與malloc的最大區別就是calloc會在返回地址之前把申請的空間的每個字節初始化為全0。
2.3realloc?
·????????realloc函數的出現讓動態內存管理更加靈活
·????????有時我們會發現過去申請的空間太小了,有時候我們又會覺得申請的空間過大了,為了合理的分配內存,我們一定會對內存的大小做靈活地調整。那么realloc函數就可以做到對動態開辟內存大小的調整。
void * realloc(void* ptr,size_t size);
·????????ptr是要調整的內存地址
·??????? size是調整后的新大小?
·????????返回為調整之后的內存起始位置。
·????????這個函數調整原內存空間大小的基礎上,還會將原來內存中的數據移動到新空間
·????????realloc在調整內存空間存在兩種情況:
1、原有空間之后有足夠大的空間;(直接追加)
2、原有空間之后沒有足夠大的空間;(會覆蓋其他數據,所以開辟一塊更大的空間,把原有數據拷貝過去)
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
int main()
{int* p = (int*)malloc(40);if (p == NULL){printf("%s\n", strerror(errno));return 1;}int i = 0;for (i = 0; i < 10; i++){*(p + i) = i+1;}//擴容int* ptr = (int*)realloc(p, 80);//不能放在p中,因為若是給的數字過大,無法申請空間,從而返回空指針,會導致p原來的數據丟失{if (ptr != NULL)//擴容成功{p = ptr;}}for (i = 0; i < 10; i++){printf("%d ", *(p + i));}free(p);p=NULL;return 0;
}
3、常見的動態內存錯誤
3.1對NULL指針解引用操作
int main()
{
?? ?int* p = (int*)malloc(40);//應該判斷p是否為空指針,否則空間可能開辟失敗
?? ?*p = 20;
?? ?return 0;
}
3.2對動態開辟空間的越界訪問
int main()
{
?? ?int* p = (int*)malloc(40);
?? ?if (p == NULL)
?? ?{
?? ??? ?return 1;
?? ?}
?? ?int i = 0;
?? ?for (i = 0; i <= 10; i++)
?? ?{
?? ??? ?p[i] = i;
?? ?}
?? ?free(p);
?? ?p = NULL;
?? ?return 0;
}
3.3對非動態開辟內存使用free釋放
int main()
{
?? ?int a = 10;
?? ?int* p = &a;
?? ?free(p);
?? ?return 0;//這個代碼會崩潰因為p所指向的空間是在棧區開辟的,并不是動態開辟的(堆區),不能進行釋放
}
3.4使用free釋放一塊動態開辟內存的一部分
int main()
{
?? ?int* p = (int*)malloc(40);
?? ?if (p == NULL)
?? ?{
?? ??? ?return 1;
?? ?}
?? ?int i = 0;
?? ?for (i = 0; i < 10; i++)
?? ?{
?? ??? ?*p = i;
?? ??? ?p++;
?? ?}
?? ?free(p);
?? ?p = NULL;//因為p的位置會發生改變不再是起始空間的地址,釋放僅僅釋放了一部分
?? ?return 0;
}
3.5對同一塊動態內存多次釋放
int main()
{
?? ?int* p = (int*)malloc(40);
?? ?if (p == NULL)
?? ?{
?? ??? ?return 1;
?? ?}
?? ?//.....
?? ?free(p);//p一旦釋放完后,所指向的空間已經回收,但p依然會記得地址,此時p就相當于一個野指針相當危險
?? ?//.....
?? ?free(p);
?? ?return 0;
}
3.6動態開辟內存忘記釋放(內存泄漏)
//情形一:
void test()
{
?? ?int* p = (int*)malloc(40);
?? ?//........
?? ?int x = 0;
?? ?scanf("%d", &x);//如果在這里用戶輸入1,是會直接返回,函數瞬間結束,malloc開辟的空間就永遠無法釋放,從而導致內存的泄露
?? ?if (x == 1)
?? ??? ?return;
?? ?//.......
?? ?free(p);
?? ?p = NULL;
}
int main()
{
?? ?test();
?? ?return 0;
}//情形二:
int* test()
{
?? ?int* p = (int*)malloc(40);
?? ?if (p == NULL)
?? ?{
?? ??? ?return p;
?? ?}
?? ?//.........
?? ?return p;
}
int main()
{
?? ?int* ptr = test();
?? ?//忘記釋放
?? ?return 0;
}
4、典型例題分析
4.1題目1:?
void GetMeory(char* p)//形參p,里面放的也是NULL
{p = (char*)malloc(100);//p被賦值,使用malloc申請100個字節的空間,此時p不再是NULL,而是所申請空間的首元素(是個地址)
}//這個函數一旦結束,因為p是形參,只能在函數內部使用,出了函數,p就銷毀了,但malloc申請的空間依然還在,從而發生空間泄露
void test(void)
{char* str = NULL;//str是局部變量GetMeory(str);//傳遞的是實參str,存放的是空指針strcpy(str, "hello world");//str此時依然為空指針,代碼必然崩潰,因為strcpy模擬實現包括解引用操作,而對NULL解引用會出現錯誤printf(str);
}
int main()
{test();return 0;
}
?改寫代碼:
void GetMeory(char** p)
{*p = (char*)malloc(100);//對p解引用得到的其實就是str
}
void test(void)
{char* str = NULL;GetMeory(&str);//這個時候str里面存放的就是動態內存開辟的100個字節的空間strcpy(str, "hello world");printf(str);//釋放free(str);str = NULL;
}
int main()
{test();return 0;
}
4.2題目2:
void GetMemory(char** p, int num)
{*p = (char*)malloc(num);
}
int main()
{char* str = NULL;GetMemory(&str, 100);strcpy(str, "hello");printf(str);return 0;
}//這個邏輯上是沒有任何問題的,可以打印出hello,唯一的問題就是沒有free
4.3題目3:?
void test(void)
{char* str = (char*)malloc(100);//申請了100個字節的空間放到strstrcpy(str, "hello");free(str);//把str指向的空間釋放掉,但str并沒有變,動態開辟的空間實際上已經還給操作系統了if (str != NULL){strcpy(str, "world");//這個時候str就已經是一個野指針了,形成非法訪問printf(str);}
}
int main()
{test();return 0;
}
5、c/c++程序的內存開辟
?簡單了解即可!
6、柔性數組
typedef struct st_type
{
?? ?int i;
?? ?int a[0];//也可以寫成a[ ],柔性數組成員
}type_a;
6.1柔性數組的特點:
·?結構中的柔性數組成員前面必須至少一個其他成員;
·?sizeof返回的這種結構大小不包括柔性數組的內存;
·?包含柔性數組成員的結構用malloc( )函數進行內存的動態分配,并且分配的內存應該大于結構的大小,以適應柔性數組的預期大小。
6.2柔性數組的使用:
//柔性數組的使用,如何訪問空間
struct S
{int n;int arr[];
};
int main()
{struct S* ps=(struct S*)malloc(sizeof(struct S) + 40);//40是柔性數組想要開辟的字節大小,ps->n = 100;int i = 0;for (i = 0; i < 10; i++){ps->arr[i] = i;}for (i = 0; i < 10; i++){printf("%d ", ps->arr[i]);}//柔性數組成員struct S* ptr=(struct S*)realloc(ps, sizeof(struct S) + 80);if(ptr!=NULL){ps=ptr;}//........free(ps);ps=NULL;//ptr已經賦給ps了所以不用釋放ptr,釋放平時即可return 0;
}
struct S
{int n;int* arr;
};
int main()
{struct S* ps = (struct S*)malloc(sizeof(struct S));if (ps == NULL){return 1;}ps->n = 100;ps->arr = (int*)malloc(40);if (ps->arr == NULL){return 1;}//使用int i = 0;for (i = 0; i < 10; i++){ps->arr[i] = i;}for (i = 0; i < 10; i++){printf("%d ", ps->arr[i]);}//釋放free(ps->arr);ps->arr=NULL;free(ps);ps=NULL;return 0;
}
6.3柔性數組的優勢?
?第一個好處:方便內存釋放
如果我們的代碼是在一個給別人使用的函數中,你在里面做了二次內存分配,并把整個結構體返回用戶。用戶調用free可以釋放結構體,但是用戶并不知道這個結構體,但是用戶并不知道這個結構體內的成員也需要free,所以不能指望用戶來發現,所以,如果我們把結構體的內存以及成員要的內存一次性分配好了,并返回給用戶一個結構體指針,用戶做一次free就可以把所有的內存給釋放掉。
第二個好處:有利于訪問速度
連續的內存有利于提高訪問速度,也有益于減少內存碎片?
?
?
?
?