C生万物 | 常见的六种动态内存错误

简介: C生万物 | 常见的六种动态内存错误

学习过C语言中的动态内存函数,例如【malloc】、【calloc】、【realloc】、【free】,那它们在使用的过程中会碰到哪些问题呢,本本文我们一起来探讨下~

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

代码:

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

分析:

  • 首先看到第一个,你要知道的是INT_MAX是什么。它是一个宏定义,表示int类型(整型)能够表示的最大值,其值为2147483647,那在上面讲malloc的时候我们有说到过,若是需要申请的空间过大的话可能就会导致申请失败的问题,所以这里很致命的一个错误就是在申请空间之后没有去及时判断是否申请成功
  • 可以看到编译器也是给我们报出了一个Warning警告说:==⚠ 取消对NULL指针的引用==

image.png改进:

  • 此时我们就可以对代码去做一个改进,对malloc之后的返回值做一个判断
void test()
{
    int* p = (int*)malloc(INT_MAX / 4);
    if (NULL == p)
    {
        perror("fail malloc");
        exit(-1);
    }
    *p = 20;//如果p的值是NULL,就会有问题
    free(p);
}
  • 这个时候我们就可以看到没有警告再报出来了

image.png

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

代码:

int main(void)
{
  int* p = (int*)malloc(100);
  if (NULL == p)
  {
    perror("malloc fail");
    exit(-1);
  }
  int i = 0;
  for (int i = 0; i < 100; i++)
  {
    *(p + i) = 0; // 当i == 25时便会越界
  }
  free(p);
  p = NULL;
  return 0;
}

分析:

  • 接下去我们来看这个越界访问的问题,首先我们使用malloc向堆区申请了100个字节的空间,但是呢在下面对这块空间进行访问的时候却访问了100个整型的大小,此时一定会造成访问越界的问题
  • 但是呢口说无凭,我们一样通过调试来进行一个观察,不过这里在进行循环的时候i没有到100的话是不会出问题的,所以为了方便调试我们需要去设置一个【条件断点】,将i从【24】开始执行,这样我们很快就能观察到结果了

image.png

  • 然后我们便可以通过调试去进行观察了,可以看到i并没有到达100,而是直接跳出了当前循环,然后在free()的时候就出现了问题,一般我们在一些其他地方观察不到的问题就会在free()的地方显现出来,因为此时是要去释放掉我们的这块申请的空间了,便会引发一些异常

  • 其实我们可以将*(p + i) = 0修改成p[i] = 0,利用[]操作符对某个下标进行访问,此时我们可以看到编译器就报出了警告说索引"99"超出了“0"至”24"的有效范围,因此100个字节的空间只能供25个整型来进行存放,因此合法的下标索引即为0 ~ 24

image.png

改进:

  • 代码修改这一块的话我们只需要在申请空间的时候保证申请到足够的、正确的容量即可
int* p = (int*)malloc(100 * sizeof(int));
  • 这个时候我们就可以看到没有警告再报出来了

image.png

3、对非动态开辟内存进行free释放

代码:

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

分析:

  • 接下去再来看第三个,这里是对非动态开辟的内存进行free()释放,那我们在介绍free()的时候说到它只能释放由【malloc】、【calloc】、【realloc】所开辟出来的空间,这些空间都是在堆区上进行申请的,但是我们在普通的函数中所创建的普通变量无非是栈区或者静态区的,它们的释放工作并不是由free()来完成的,因此强行去这样做的话就会造成了一个很大的问题
  • 可以看到一样出现了我们刚才那样类似的问题

image.png

改进:

  • 本代码并没有什么通用的改进办法,如果不想出现问题的话就不要free()普通栈区上的变量即可,或者按照常规去动态申请然后在进行free()

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

代码:

void test()
{
    int* p = (int*)malloc(100);
    if (NULL == p)
    {
      perror("malloc fail");
      exit(-1);
    }
    for (int i = 0; i < 10; i++)
    {
        p++;
    }
    free(p);    //p不再指向动态内存的起始位置
}

分析:

  • 本题的情境是这样的,我们在堆区申请了100个字节后,让指针p指向这块地址的起始位置,然后让其偏移了10个整型的位置,即40B的大小,那么此时指针p其实就指向了当前这一块地址的中间位置,那么此时再去free的时候其实就会出问题
  • 因为该函数在释放动态申请的内存时需要从这块地址其实位置开始,然后释放制定的字节数,若是从某个中间位置开始的话就不对了

从下图可以看出,因为free()函数需要做到申请多少释放多少,所以当其释放了一部分之后,就不够了,便造成了访问内存错误的问题image.png

  • 一样,我们通过调试去进行观察,首先在一开始申请出这块空间的时候先记录一下初始位置的地址,然后我们便可以观察到其进行了一个偏移,

image.png

  • 可以看到,此时若是去free()的话就会出现警告,很明显这个debug_heap.cpp就是【堆】这一块出的问题

image.png

改进:

  • 要如何改进的话就会不要去free()一块动态开辟出来内存的一部分,而是要从起始地址开始释放,申请多少释放多少

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

代码:

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

分析:

  • 这一点的话就是在我们释放完一块内存空间后忘了,然后再去对其进行了一次释放,这种操作的话其实也是很危险的,当我们在第一次释放的时候p所指向的那块空间的使用权已经还给操作系统了,但是呢我们并没有对这个指针p做置空的操作,于是它还指向那块空间所在的地址,不过里面的内容已经是随机的了,那么这个指针就是一个【野指针
  • 此时再对其做一个free()的操作,就会造成操作野指针的问题

image.png

改进:

  • 此时我们就可以对代码去做一个简单的改进,在第一次free后将指针p置为NULL即可,此刻若是后面再去free的话,就不会出现问题了,因为当我们传递NULL作为参数的时候,free(NULL)便不会去做任何的事情
void test()
{
    int* p = (int*)malloc(100);
    //使用...
    free(p);
    p = NULL;   // 将不使用的指针置为NULL
    //...
    free(p);  //重复释放
}

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

代码:

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

分析:

  • 那最后一个呢就是我们最常见的,在动态开辟内存后忘记去释放了,例如上面有一个test()函数,函数内部去申请了100个字节的数据,并为其做了一个初始化,此时main函数就正常地去调用它,但是呢这中间却没有任何地free()释放操作,就会存在【内存泄漏】的问题

💬 那有同学说:既然函数内部没有做释放的话我在调用结束后去free一下这个p不就好了

  • 这句话其实就存在很大的问题,如果读者有看过我的函数栈帧一文的话,就会很清楚了,对于一个在一个函数创建的变量,是处在当前这个函数所维护的栈帧中的,所以当这个函数调用结束后局部变量就会随着栈帧的销毁而不复存在,那此时我们再想去free()释放这块空间的时候,是无法访问到这个指针p的。因此要释放的话只能在函数内部进行才可以

改进:

  • 那改进这一块的话我们只需要在函数调用结束前去将其释放即可,不过别忘了在free()之后要将指针置为NULL防止野指针
void test()
{
  int* p = (int*)malloc(100);
  if (NULL != p)
  {
    *p = 20;
  }
  free(p);
  p = NULL;
}
  • 所以当我们在使用动态内存的时候,一定要保证在【malloc】之后及时【free】,此时才能保证不会内存泄漏

但是它们两个成对出现就一定不会出现问题吗?

  • 我们来看看下面这段代码,可以看到中间有一个if(1)的条件判断,我们知道这个条件是天然成立的,然后看到当这个条件成立后就会执行return语句,那么当前这个函数就会结束了,此时并没有运行到free(p)这句话
  • 那么聪明的你一定很快反应过来了,即使是存在【malloc】和【free】成对出现的情况下,可能也无法百分百保证不会产生内存泄漏的问题,所以还是需要我们在写程序的时候多注意细节🤗
void test()
{
  int* p = (int*)malloc(100);
  if (NULL != p)
  {
    *p = 20;
  }
  if (1)
    return;   // 因为某些条件中途return了, 没到free()
  free(p);
}
int main()
{
  test();
}

总结与提炼

最后来总结一下本文所学习的内容

  • 通过上面的六个案例,我们总共了解到了六种动态内存错误的形式,分别是
  • 对NULL指针的解引用操作】 —— 操作空指针是非常危险的一件事,记得判空哦
  • 对动态开辟空间的越界访问】 —— 有多少就拿多少,不要贪心哦
  • 对非动态开辟内存进行free释放】 —— 请正确分类,送它去该去的地方
  • 使用free释放一块动态开辟内存的一部分 】—— 借了多少还多少,不要私藏哦
  • 对同一块动态内存多次释放】 —— 借了多少还多少,不要私藏哦
  • 动态开辟内存忘记释放】 —— 借了别人的东西要记得还
  • 在使用动态内存函数开辟出空间后,使用的时候一定要牢记以上几点,否则要出大问题的!

以上就是本文要介绍的所有内容 ,感谢您的阅读:rose:

相关文章
C生万物 | 从浅入深理解指针【第二部分】(二)
C生万物 | 从浅入深理解指针【第二部分】(二)
C生万物 | 从浅入深理解指针【最后部分】(二)
C生万物 | 从浅入深理解指针【最后部分】(二)
|
7月前
|
存储 自然语言处理 Unix
【C生万物】初始C语言
【C生万物】初始C语言
C生万物 | 从浅入深理解指针【第四部分】(qsort的使用和模拟实现)
C生万物 | 从浅入深理解指针【第四部分】(qsort的使用和模拟实现)
|
7月前
|
C语言 C++
C生万物 | 从浅入深理解指针【最后部分】(一)
C生万物 | 从浅入深理解指针【最后部分】(一)
|
编译器 Linux Go
C生万物 | 动态内存管理-2
C生万物 | 动态内存管理
59 0
C生万物 | 动态内存管理-2
|
存储 程序员 编译器
C生万物 | 动态内存管理-3
C生万物 | 动态内存管理
43 0
|
编译器 C语言 C++
C生万物 | 动态内存管理-1
C生万物 | 动态内存管理
48 0
|
C语言
【C生万物】 指针篇 (进级) 下(一)
【C生万物】 指针篇 (进级) 下
84 0
【C生万物】 指针篇 (进级) 下(一)
|
算法
【C生万物】 字符串&内存函数篇 (下)
【C生万物】 字符串&内存函数篇 (下)
89 0