C语言【动态内存管理 前篇】

简介: 动态内存管理 前篇

1. 为什么存在动态内存管理

C语言中的数据结构通常是固定大小的。例如,一旦程序完成编译,数组元素的数量就是固定的。

说到这里,有人就要说:变长数组呢?在C99中,变长数组的长度在运行时确定,但在数组的生命周期内仍然是固定的,因为在编写程序时强制选择了大小,所以固定大小的数据结构可能会有问题。也就是说,在不修改程序并且再次编译程序的情况下无法改变数据结构的大小


对于空间的需求,有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足了。

这时候就只能试试动态存开辟了。


🫅2. 动态内存函数

🤦‍♂️(1)空指针

在调用内存分配函数时,总存在这样的可能性:找不到我们需要的足够大的内存块。那这样的话,函数就会返回空指针(NULL)


1.空指针就是“不指向任何地方的指针”

2.区别于所有有效指针的特殊值

3.名为NULL的宏有定义的头文件:<locale.h>、<stddef.h>、<stdlib.h>、<string.h>、<time.h>、<wchar.h>(C99)

4.数测试真假的方法:0为假,非0为真;指针测试真假的方法:空指针为假,非空为真

注意:

1. 程序员的任务是测试任意内存分配函数的返回值,并且要在返回值为空指针时采取适当措施

2. 通过空指针访问内存的行为是未定义的,程序可能会出现崩溃


🤦‍♂️(2)malloc

在介绍具体的内存分配函数之前,先了解一下他们的功能:


malloc:分配内存块,但是不对内存块进行初始化

calloc:分配内存块,并且对内存块进行清零

realloc:调整先前分配的内存块大小

free :释放内存块

所有函数的头文件都是<stdlib.h>

void* malloc (size_t size);
这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针

🌰


#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
int main()
{
  //申请10个整形类型
  int* p = (int*)malloc(10 * sizeof(int));
  //int* p=(int*)malloc(40);
  //判断是否申请空间成功
  if (p == NULL)
  {
    printf("%s\n", strerror(errno));
    return 1;
  }
  //使用   存放1-10
  for (int i = 0; i < 10; i++)
  {
    *(p + i) = i + 1;
  }
  //打印
  for (int i = 0; i < 10; i++)
  {
    printf("%d ", *(p + i));
  }
  //释放申请的空间
  free(p);
  p = NULL;
  return 0;
}

在这里解释一下:


malloc函数申请空间:

1.如果开辟成功,则返回一个指向开辟好空间的指针

2.如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查


void* :是通用型指针类型:

3.返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定

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


free函数:

5.free函数是释放动态开辟的空间(堆区上的空间),否则是未定义的行为。如果参数是空指针,那么free函数什么事都不用做

6.malloc函数和free函数搭配使用


判断是否申请空间成功:

7.程序员的任务是测试任意内存分配函数的返回值,并且要在返回值为空指针时采取适当措施

8.通过空指针访问内存的行为是未定义的,程序可能会出现崩溃


一旦p指向动态分配的内存块,就可以忽略p是指针的事实,可以把它看成数组的名字。上面代码的使用和打印部分就是利用了这一特点


🐉🐉🐉🐉🐉

当然了,有人还有疑问:为什么一定要释放申请的内存空间?释放了空间之后,为什么要置为空指针?

在这里,我来单独解释:


1.向堆区申请了空间,当然要还了 (还给操作系统)

2.free函数的作用是切断操作系统与该变量之间的联系

3.所以,光是将空间还给操作系统是不行的,因为该变量还保留着起始位置的地址,需要我们手动置空

🐉🐉🐉🐉🐉

有人又有疑问了:博主,你申请空间的时候,怎么写了两种方法?那种更推荐?

其实,个人而言,我更推荐第一种,原因是:

计算变量类型所需要的空间数量时,始终要使用sizeof运算符,如果不能分配足够的空间,将会产生严重的后果。比如,不同的机器上,int类型的大小就不一样,有的是2字节,有的是4字节…

假如:你写的是第二种 p = (int*) malloc(38); 你所用的机器上int是4个字节,这种情况下,就会产生分配错误的结果。

🤦‍♂️(3)calloc

void* calloc (size_t num, size_t size);

🌰


#include<stdio.h>
#include<stdlib.h>
int main()
{
  int* p = calloc(10, sizeof(int));
  if (p == NULL)
  {
    perror(p);
    return 1;
  }
  for (int i = 0; i < 10; i++)
  {
    printf("%d ", p[i]);
  }
  free(p);
  p = NULL;
  return 0;
}
//运行结果:
******
0 0 0 0 0 0 0 0 0 0 
*****

malloc 和 calloc


相同:

1.堆区上申请空间

2.返回起始地址

3.由于是申请空间,不用的话,要释放内存并置空


不同:

1.malloc一个参数,calloc两个参数,相当于 40(总字节大小) = 10(变量个数) * 4(每个变量的大小)

2.malloc没有初始化,calloc会把空间初始化为0 - 故malloc比calloc的效率高


🤦‍♂️(4)realloc

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


void* realloc (void*ptr, size_t size)

ptr是要调整的内存地址

size是调整之后的大小

返回值是调整之后的内存起始地址

realloc在调整内存空间是存在两种情况:

1.情况1:原有空间之后有足够大的空间

要扩展内存就直接在原有内存之后追加空间,原来空间的数据不发生变化,并返回旧空间的起始地址

2.情况2:原有空间之后没有足够的空间

在堆空间上另外找一个大小合适的连续空间来使用,将原有数据拷贝到这个新空间上,释放掉就空间,并返回新空间的起始地址

#include<stdio.h>
#include<stdlib.h>
int main()
{
  int* p = malloc(10 * sizeof(int));
  //判断malloc函数是否开辟成功
  if (p == NULL)
  {
    perror(p);
    return 1;
  }
  int* ptr = realloc(p, 5 * sizeof(int));
  //判断realloc函数是否开辟成功
  if (ptr != NULL)
  {
    p = ptr;
  }
  for (int i = 0; i < 5; i++)
  {
    printf("%d ", p[i]);
  }
  free(p);
  p = NULL;
  return 0;
}
//运行结果:
*****
5个随机数
*****

🐉🐉🐉🐉🐉

看到这里,有人就有疑问了:realloc函数如果开辟失败,原有内存中的数据会怎么变化?为什么不是直接让p来接收realloc的结果?

解释:


如果realloc函数不能按照要求来进行扩展,那么就会返回NULL,原有数据都不会发生变化

1.realloc的结果一定要让一个新指针来接收。因为:

2.realloc函数可能将内存块移动到堆区上的其他位置

如果开辟失败返回NULL,那么以后进行的操作就会导致系统崩溃


🐉🐉🐉🐉🐉

此外,realloc函数还需要注意:


传给realloc函数的第一个指针必须来自于先前malloc、calloc 或 realloc的调用,如果不是这样的指针,程序导致崩溃

realloc函数不会对添加进内存块的函数进行初始化

第一个参数如果是NULL,那么相当于调用malloc函数

第二个参数是0,将会释放掉该内存块


🤦‍♂️(5)free

void free (void* ptr);

free函数是专门来释放动态开辟的内存

如果参数ptr不是指向动态开辟来的空间,将会导致程序崩溃

如果参数ptr是NULL,函数将什么事都不会做

🫅3. 常见的动态内存错误


🤦‍♂️(1)对NULL指针的解引用操作

#include<stdio.h>
#include<stdlib.h>
int main()
{
    int* p = (int*)malloc(INT_MAX );
    *p = 20;//如果p的值是NULL,就会有问题
    free(p);
  return 0;
}

补充一下:

INT_MAX:2147483647


🤦‍♂️(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);
}

申请空间的单位一般都是字节


🤦‍♂️(3)对非动态开辟内存使用free释放

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

free函数是专门来释放动态开辟的内存空间的


🤦‍♂️(4)使用free释放动态开辟内存的一部分

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

free函数的参数是要释放动态内存空间的起始地址


🤦‍♂️(5)对同一块动态内存多次释放

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

对同一个空间释放两次了 - 因为p一直保留着起始位置的地址

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


这种不是对同一个空间释放空间两次 - 因为p已经置为空了,对空指针进行free操作是没有意义的


🤦‍♂️(6)动态开辟内存忘记释放(内存泄漏)

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

动态开辟的空间一定要释放,并且要正确释放,否则会造成内存泄漏

490519de981944c1b47ce06b0abaae1f.png


相关文章
|
10天前
|
C语言 C++
C语言 之 内存函数
C语言 之 内存函数
24 3
|
11天前
|
编译器 程序员 C语言
深入C语言:动态内存管理魔法
深入C语言:动态内存管理魔法
|
16天前
|
存储 程序员 编译器
C语言——动态内存管理与内存操作函数
C语言——动态内存管理与内存操作函数
|
6天前
|
C语言
保姆级教学 - C语言 之 动态内存管理
保姆级教学 - C语言 之 动态内存管理
11 0
|
11天前
|
存储 C语言
深入C语言内存:数据在内存中的存储
深入C语言内存:数据在内存中的存储
|
11天前
|
C语言 C++
c语言回顾-内存操作函数
c语言回顾-内存操作函数
34 0
|
13天前
|
存储 C语言 C++
来不及哀悼了,接下来上场的是C语言内存函数memcpy,memmove,memset,memcmp
本文详细介绍了C语言中的四个内存操作函数:memcpy用于无重叠复制,memmove处理重叠内存,memset用于填充特定值,memcmp用于内存区域比较。通过实例展示了它们的用法和注意事项。
43 0
|
18天前
|
存储 程序员 C语言
C语言动态内存管理
C语言动态内存管理
|
18天前
|
程序员 C语言
C语言内存函数精讲
C语言内存函数精讲
|
19天前
|
编译器 C语言 C++
【C语言】精妙运用内存函数:深入底层逻辑的探索
【C语言】精妙运用内存函数:深入底层逻辑的探索