当前位置: 代码迷 >> 综合 >> 【C语言精讲】动态内存管理(malloc、calloc、relloc、柔性数组)
  详细解决方案

【C语言精讲】动态内存管理(malloc、calloc、relloc、柔性数组)

热度:22   发布时间:2023-11-22 04:23:22.0

目录

1. 为什么存在动态内存分配

2. 动态内存函数的介绍

2.1 malloc和free

malloc

free

实例

2.2 calloc

2.3 realloc

3. 常见的动态内存错误

3.1 对NULL指针的解引用操作

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

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

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

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

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

4. 柔性数组

4.1 柔性数组的特点

4.2 柔性数组的使用和优势


1. 为什么存在动态内存分配

传统开辟空间方式有两个缺点:

  1. 空间开辟大小是固定的。
  2. 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。

2. 动态内存函数的介绍

2.1 malloc和free

malloc和free都声明在 <stdlib.h> 头文件中。

C语言提供了一个动态内存开辟的函数:

malloc

void* malloc (size_t size);

这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。

  • 如果开辟成功,则返回一个指向开辟好空间的指针。
  • 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
  • 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
  • 如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。

C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的,函数原型如下:

free

void free (void* ptr);

free函数用来释放动态开辟的内存。

  • 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
  • 如果参数 ptr 是NULL指针,则函数什么事都不做。

实例

#include <stdio.h>
#include <string.h>
#include <stdlib.h>int main()
{//开辟10个整型空间int* p = (int*)malloc(40);if (NULL == p){printf("%s\n", strerror(errno));return 0;}//使用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;
}

2.2 calloc

C语言还提供了一个函数叫 calloc , calloc 函数也用来动态内存分配。原型如下:

void* calloc (size_t num, size_t size);

  • 函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。
  • 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。
#include <stdio.h>
#include <stdlib.h>
int main()
{int* p = (int*)calloc(10, sizeof(int));if (NULL != p){//使用空间}free(p);p = NULL;return 0;
}

如果我们对申请的内存空间的内容要求初始化,那么可以很方便的使用calloc函数来完成任务。 

2.3 realloc

  • realloc函数的出现让动态内存管理更加灵活。
  • 有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时 候内存,我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小的调整。

realloc函数原型如下:

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

  • ptr 是要调整的内存地址
  • size 调整之后新大小
  • 返回值为调整之后的内存起始位置。
  • 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到 的空间。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>int main()
{//开辟10个整型空间int* p = (int*)malloc(40);if (NULL == p){printf("%s\n", strerror(errno));return 0;}//使用int i = 0;for (i = 0; i < 10; i++){*(p + i) = i;}for (i = 0; i < 10; i++){printf("%d ", p[i]);}//需要增容 - 80int* ptr = (int*)realloc(p, 80);if (NULL != ptr){p = ptr;ptr = NULL;}//继续使用//释放free(p);p = NULL;return 0;
}
  • realloc在调整内存空间的是存在两种情况:

情况1:原有空间之后有足够大的空间,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。

情况2:原有空间之后没有足够大的空间,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小 的连续空间来使用。这样函数返回的是一个新的内存地址。

3. 常见的动态内存错误

3.1 对NULL指针的解引用操作

解决方案:申请完动态内存后及时判断指针是否为空(检测)

#include <limits.h>
#include <stdlib.h>//对NULL指针的解引用操作
int main()
{int* p = (int*)malloc(INT_MAX);//记得判断指针是否为空的情况if (p == NULL)return 0;int	i = 0;for (i = 0; i < 10; i++){*(p + i) = i;}free(p);return 0;
}

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

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>int main()
{char* p = (char*)malloc(10 * sizeof(char));if (NULL == p){printf("%s\n", strerror(errno));return 0;}//使用int i = 0;//for (i = 0; i <= 10; i++)//越界for (i = 0; i < 10; i++){*(p + i) = 'a' + i;}//释放free(p);p = NULL;return 0;
}

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

注意free只能释放动态开辟的空间。

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

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

free一次就可以了,不可以free释放两次及以上。

如果free完空间及时给p置为NULL空指针,就会安全一些。

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

切记: 动态开辟的空间一定要释放,并且正确释放 。(malloc和free成对出现)

忘记释放不再使用的动态开辟的空间会造成内存泄漏。

4. 柔性数组

C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。

typedef struct st_type
{int i;int a[0];//柔性数组成员//int a[];    //有些编译器会报错无法编译可以改成这样
}type_a;

4.1 柔性数组的特点

  • 结构中的柔性数组成员前面必须至少一个其他成员。
  • sizeof 返回的这种结构大小不包括柔性数组的内存。
  • 包含柔性数组成员的结构用malloc() 函数进行内存的动态分配,并且分配的内存应该大于结构的大 小,以适应柔性数组的预期大小。
#include <stdio.h>
#include <stdlib.h>
struct S
{int n;int arr[];	//柔性数组
};int main()
{struct S* ps = (struct S*)malloc(sizeof(struct S) + 40);ps->n = 100;int i = 0;for (i = 0; i < 10; i++){ps->arr[i] = i;}//增容struct S* ptr = (struct S*)realloc(ps, sizeof(struct S) + 80);if (NULL == ptr)return 0;else{ps = ptr;}//释放free(ps);ps = NULL;return 0;
}

4.2 柔性数组的使用和优势

下面代码1和代码2可以完成同样的功能,但是 代码1 的实现有两个好处:

  1. 方便内存释放。如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给 用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你 不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好 了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。
  2. 有利于访问速度。连续的内存有益于提高访问速度,也有益于减少内存碎片。
//代码1
int i = 0;
type_a* p = (type_a*)malloc(sizeof(type_a) + 100 * sizeof(int));
//业务处理
p->i = 100;
for (i = 0; i < 100; i++)
{p->a[i] = i;
}
free(p);
//代码2
typedef struct st_type
{int i;int* p_a;
}type_a;
type_a* p = (type_a*)malloc(sizeof(type_a));
p->i = 100;
p->p_a = (int*)malloc(p->i * sizeof(int));
//业务处理
for (i = 0; i < 100; i++)
{p->p_a[i] = i;
}
//释放空间
free(p->p_a);
p->p_a = NULL;
free(p);
p = NULL;

  相关解决方案