C語言記憶體管理總結
文章目錄
目錄
C語言記憶體管理總結
文章目錄
前言
一、記憶體管理簡介以及常見的記憶體使用錯誤
二、記憶體分類
1.堆疊區(stack)
2.全域區
3.常量區
4.堆區(heap)
三、malloc(),calloc(),realloc()函式
1.malloc:
2.calloc:
3.realloc:
四、strcpy(),memcpy(),memmove()函式
1.strcpy:
2.memcpy:
3.memmove:
4.memset:
五、堆疊區(stack)
六、堆區(heap)
申請方式:
申請后系統的回應:
申請大小的限制:
申請效率的比較:
堆和堆疊中的存盤內容:
七、全域區(靜態區)
八、常量區
九、常見錯誤之記憶體越界與記憶體泄露(Memory Leak)
1.記憶體越界
十、記憶體碎片解決
總結
前言
- 最近也是比較忙,而在學習嵌入式系統的程序中,問題不斷出現,其中一個很重要的問題,也就是記憶體管理知識點的不牢固導致的記憶體分配的各類錯誤,學得我心血來潮,于是就打算寫一篇文章來記錄學習的時候遇到的問題以及記憶體管理的知識點,
- 本文參照了大量的文章與視頻,鏈接將會于“總結”部分發布,
- 因為自我感覺也不怎么良好,所以該文也是比較偏向于小白,適合于萌新食用,當然,如果有哪里不足也希望大佬能夠指點指出
一、記憶體管理簡介以及常見的記憶體使用錯誤
雖然我們現在的計算機系統的記憶體已經在“宏大”的方向發展,但學會記憶體管理,并不為一件壞事,相反,這種“優良傳統”在各類場景下會給我們帶來益處,在計算機系統中,特別是嵌入式系統中,記憶體資源是十分有限的,尤其是對于移動端的開發者來說,硬體資源的限制使得其在程式的設計中首先要考慮的問題就是,如何去合理的分配那“一丟丟”的記憶體資源,
日常中我們可能遇到的錯誤:
- 記憶體申請沒成功,就去使用了.
- 記憶體申請成功,但沒有初始化.
- 記憶體初始化成功,但越界訪問.
- 忘記釋放記憶體或者釋放一部分.
而記憶體管理不當會導致些什么了?
1.記憶體泄露
- 記憶體泄露是什么呢,會導致什么?
- 記憶體泄漏也稱作"存盤滲漏",用動態存盤分配函式動態開辟的空間,在使用完畢后未釋放,結果導致一直占據該記憶體單元,直到程式結束,(其實說白了就是該記憶體空間使用完畢之后未回收)即所謂記憶體泄漏,
- 最終結果是程式運行時間越長,占用存盤空間越來越多,最終用盡全部存盤空間,整個系統崩潰
2.越界訪問
- 越界訪問就比較好理解了吧,學習過C語言的xdm,都知道陣列越界,若是越界訪問到陣列之外的的元素,那導致的必然是error,error,error,
3.記憶體出錯
- 記憶體出錯這個問題,一般都是記憶體申請后沒有初始化,類比野指標,
C語言為用戶提供了很多相應的AP介面,如malloc(),realloc(),calloc(),free(),new()等函式,需要開發者進行手動管理,許多高級語言都有記憶體自動回收的機制,比如python,很遺憾,C語言沒有,也便需要我們自行解決記憶體釋放的問題
二、記憶體分類
1.堆疊區(stack)
堆疊(stack)又名堆疊,它是一種運算受限的線性表,限定僅在表尾進行插入和洗掉操作的線性表,
2.全域區
靜態區存放程式中所有的全域變數和靜態變數,程式結束后有系統釋放,
3.常量區
常量字串就是放在這里的, 程式結束后由系統釋放,
4.堆區(heap)
一般由程式員分配釋放, 若程式員不釋放,程式結束時可能由OS回收 ,分配方式類似于鏈表,
三、malloc(),calloc(),realloc()函式
這三個函式它們都能分配堆記憶體,并且回傳記憶體的首地址,如果失敗就回傳NULL
1.malloc:
函式原形:
void *malloc(
size_t size
);
該函式會在堆上分配一個size(byte)大小的記憶體,不對記憶體進行初始化,所以其記憶體值是隨機的
示例:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *ptr;
ptr = (int *)malloc(sizeof(int));
if (ptr == NULL)
{
printf("memory allocation error!!");
exit(1);
}
printf("請輸入一個整數 :");
scanf("%d", ptr);
printf("你輸入的整數是 :%d\n",*ptr);
free(ptr);
return 0;
}
結果:
- 請輸入一個整數:666↓
- 你輸入的整數是:666
2.calloc:
函式原形:
void *calloc(
size_t number,
size_t size
);
該函式與malloc函式幾乎一致,唯一不同是它將分配count個size大小的記憶體空間并自動初始化該記憶體空間為0,
3.realloc:
函式原型
void *realloc(
void *memblock,
size_t size
);
該函式可將ptr記憶體大小動態變化,增大或變小,但在某種程度,這樣的代碼段運行效率會變低,不太建議使用這個函式,
示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
int i, num;
int count = 0;
int* ptr = NULL;
do
{
printf("請輸入一個整數(輸入-1表示結束):");
scanf("%d", &num);
count++;
ptr = (int *)realloc(ptr, count *sizeof(int));
if (ptr == NULL)
{
exit(1);
}
ptr[count - 1] = num;
} while (num != -1);
printf("輸入的整數分別是 :");
for (i = 0; i < count; i++)
{
printf("%d ", ptr[i]);
}
putchar('\n');
free(ptr);
return 0;
}
四、strcpy(),memcpy(),memmove()函式
頭檔案:
#include <string.h>
1.strcpy:
函式原型:
char *strcpy(
char *strDestination,
const char *strSource
);
把src所指由\0結束的字串復制到dest所指的陣列中,
注意事項:
src和dest所指記憶體區域不能重疊,且dest必須有足夠的空間來容納src的字串,src的結尾必須是'\0',回傳指向dest的指標,
2.memcpy:
函式原型:
void *memcpy(
void *dest,
const void *src,
size_t count
);
由src所指記憶體區域復制 count個位元組到dest所指記憶體區域,
注意事項:
函式回傳指向
dest的指標和strcpy相比,memcpy不是遇到\0就結束,而一定會拷貝n個位元組注意src和dest所指記憶體區域不能重疊,否則不能保證正確
3.memmove:
函式原型:
void *memmove(
void *dest,
const void *src,
size_t count
);
函式功能:與 memcpy相同,
注意事項:
src和dest所指記憶體區域可以重疊,memmove可保證拷貝結果正確,而memcpy不能保證,函式回傳指向dest的指標,
4.memset:
函式原型:
void *memset(
void *dest,
int c,
size_t count
);
常用于內存空間的初始化,將已開辟記憶體空間s的首n個位元組的值設為值c,并回傳s,
示例代碼:
#include<stdio.h>
#include<string.h>
#include<assert.h>
//模擬memcpy函式實作
void * MyMemcpy(void *dest, const void *source, size_t count)
{
assert((NULL != dest) && (NULL != source));
char *tmp_dest = (char *)dest;
char *tmp_source = (char *)source;
while (count--)//不判斷是否重疊區域拷貝
*tmp_dest++ = *tmp_source++;
return dest;
}
//模擬memmove函式實作
void * MyMemmove(void *dest, const void *src, size_t n)
{
char temp[256];
int i;
char *d =(char*) dest;
const char *s =(char *) src;
for (i = 0; i < n; i++)
temp[i] = s[i];
for (i = 0; i < n; i++)
d[i] = temp[i];
return dest;
}
int main()
{
//strcpy進行字串拷貝
//注意: 1. src字串必須以'\0'結束, 2. dest記憶體大小必須>=src
char a[5];
//char b[5] = "ABC";//字串結尾會自動的有\0 , 此處 b[4]就是'\0'
char b[5];
b[0] = 'A';
b[1] = 'B';
b[2] = 'C';
b[3] = '\0';//必須加\0,否則strcpy一直向后尋找\0
strcpy(a, b);
printf("%s\n", a);
//memcpy函式, 直接拷貝記憶體空間,指定拷貝的大小
int a2[5];
int b2[5] = { 1,2,3,4,5 };//不需要'\0'結束
memcpy(a2, b2, 3 *sizeof(int) );//指定拷貝的大小, 單位 位元組數
printf("%d , %d ,%d\n" , a2[0] , a2[1], a2[2]);
MyMemcpy(a2 + 3, b2 + 3, 2 * sizeof(int));
printf("%d , %d \n", a2[3], a2[4]);
//演示記憶體重疊的情況
char a3[6] = "123";
//MyMemcpy(a3 + 1, a3, 4); //得到11111
memcpy(a3 + 1, a3, 4);//雖然它是正確的,但是不保證,重疊拷貝應該避免使用它
printf("%s\n", a3);
//memmove功能與memcpy一樣,但是了考慮了重疊拷貝的問題,可以保證正確
char a4[6] = "123";
//MyMemmove(a4 + 1, a4, 4);//可以保證正確
memmove(a4 + 1, a4, 4);//可以保證正確
printf("%s\n", a4);
//memset比較簡單, 把記憶體區域初始化化為某個值
char a5[6];
memset(a5, 0, 6);
for (int i = 0; i < 6; ++i)
{
printf("%d", a5[i]);
}
return 0;
}
五、堆疊區(stack)
- 由編譯器自動分配,自動釋放的記憶體區,存放函式的引數值.區域變數的值.回傳資料.回傳地址等,其主要特點為:先進后出(后進先出)
- 在函式呼叫時,第一個進堆疊的是主函式中函式呼叫后的下一條指令(函式呼叫陳述句的下一條可執行陳述句)的地址,然后是函式的各個引數,在大多數的C編譯器中,引數是由右往左入堆疊的,然后是函式中的區域變數,
- 生命周期與函式以及區域變數一致
六、堆區(heap)
- 需要程式員自己手動申請,并且可以在運行時指定空間大小,并由程式員手動進行釋放,易導致記憶體泄露(memory leak)
- 上面提到的三個函式malloc(),calloc(),realloc(),便為申請堆區記憶體所需要使用的函式,
- 生命周期:什么時候釋放什么時候結束
堆和堆疊的比較:
-
申請方式:
- stack: 由系統自動分配, 例如,宣告在函式中一個區域變數 int b; 系統自動在堆疊中為b開辟空間,
- heap: 需要程式員自己申請,并指明大小,在C中malloc函式,C++中是new運算子,
- 如p1 = (char *)malloc(10); p1 = new char[10];
- 如p2 = (char *)malloc(10); p2 = new char[20];
- 但是注意p1、p2本身是在堆疊中的,
-
申請后系統的回應:
- 堆疊:只要堆疊的剩余空間大于所申請空間,系統將為程式提供記憶體,否則將報例外提示堆疊溢位,
- 堆:首先應該知道作業系統有一個記錄空閑記憶體地址的鏈表,當系統收到程式的申請時,會遍歷該鏈表,尋找第一個空間大于所申請空間的堆結點,然后將該結點從空閑結點鏈表中洗掉,并將該結點的空間分配給程式,
- 對于大多數系統,會在這塊記憶體空間中的首地址處記錄本次分配的大小,這樣,代碼中的delete陳述句才能正確的釋放本記憶體空間,
- 由于找到的堆結點的大小不一定正好等于申請的大小,系統會自動的將多余的那部分重新放入空閑鏈表中,
-
申請大小的限制:
- 堆疊:在Windows下,堆疊是向低地址擴展的資料結構,是一塊連續的記憶體的區域,這句話的意思是堆疊頂的地址和堆疊的最大容量是系統預先規定好的,在 WINDOWS下,堆疊的大小是2M(也有的說是1M,總之是一個編譯時就確定的常數),如果申請的空間超過堆疊的剩余空間時,將提示overflow,因 此,能從堆疊獲得的空間較小,
- 堆:堆是向高地址擴展的資料結構,是不連續的記憶體區域,這是由于系統是用鏈表來存盤的空閑記憶體地址的,自然是不連續的,而鏈表的遍歷方向是由低地址向高地址,堆的大小受限于計算機系統中有效的虛擬記憶體,由此可見,堆獲得的空間比較靈活,也比較大,
-
申請效率的比較:
- 堆疊由系統自動分配,速度較快,但程式員是無法控制的,
- 堆是由new分配的記憶體,一般速度比較慢,而且容易產生記憶體碎片,不過用起來最方便,
- 另外,在WINDOWS下,最好的方式是用VirtualAlloc分配記憶體,他不是在堆,也不是堆疊,而是直接在行程的地址空間中保留一快記憶體,雖然用起來最不方便,但是速度快,也最靈活,
-
堆和堆疊中的存盤內容:
- 堆疊:在函式呼叫時,第一個進堆疊的是主函式中后的下一條指令(函式呼叫陳述句的下一條可執行陳述句)的地址,然后是函式的各個引數,在大多數的C編譯器中,引數是由右往左入堆疊的,然后是函式中的區域變數,注意靜態變數是不入堆疊的,當本次函式呼叫結束后,區域變數先出堆疊,然后是引數,最后堆疊頂指標指向最開始存的地址,也就是主函式中的下一條指令,程式由該點繼續運行,
- 堆:一般是在堆的頭部用一個位元組存放堆的大小,堆中的具體內容有程式員安排,
七、全域區(靜態區)
- 全域變數和靜態變數的存盤是放在一塊的,初始化的全域變數和靜態變數在塊區域,
- 全域變數及靜態變數都是在整個程式運行時都一直存在的,其生命周期為程式結束
八、常量區
- 字串常量是放在常量區,當你初始化賦值的時候,這些常量就先在常量區開辟一段空間,保存此常量,以后相同的常量就都使用一個地址,
九、常見錯誤之記憶體越界與記憶體泄露(Memory Leak)
1.記憶體越界
道理很簡單,如其名稱一樣,記憶體越界也就是你申請了一塊記憶體,但在你使用這塊記憶體的時候,你使用的范圍超出了你申請到的記憶體的范圍導致記憶體越界
- 訪問到野指標指向的區域,越界訪問
- 陣列下標越界訪問
- 使用已經釋放的記憶體
- 企圖訪問一段釋放的堆疊空間
- 容易忽略 字串后面的
'\0'
注意:
strlen所作的是一個計數器的作業,它從記憶體的某個位置(可以是字串開頭,中間某個位置,甚至是某個不確定的記憶體區域)開始掃描,直到碰到第一個字串結束符'\0'為止,然后回傳計數器值( 長度不包含’\0’),
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
char * fun()
{
char arr[10];
return arr;
}//arr是堆疊記憶體,離開此花括號,堆疊被釋放回收
int main()
{
//1.訪問到野指標指向的區域,越界訪問
char *p;//沒有初始化,野指標,亂指一氣
//strcpy(p, "hello");//非法越界訪問
//2.陣列下標越界訪問
int * p2 = (int *)calloc(10, sizeof(int));
for (size_t i = 0; i <= 10; i++)
{
p2[i] = i;//很難察覺的越界訪問, 下標越界
}
//3.使用已經釋放的記憶體
char *p3 = (char *)malloc(10);
free(p3);
if (p3 != NULL)//這里if不起作用
{
strcpy(p3, "hello");//錯誤,p3已經被釋放
}
//4.企圖訪問一段釋放的堆疊空間
char *p4 = fun(); //p4指向的堆疊空間已經被釋放
strcpy(p4, "hello");
printf("%s\n",p4);
//5.容易忽略 字串后面的'\0'
char *p5 = (char *)malloc(strlen("hello"));//忘記加1
strcpy(p5, "hello");//導致p5的長度不夠,越界
return 0;
}
2.記憶體泄露(Memory Leak)
前面相信大家都看到了free(ptr);這一簡短的代碼,其作用便是釋放我們在堆上申請的記憶體,防止程式的未釋放,造成系統記憶體的浪費,導致記憶體運行速度減慢,甚至是系統崩潰等后果
雖然現在的電腦的記憶體已經普遍較高,最低顯存現在都是8G以上,所以很多人似乎對記憶體釋放這件事并不擔心
如果你也是抱著這樣的心態:那請試試下面這行代碼hhhh
看看你的電腦能撐多久呢?
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *ptr = NULL;
while(1)
{
ptr = malloc(1024);
}
return 0;
}
十、記憶體碎片解決
記憶體碎片一般是由于空閑的內存空間比要連續申請的空間小,導致這些小記憶體塊不能被充分的利用,當你需要分配大的連續記憶體時,盡管剩余記憶體的總和足夠,但系統找不到連續的記憶體,所以導致分配失敗malloc/free大量使用會造成記憶體碎片
- 碎片問題:
- 對于堆來講,頻繁的new/delete勢必會造成記憶體空間的不連續,從而造成大量的碎片,使程式效率降低,
- 對于堆疊來講,則不會存在這個問題,因為堆疊是先進后出的佇列,他們是如此的一一對應,以至于永遠都不可能有一個記憶體塊從堆疊中間彈出,在他彈出之前,在他上面的后進的堆疊內容已經被彈出,
解決方法:
- 記憶體池技術
- 記憶體的申請、釋放是低效的,我們只在開始申請一塊大內存(不夠繼續申請),然后每次需要時都從這塊記憶體取出,并標記這塊記憶體是否被使用,釋放時僅僅標記而不真的
free,只有記憶體都空閑的時候,才釋放給作業系統,這樣減少了malloc、free次數,從而提高效率,
設計思路:
- 先分配幾個大的連續記憶體塊(MemoryBlock),每個記憶體塊用鏈表鏈接起來,然后通過一個內存池結構(MemoryPool)管理,
- 詳細看https://blog.csdn.net/u014779536/article/details/116354403?utm_source=app這篇文章
總結
文章內容大部分來源于百度,CSDN,以及視頻以下為這些資料的鏈接,在這里統一貼出來
- https://blog.csdn.net/u014779536/article/details/116354403?utm_source=app
- https://blog.csdn.net/qq_34793133/article/details/85713413
- https://fishc.com.cn/forum.php?mod=viewthread&tid=35614&highlight=%C4%DA%B4%E6
- 百度:https://baike.baidu.com/item/%E6%A0%88/12808149?fr=aladdin,https://baike.baidu.com/item/%E5%A0%86/20606834?fr=aladdin
- 視頻:https://www.bilibili.com/video/BV1jW411K7yg?from=search&seid=18059219221845422969
- 看到這里十分感謝
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/283160.html
標籤:AI
上一篇:RRT演算法的一種關于最終路徑的創新matlab仿真
下一篇:學習系統呼叫介面
