有关C语言内存管理的一些总结

Posted Z_FIEND°

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了有关C语言内存管理的一些总结相关的知识,希望对你有一定的参考价值。

C语言内存管理总结


 


前言

  • 最近也是比较忙,而在学习嵌入式系统的过程中,问题不断出现,其中一个很重要的问题,也就是内存管理知识点的不牢固导致的内存分配的各类错误,学得我心血来潮,于是就打算写一篇文章来记录学习的时候遇到的问题以及内存管理的知识点。
  • 本文参照了大量的文章与视频,链接将会于“总结”部分发布。
  • 因为自我感觉也不怎么良好,所以该文也是比较偏向于小白,适合于萌新食用。当然,如果有哪里不足也希望大佬能够指点指出

 

一、内存管理简介以及常见的内存使用错误

虽然我们现在的计算机系统的内存已经在“宏大”的方向发展,但学会内存管理,并不为一件坏事,相反,这种“优良传统”在各类场景下会给我们带来益处,在计算机系统中,特别是嵌入式系统中,内存资源是十分有限的。尤其是对于移动端的开发者来说,硬件资源的限制使得其在程序的设计中首先要考虑的问题就是,如何去合理的分配那“一丢丢”的内存资源。

日常中我们可能遇到的错误:

  1. 内存申请没成功,就去使用了.
  2. 内存申请成功,但没有初始化.
  3. 内存初始化成功,但越界访问.
  4. 忘记释放内存或者释放一部分.

而内存管理不当会导致些什么了?

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所指的数组中。

注意事项:

srcdest所指内存区域不能重叠,且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个字节注意srcdest所指内存区域不能重叠,否则不能保证正确

3.memmove:

函数原型:

void *memmove(
  void *dest,
  const void *src,
  size_t count
);

函数功能:与 memcpy相同。

注意事项:

srcdest所指内存区域可以重叠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,只有内存都空闲的时候,才释放给操作系统。这样减少了 mallocfree次数,从而提高效率。

设计思路:


总结

文章内容大部分来源于百度,CSDN,以及视频以下为这些资料的链接,在这里统一贴出来

  1. https://blog.csdn.net/u014779536/article/details/116354403?utm_source=app
  2. https://blog.csdn.net/qq_34793133/article/details/85713413
  3. https://fishc.com.cn/forum.php?mod=viewthread&tid=35614&highlight=%C4%DA%B4%E6
  4. 百度:https://baike.baidu.com/item/%E6%A0%88/12808149?fr=aladdinhttps://baike.baidu.com/item/%E5%A0%86/20606834?fr=aladdin
  5. 视频:https://www.bilibili.com/video/BV1jW411K7yg?from=search&seid=18059219221845422969
  6. 看到这里十分感谢

 

 

以上是关于有关C语言内存管理的一些总结的主要内容,如果未能解决你的问题,请参考以下文章

C语言_结构体总结

C语言内存管理 static

C语言数据的存储

C语言动态内存管理及使用总结篇初学者保姆级福利

C语言中几种常见的与内存有关的错误

C内存管理