c语言动态内存管理

本文详细介绍了动态内存函数malloc、free、calloc和realloc的用途、常见错误以及柔性数组的概念,强调了动态内存管理的重要性及其在链表和联合体等高级概念中的基础作用。
摘要由CSDN通过智能技术生成

看到标题的小伙伴会问动态内存是可以变化吗?它有什么用呢?为什么存在动态内存呢?一次性开辟一个空间不可以吗?


前言

在我们日常写代码的时候我们是否思考过,我们向内存申请的空间存的数组,数字,字符串存在了那里,当我们空间不够了又应该怎么办呢?空间大了又不是又浪费了吗?


一、为什么需要动态内存呢?

在我们没有学习本章内容的时候我们开辟一块空间是如下操作

int main()
{
    int arr[10]={1,2,3,4,5,6,7,8,9,0};
    int ch[]="abcdefg";
    return 0;
}

但是上述的开辟空间的方式有两个特点: 1. 空间开辟大小是固定的。 2. 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。 但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道, 那数组的编译时开辟空间的方式就不能满足了。 这时候就只能试试动态存开辟了。

二、动态内存函数的介绍

1.动态内存相关的函数介绍

在学习之前,我们要先知道我们动态内存相关函数有一下几个

1.malloc分配内存块,向内存申请空间,它所需要的参数是size_t ,通俗来说也就是多少个字节。函数类型及参数如下:

void* malloc (size_t size);

思考

1.返回类型为一个指针当我们内存申请失败的时候,那么返回什么呢?

2.函数返回一个指针,那么我们是否需要创建一个指针用来接受它的返回值呢?

3.如果申请成功返回的不是空指针又是什么呢?

2.free解除分配内存块,当我们申请完空间,空间应用完需要解除和销毁当前空间,否则我们重复开辟空间,空间就回不够用,空间满了电脑就可能无法正常运行了

void free (void* ptr);

思考

1.它的作用就是销毁一块空间,他又是如何知道要销毁那一块空间呢?

3.calloc分配和零初始化数组,它和malloc其实是相似的,只不过传递的参数不同;

void* calloc (size_t num, size_t size);

思考

1.num我们知道是元素个数,size是类型大小,那么他们两的关系是什么呢?

2.calloc是两个参数,malloc是一个参数,为何说他们两是相似的呢?

4.realloc重新分配内存块,看了名字我们知道是重新分配内存,分配一块新的空间;

void* realloc (void* ptr, size_t size);

思考

1.参数的指针是干什么的,有什么用呢?

2.size是什么?是和malloc一样吗?

接下来,我们详细介绍这几个函数,并且讲解他们的用法

2、malloc和free

我们知道了malloc返回的是有一个指针,当我们开辟空间失败的时候,指针也就没有东西指了,那么只能返回一个空指针;

当申请成功的时候,我们就直接返回开辟空间的起始位置的地址了,我们要想运用这快空间,我们就需要创建一个指针,让指针接受开辟空间的位置,然后在向开辟空间储存内容的时候我们只需要调用指针就可以了。代码介绍

#include<stdio.h>
#include<stdlib>//我们动态空间是需要这个头文件的
int main()
{
	int* p=(int*)malloc(40);//申请了40个字节,将其开辟空间的地址返回;
	//malloc可能内存申请失败,而且返回地址为void*
    //我们想要向开辟空间存放数字是,我们用int*类型的指针在我们指针运算时会非常的方便
	memset(p, 0, 40);//初始化,在我们学习字符串和内存函数时我们已经学习过了,如果还是不太理解可以查看本博主相关博客
	if (p == NULL)
	{
		printf("申请失败\n");//此写法也可以换成perror("malloc");这个写法和memset这个内存函数中都有介绍和说明
	}
	for (int i = 0; i < 10; i++)
	{
		printf("%p\n" ,*(p + i));
	}//malloc申请到空间后,只会返回地址,并且+i打印为地址是同一个地址,不会初始化,若不初始化则打印的为随机值
	//可以运用memset(p,0,40)进行修改
	//malloc不会在程序退出后不会主动释放空间;
	//用free释放
	free(p);
 void free (void* ptr);

	p = NULL;//空间被释放,需要让p指向空指针
	//free只能释放malloc;
	//err示范
	//int a = 0;
	//int* ter = &a;
	//free(ter);//不可取,错误代码
	return 0;
}

在上述代码中都有相关说明,并且还有”字符串+内存函数“这篇博客相关知识,如果不太了解可以查看本人博客;

2.calloc

其实和malloc是一样的,只过接受参数有所不同,malloc接受的参数是字节,需要开辟的空间,通常还需自行计算,而calloc就不一样,它是接受所需要放入数据个数,并且还需要一个所需放入数据的类型,如果将两者相乘不就是malloc所需要字节这个参数了嘛!!!

typedef struct S {
	int a;//4个字节
	char c;//1个字节
	int b;//4个字节
	//在我们学习结构体时有所讲到这个自定义结构体所占的空间,大小为12个字节
};
 int main()
{
	 struct S s = { 0 };
	struct S* p = (struct S*)calloc(10, sizeof(struct S));//INT_MAX为整型最大空间
	if (p == NULL)
	{
		printf("申请失败\n");
	}
	for ( s.a =0 ; s.a < 10; s.a++)
	{
		printf("%p\n ", *(p+s.a));
	}
	return 0;
}

3.realloc

void* realloc(void* ptr,size_t size),
ptr 是要调整的内存地址
size 调整之后新大小
返回值为调整之后的内存起始位置。
这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到 新 的空间。
realloc在调整内存空间的是存在两种情况:
情况1:原有空间之后有足够大的空间
 正常调整
情况2:原有空间之后没有足够大的空间
创建以个新的空间,将旧的空间中的数据拷贝到新的空间中,将旧的空间释放掉,返回新的空间

int main()
{
    int* ptr = (int*)malloc(100);
    if (ptr != NULL)
    {
        printf("malloc开辟失败\n");
    }
    else
    {
        exit(EXIT_FAILURE);
    }
    //扩展容量
    //代码1
    ptr = (int*)realloc(ptr, 1000);//这样可以吗?(如果申请失败会如何?)

    //代码2
    int* p = NULL;
    p = (int*)realloc(ptr, 1000);
    if (p != NULL)
    {
        ptr = p;
    }
 
    free(ptr);
    return 0;
}

我们先用malloc开辟一块空间,当空间大小不够的时候,再用realloc进行扩容,扩容后返回指针也就是扩容后的空间所在的地址;

二、常见的动态内存错误

1.对NULL指针的解引用操作

代码如下(示例):

void test()
{
 int *p = (int *)malloc(INT_MAX/4);
 *p = 20;//如果p的值是NULL,就会有问题
 free(p);

当我们开辟空间时,如果申请空间失败,返回的地址是空指针,我们p=NULL;*p就会用问题 

2.对动态开辟空间的越界访问

代码如下(示例):

void test()
{
 int i = 0;
 int *p = (int *)malloc(10*sizeof(int));
 if(NULL == p)
 {
 exit(EXIT_FAILURE);
 }
 for(i=0; i<=10; i++)
 {
 *(p+i) = i;//当i是10的时候越界访问
 }
 free(p);
}

当i=10 的时候,p+i的解引用就会越界,在一个非开辟的任何地方可能放一个10进去。(因为我们开辟空间的地址不是每次都是固定的,所以p+i也就不是固定的值)

3.对非动态开辟内存使用free释放

代码如下(示例):

void test()
{
 int a = 10;
 int *p = &a;
 free(p);//ok?
}

当我们将a的地址释放时,这里将不是自己的空间个释放了,我们开辟的空间在堆上,而这里的a在寄存器中;

4.使用free释放一块动态开辟内存的一部分

代码如下(示例):

void test()
{
 int *p = (int *)malloc(100);
 p++;
 free(p);//p不再指向动态内存的起始位置
}

这里空间释放不完全就可能导致内存泄漏;p不再指向动态内存的起始位置,只释放了一部分。

5.对同一块动态内存多次释放

代码如下(示例):

void test()
{
 int *p = (int *)malloc(100);
 free(p);
 free(p);//重复释放
}

当我们释放第一次时,系统就将这一块空间给回收了,再次释放这里就可能放入了其他数据进来,再次释放,就可能导致程序崩溃; 养成习惯,释放完就将指针制空,再次释放不会出错。

6.动态开辟内存忘记释放(内存泄漏)

代码如下(示例):

void test()
{
 int *p = (int *)malloc(100);
 if(NULL != p)
 {
 *p = 20;
 }
}
int main()
{
 test();
 while(1);
}

在函数中申请了,但是这个地址找不到,无法再次寻找到这个空间,这个空间就会浪费,只有程序结束才能将内存释放。

动态内存申请的空间,就一定要将其释放。
内存只有在free或者退出程序才会将内存释放,否则就会内存泄露。

三、柔性数组

柔性数组就是数组可以随意大或者小,当想要放入的数组太多时,我们可以让空间变大;然而我们曾经学习的数组空间大小是固定的,无法改变大小,也称之为静态数组。

1.柔性数组的特点:

结构中的柔性数组成员前面必须至少一个其他成员。

sizeof 返回的这种结构大小不包括柔性数组的内存。

包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大 小,以适应柔性数组的预期大小。

代码如下(示例):

typedef struct st_type
{
 int i;
 int a[0];//柔性数组成员
}type_a;
printf("%d\n", sizeof(type_a));//输出的是4//结构的大小是未知的,至少有一个成员,,在计算大小的时候,柔性数组不参与计算;柔性数组成员

2.柔性数组的使用

代码如下(示例):

typedef struct st_type
{
 int i;
 int a[0];//柔性数组成员
}type_a;

//有些编译器会报错无法编译可以改成:

typedef struct st_type
{
 int i;
 int a[];//柔性数组成员
}type_a;


typedef struct st_type
{
	int i;
	int a[0];//结构的大小是未知的,至少有一个成员,,在计算大小的时候,柔性数组不参与计算;柔性数组成员
}type_a;
int main()
{
	struct st_type* ps= (struct st_type*)malloc(sizeof(st_type) + 40);//在开辟空间的时候得另加上柔性数组成员大小
	if (ps == NULL)
	{
		perror("malloc");
		return 1;
	}
	free(ps);
	ps = NULL;
	//当空间不够得时候用realloc增加大小,数组大小就发生变化了;
	return 0;
}

3.柔性数组的优势

第一个好处是:

方便内存释放 如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给 用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你 不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好 了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。

第二个好处是:

这样有利于访问速度. 连续的内存有益于提高访问速度,也有益于减少内存碎片。(其实,我个人觉得也没多高了,反正 你跑不了要用做偏移量的加法来寻址


总结

动态内存管理核心就是熟练掌握malloc、free、calloc、realloc 这四个函数,当我们熟练掌握了这几个函数,知道其实用方法,我们也就为以后链表和联合体大小坚实的基础了。

  • 8
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱吃喵的鲤鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值