这篇文章给大家介绍怎么在C语言中实现内存管理,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。
C语言是什么
C语言是一门面向过程的、抽象化的通用程序设计语言,广泛应用于底层开发,使用C语言可以以简易的方式编译、处理低级存储器。
1. 内存管理简介
在计算机系统,特别是嵌入式系统中,内存资源是非常 有限的。尤其对于移动端开发者来说,硬件资源的限制使得其在程序设计中首要考虑的问题就是如何 有效地管理内存资源。
常见内存使用错误:
内存申请未成功,就进行使用
内存申请成功,但没有初始化
内存初始化成功,但越界访问
忘记释放内存或者释放一部分
内存管理不当的危害?
没有初始化,会造成内存出错
越界访问内存可能导致崩溃
忘记释放内存造成内存泄露
C语言的内存管理:
C语言为用户提供了相应内存管理的AP接口,如 malloc()
,free()
,new()
等函数,需要开发者手动管理。而java
、C#
则有自动内存回收机制,基本无需再对内存进行操作了。
2. 内存分类 栈区(stack)
由系统自动分配
堆区(heap)
在程序的执行过程中才能分配,由程序员决定
全局区(静态区)
静态区存放程序中所有的全局变量和静态变量
常量区
常量字符串就是放在这里的
代码段:
代码段(code segment/text segment)。通常是指用来存放程序执行代码的一块內存区域。代码区的指令中包括操作码和要操作的对象(或对象地址引用)。如果是立即数(即具体的数值,如5
)直接包含在代码中;如果是局部数据,将在栈区分配空间,然后引用该数据地址。
数据段:
数据段(data segment)通常是指用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。
BSS段:
BSS段(Block Started by Symbol)。指用来存放程序中未初始化的全局变量的一块内存区域。
BSS段本质上也属于数据段,都用来存放C程序中的全局变量。区别在于.data段中存放初始化为非零的全局变量,而把显式初始化为0或者并未显式初始化(C语言规定未显式初始化的全局变量值默认为0)
的全局变量存在BSS段。
3. 栈区(stack)
由编译器 自动分配释放,存放函数的参数值、局部变量的值等,是一种先进后出的内存结构。
哪些是分配在栈空间?
局部变量的值存放在栈上
在函数体中定义的变量通常是在栈上
函数栈分配:
在函数调用时,第一个进栈的是主函数中函数调用后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。
栈内存什么时候回收?
栈内存的分配和释放也由编译器在函数进入和退出时插入指令自动完成,生命周期和函数、局部变量一样。
栈空间的大小:
在 Windows下,栈是向低地址扩展的数据结构,是块连续的内存的区域。栈空间一般较小,栈大小与编译器有关。默认情况下,visual studio 2010
的栈大小为1M
。但在平时应用程序中,由于函数会使用栈结果,所以只能用略小于1M大小的栈如果申请的空间超过栈的剩余空间时,将提示Stack overflow。
示例代码:
#include<stdio.h>struct A{};class B{};void fun(int a , int b) //参数a,b在栈上, 在函数体结束的时候,栈内存释放{ int c;//局部变量,在栈上, 在函数体结束的时候,栈内存释放}int main(){ int a = 10;//局部变量在栈上, 在main函数结束的时候,栈内存释放 char b[] = "hello";//数组变量也在栈上, 在main函数结束的时候,栈内存释放 char *c = NULL;//在栈上, 在main函数结束的时候,栈内存释放 { A d;//结构体变量, 在栈上 B e;//类对象在栈上 } //d,e 在离开这个{}时,栈内存销毁释放 //测试栈的大小 //char buf[1024 * 1024] = { 'A' };//1M时崩溃了 char buf[1000* 1024] = { 'A' };//栈空间略小于1M //经过编译期设置为5M之后,栈空间变大了 char buf[49 * 1024 * 1024 / 10] = { 'A' };//栈空间略小于5M printf("%d" , sizeof(buf) ); return 0;}
4. 堆区(heap)
需程序员自己申请,并可在运行时指定空间大小,并由程序员手动进行释放,容易产生 memory leak
。
哪些是分配在堆空间?
调用 malloc
,realloc
,calloc
函数
//分配得来得10*4字节的区域在堆区p1 = (char*)malloc(10*sizeof(int));
堆空间需要手动释放:
堆是由 malloc()
等函数分配的内存块,内存释放由程序员调用free()
函数手动释放
堆空间的大小:
堆空间一般较大,与64位/32位,编译器有关,受限于计算机系统中有效的虚拟内存;理论上32位系统堆内存可以达到4G的空间,实际上2G以内,64位128G以内(虚拟内存16TB)
示例代码:
#include<stdio.h>#include<stdlib.h>int main(){ //手动分配、这里就是分配了堆内存 int *p = (int*)malloc(10 * sizeof(int )); //手动释放 free(p); int MB = 0; while (malloc(1024 * 1024))//每次分配1M { MB++; } printf("分配了 %d MB \n", MB); return 0;}
栈与堆的区别:
类型 | 分配释放 | 大小 | 是否连续 | 申请效率 |
---|---|---|---|---|
栈区 | 由编译器自动分配释放 | 较小 | 一块连续的内存区域 | 由系统自动分配,速度快 |
堆区 | 由程序员分配释放 | 较大 | 堆是向高地址扩展的数据结构,是不连续的内存区域 | 速度慢,容易产生内存碎片 |
5. 全局区(静态区)
全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在块区域。
哪些是分配在全局静态区?
全局变量
static静态变量
全局静态区何时释放?
全局变量、静态变量在整个程序运行的生存期都存在,所以在程序结束时才释放
示例代码:
#include<stdio.h>//储存在全局静态区int a; //全局变量,未初始化short b = 10;//全局变量,已赋值char *c = NULL;//全局变量,已赋值static int f = 200;//静态变量int main(){ static int d = 100; static int e = 200; printf("%p\n", &a); printf("%p\n", &b); printf("%p\n", &c); printf("%p\n", &d); printf("%p\n", &e); printf("%p\n", &f);}
6. 常量区
字符串常量是放在常量区,当你初始化赋值的时候,这些常量就先在常量区开辟一段空间,保存此常量,以后相同的常量就都使用一个地址。
示例代码:
#include<stdio.h>//“AAA”是字符串常量,存放在常量区char *p = "AAA";int main(){ //p1是局部变量,在栈上, “AAA”是字符串常量,存放在常量区 char *p1 = "AAA"; //p2是局部变量,在栈上,“AAA”不是字符串常量,她只是一种初始化的写法 char p2[]= "AAA"; //p3是局部变量,在栈上, “AAA”是字符串常量,存放在常量区 char *p3 = "AAA"; //p4是局部变量,在栈上, “AAB”是字符串常量,存放在常量区 char *p4 = "AAB"; printf("%p\n", p); printf("%p\n", p1); printf("%p\n", p2); printf("%p\n", p3); printf("%p\n", p4);}
7. malloc、calloc、realloc函数
三个函数的作用?
它们都能分配堆內存、成功返回内存的首地址,失败就返回NULL
。
malloc
函数:
void *malloc( size_t size);
该函数将在堆上分配一个 size
byte大小的内存。不对内存进行初始化,所以新内存其值将是随机的。
calloc
函数:
void *calloc( size_t number, size_t size);
该函数功能与 malloc
相同,它将分配count
个size
大小的内存,自动初始化该内存空间为零。
realloc
函数:
void *realloc( void *memblock, size_t size);
该函数将ptr
内存大小增大或减小到newsize
。
realloc
函数返回的两种情况:
如果当前连续内存块足够
realloc
的话,只是将p1所指向的空间扩大,并返回p1的指针地址。如果当前连续内存块不够长度,再找一个足够长的地方,分配一块新的内存p2,并将p1指向的内容Copy到p2,并释放p1指向的旧内存,然后返回p2。
示例代码:
#include<stdio.h>#include<stdlib.h>int main(){ //malloc ,参数是字节数 , 并且这块内存空间的值是随机的 int *p = (int *)malloc(5 * sizeof(int)); p[0] = 123; for (int i = 0; i < 5; ++i) { printf("%d ", p[i]); //后面4个值随机 } printf("\n------------------------------------------------------------\n " ); //calloc,参数两个, 自动将内存空间初始化为0 int *p2 = (int *)calloc(5, sizeof(int)); p2[4] = 123; for (int i = 0; i < 5; ++i) { printf("%d ", p2[i]); } printf("\n------------------------------------------------------------\n "); //realloc ,可以调整内存空间的大小 ,并且拷贝原来的内容(调大,或者 缩小) //int *p3 =(int *) realloc(p, 6* sizeof(int));//调大一点点,两个地址相同 //int *p3 = (int *)realloc(p, 2 * sizeof(int));//缩小,两个地址相同 int *p3 = (int *)realloc(p, 100 * sizeof(int));//调很大,两个地址不同 ,释放原来的内存空间 for (int i = 0; i <2; ++i) { printf("%d ", p3[i]); } printf("\np地址: %p , p3的地址: %p ", p, p3); return 0;}
8. strcpy、memcpy、memmove函数
头文件:
#include <string.h>
strcpy
函数
char *strcpy( char *strDestination, const char *strSource);
把src
所指由\0
结束的字符串复制到dest
所指的数组中。
注意事项:
src
和dest
所指内存区域不能重叠,且dest
必须有足够的空间来容纳src
的字符串,src
的结尾必须是'\0'
,返回指向dest
的指针。
memcpy
函数
void *memcpy( void *dest, const void *src, size_t count);
由src
所指内存区域复制 count
个字节到dest
所指内存区域。
注意事项:
函数返回指向dest
的指针和 strcpy
相比,memcpy
不是遇到\0
就结束,而一定会拷贝n
个字节注意src
和dest
所指内存区域不能重叠,否则不能保证正确。
memmove
函数
void *memmove( void *dest, const void *src, size_t count);
函数功能:与 memcpy
相同。
注意事项:
src
和dest
所指内存区域可以重叠,memmove
可保证拷贝结果正确,而memcpy
不能保证。函数返回指向dest
的指针。
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;}
9. 实现动态数组
思路:
利用 realloc
函数,当数组元素满的时候,扩充内存区域,然后加入元素!
示例代码:
#include<stdio.h>#include<stdlib.h>#include<assert.h>//为了代码的可读性,将设计为C++中的类,利用struct 代替//动态数组struct Array{ //自动构造函数,它初始化 Array() { grow = 3; size = 3; n = 0; //分配并初始化内存 pHead = (int *)calloc(size , sizeof(int)); assert(pHead != NULL); } void AddElem(int e) { if (n >= size)//说明数组满了 { //需要扩大内存 size += grow; pHead = (int *)realloc( pHead, size * sizeof(int) ); assert(pHead != NULL); } pHead[n++] = e; //添加元素 } void Print() { printf("\n\n数组总空间:%d , 元素个数: %d \n", size, n); for (int i = 0; i < n; ++i) { printf("%d " , pHead[i]); } } int size;//总空间, 不是固定的,可以增大的 int n;//当前数组的元素个数 int grow;//每次数组内存满了的时候,增长量 int *pHead;//数组的起始地址};int main(){ Array arr; arr.AddElem(1); arr.AddElem(2); arr.AddElem(3); arr.AddElem(4); arr.AddElem(5); arr.AddElem(6); arr.AddElem(7); arr.AddElem(8); arr.Print(); arr.AddElem(11); arr.AddElem(22); arr.AddElem(33); arr.AddElem(44); arr.AddElem(55); arr.Print(); return 0;}
10. 内存越界
何谓內存访问越界,简单的说,你向系统申请了一块内存,在使用这块内存的时候,超出了你申请的范围。
访问到野指针指向的区域,越界访问
数组下标越界访问
使用已经释放的内存
企图访问一段释放的栈空间
容易忽略 字符串后面
的'\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;}
内存泄露(Memory Leak)
是指程序中己动态分配的堆內存由于某种原因程序未释放或无法释放,造成系统內存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
丢失了分配的内存的首地址,导致无法释放
丢失分配的内存地址
企图希望传入指针变量获取对内存,殊不知是拷贝
每循环一次,泄露一次内存
非法访问常量区
示例代码:
#include<stdio.h>#include<stdlib.h>#include<string.h>char * GetBuf(){ return (char *)malloc(10);}void GetBuf2(char *p)//p已经是一份拷贝,和原参数无任何关系{ p= (char *)malloc(10);}char * GetBuf3(){ char *p = "hello";//常量内存区,不可更改 return p;}int main(){ //1.丢失了分配的内存的首地址,导致无法释放 GetBuf();//忘记接收返回值了 //2.丢失分配的内存地址 char *p1= (char *)malloc(10); char *p2 = (char *)malloc(10); p1 = p2;//这一步,导致第一次分配的堆内存丢失,无法释放 //3.企图希望传入指针变量获取对内存,殊不知是拷贝 char *p3 = NULL; GetBuf2(p3); //应该使用指针的指针,或者引用 //strcpy(p3, "hello"); //错误,这里的p3仍然为NULL //4.每循环一次,泄露一次内存 char * p4 = NULL; for (int i = 0; i < 10; ++i) { p4= (char *)malloc(10); } strcpy(p4, "hello"); // 这里的p4只指向最后一次分配的,前面的全部内存泄漏 //5.非法访问常量区 char *p5 = GetBuf3(); strcpy(p5, "hello"); return 0;}
12. 内存池技术简介
内存碎片:
内存碎片一般是由于空闲的內存空间比要连续申请的空间小,导致这些小内存块不能被充分的利用,当你需要分配大的连续内存时,尽管剩余内存的总和足够,但系统找不到连续的内存,所以导致分配失败malloc/free
大量使用会造成内存碎片
为什么会产生内存碎片?
如果有100个单位的连续空闲内存,那么先申请5单元的连续内存,再申请50单元的内存这时释放一开始的5单元的内存。这时,如果你一直申请比5单元大的内存单元,那么开始的那连续的5单元就一直不能被使用。
内存池技术:
内存的申请、释放是低效的,我们只在开始申请一块大內存(不够继续申请),然后每次需要时都从这块内存取出,并标记这块内存是否被使用。释放时仅仅标记而不真的free
,只有内存都空闲的时候,才释放给操作系统。这样减少了 malloc
、free
次数,从而提高效率。
13. C语言实现内存池
设计思路:
先分配几个大的连续内存块(MemoryBlock),每个内存块用链表链接起来,然后通过一个內存池结构(MemoryPool)管理!
代码实现:
#include<stdio.h>#include<stdlib.h>#include<string.h>#include<assert.h>class MemoryBlock{public: int nSize; //该内存块的总大小 (单元个数X每个单元大小),以字节为单位 int nFree; //该内存块还有多少个空闲的单元 int nFirst; //当前可用空闲单元的序号,从0开始 MemoryBlock* pNext; //指向下一个MemoryBlock内存块 char aData[1]; //用于标记分配内存开始的位置 //.....这个结构下面全是内存public: MemoryBlock(int unitCount, int unitSize) { nSize = unitCount* unitSize; nFree = unitCount; nFirst = 0; pNext = NULL; char *p = aData;//获取内存单元的首地址 for (int i = 0; i < unitCount -1; ++i) { *((short *)p) = i + 1; //第0块的下个空闲索引是不是第1块 p += unitSize; } *((short *)p) = -1;//最后一块没有下一个空闲空间了,为-1 } void * operator new (size_t t, int size) { int headSize = sizeof(MemoryBlock); return ::operator new(headSize + size); }};//分配固定内存的内存池class MemoryPool{public: //初始大小 (每一个MemoryBlock中初始的单元个数) int nInitCount; //(后面增加的MemoryBlock中单元个数) int nGrowSize; //分配单元大小,MemoryBlock中每个单元的大小 int nUnitSize; //内存块链表 MemoryBlock* pBlock;public: MemoryPool( int _nInitCount, int _nGrowSize, int _nUnitSize) { nInitCount = _nInitCount; nGrowSize = _nGrowSize; nUnitSize = _nUnitSize; pBlock = NULL; } char * Alloc() //每次只返回 nUnitSize 大小的内存 { if (pBlock == NULL) { MemoryBlock * p =(MemoryBlock *) new (nInitCount * nUnitSize) MemoryBlock(nInitCount, nUnitSize); assert(p != NULL); pBlock = p; } MemoryBlock * pB = pBlock; while (pB !=NULL && pB->nFree==0) { pB = pB->pNext; } if (pB == NULL)//一直没找到了可以分配的MemoryBlock,说明内存池已满 { pB = (MemoryBlock *) new (nGrowSize * nUnitSize) MemoryBlock(nGrowSize, nUnitSize); assert(pB != NULL); pB->pNext = pBlock; pBlock = pB; } //得到第一个可用的空闲内存地址 char *pFree = pB->aData + pB->nFirst * nUnitSize; //把nFirst值改为下一个空闲的索引 (存储在当前内存的前两个字节) pB->nFirst = *((short*)pFree); pB->nFree--; return pFree; } void Free(void *p) { //考虑这个地址落在哪个 MemoryBlock 上 MemoryBlock * pB = pBlock; while (pB != NULL && p < pB->aData || p > pB->aData+ pB->nSize ) { pB = pB->pNext; } if (pB!= NULL)//找到了p所在的MemoryBlock { //销毁之前先让它的前两个字节指向nFirst (当前空闲的索引) *((short*)p) = pB->nFirst; //nFirst的值指向当前释放的 pB->nFirst = ((char *)p - pB->aData) / nUnitSize; pB->nFree++; } else { printf("错误,此内存并非内存池分配的!\n"); } } void Print() { printf("\n\n\n"); MemoryBlock * pB = pBlock; while (pB != NULL ) { printf("\n首地址:%p 总大小:%d 空闲个数: %d 下一个空闲:%d \n", pB->aData , pB->nSize, pB->nFree ,pB->nFirst); for (int i = 0; i < pB->nSize / nUnitSize; ++i) { printf("\t %d" , * ((int *) ( pB->aData + i * nUnitSize ))); } pB = pB->pNext; printf("\n---------------------------------------------------------\n"); } }};int main(){ MemoryPool pool(3, 3, 4); int *p1 = (int *)pool.Alloc(); *p1 = 111; int *p2 = (int *)pool.Alloc(); *p2 = 222; int *p3 = (int *)pool.Alloc(); *p3 = 333; pool.Print(); int *p4 = (int *)pool.Alloc(); *p4 = 444; pool.Print(); int *p5 = (int *)pool.Alloc(); *p5 = 555; pool.Print(); pool.Free( p1); pool.Free(p2); pool.Free(p3); pool.Print(); p1 = (int *)pool.Alloc(); *p1 = 111; p2 = (int *)pool.Alloc(); *p2 = 222; p3 = (int *)pool.Alloc(); *p3 = 333; pool.Print(); return 0;}
关于怎么在C语言中实现内存管理就分享到这里了,希望以上内容可以对大家有一定的帮助,可以学到更多知识。如果觉得文章不错,可以把它分享出去让更多的人看到。