C语言中:递归问题的深入研究

C语言中:递归问题的深入研究

函数的递归有两个限制条件:

1.递归存在限制条件,当满⾜这个限制条件的时候,递归便不再继续。

2.每次递归调⽤之后越来越接近这个限制条件。

例子:

#include <stdio.h>
int main()
{printf("haha\n");main();//自己调用自己的主函数return 0;
}
//出现死循环的打印

这是为什么呢?

这是递归的==常见错误:==栈溢出

  • 浅讲一下:
  • 每次调用函数的时候都会向内存申请空间,在C语言中内存一般划分为三个区域:栈区堆区静态区,代码区。
  • 在写代码的时候要创建变量,局部变量创建申请的内存空间在栈区上面(还有函数的形参);

堆区里面放的是动态开辟的内存(比如malloc、calloc);

全局变量的创建放在静态区里面(包括static修饰的变量)。

函数的调用都需要从栈区里面申请内存空间。

  • 我们调用main函数的时候就要从栈区里面分配一块内存空间,调用printf函数的时候也从栈区分配一块内存空间,接下来又遇到main函数又要从栈区分配一块内存空间。然后陷入死循环,不断从栈区里面申请空间,当空间耗尽的时候,就出现错误:(栈溢出)

  • 在C语⾔中每⼀次函数调⽤,都要需要为本次函数调⽤在栈区申请⼀块内存空间来保存函数调⽤期间的各种局部变量的值,这块空间被称为运⾏时堆栈,或者函数栈帧。函数不返回,函数对应的栈帧空间就⼀直占⽤。所以如果函数调⽤中存在递归调用的话,每⼀次递归函数调⽤都会开辟属于自己的栈帧空间,直到函数递归不再继续,开始回归,才逐层释放栈帧空间。所以如果采⽤函数递归的⽅式完成代码,递归层次太深,就会浪费太多的栈帧空间,也可能引起栈溢出(stack over flow)的问题。

函数栈帧(运行时堆栈)的本质

1.栈帧的作用
  • 每个函数调用在栈区分配的内存块称为栈帧(Stack Frame),用于存储:
    • 局部变量:函数内定义的变量(如int a, char b)
    • 形参值:调用函数时传递的参数副本。
    • 返回地址:函数执行完毕后,返回调用者的下一条指令地址。
    • 上下文信息:保存函数调用前的寄存器状态(如ebp、esp,用于恢复调用者的栈帧)。
2.栈帧的生命周期
  • 创建:函数调用时,系统从栈顶分配空间(栈向下增长,堆向上增长)。
  • 销毁:函数返回时,系统释放栈帧空间,栈顶指针回退。
3. 栈帧结构示例(x86 架构)

高地址
┌───────────────┐
│ 调用者栈帧 │
├─────────────── ┤
│ 返回地址 │ <- 调用者的下一条指令地址
├─────────────── ┤
│ 形参值 │ <- 按从右到左顺序压栈(C语言特性)
├───────────────┤
│ 保存的ebp │ <- 调用者的ebp寄存器值
├───────────────┤
│ 局部变量 │ <- 函数内定义的变量
├───────────────┤
│ 临时变量 │ <- 表达式计算临时值
└───────────────┘ <- ebp(当前栈帧基址)
低地址

递归调用与栈帧的关系

1.递归的栈帧特征

  • 每层递归都是独立栈帧:每次递归调用都会创建新的栈帧,保存当前层的部变量、形参和返回地址。

  • 栈帧数量等于递归深度:若递归深度为n,则栈中存在n个未释放的栈帧。

2.阶乘递归的栈帧变化

int factorial(int n)
{if (n == 0) return 1;return n * factorial(n-1); // 递归调用
}

当调用factorial(3)时,栈帧变化如下:
第 1 层:n=3,调用factorial(2),栈帧 1 入栈。
第 2 层:n=2,调用factorial(1),栈帧 2 入栈。
第 3 层:n=1,调用factorial(0),栈帧 3 入栈。
终止层:n=0,返回 1,栈帧 3 释放,返回栈帧 2。
回归层:栈帧 2 计算2*1,释放后返回栈帧 1,依此类推。

  1. 栈帧的空间占用

每个栈帧的大小由函数内局部变量决定。例如:

void func() {int a[1000]; // 占用4KB(假设int占4字节)char b;      // 占用1字节
}

每次调用func需分配约 4KB 栈空间。

三、栈溢出(Stack Overflow)的成因与风险

1.直接原因

  • 递归深度过大:如计算factorial(10000),若每层栈帧占 1KB,总需约 10MB 空间,远超默认栈大小(通常 8MB 以下)。
  • 局部变量过大:函数内定义超大数组(如int arr[1000000]),单个栈帧耗尽栈空间。

2.系统默认栈大小

  • Linux:通过ulimit -s查看,默认通常为 8192 KB(8MB)。
  • Windows:Visual Studio 默认约 1MB(可通过链接器选项调整)。

四、避免栈溢出的策略

  1. 限制递归深度
  • 尾递归优化:若递归调用是函数最后一步操作(尾递归),部分编译器(如 GCC,需加-O2参数)可复用栈帧,避免栈增长。

  • int factorial_tail(int n, int result)
    {if (n == 0) return result;return factorial_tail(n-1, n*result); // 尾递归,可优化
    }
    int factorial(int n) {return factorial_tail(n, 1);
    }
    
  1. 手动模拟递归(迭代法):

    • 用循环和显式栈(如数组、链表)替代递归,避免依赖系统栈。
  2. 最佳实践:
    优先使用迭代,除非递归能显著简化代码(如树 / 图的遍历)。
    对递归深度可预估且较小的场景(如n < 1000),可直接使用递归。
    对深四、使用动态内存(堆)模拟栈
    手动用堆内存实现栈结构,替代函数调用栈(即 “递归转迭代”)。
    示例:用堆模拟栈计算阶乘

  3. 度可能较大的场景(如未知输入的递归),必须用迭代或尾递归优化。

  4. 使用动态内存(堆)模拟栈,手动用堆内存实现栈结构,替代函数调用栈(即 “递归转迭代”)。

    #include <stdio.h>
    #include <stdlib.h>typedef struct Stack
    {int* data;int top;int capacity;
    }Stack;Stack* create_stack(int size) 
    {Stack* stack = (Stack*)malloc(sizeof(Stack));stack->data = (int*)malloc(size * sizeof(int));stack->top = -1;stack->capacity = size;return stack;
    }void push(Stack* stack, int val)
    {if (stack->top < stack->capacity - 1){stack->data[++stack->top] = val;}
    }int pop(Stack* stack) 
    {if (stack->top >= 0) {return stack->data[stack->top--];}return -1;  // 错误处理
    }int factorial(int n) 
    {if (n == 0) return 1;Stack* stack = create_stack(n);int result = 1;// 入栈:模拟递归调用while (n > 1) {push(stack, n);n--;}// 出栈:模拟递归回归while (stack->top >= 0) {result *= pop(stack);}free(stack->data);free(stack);return result;
    }
    
  • 栈结构定义与初始化
typedef struct Stack
{int* data;     // 动态数组存储栈元素int top;       // 栈顶指针(初始为-1表示空栈)int capacity;  // 栈的最大容量
} Stack;Stack* create_stack(int size) 
{Stack* stack = (Stack*)malloc(sizeof(Stack));  // 分配栈结构内存stack->data = (int*)malloc(size * sizeof(int)); // 分配数据存储区stack->top = -1;      // 初始化栈顶指针stack->capacity = size;  // 设置栈容量return stack;
}
  • 关键点:

    • 动态内存分配:通过两次malloc分别分配栈结构和数据区
    • 栈顶指针:top初始为 - 1,表示栈为空;入栈时先自增再赋值
  • 栈操作实现

void push(Stack* stack, int val)
{if (stack->top < stack->capacity - 1)  // 检查栈未满{stack->data[++stack->top] = val;  // 先移动栈顶指针,再存入数据}
}int pop(Stack* stack) 
{if (stack->top >= 0)  // 检查栈非空{return stack->data[stack->top--];  // 先返回数据,再移动栈顶指针}return -1;  // 错误处理(栈空时返回-1)
}
  • 操作逻辑:
    • 入栈(Push):栈顶指针top先自增,再将值存入data[top]
    • 出栈(Pop):先返回data[top]的值,再将top自减
      边界检查:入栈时检查top < capacity-1,出栈时检查top >= 0
  • 阶乘计算的栈模拟
int factorial(int n) 
{if (n == 0) return 1;  // 基准条件:0! = 1Stack* stack = create_stack(n);  // 创建容量为n的栈int result = 1;// 阶段1:入栈过程(模拟递归调用)while (n > 1) {push(stack, n);  // 将n压入栈n--;             // n递减,模拟递归深入}// 阶段2:出栈过程(模拟递归回归)while (stack->top >= 0) {result *= pop(stack);  // 弹出栈顶元素并累乘到结果}free(stack->data);  // 释放数据区内存free(stack);        // 释放栈结构内存return result;
}

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

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

相关文章

《C++20新特性全解析:模块、协程与概念(Concepts)》

引言&#xff1a;C20——现代C的里程碑 C20是继C11之后最具革命性的版本&#xff0c;它通过模块&#xff08;Modules&#xff09;、协程&#xff08;Coroutines&#xff09;和概念&#xff08;Concepts&#xff09;三大核心特性&#xff0c;彻底改变了C的代码组织方式、并发模…

xcode卡死问题,无论打开什么程序xcode总是在转菊花,重启电脑,卸载重装都不行

很可能是因为我们上次没有正常关闭Xcode&#xff0c;而Xcode保留了上次错误的一些记录&#xff0c;而这次打开Xcode依然去加载错误的记录&#xff0c;所以必须完全删除这些记录Xcode才能加载正常的项目。 那么也就是说&#xff0c;我们是不是只需要删除这部分错误记录文件就可以…

华为云Flexus+DeepSeek征文|华为云Flexus云服务器X实例上部署Dify:打造高效的开源大语言模型应用开发平台

目录 前言 1 Dify与华为云部署概述 1.1 什么是 Dify 1.2 华为云与 Flexus 云服务器的优势 2 云服务器部署 Dify 的步骤详解 2.1 模板选择 2.2 参数配置 2.3 资源栈设置 2.4 确认部署信息并执行 3 部署成功后的操作与平台使用指南 3.1 访问平台 3.2 设置管理员账号 …

物流项目第九期(MongoDB的应用之作业范围)

本项目专栏&#xff1a; 物流项目_Auc23的博客-CSDN博客 建议先看这期&#xff1a; MongoDB入门之Java的使用-CSDN博客 需求分析 在项目中&#xff0c;会有两个作业范围&#xff0c;分别是机构作业范围和快递员作业范围&#xff0c;这两个作业范围的逻辑是一致的&#xf…

网络拓扑如何跨网段访问

最近领导让研究下跟甲方合同里的&#xff0c;跨网段访问怎么实现&#xff0c;之前不都是运维网工干的活么&#xff0c;看来裁员裁到动脉上了碰到用人的时候找不到人了&#xff0c; 只能赶鸭子上架让我来搞 IP 网络中&#xff0c;不同网段之间的通信需要通过路由器&#xff0c;…

【前端】PWA

目录 概述实战vue项目问题汇总 PWA&#xff08;渐进式 Web 应用&#xff0c;Progressive Web App&#xff09; 2015提出 概述 PWA 是一种提升 Web 应用体验的技术&#xff0c;使其具备与原生应用相似的功能和性能。PWA不仅能够在网页上运行&#xff0c;还能在手机或桌面上像传…

湖北理元理律师事务所:从法律合规到心灵契合的服务升维

债务优化不仅是数字游戏&#xff0c;更是信任重建的过程。湖北理元理律师事务所在实践中发现&#xff1a;68%的债务纠纷中存在沟通断裂。为此&#xff0c;机构构建了“三维信任修复机制”。 维度一&#xff1a;信息透明的技术实现 区块链存证舱&#xff1a;客户手机实时查看律…

香橙派3B学习笔记2:Vscode远程SSH登录香橙派_权限问题连接失败解决

Vscode下载插件&#xff0c;ssh远程登录香橙派。 ssh &#xff1a; orangepi本地ip 密码 &#xff1a; orangepi 安装 Remote - SSH 扩展SSH插件&#xff1a; SSH远程连接&#xff1a; ssh usernameremote_host ssh -p port_number usernameremote_host默认22端口号就用第一行…

VMware安装Ubuntu实战分享大纲

深入解析快速排序 一、分治策略分解 分解阶段&#xff1a; 选择基准元素 $pivot$将数组划分为三个子集&#xff1a; $$ left {x | x < pivot} $$ $$ equal {x | x pivot} $$ $$ right {x | x > pivot} $$ 递归排序&#xff1a; 对 left 和 right 子集递归调用快速排…

AI 让无人机跟踪更精准——从视觉感知到智能预测

AI 让无人机跟踪更精准——从视觉感知到智能预测 无人机跟踪技术正在经历一场前所未有的变革。曾经,我们只能依靠 GPS 或简单的视觉识别来跟踪无人机,但如今,人工智能(AI)结合深度学习和高级视觉算法,正让无人机的跟踪变得更加智能化、精准化。 尤其是在自动驾驶、安防监…

GATED DELTA NETWORKS : IMPROVING MAMBA 2 WITH DELTA RULE

TL;DR 2024 年 Nvidia MIT 提出的线性Transformer 方法 Gated DeltaNet&#xff0c;融合了自适应内存控制的门控机制&#xff08;gating&#xff09;和用于精确内存修改的delta更新规则&#xff08;delta update rule&#xff09;&#xff0c;在多个基准测试中始终超越了现有…

Laravel单元测试使用示例

Date: 2025-05-28 17:35:46 author: lijianzhan 在 Laravel 框架中&#xff0c;单元测试是一种常用的测试方法&#xff0c;它是允许你测试应用程序中的最小可测试单元&#xff0c;通常是方法或函数。Laravel 提供了内置的测试工具PHPUnit&#xff0c;实践中进行单元测试是保障代…

【FastAPI】--3.进阶教程(二)

【FastAPI】--进阶教程1-CSDN博客 【FastAPI】--基础教程-CSDN博客 目录 1.FastAPI - CORS ​2.FastAPI - CRUD 操作 2.1.Create 2.2.Read 2.3.Update 2.4.Delete 3.FastAPI - 使用 GraphQL 4.FastAPI - Websockets 5.FastAPI - 事件处理程序 6.FastAPI - 安装 Fla…

FEMFAT许可的更新与升级流程

随着工程仿真技术的不断发展&#xff0c;FEMFAT作为一款领先的疲劳分析软件&#xff0c;持续为用户提供卓越的性能和创新的功能。为了保持软件的最新性和高效性&#xff0c;了解FEMFAT许可的更新与升级流程至关重要。本文将为您详细介绍FEMFAT许可的更新与升级流程&#xff0c;…

麒麟v10,arm64架构,编译安装Qt5.12.8

Window和麒麟x86_64架构&#xff0c;官网提供安装包&#xff0c;麒麟arm64架构的&#xff0c;只能自己用编码编译安装。 注意&#xff0c;“桌面”路径是中文&#xff0c;所以不要把源码放在桌面上编译。 1. 下载源码 从官网下载源码&#xff1a;https://download.qt.io/arc…

20250528-C#知识:结构体

C#知识&#xff1a;结构体 结构体是一种自定义数据类型&#xff0c;用户可以根据自身需求设计自己的结构体用来表示某种数据集合。结构体是一种值类型&#xff0c;结合了值类型的优点&#xff0c;避免了引用类型的缺点。本文简单介绍并探究一下C#中的结构体。 结构体一般写在命…

CRM系统的功能模块划分

基础管理模块 用户管理 用户注册与登录角色权限管理部门组织架构用户信息管理 系统设置 基础参数配置系统日志管理数据字典管理系统监控 客户管理模块 客户信息管理 客户基本信息客户分类管理客户标签管理客户关系图谱 联系人管理 联系人信息联系记录沟通历史重要日期提醒 …

Python中的跨域资源共享(CORS)处理

在Web开发中&#xff0c;跨域资源共享&#xff08;CORS&#xff09;是浏览器强制执行的安全机制&#xff0c;用于控制不同源&#xff08;协议域名端口&#xff09;之间的资源交互。下面我将通过Python示例详细讲解CORS的实现。 原生Python实现CORS Flask框架手动实现CORS fr…

Kruskal算法剖析与py/cpp/Java语言实现

Kruskal算法剖析与py/cpp/Java语言实现 一、Kruskal算法的基本概念1.1 最小生成树1.2 Kruskal算法核心思想 二、Kruskal算法的执行流程三、Kruskal算法的代码实现3.1 Python实现3.2 C实现3.3 Java实现 四、算法复杂度分析4.1 时间复杂度4.2 空间复杂度 五、Kruskal算法应用场景…

微信小程序返回上一页监听

本文实现的是微信小程序在返回上一页时获取通知并自定义业务。 最简单的实现&#xff1a; 使用 wx.enableAlertBeforeUnload() 优点&#xff1a;快速接入 缺点&#xff1a;手势不能识别、无法自定义弹窗内容&#xff08;仅询问&#xff09; 方法二&#xff1a; page-conta…