C语言中递归什么时候能够省略return引发的思考:通过内联汇编解读C语言函数return的本质...

C语言中递归什么时候能够省略return引发的思考:通过内联汇编解读C语言函数return的本质
  • 事情的经过是这种,博主在用C写一个简单的业务时使用递归,因为粗心而忘了写return。结果发现返回的结果依旧是正确的。经过半小时的反汇编调试。证明了我的猜想,如今在博客里分享。也是对C语言编译原理的一次加深理解。
  • 引子:
  • 首先我想以一道题目引例,比較能体现出问题。
1:
#include <stdio.h>
/**函数功能:用递归实现位运算加法*/
int Add_Recursion(int a,int b)
{int carry_num = 0, add_num = 0;if (b == 0){return a;}else{add_num = a^b;carry_num = (a&b)<<1;Add_Recursion(add_num, carry_num);}
}
int main()
{int num = Add_Recursion(1, 1);printf("%d\n",num);getchar();
}
  • 问题是。运行如上的程序,打印出来的数值是多少?
  • 大家可能会觉得这个非常的弱智,即使作为小公司的笔试题来说都登不上大雅之堂。


    图1 例题1的运行结果
    ——————————–图1 例题1的运行结果———————

  • 答案是2,毫无疑问,仅仅是一个简单的递归而已。


    可是假设我把题目改一下

2:
#include <stdio.h>
int changestack()
{return 3;
}
/**函数功能:用递归实现位运算加法*/int Add_Recursion(int a,int b)
{int carry_num = 0, add_num = 0;if (b == 0){return a;}else{add_num = a^b;carry_num = (a&b)<<1;Add_Recursion(add_num, carry_num);changestack();}
}int main()
{int num = Add_Recursion(1, 1);printf("%d\n",num);getchar();
}
  • 大家看看上边的程序。运行结果会是多少?
    可能有非常多朋友细心已经发现了猫腻。


    可能也有部分朋友会有些困惑,这个程序仅仅是在递归的实现函数后中加了一个无关紧要的函数调用,为什么会影响函数返回的结果呢。
    其实printf打印出来的结果不对。运行结果是3
    图2 例题2的运行结果
    —————————-图2 例题2的运行结果————————-

  • 为什么会出现这个问题呢。实际上正常情况下的递归。

    在else语句里进行递归调用时。应当加上return。

    因为return的缺失,导致了函数返回值被changestack()函数篡改。从而在main函数中读到了错误的返回值。

else{add_num = a^b;carry_num = (a&b)<<1;return Add_Recursion(add_num, carry_num);changestack();}
  • 假设将上文的代码改正如上,那不会出现不论什么问题。

    (当然不会出错,此时有了return,return后边的changestack根本就不会有不论什么机会运行)
    如今来一步一步来分析发生错误的本质。
    这里写图片描写叙述

  • ——————–图三 例二函数的递归分析—————————

  • 我们分析上边代码的运行过程。首先在main函数中调用Add_Recursion(1,1),本意就是计算1+1的值,而且将函数返回值传递给printf打印出来。


    在递归调用Add_Recursion函数(简称add)计算1+1时,前两次递归调用因为不满足递归出口条件(进位加数carry_num为0)。会跳入else分支进行递归调用。

    直到第三次递归调用时因为carry_num为0。这时返回了累加结果。

  • 问题是仅仅有第三次的add递归调用进行了return,第一次和第二次在函数返回时,都没有return,而是在返回子层次递归后调用changestack()函数后返回调用自己的函数层级。

    在第一层递归调用返回给main的时候,add_recursion并没有return,而是在运行完changestack直接返回main函数,而此时main函数的printf在解析返回值时,实际上错误的解析了changestack的返回值。

    因此才出现1+1=3的错误

  • 综上分析发生这一切的原因,就是:
    函数运行结束返回时。会将返回值压栈(理论上如此,实际上编译器会优化,将返回值给eax寄存器过渡。VC就是使用的eax临时保存)。VC编译器解析函数返回值(整型)时,直接将eax的值读出当做返回值。


    这里写图片描写叙述
    ———————-图四 反汇编分析VC编译器对return的处理———-

  • 依据反汇编分析能够看到,VC编译器对changestack()中的return 3汇编的结果,也就是 mov eax,3。实际上就是把返回值赋予eax,由eax寄存器过渡给此函数的调用函数使用。

  • 我们在下图中能够看到main函数中将changestack()的返回值给num赋值的详细过程,也就是将eax的值返回给num的所在的内存地址。
    这里写图片描写叙述
    ——————————图五 函数返回值的“弹栈”细则——————————-

  • 这样一切就有了解释。

  • 这里写图片描写叙述

——————-图六 例题一为什么会碰巧正确的递归分析—————

  • 尽管第一题的结果尽管正确,printf在读取Add_Recursion返回值时。读取的不是第一次递归调用的结果,而是第三次递归调用return b的结果(第三次递归返回时,暂存在eax寄存器中)。而在之后的递归返回中,凑巧eax都没有被改变。

    因此这样使用递归(尽管没有在须要return的地方return)是能够得到正确结果。
    实际上我们能够用一条内联汇编代码验证我们的猜想是否正确。

    我们在递归调用的后边,使用内联汇编加上一条汇编代码改变eax的值。


    这里写图片描写叙述

——————————-图七 用内联汇编解读C语言的return本质—————————–

  • 我们在递归函数Add_Recursion的后边加了一条汇编代码,让函数结束时改变eax的值。能够看到。主函数中,将函数返回值误觉得了我们在汇编语言中设定的3.打印出了1+1=3这种谬论。

  • 实际上,我们在编译例题中的程序在编译时C编译器会提出警告
    warning C4715: “Add_Recursion”: 不是全部的控件路径都返回值
    有返回值的函数,不是全部的支路都会进行返回值,假设大家把博客中的程序在更加严格的C++编译器上编译会报错。

  • 这仅仅是一个非常easy的案例。或许我们会运气好实现函数的功能,可是在进行复杂情况的树状甚至图状递归中,假设不确定自己是否一定能得到终于结果,请务必将每一种情况都return返回值,这样来避免程序意外出错。

    C语言的灵活性应该给我们造福,而不应该给我们的程序提供不稳定的因素。

posted on 2017-08-07 18:01 mthoutai 阅读(...) 评论(...) 编辑 收藏

转载于:https://www.cnblogs.com/mthoutai/p/7300489.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/282837.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C# 为什么说CM+Fody+HC是WPF开发的最强组合?

01—名词解析CM&#xff1a;Caliburn.Micro(简称CM)一经推出便备受推崇&#xff0c;作为一款MVVM开发模式的经典框架&#xff0c;越来越多的受到wpf开发者的青睐.我们看一下官方的描述&#xff1a;Caliburn是一个为Xaml平台设计的小型但功能强大的框架。Micro实现了各种UI模式&…

c语言逻辑运算符两侧运算对象,逻辑运算符两侧运算对象的数据类型是什么?...

逻辑运算符两侧运算对象的数据类型&#xff1a;可以是任何合法的类型数据&#xff1b;因为逻辑运算符两边的运算对象&#xff0c;最终都被转换成bool值(逻辑值)操作。0、null转换为false&#xff0c;而所有非零、非false、非null值转换为true&#xff1b;然后进行运算。逻辑运算…

python-list:列表-元组-字符串

列表 “列表”是一个值&#xff0c;它包含多个字构成的序列。术语“列表值”指的是列表本身&#xff08;它作为一个值&#xff0c;可以保存在变量中、传递给函数&#xff09;--&#xff1a;按下标取值、切片、for循环、用于len()以及in not in等 list [aa,bb,cc,dd]是一个简单的…

创建相似对象,就交给『工厂模式』吧

源码&#xff1a; 源代码C# 系列导航&#xff1a; 目录 定义&#xff08;Factory Pattern&#xff09;&#xff1a; 用来创建目标对象的类&#xff0c;将相似对象的创建工作统一到一个类来完成。 一、简单工厂模式&#xff1a; 代码&#xff1a; /// <summary>/// 产品枚…

《ASP.NET Core 6框架揭秘》实例演示[26]:跟踪应用接收的每一次请求

很多人可能对ASP.NET Core框架自身记录的诊断日志并不关心&#xff0c;其实这些日志对纠错排错和性能监控提供了很有用的信息。如果需要创建一个APM&#xff08;Application Performance Management&#xff09;系统来监控ASP.NET Core应用处理请求的性能及出现的异常&#xff…

C语言循环为1404的循环,考试,求大神帮忙,C语言,小弟感激不尽

若有定义语句&#xff1a;int a10; double b3.14;&#xff0c;则表达式Aab值的类型是___________。  (1)A).char B)int C) double D)float(2)若有定义语句&#xff1a;int x12,y8,z;&#xff0c;在其后执行语句z0.9x/y;&#xff0c;则z的值为___________。A)1.9 B)1 C)2 D)2.…

js题集19

1.实现斐波那契数列。达到题目中的效果。不知道斐波那契数列是啥的请自行百度。 function fibonacci(){ } var ffibonacci(); for(var i0;i<10;i){ console.log(f()); } //output:按顺序输出斐波那契数列的数字。 eg&#xff1a; 1 2 3 5 8 13 21 34 55 89转载于:https://ww…

阿里云Maven镜像配置

2019独角兽企业重金招聘Python工程师标准>>> <mirror><id>alimaven</id> <name>aliyun maven</name> <url>http://maven.aliyun.com/nexus/content/groups/public/</url> <mirrorOf>central</mirrorOf> …

c语言中有12个球,数学老师做不出来的一道逻辑推理题

同志们 那个球不一定轻啊正确的是平分三份 取两分称if(平)。。。。。。在未称过的4球中取两个放左边 和标准的球称(称过的球一定标准)。。。。。。if(平)。。。。。。。。。。。。在两次都未称过的球中取一个 和标准的称。。。。。。。。。。。。if(平)。。。。。。。。。。。。…

WPF 实现弹幕效果

WPF 实现弹幕效果控件名&#xff1a;BarrageExample作者&#xff1a;WPFDevelopersOrg原文链接&#xff1a; https://github.com/WPFDevelopersOrg/WPFDevelopers框架使用大于等于.NET40&#xff1b;Visual Studio 2022;项目使用 MIT 开源许可协议&#xff1b;此篇代码目的只…

js题集23

1.实现函数--defaultArguments 功能如下&#xff1a; function add(a,b) { return ab;}; var add_ defaultArguments(add,{b:9}); add_(10); // returns 19 add_(10,7); // returns 17 add_(); // returns NaN add_ defaultArguments(add_,{b:3, a:2}); add_(10); // returns…

iteritems()与items()

iteritems&#xff1a;以迭代器对象返回字典键值对 item:以列表形式返回字典键值对 >>> dic {a:3,c:1,b:2} >>> print dic.iteritems() <dictionary-itemiterator object at 0x7fa381599628> >>> print dic.items() [(a, 3), (c, 1), (b, 2)…

WPF效果第一百九十八篇之模块对比

前面效果中分享了彩色马蹄图的效果和范围内拖拽;这不大假期的时间反正没啥事就在家撸代码;今天又是LisBox实现的效果,看最终效果:1、刚开始一朋友说用DataGrid来实现.首先把行对象转换成列对象,至于控制列的话,就后台重新赋值对象来控制前台.我是觉得太费劲直接放弃了;还是首选…

android 与后台通信,Android后台线程和UI线程通讯实例

本节向你展示如何在任务中发送数据给UI线程里的对象&#xff0c;这个特性允许你在后台线程工作&#xff0c;完了在UI线程展示结果。在UI线程定义一个HandlerHandler是Android系统线程管理框架里的一部分。一个Handler对象接收消息&#xff0c;并且运行代码来处理消息。正常情况…

saltstack的状态文件

saltstack状态文件设定&#xff1a;编辑/etc/salt/master&#xff0c;修改其中关于“设置文件的目录”的设置&#xff1a;说明&#xff1a;注意语法格式&#xff0c;顶格/冒号/两个空格state_top: top.sls # The state system uses a "top" file to tell the minions…

POJ 2798:二进制转换十六进制

很郁闷&#xff0c;这道题一直WA&#xff0c;然而本地我测了好几组数据都是通过的&#xff0c;上网找了网友陈宇龙加油加油加油的AC的代码&#xff0c;http://blog.csdn.net/Since_natural_ran/article/details/51742149&#xff0c;发现没有什么不同。。。很无语。。 #include…

【Shashlik.EventBus】.NET 事件总线,分布式事务最终一致性简介

分布式事务、CAP定理、事件总线&#xff0c;在当前微服务、分布式、集群大行其道的架构前提下&#xff0c;是不可逃避的几个关键字&#xff0c;在此不会过多阐述相关的理论知识。Shashlik.EventBus就是一个基于.NET6的开源事件总线解决方案&#xff0c;同时也是分布式事务最终一…

5个超实用的Visual Studio插件

工欲善其事&#xff0c;必先利其器,整理的一些我必装的5款Visual Studio插件&#xff0c;希望你们能get到。01 CodeMaidCodeMaid快速整理代码文件&#xff0c;规范你的代码&#xff0c;提高代码阅读体验。代码自动对齐&#xff0c;格式化代码&#xff08;ps&#xff1a;不用再按…

BZOJ1509: [NOI2003]逃学的小孩(树的直径)

Time Limit: 5 Sec Memory Limit: 64 MBSubmit: 1126 Solved: 567[Submit][Status][Discuss]Description Input 第一行是两个整数N&#xff08;3  N  200000&#xff09;和M&#xff0c;分别表示居住点总数和街道总数。以下M行&#xff0c;每行给出一条街道的信息。第i1行…

Blazor University (52)依赖注入 —— 拥有多个依赖项:正确的方式

原文链接&#xff1a;https://blazor-university.com/dependency-injection/component-scoped-dependencies/owning-multiple-dependencies-the-right-way/拥有多个依赖项&#xff1a;正确的方式在上一节[1]中&#xff0c;我们看到了将多个拥有的依赖项注入组件的错误方法。本节…