【C进阶】——动态内存管理详解 及 经典笔试题解析(二)

简介: 【C进阶】——动态内存管理详解 及 经典笔试题解析(二)

3. 常见的动态内存错误

在进行动态内存管理时,有很多需要注意的,一旦我们使用不当,就有可能会导致错误的发生。


接下来我们就来总结一下,哪些操作可能会引发动态内存错误。


3.1 对NULL指针的解引用操作

通过上面的学习我们已经知道了,malloc,realloc,calloc在开辟空间时,一旦开辟失败,就会返回空指针,如果我们不小心对这些空指针进行了解引用,就会导致错误的发生。


举个例子:

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

大家看这段代码有问题吗?

因为malloc有可能返回空指针,所以像上面这样不做判断,直接对malloc返回的指针,解引用,就可能会导致问题出现。

我们写出这样的代码,有的编译器可能就直接会报警告:

de83394ab48a49abbaa2743a019b8f99.png

不过上面的代码中我们申请的空间比较小,只有4个字节,可能不会申请失败。

如果我们要申请一块特别大的空间,很有可能就会开辟失败:

我们来试一下:

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

开辟INT_MAX字节的空间,INT_MAX是整型的最大值:

93c4ccd19fce4c97acae11ea79ebd8be.png

这次,很有可能就会失败,所以我们最后加一个判断

int main()
{
  int* p = (int*)malloc(INT_MAX);
  if (p == NULL)
  {
    perror("malloc");
    return 1;
  }
  else
  {
    *p = 20;
  }
  free(p);
  p = NULL;
  return 0;
}

这里的perror也是一个打印错误信息的函数(和strerror差不多),不过它可以在前面加上我们自定义的信息。

我们看结果是什么:

cf41cbfe49764a81a563178353c05f0f.png

说明,这里malloc就开辟失败了,返回的是空指针

所以,对于malloc,realloc,calloc的返回值,我们一定要进行一个检查,防止对空指针解引用。

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

对动态开辟空间的越界访问,也会发生错误。

举个例子:

int main()
{
  int i = 0;
  int* p = (int*)malloc(5 * sizeof(int));
  assert(p);//断言,防止p为空指针
  for (i = 0; i <= 10; i++)
  {
    *(p + i) = i;//越界访问
    printf("%d ", *p);
  }
  free(p);
  p = NULL;
  return 0;
}

上面的代码中,我们使用malloc开辟了5个整型大小的空间,即20个字节,那p 作为1个整型指针,加1跳过4个字节,那我们循环10次,是不是就越界访问了啊。

这时我们运行就出错了:

0fd2c4b827a94b3b98ef1d89e907b76e.png

因为我们越界访问了。

所以,也注意不能越界访问。


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

这个我们在上面其实也已经提到过了。


我们要知道,free是用来释放动态开辟的内存空间的,

如果我们用free去释放非动态开辟的内存,此时free的行为是标准未定义的。


比如:

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

num 是我们定义的局部变量,是保存在栈区的,而堆区才是用来动态内存分配的。

这样的代码运行,可能是会出错的。

e57e45eb72b84962b337e7be3979bd3c.png

所以我们不要用free去释放非动态开辟的内存。

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

什么意思呢?


我们在使用free释放一块动态开辟的内存空间的时候,传给free那个指针必须是指向这块空间的起始位置。

如果在使用过程中,原本指向内存块起始位置的指针发生了改变,不再指向该空间的起始位置,那我们最后用free去释放这块空间的时候,就不能再传这个指针了。


举个例子:

int main()
{
  int* p = (int*)malloc(10);
  assert(p);
  p++;
  free(p);//p不再指向动态内存的起始位置
  p = NULL;
  return 0;
}

这样的操作就是错误的,我们free(p)的时候,p 已经不再指向这块动态内存的起始位置了。

运行这样的代码,程序就出错了。

656162a3e78048cda0a4eb3976ccad8c.png

如果确实需要改变:

我们可以再创建一个指针变量,保存一下最初指向起始地址的指针,这样最后释放的时候,我们依然能找到起始位置的地址。

像这样:

int main()
{
  int* p = (int*)malloc(10);
  assert(p);
  int* ptr = p;//用ptr保存malloc开辟空间的起始位置
  p++;
  free(ptr);//释放的时候传ptr
  ptr = NULL;
  p = NULL;
  return 0;
}

这样,程序就不会出错了。

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

什么意思呢?

我们动态开辟一块内存空间,使用完直接释放了,并且没有将指向该内存块起始位置的指针(假设是指针p)置空,过了一会儿可能忘记已经释放过了,然后再后面又把p传给free,又对这块空间进行一次释放。

这样的话我们运行代码就也会出错的。

像这样:

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

55cc04aaa0c940cfb05523cc2cbb2667.png

这样程序是会崩掉的。

为了避免这种情况发生:

我们在释放掉p指向的空间之后,要及时将p置空

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

这样,虽然我们释放了两次,但因为我们第二次传的是空指针,所以不会有问题。

因为如果free的参数 ptr 接收的是NULL指针,不执行任何操作。

所以:

在使用free释放一块动态内存空间后,及时将指向起始位置的指针置空是一个好习惯。

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

就是我们动态开辟的空间在使用完之后,一定要记得释放,不释放的话有可能会造成内存泄漏。

切记:

动态开辟的空间一定要释放,并且正确释放

4.经典笔试题讲解

下面,我们一起来看几个动态内存管理相关的经典笔试题。

4.1 题目1

我们来看这段代码:

#include <stdio.h>
void GetMemory(char* p) 
{
  p = (char*)malloc(100);
}
void Test(void) 
{
  char* str = NULL;
  GetMemory(str);
  strcpy(str, "hello world");
  printf(str);
}
int main()
{
  Test();
  return 0;
}

请问运行 Test 函数会有什么样的结果?


经过上面的学习,我们相信大家应该很容易能够看出来,上面这段代码存在一些比较严重的问题。


我们一起来分析一下:


首先,test函数进去,定义了一个字符指针char* str,给它赋了一个空指针NULL,然后,调用GetMemory函数,实参传的是str,GetMemory函数用了一个字符指针p接收,在GetMemory内部,用malloc动态开辟了100个字节的空间,返回值强制类型转换为char*赋给了p。


走到这一步我们其实就能发现一个小问题:


这里没有对malloc进行一个判断或者断言,因为malloc有可能开辟空间失败返回空指针。

当然这还不算这段代码中最严重的问题。


那我们接着往下看:


GetMemory(str);调用结束,下一句代码是:

strcpy(str, "hello world");

看到这里我们应该能猜出来这段代码的目的:

应该是想把字符串"hello world"拷贝到函数GetMemory(str)中动态开辟的那100个字节空间里,然后打印出来。


那到这里第二个问题就出来了。


strcpy(str, "hello world")

既然是想把"hello world"拷贝到函数GetMemory(str)中动态开辟的那100个字节空间里,那第一个参数str是不是应该指向malloc开辟的那100个字节才对啊。

但是,上面代码里面传参传的是指针变量str,形参p实际只是str的一个临时拷贝。我们把malloc的返回值赋给了p,让p指向了这100个字节空间的首地址,但是str是不是并没有改变啊,Test 函数中的 str 一直都是 NULL。

而strcpy在拷贝是应该是会对str解引用的,这样会导致程序崩溃的!!!


还有一个问题:


malloc开辟的空间使用完是需要使用free释放的,但是上述代码并没有释放,这样就可能导致内存泄漏,因此,这也是一个比较严重的错误。


那接下来我们就来修改一下这段代码,将它变成正确的:

#include <stdlib.h>
#include <string.h>
void GetMemory(char** p)
{
  *p = (char*)malloc(100);
}
void Test(void)
{
  char* str = NULL;
  GetMemory(&str);//不传地址,将p作为返回值赋给str也可以
  strcpy(str, "hello world");
  printf(str);
  free(str);
  str = NULL;
}
int main()
{
  Test();
  return 0;
}

这样代码就正确了:

cf300fe349624067b78613c7e50b85eb.png

4.2 题目2(返回栈空间地址的问题)

char* GetMemory(void) 
{
  char p[] = "hello world";
  return p;
}
void Test(void) 
{
  char* str = NULL;
  str = GetMemory();
  printf(str);
}
int main()
{
  Test();
  return 0;
}

大家再来看看这段代码有没有什么问题?

我们一起来分析一下:


首先str还是一个char*的指针,给它赋值为NULL,然后调用GetMemory(),

GetMemory()内部创建了一个字符数组p,放了一个字符串"hello world",p是数组名,是首字符’h’的地址,将p作为返回值赋给str,那我们是不是就可以通过str访问数组p了,printf(str)就把"hello world"打印出来了。


是这样吗?

如果这样想,那就错了。


为什么呢?

数组p是我们在函数内部创建的一个局部的数组,当函数调用结束就被销毁了,数组所在的这块空间就还给操作系统了,那这时候我们再去打印这块空间里的内容,是不是就非法访问内存了。

这样也是不行的。


4.3 题目3

void GetMemory(char** p, int num) 
{
  *p = (char*)malloc(num);
}
void Test(void) 
{
  char* str = NULL;
  GetMemory(&str, 100);
  strcpy(str, "hello");
  printf(str);
}
int mani()
{
  Test();
  return 0;
}

再来看这段代码,有什么问题:


这段代码前面都没有什么大问题,当然这里还是没有对malloc进行是否为空指针的判断。

传的是str的地址,GetMemory调用结束后,str指向的就是malloc开辟的那100字节的空间,那strcpy(str, "hello");就能够成功把字符串"hello"拷贝到str指向的空间,然后打印出来,这都没什么问题。

但是:

是不是没有对malloc开辟的空间进行释放,还是存在一个内存泄漏问题。


我们可以来修改一下:

void GetMemory(char** p, int num)
{
  *p = (char*)malloc(num);
  assert(*p);
}
void Test(void)
{
  char* str = NULL;
  GetMemory(&str, 100);
  strcpy(str, "hello");
  printf(str);
  free(str);
  str = NULL;
}
int mani()
{
  Test();
  return 0;
}

这样就没什么问题了。

4.4 题目4

void Test(void) 
{
  char* str = (char*)malloc(100);
  strcpy(str, "hello");
  free(str);
  if (str != NULL)
  {
    strcpy(str, "world");
    printf(str);
  }
}

来看这段代码,有没有问题:


首先,第一个问题还是没有对malloc的返回值进行一个判断,有可能是空指针。

其次,我们发现,在strcpy(str, "hello");之后,就直接free(str)了,这时str指向的空间已经被释放了,但是str还保留这块空间的地址,因为释放后我们并没有将它置空,那此时的str是不是已经成为野指针了,因为它指向了一块已经被释放的空间。

那下面的if (str != NULL)判断结果为真,就会进入if语句,而在if语句里面又有strcpy(str, "world"),把"world"拷贝到已经不属于我们的动态内存区,篡改动态内存区的内容,后果难以预料,非常危险。


所以这段代码也是有问题的。


以上就是对C语言动态内存管理的讲解及一些笔试题练习,欢迎大家指正!!!

8075029d35ca4ea491bab6e67de6dec8.png

目录
相关文章
|
1月前
|
C语言
【进阶C语言】数组笔试题解析
【进阶C语言】数组笔试题解析
17 0
|
1月前
|
C语言
c语言内存函数的深度解析
c语言内存函数的深度解析
32 3
|
2天前
|
Java 容器 Spring
Javaweb之SpringBootWeb案例之 Bean管理的Bean作用域详细的解析
Javaweb之SpringBootWeb案例之 Bean管理的Bean作用域详细的解析
10 0
|
2天前
|
JSON 前端开发 测试技术
Javaweb之SpringBootWeb案例员工管理之新增员工的详细解析
Javaweb之SpringBootWeb案例员工管理之新增员工的详细解析
14 0
|
16天前
|
存储 算法 安全
深度解析JVM世界:JVM内存分配
深度解析JVM世界:JVM内存分配
|
21天前
|
存储 缓存 监控
深入解析linux内存指标:快速定位系统内存问题的有效技巧与实用方法(free、top、ps、vmstat、cachestat、cachetop、sar、swap、动态内存、cgroops、oom)
深入解析linux内存指标:快速定位系统内存问题的有效技巧与实用方法(free、top、ps、vmstat、cachestat、cachetop、sar、swap、动态内存、cgroops、oom)
|
28天前
|
监控 Linux 调度
【Linux 应用开发 】Linux 下应用层线程优先级管理解析
【Linux 应用开发 】Linux 下应用层线程优先级管理解析
45 0
|
1月前
|
存储 编解码 Linux
深入解析Linux C/C++ 编程中的内存泄漏问题
深入解析Linux C/C++ 编程中的内存泄漏问题
112 1
|
1月前
|
存储 关系型数据库 MySQL
|
1月前
|
分布式计算 NoSQL 大数据
探索数据宇宙:深入解析大数据分析与管理技术
探索数据宇宙:深入解析大数据分析与管理技术
55 1

热门文章

最新文章

推荐镜像

更多