【一文教你学会动态内存管理】

简介: 1.为什么会存在动态内存分配?我们现在知道的开辟内存方式是创建变量,创建数组。而这些东西是在栈区上开辟空间的,一旦创建完成,是无法更改的,是固定死的。就像数组,在创建数组时,已经指定了数组的大小,编译后无法再更改。这时候就需要动态内存分配。

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

我们现在知道的开辟内存方式是创建变量,创建数组。而这些东西是在栈区上开辟空间的,一旦创建完成,是无法更改的,是固定死的。就像数组,在创建数组时,已经指定了数组的大小,编译后无法再更改。

这时候就需要动态内存分配

2. 动态内存函数的介绍

2.1 malloc函数和free函数

1.malloc函数

学习新函数时,就从库里面找函数的声明,解析来学习。

void* malloc (size_t size);

该函数的功能是,向堆区申请一块size个字节大小的空间。


如果申请成功,返回申请的空间的起始地址,如果申请失败,返回空指针NULL。 至于malloc函数在库里的为什么是void*,这是因为malloc函数也不知道使用者需要这块空间来存放什么,具体要什么类型的指针,使用者自己决定。

如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。

下面举个例子来深入了解:

97bdfb18d9a44fcaad271b175182d0c7.png

在这段代码中,我们用了malloc函数来社区内40个字节的空间,(一个int是4个字节,*10是40字节)

但是这样写是不对的,前面说过,malloc是向内存中的堆区申请空间,如果申请失败,就返回NULL,如果现在不判断p的值,直接使用的话,就会出问题,所以我们应该判断p的有效性。

aa7579d2588b4e9181f9fa23323c3c95.png

顺便提一下,perror是一个报错的函数,假如申请空间失败返回NULL,perror会报告相应的错误信息。来演示一下:

假如申请INT_MAX个字节的空间:

19bec4050f63431ba3a9fbb32c95cc03.png

这是一个极大的数字,看看是否能够申请成功。

d26bbcdb750f4a02b81cfd19acf8d6d0.png

在这里,perror报告错误:没有足够的空间。具体的用法请学习一下。

free函数

free函数和malloc函数是成对出现的

free函数是专门用来释放申请的空间的。

具体用法如下:

int main()
{
  int* p = (int *)malloc(sizeof(int)*10);
  if (p == NULL)
  {
    perror("malloc");
  }
  free(p);
  p = NULL;
  return 0;
}

在申请完空间之后,如果不用了,就需要释放掉,把申请的空间还给操作系统,即 有借有还,再借不难 的道理。

总结:malloc函数是向堆区申请一块空间,如果申请成功,则返回该空间的首地址,如果申请失败,则返回NULL。

free函数是将申请的空间释放掉

2.2 calloc函数

calloc函数也是向堆区中申请内存的函数。具体参数如下:

void* calloc (size_t num, size_t size);

第一个参数是申请的元素个数,第二个参数是申请的每个元素大小。

注意:calloc为元素数组分配一个内存块,并将其所有位初始化为零。

也就是说,calloc函数不仅申请空间,还将该空间初始化为0。

举个例子来证明:

int main()
{
  int *p = (int*)calloc(10, sizeof(int)); 
  if (p == NULL)
  {
    perror("calloc");
  }
  for (int i = 0; i < 10; i++)
  {
    printf("%d ", *(p + i));
  }
  return 0;
}

结果如下:


547d0ed5592a461abd54f76a588a21b6.png

结果就是:将申请的空间自动初始化为0。

calloc函数与malloc函数类似,只是malloc函数只负责申请空间,没有初始化,而calloc函数会申请空间并初始化。

不过也不是说calloc函数比malloc函数更高级,calloc函数 初始化也会花费时间,malloc函数不初始化,所以它也节省了一部分时间。

每个函数之间各有优缺点。

所以在使用函数的时候,结合实际情况来使用。

2.3 realloc函数

realloc函数,让我们动态申请的空间更加灵活。

当我们觉得动态申请的空间太大或者太小时,可以用realloc函数来调整动态申请的空间大小。

realloc函数的原型如下:

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

ptr是需要重新调整的空间的起始地址,

size 调整之后新大小

注意:是调整后的大小

假如刚开始动态申请的空间是40字节,发现不够用了,想加大10个字节的空间,那么使用realloc函数重新调整空间时,参数就是50。

realloc函数的返回值是调整后的空间的起始地址。

既然返回调整后的起始地址,那会不会出现申请失败的情况?会不会出现空间不足以申请的情况?

会的。

情况1: 重新申请空间时,假如后面的空间足够大,那么realloc函数就会返回空间的首地址。原来有的数据不会变化:

2384d20c06bb4fef91cfea76409de1d9.png

情况2: 如果重新申请空间失败,则返回NULL。

情况3:****如果在原来的空间后面没有足够大的空间来增容,realloc函数会自动在内存中的其他位置找一块满足我们要求的空间,并把原空间的数据拷贝到新空间中,然后返回新空间的起始地址。

4e36064c48dd417b8cb2a9fb475403eb.png基于情况2和情况3,我们是不是用原空间的指针来接收呢?

int main()
{
  int* p = (int*)malloc(sizeof(int) * 5);
  if (p == NULL)
  {
    perror("malloc");
    return ;
  }
  int i = 0;
  for (i = 0; i < 5; i++)
  {
    *(p + i) = i;
  }
  不够了,增加空间
  int* p = (int*)realloc(p, sizeof(int) * 10);

还是刚才的例子,上面这段代码对吗?

不对。

因为在重新realloc调整空间的时候,如果用原空间的地址来接收的话,万一申请失败呢?

如果申请失败,realloc函数会返回一个NULL,如果用p来接收的话,p原来指向的空间的数据就丢失了!就找不到了。

所以不能用原空间p来接收realloc返回的地址,我们需要用一个临时指针来接收返回的地址。

  int* ptr = (int*)realloc(p, sizeof(int) * 10);

这样写才是正确的,然后判断ptr是否为空,如果不为空,再把这个ptr存的地址赋给p。

  if (ptr != NULL)
  {
    p = ptr;
    ptr = NULL;(防止ptr成为野指针)
  }

以上就是三个申请空间的函数的基本情况,根据需求,选择不同的函数。

3. 常见的动态内存错误

3.1 对NULL指针的解引用操作

void test()
{
  int* p = (int*)malloc(INT_MAX);
  *p = 20;
  free(p);
}
int main()
{
  test();
  return 0;
}

这段代码的错误是,没有判断p的值就对p进行解引用,如果申请的空间失败,返回NULL,对NULL进行*操作,是非法的。

改正很简单:只需判断p是否为NULL即可。

void test()
{
  int* p = (int*)malloc(INT_MAX / 4);
  if (p == NULL)
  {
    perror("malloc");
    return;
  }
  *p = 20;
  free(p);
  p = NULL;
}

改正结果如上。

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

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

上面这段代码的错误是,malloc函数只申请了10个整型大小的空间,(40)字节,但是在赋值的时候,i的范围取到了20,造成对后面的空间非法访问。

改正如下:只需把i的范围调整到i<10即可,或者最初申请的空间到20个整型大小(80字节

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

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

void test()
{
int a = 10;
int *p = &a;
free(p);
p = NULL;
}
int main()
{
  test();
  return 0;
}

上面代码的错误在于,a是一个变量,在内存中的栈区创建,不是用malloc等函数申请出来的空间,后面free§的时候,free释放的是堆区上申请的空间,明显两者有差异。释放是非法的。


6ec173ca534041aa8aef225fa6e031bb.png

运行时也产生错误,并且关掉这个程序会很卡顿。

改正方法有多种,可以把free§去掉。

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

void test()
{
  int* p = (int*)malloc(100);
  if(p == NULL)
  {
    perror("malloc");
    return;
  }
  p++;
  free(p);
  p = NULL;
}
int main()
{
  test();
  return 0;
}

上面代码的错误在于,申请的100字节的空间,用p来接收,但是p又++了,此时p不再指向申请的空间的起始地址,释放的时候,只释放一部分,剩下的空间没有释放完,造成内存泄漏

5ffb525547d243189bdb09b42685c2dd.png

运行时一样会报错。

所以申请的空间的起始地址不能丢失。

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

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

重复释放也会出现错误。


5bde32b2fac044b58e59fb7ed2e39215.png

运行时依然报错。

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

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

在test函数内部申请的空间,除了test函数后,没有返回值,意味着申请的这块空间丢失了,没有人记得这块空间的存在,造成了内存泄露!

关于动态内存,你学会了吗?学会了不妨关注我吧!

相关文章
|
6月前
|
安全 程序员 编译器
动态内存管理学习分享
动态内存管理学习分享
71 0
|
6月前
|
存储 程序员 C语言
【动态内存管理助力程序优化与性能飞升】(下)
【动态内存管理助力程序优化与性能飞升】
|
6月前
|
C语言
【动态内存管理助力程序优化与性能飞升】(中)
【动态内存管理助力程序优化与性能飞升】
|
15天前
|
C语言
保姆级教学 - C语言 之 动态内存管理
保姆级教学 - C语言 之 动态内存管理
13 0
|
5月前
|
存储 编译器 程序员
动态内存管理(想要了解动态内存管理,只需要看这一篇就足够了!)
动态内存管理(想要了解动态内存管理,只需要看这一篇就足够了!)
|
6月前
|
C语言
动态内存:灵活管理之道
动态内存:灵活管理之道
|
6月前
|
编译器 C语言
【动态内存管理助力程序优化与性能飞升】(上)
【动态内存管理助力程序优化与性能飞升】
104 0
|
11月前
|
编译器 Linux Go
C生万物 | 动态内存管理-2
C生万物 | 动态内存管理
48 0
C生万物 | 动态内存管理-2
|
6月前
|
Java 程序员 编译器
深度解剖动态内存管理
深度解剖动态内存管理
60 0
|
11月前
|
编译器 C语言 C++
C生万物 | 动态内存管理-1
C生万物 | 动态内存管理
40 0