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的运行结果———————

  • 答案是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的运行结果————————-

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

    在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语言的灵活性应该给我们造福,而不应该给我们的程序提供不稳定的因素。

时间: 2024-10-12 17:40:09

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 (

在Visual C++中使用内联汇编

一.内联汇编的优缺点 因为在Visual C++中使用内联汇编不需要额外的编译器和联接器,且可以处理Visual C++中不能处理的一些事情,而且可以使用在C/C++中的变量,所以非常方便.内联汇编主要用于如下场合: 1.使用汇编语言写函数: 2.对速度要求非常高的代码: 3.设备驱动程序中直接访问硬件: 4."Naked" Call的初始化和结束代码. //(."Naked",理解了意思,但是不知道怎么翻译^_^,大概就是不需要C/C++的编译器(自作聪明)生成的

Linux C中内联汇编的语法格式及使用方法(Inline Assembly in Linux C)

在阅读Linux内核源码或对代码做性能优化时,经常会有在C语言中嵌入一段汇编代码的需求,这种嵌入汇编在CS术语上叫做inline assembly.本文的笔记试图说明Inline Assembly的基本语法规则和用法(建议英文阅读能力较强的同学直接阅读本文参考资料中推荐的技术文章 ^_^). 注意:由于gcc采用AT&T风格的汇编语法(与Intel Syntax相对应,二者的区别参见这里),因此,本文涉及到的汇编代码均以AT&T Syntax为准. 1. 基本语法规则 内联汇编(或称嵌入汇

[转载]【Linux学习笔记】Linux C中内联汇编的语法格式及使用方法(Inline Assembly in Linux C)

在阅读Linux内核源码或对代码做性能优化时,经常会有在C语言中嵌入一段汇编代码的需求,这种嵌入汇编在CS术语上叫做inline assembly.本文的笔记试图说明Inline Assembly的基本语法规则和用法(建议英文阅读能力较强的同学直接阅读本文参考资料中推荐的技术文章 ^_^).        注意:由于gcc采用AT&T风格的汇编语法(与Intel Syntax相对应,二者的区别参见这里),因此,本文涉及到的汇编代码均以AT&T Syntax为准. 1. 基本语法规则    

Linux C中内联汇编的语法格式及使用方法(Inline Assembly in Linux C)---- asm [volatile](**)

在阅读Linux内核源码或对代码做性能优化时,经常会有在C语言中嵌入一段汇编代码的需求,这种嵌入汇编在CS术语上叫做inline assembly.本文的笔记试图说明Inline Assembly的基本语法规则和用法(建议英文阅读能力较强的同学直接阅读本文参考资料中推荐的技术文章 ^_^). 注意:由于gcc采用AT&T风格的汇编语法(与Intel Syntax相对应,二者的区别参见这里),因此,本文涉及到的汇编代码均以AT&T Syntax为准. 1. 基本语法规则 内联汇编(或称嵌入汇

VC内联汇编,引用程序中的变量

[cpp] view plain copy print? int a=5; //变量a _asm { mov eax,a;       //将变量a的值放入寄存器eax add eax,eax;   //相当于a=a+a mov a,eax;      //将a+a的结果赋给a } //查看结果,注意a的初值为5 CString rst; rst.Format(_T("a=%d"),a); AfxMessageBox(rst);

C语言中一些乱七八糟的用法与细节(不断更新)

用C语言比较多,这篇是平时攒下的.有些内容在工作后可能会很常见,但是不用容易忘,所以就写篇博客吧. 一.printf的用法 %*可以用来跳过字符,可以用于未知缩进.像下面一样. for(i = 1; i < 10; i++) { printf("%*c\r%*c\n",  9 - abs(i - 5), '*', abs(i - 5) + 1, '*'); } %[]可以用来读取指定的内容,%[^]可以用来忽略指定内容(正则表达式?) %m可以不带参数,输出产生的错误信息 二.关

linux平台学x86汇编(十九):C语言中调用汇编函数

[版权声明:尊重原创,转载请保留出处:blog.csdn.net/shallnet,文章仅供学习交流,请勿用于商业用途] 除了内联汇编以外,还有一种途径可以把汇编代码整合到C/C++语言中,C/C++语言可以直接调用汇编函数,把输入值传递给函数,然后从函数获得输出值. 如果希望汇编语言函数和C/C++程序一起工作,就必须显示地遵守C样式的函数格式,也就是说所有输入变量都必须从堆栈读取,并且大多数输入值都返回到EAX嫁寄存器中.在汇编函数代码中,C样式函数对于可以修改哪些寄存器和函数必须保留哪些寄

C语言中容易被忽略的细节(第四篇)

前言:本文的目的是记录C语言中那些容易被忽略的细节.我打算每天抽出一点时间看书整理,坚持下去,今天是第一篇,也许下个月的今天是第二篇,明年的今天又是第几篇呢?--我坚信,好记性不如烂笔头.第四篇了,fight~... 第一篇链接:C语言中容易被忽略的细节(第一篇) 第二篇链接:C语言中容易被忽略的细节(第二篇) 第三篇链接:C语言中容易被忽略的细节(第三篇) 1.void*类型的指针不能参与算术运算,只能进行赋值.比较和sizeof操作的原因? 指针的算术运算还要包含指针所指对象的字节数信息.