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

这篇具有很好参考价值的文章主要介绍了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指针的引用

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

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

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

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】开始执行,这样我们很快就能观察到结果了

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

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

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

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

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

改进:

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

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

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

代码:

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

分析:

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

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

改进:

  • 本代码并没有什么通用的改进办法,如果不想出现问题的话就不要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()函数需要做到申请多少释放多少,所以当其释放了一部分之后,就不够了,便造成了访问内存错误的问题
C生万物 | 常见的六种动态内存错误

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

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

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

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

改进:

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

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

代码:

void test()
{
    int* p = (int*)malloc(100);
    //使用...
    free(p);

    //...
    free(p);	//重复释放
}

分析:

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

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

改进:

  • 此时我们就可以对代码去做一个简单的改进,在第一次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释放一块动态开辟内存的一部分 】—— 借了多少还多少,不要私藏哦
    • 对同一块动态内存多次释放】 —— 借了多少还多少,不要私藏哦
    • 动态开辟内存忘记释放】 —— 借了别人的东西要记得还
  • 在使用动态内存函数开辟出空间后,使用的时候一定要牢记以上几点,否则要出大问题的!

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

C生万物 | 常见的六种动态内存错误文章来源地址https://www.toymoban.com/news/detail-511998.html

到了这里,关于C生万物 | 常见的六种动态内存错误的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 关于Bean的六种作用域

    在JavaSE中,我们学习过了全局变量以及局部变量,这里就涉及到了作用域问题,那么什么是作用域呢? 限定程序中变量的使用范围叫做作用域,或者说在源代码中定义变量的某个区域就叫做作用域。 而Bean的作用域指的是 Bean在Spring整个框架中的某种行为模式 , 比如singleto

    2024年02月08日
    浏览(42)
  • Python中的六种基本数据类型

    Python中分为六种基本数据类型 不可变类型(又叫静态数据类型,没有增删改操作):数字(number)、字符串(string)、元组(tuple) 可变类型(又叫动态数据类型,支持增删改操作):列表(list)、字典(dictionary)、集合(set) 1. 数字类型(numbers): 数字类型下还可分为整数(int)、浮点数(f

    2024年02月04日
    浏览(27)
  • 【SpringMVC]获取参数的六种方式

    目录 1.通过ServletAPI获取 2.通过控制器方法的形参获取 3.@RequestParam:将请求参数和控制器方法的形参绑定 4.@RequestHeader:将请求头信息与控制器方法的形参的值进行绑定 5. CookieValue:将cookie数据和控制器方法的形参绑定 Cookie: ​编辑 6.通过控制器方法的实体类类型的形参获取

    2024年02月09日
    浏览(42)
  • SpringBoot 实现跨域的六种方式

    目录 1.通过SpringSecurity方式配置 2.使用Spring提供的CorsFilter注入Bean(推荐) 3.使用注解@CrossOrigin注解(繁琐) 4.通过ResponseBodyAdvice 实现跨域 5.通过HttpServletResponse设置跨域 6.通过WebMvcConfigurer 实现跨域 与第5类似

    2024年02月14日
    浏览(39)
  • uniapp路由跳转的六种方式

    uniapp官方文档详解: 一、uni.navigateTo保留当前页面,跳转到应用内的某个页面,使用uni.navigateBack可以返回到原页面。 注意: 页面跳转路径有层级限制,不能无限制跳转新页面 跳转到 tabBar 页面只能使用 switchTab 跳转 二、uni.redirectTo关闭当前页面,跳转到应用内的某个页面。

    2024年02月11日
    浏览(36)
  • 斐波那契数列的六种解法

    做这个问题之前,我们需要了解到斐波那契数列是什么东西?是干什么的? 斐波那契数列是什么? 一、斐波那契数列指的是这样一个数列:1、1、2、3、5、8、13、21、…… 这个数列从第三项开始,每一项都等于前两项之和。 二、应用:通常在个别股票中不是太准确,通常在指数上

    2024年02月08日
    浏览(43)
  • vue组件间传值的六种方法

    父组件代码: 子组件代码: 页面显示: 子组件代码: 父组件代码: 页面显示: 父子组件传值原理:父子组件的关系可以总结为 prop 向下传递,事件向上传递。父组件通过 prop 给子组件下发数据,子组件通过事件给父组件发送消息,如下图所示: ref:如果在普通的 DOM 元素

    2024年02月15日
    浏览(41)
  • bitmap的六种压缩方式,Android图片压缩

    Android中图片是以bitmap形式存在的,那么bitmap所占内存,直接影响到了应用所占内存大小,首先要知道bitmap所占内存大小计算方式: 图片长度 x 图片宽度 x 一个像素点占用的字节数 以下是图片的压缩格式: 其中,A代表透明度;R代表红色;G代表绿色;B代表蓝色。 ALPHA_8 表示

    2024年02月09日
    浏览(37)
  • C语言实现排序算法的六种方式

    1、冒泡法 2、交换法 每次用当前的元素一一的同其后的元素 3、选择法 从数据中选择最小的同第一个值交换,在从剩下的部分中选择最小的与第二个交换,这样往复下去 4、插入法 在前面的数中寻找相应的位置插入, 然后继续下一张 插入排序就是每一步都将一个待排数据按

    2024年01月25日
    浏览(37)
  • 工业机器人常用的六种坐标系

      1. 基坐标系 基坐标系是以机器人安装基座为基 准、用来描述机器人本体运动的直角坐标系。 任何机器人都离不开基坐标系,也是机器人TCP在三维空间运动空间所必须的基本坐标系(面对机器人前后:X轴 ,左右:Y轴, 上下:Z轴)。坐标系遵守右手准则: 2. 大地坐标系

    2024年02月09日
    浏览(35)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包