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

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 【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

目录
相关文章
|
22天前
|
存储 缓存 安全
Java内存模型深度解析:从理论到实践####
【10月更文挑战第21天】 本文深入探讨了Java内存模型(JMM)的核心概念与底层机制,通过剖析其设计原理、内存可见性问题及其解决方案,结合具体代码示例,帮助读者构建对JMM的全面理解。不同于传统的摘要概述,我们将直接以故事化手法引入,让读者在轻松的情境中领略JMM的精髓。 ####
32 6
|
1月前
|
存储 Java 编译器
Java内存模型(JMM)深度解析####
本文深入探讨了Java内存模型(JMM)的工作原理,旨在帮助开发者理解多线程环境下并发编程的挑战与解决方案。通过剖析JVM如何管理线程间的数据可见性、原子性和有序性问题,本文将揭示synchronized关键字背后的机制,并介绍volatile关键字和final关键字在保证变量同步与不可变性方面的作用。同时,文章还将讨论现代Java并发工具类如java.util.concurrent包中的核心组件,以及它们如何简化高效并发程序的设计。无论你是初学者还是有经验的开发者,本文都将为你提供宝贵的见解,助你在Java并发编程领域更进一步。 ####
|
2月前
|
编译器 C语言
动态内存分配与管理详解(附加笔试题分析)(上)
动态内存分配与管理详解(附加笔试题分析)
58 1
|
2月前
|
C++
【C++】深入解析C/C++内存管理:new与delete的使用及原理(二)
【C++】深入解析C/C++内存管理:new与delete的使用及原理
|
2月前
|
编译器 C++ 开发者
【C++】深入解析C/C++内存管理:new与delete的使用及原理(三)
【C++】深入解析C/C++内存管理:new与delete的使用及原理
|
14天前
|
存储 算法 Java
Java内存管理深度解析####
本文深入探讨了Java虚拟机(JVM)中的内存分配与垃圾回收机制,揭示了其高效管理内存的奥秘。文章首先概述了JVM内存模型,随后详细阐述了堆、栈、方法区等关键区域的作用及管理策略。在垃圾回收部分,重点介绍了标记-清除、复制算法、标记-整理等多种回收算法的工作原理及其适用场景,并通过实际案例分析了不同GC策略对应用性能的影响。对于开发者而言,理解这些原理有助于编写出更加高效、稳定的Java应用程序。 ####
|
2月前
|
存储 监控 算法
Java中的内存管理与垃圾回收机制解析
本文深入探讨了Java编程语言中的内存管理方式,特别是垃圾回收机制。我们将了解Java的自动内存管理是如何工作的,它如何帮助开发者避免常见的内存泄漏问题。通过分析不同垃圾回收算法(如标记-清除、复制和标记-整理)以及JVM如何选择合适的垃圾回收策略,本文旨在帮助Java开发者更好地理解和优化应用程序的性能。
|
2月前
|
程序员 编译器 C语言
动态内存分配与管理详解(附加笔试题分析)(下)
动态内存分配与管理详解(附加笔试题分析)(下)
56 2
|
2月前
|
存储 安全 Java
JVM锁的膨胀过程与锁内存变化解析
在Java虚拟机(JVM)中,锁机制是确保多线程环境下数据一致性和线程安全的重要手段。随着线程对共享资源的竞争程度不同,JVM中的锁会经历从低级到高级的膨胀过程,以适应不同的并发场景。本文将深入探讨JVM锁的膨胀过程,以及锁在内存中的变化。
44 1
|
2月前
|
Java C语言 iOS开发
MacOS环境-手写操作系统-16-内存管理 解析内存状态
MacOS环境-手写操作系统-16-内存管理 解析内存状态
43 0

推荐镜像

更多