《C语言深度解剖》(3):探索函数递归、传值、传址调用的奥秘

🤡博客主页:醉竺

🥰本文专栏:《C语言深度解剖》

😻欢迎关注:感谢大家的点赞评论+关注,祝您学有所成!


✨✨💜💛想要学习更多数据结构与算法点击专栏链接查看💛💜✨✨ 


1. 函数是什么?

数学中我们常见到函数的概念。但是你了解C语言中的函数吗?

维基百科中对函数的定义:子程序

  • 在计算机科学中,子程序(英语:Subroutine, procedure, function, routine, method, subprogram, callable unit),是一个大型程序中的某部分代码, 由一个或多个语句块组成。它负责完成某项特定任务,而且相较于其他代码,具备相对的独立性。
  • 一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软 件库。

2. C语言中函数的分类:

  1. 库函数
  2. 自定义函数 

2.1 库函数

为什么会有库函数?

  1. 我们知道在我们学习C语言编程的时候,总是在一个代码编写完成之后迫不及待的想知道结果,想 把这个结果打印到我们的屏幕上看看。这个时候我们会频繁的使用一个功能:将信息按照一定的格 式打印到屏幕上(printf)。
  2. 在编程的过程中我们会频繁的做一些字符串的拷贝工作(strcpy)。
  3. 在编程是我们也计算,总是会计算n的k次方这样的运算(pow)。 

像上面我们描述的基础功能,它们不是业务性的代码。我们在开发的过程中每个程序员都可能用的到, 为了支持可移植性和提高程序的效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员进行软件开发。 

那怎么学习库函数呢?

这里我们简单的看看:www.cplusplus.com

有非常非常多的库函数,简单的总结,C语言常用的库函数都有:

IO函数

  • 字符串操作函数
  • 字符操作函数
  • 内存操作函数
  • 时间/日期函数
  • 数学函数
  • 其他库函数  

注: 但是库函数必须知道的一个秘密就是:使用库函数,必须包含 #include 对应的头文件。

2.1.1 如何学会使用库函数? 

需要全部记住吗?No

需要学会查询工具的使用: 

MSDN(Microsoft Developer Network)

www.cplusplus.com

http://en.cppreference.com(英文版)

http://zh.cppreference.com(中文版) 

注意⚠️:这里暂时不学习库函数,后面会有重要的相关库函数的学习,库函数的学习并不是所有都要记住,最重要的是也不可能记完的,常用的能熟练运用就够了,其它的学会查询,忘了再看看就可以。 

2.2 自定义函数

如果库函数能干所有的事情,那还要程序员干什么?

所以更加重要的是自定义函数。

自定义函数和库函数一样,有函数名,返回值类型和函数参数。

但是不一样的是这些都是我们自己来设计。这给程序员一个很大的发挥空间。

函数的组成:

ret_type fun_name(para1, *)
{
	statement;//语句项
}

ret_type 返回类型
fun_name 函数名
para1    函数参

我们举一个例子:

  • 写一个函数可以找出两个整数中的最大值。 
#include <stdio.h>
//get_max函数的设计
int get_max(int x, int y)
{
	return (x > y) ? (x) : (y);
}
int main()
{
	int num1 = 10;
	int num2 = 20;
	int max = get_max(num1, num2);
	printf("max = %d\n", max);
	return 0;
}

再举个例子:

  • 写一个函数可以交换两个整形变量的内容。 
#include <stdio.h>
//实现成函数,但是不能完成任务
void Swap1(int x, int y)
{
	int tmp = 0;
	tmp = x;
	x = y;
	y = tmp;
}
//正确的版本
void Swap2(int* px, int* py)
{
	int tmp = 0;
	tmp = *px;
	*px = *py;
	*py = tmp;
}
int main()
{
	int num1 = 1;
	int num2 = 2;
	Swap1(num1, num2);
	printf("Swap1::num1 = %d num2 = %d\n", num1, num2);
	Swap2(&num1, &num2);
	printf("Swap2::num1 = %d num2 = %d\n", num1, num2);
	return 0;
}

当函数调用的时候,实参传递给形参,这时形参是实参的一份临时拷贝,对形参的修改不影响实参。 

当函数想要改变实参的内容时,这时候就需要把实参的地址传给形参 (传址调用),再通过解引用从而操作实际参数的内容。

3. 函数的参数

3.1 实际参数(实参)

  • 真实传给函数的参数,叫实参。
  • 实参可以是:常量、变量、表达式、函数等。
  • 无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。 

3.2 形式参数(形参)

形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内 存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。

上面 Swap1 和 Swap2 函数中的参数 x,y,px,py 都是形式参数。在main函数中传给 Swap1 的 num1 , num2 和传 给 Swap2 函数的 &num1 , &num2 是实际参数。 

这里我们对函数的实参和形参进行分析:

这里可以看到 Swap1 函数在调用的时候, x , y 拥有自己的空间,同时拥有了和实参一模一样的内容。 所以我们可以简单的认为:形参实例化之后其实相当于实参的一份临时拷贝。 

4.1 传值调用

  • 函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。 

4.2 传址调用

  • 传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
  • 这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操 作函数外部的变量。 

5. 函数的嵌套调用和链式访问

函数和函数之间可以根据实际的需求进行组合的,也就是互相调用的。

5.1 嵌套调用

#include <stdio.h>
void new_line()
{
    printf("hehe\n");
}
void three_line()
{
    int i = 0;
    for (i = 0; i < 3; i++)
    {
        new_line();
    }
}
int main()
{
    three_line();
    return 0;
}

函数可以嵌套调用,但是不能嵌套定义。 

5.2 链式访问

  • 把一个函数的返回值作为另外一个函数的参数。 
#include <stdio.h>
#include <string.h>
int main()
{
    char arr[20] = "hello";
    int ret = strlen(strcat(arr, "bit"));//这里介绍一下strlen函数
    printf("%d\n", ret);
    return 0;
}
#include <stdio.h>
int main()
{
    printf("%d", printf("%d", printf("%d", 43)));
    //结果是啥?
    //注:printf函数的返回值是打印在屏幕上字符的个数
    return 0;
}

注:printf函数在C语言中的返回值是int类型。它返回的值是成功“打印出的字符数”(不包括结尾的空字符) 

6. 函数的声明和定义 

6.1 函数声明 

  1. 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数 声明决定不了。
  2. 函数的声明一般出现在函数的使用之前。要满足先声明后使用。
  3. 函数的声明一般要放在头文件中的。 

函数的返回值类型不写的时候,默认返回的是 int 类型;当函数没有参数和不需要返回值时,最好写成:

void 函数名(void)
{
	函数体;
}

6.2 函数定义 

  • 函数的定义是指函数的具体实现,交待函数的功能实现。 

一般来说,一个完整函数的实现,包括函数的声明,和函数的定义,并且生命和定义分别在两个文件中。例如:

test.h的内容 放置函数的声明 

#ifndef __TEST_H__
#define __TEST_H__

//函数的声明
int Add(int x, int y);

#endif //__TEST_H__

test.c的内容:放置函数的实现 

#include "test.h"
//函数Add的实现
int Add(int x, int y)
{
 return x+y;
}

这种分文件的书写形式,后续写几个小游戏的文章中会有体现。 

7. 函数递归 

7.1 什么是递归? 

程序调用自身的编程技巧称为递归( recursion)。 递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接 调用自身的 一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解。递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。 

递归的主要思考方式在于:把大事化小 

7.2 递归的两个必要条件 

  • 存在限制条件,当满足这个限制条件的时候,递归便不再继续。
  • 每次递归调用之后越来越接近这个限制条件。 

7.2.1 练习1 

  • 接受一个整型值(无符号),按照顺序打印它的每一位。 例如: 输入:1234,输出 1 2 3 4. 

参考代码:

拓展: 此题为打印一个数的十进制形式的每一位。若题目要求打印其它进制形式的每一位数字,原理一样。

7.2.2 练习2

  • 编写函数不允许创建临时变量,求字符串的长度。 (递归模拟实现strlen函数)

 参考代码:(递归方法)

7.3 递归与迭代 

7.3.1 练习3 

  • 求n的阶乘。(不考虑溢出) 

参考代码: 

int factorial(int n)
{
	if (n <= 1)
		return 1;
	else
		return n * factorial(n - 1);
}

《函数栈帧》每一次函数调用都会为本次函数调用分配内存空间(在内存的栈区),为本次函数调用分配的内存空间被称为这次函数调用的栈帧空间。函数栈帧有创建和销毁。 

7.3.2 练习4 

  • 求第n个斐波那契数。(不考虑溢出)

参考代码:

int fib(int n)
{
    if (n <= 2)
        return 1;
    else
        return fib(n - 1) + fib(n - 2);
}

但是我们发现有问题; 

  • 在使用 fib 这个函数的时候如果我们要计算第50个斐波那契数字的时候特别耗费时间。
  • 使用 factorial 函数求10000的阶乘(不考虑结果的正确性),程序会崩溃。 

 为什么呢?

  • 我们发现 fib 函数在调用的过程中很多计算其实在一直重复。 如果我们把代码修改一下: 

最后我们输出看看count,是一个很大很大的值。 

那我们如何改进呢?

  • 在调试 factorial 函数的时候,如果你的参数比较大,那就会报错: stack overflow(栈溢出) 这样的信息。 系统分配给程序的栈空间是有限的,但是如果出现了死循环,或者(死递归),这样有可能导致一 直开辟栈空间,最终产生栈空间耗尽的情况,这样的现象我们称为栈溢出。 

那如何解决上述的问题: 

  1. 将递归改写成非递归。
  2. 使用static对象替代 nonstatic 局部对象。在递归函数设计中,可以使用 static 对象替代 nonstatic 局部对象(即栈对象),这不仅可以减少每次递归调用和返回时产生和释放 nonstatic 对象的开销,而且 static 对象还可以保 存递归调用的中间状态,并且可为 各个调用层所访问。 

 比如,下面代码就采用了,非递归的方式来实现:

提示:

  1. 许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。
  2. 但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些。
  3. 当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销。 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Linux锁的使用

一、临界资源与临界区 多线程会共享例如全局变量等资源&#xff0c;我们把会被多个执行流访问的资源称为临界资源&#xff0c;我们是通过代码访问临界资源的&#xff0c;而我们访问临界资源的那部分代码称为临界区。 实现一个抢票系统 只有一个线程抢票时 #include <ios…

【二分查找】Leetcode 寻找峰值

题目解析 162. 寻找峰值 题目中有一个很重要的提示&#xff1a;对所有有效的i都存在nums[i] ! nums[i1],因此这道题不需要考虑nums[mid] 和 nums[mid1]之间的相等与否的关系 算法讲解 1. 暴力枚举 我们按照顺序判断每个数字是否是当前的峰值&#xff0c;如果是直接返回&#…

【WEEK7】 【DAY1】索引【中文版】

2024.4.8 Monday 目录 7.索引7.1.内涵7.2.索引的作用7.3.索引的分类7.3.1.主键索引&#xff08;PRIMARY KEY&#xff09;7.3.1.1.主键 : 某一个属性组能唯一标识一条记录7.3.1.2.特点 : 7.3.2.唯一索引&#xff08;UNIQUE KEY&#xff09;7.3.2.1.作用 : 避免同一个表中某数据…

第一部分 Vue讲解(代码版)

1.第一个vue实例 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport" content"widthdevice-w…

获得乙级风力发电设计资质需要哪些注册工程师资格?

要获得乙级风力发电设计资质&#xff0c;通常需要以下注册工程师资格&#xff1a; 注册电气&#xff08;发输电&#xff09;工程师&#xff1a;他们负责风力发电项目的电气系统设计、优化、设备选型和配置&#xff0c;确保电力供应的安全性和可靠性。他们的专业知识对于保证风…

SpringBoot的旅游管理系统+论文+ppt+免费远程调试

项目介绍: 基于SpringBoot旅游网站 旅游管理系统 本旅游管理系统采用的数据库是Mysql&#xff0c;使用SpringBoot框架开发。在设计过程中&#xff0c;充分保证了系统代码的良好可读性、实用性、易扩展性、通用性、便于后期维护、操作方便以及页面简洁等特点。 &#xff08;1&…

吴恩达深度学习笔记:深层神经网络(Deep Neural Networks)4.5-4.8

目录 第一门课&#xff1a;神经网络和深度学习 (Neural Networks and Deep Learning)第四周&#xff1a;深层神经网络(Deep Neural Networks)4.5 为什么使用深层表示&#xff1f;&#xff08;Why deep representations?&#xff09; 第一门课&#xff1a;神经网络和深度学习 (…

5.网络编程-socker(golang版)

目录 一、什么是socket&#xff1f; 二、Golang中使用TCP TCP服务端 TCP客户端​​​​​​​ 三、TCP黏包&#xff0c;拆包 1.什么是粘包&#xff0c;拆包&#xff1f; 2.为什么UDP没有粘包&#xff0c;拆包&#xff1f; 3.粘包拆包发生场景 4.TCP黏包 黏包服务端 …

官宣定档“2024上海国际半导体产业展会”定于11月份在沪召开

2024上海国际半导体产业展览会 2024 Shanghai Semiconductor Expo 时间:2024年11月18-20日 地点:上海新国际博览中心 前言 近年来&#xff0c;上海半导体产业呈现出快速发展的态势。一方面&#xff0c;得益于国家政策的大力支持&#xff0c;上海半导体产业得到了迅猛的发展。…

计算机组成结构2

概念 存储系统 解决成本-速度-容量之前的矛盾问题 寄存器–cache–内存–硬盘–外存储 局部性原理 时间局部&#xff1a;相邻的时间访问同一个数据空间局部&#xff1a;相邻的空间地址会被连续访问 cache cpu与主存之间&#xff0c;命中cache后就不需要访问主存&#xff0c;…

景联文科技:为AI大模型提供高质海量训练数据

在全球AI浪潮的推动下&#xff0c;大量训练数据已成为AI算法模型发展和演进中的关键一环。 艾瑞咨询数据显示&#xff0c;包括数据采集、数据处理&#xff08;标注&#xff09;、数据存储、数据挖掘等模块在内的AI基础数据服务市场&#xff0c;将在未来数年内持续增长。 预计到…

跟着GPT学设计模式之适配器模式

题图来自APOD 你好&#xff0c;这里是codetrend专栏“跟着GPT学设计模式”。 说明 适配器模式&#xff08;Adapter Pattern&#xff09;是一种结构型设计模式&#xff0c;用于将一个类的接口转换为客户端所期望的另一个接口。适配器模式允许不兼容的接口协同工作&#xff0c…

面向跳转编程JOP问题及挑战

BTI分支目标识别精讲与实践系列 思考 1、什么是代码重用攻击?什么是ROP攻击?区别与联系? 2、什么是JOP攻击?间接分支跳转指令? 3、JOP攻击的缓解技术?控制流完整性保护? 4、BTI下的JOP如何缓解?什么是目标着陆台? 5、BTI的架构细节?硬件原理?间接分支类型?指…

数据库(mysql)-基本查询语句(DQL)

查询语句 这边查询是给予一定表格,这边先做个解释 教师表包括(name(姓名),gender(性别),salary(工资),title(职位),subject_id(课程的编号),comm(奖金)) 学生表包括(姓名(name),gender(性别),job(职位),生日(birth)) 模版 SELECT 字段名 FROM 查询表 WHERE 查询语句 或与非…

Laravel 项目如何运行

如有一个 Laravel 项目&#xff0c;在配置好 PHP 版本和运行环境后&#xff0c;可以直接在项目下直接运行&#xff1a; php artisan serve 来启动你的项目。 通过浏览器查看 当项目运行后&#xff0c;默认的启动端口为 8000&#xff0c;可以通过浏览器来进行查看运行的 Larav…

c++的学习之路:17、stack、queue与priority_queue

摘要 本文主要是介绍一下stack、queue、priority_queue的使用以及模拟实现&#xff0c;文章末附上代码以及思维导图。 目录 摘要 一、stack的介绍和使用 1、stack的介绍 2、stack的使用 3、stack的模拟实现 二、queue的介绍和使用 1、queue的介绍 2、queue的使用 3、…

leetcode刷题日记之接雨水问题

题目描述 解题思路 这个题目相当于一个桶的容量是多少&#xff0c;这取决于最短的模板的高度&#xff0c;&#xff0c;对于位置来讲&#xff0c;第i个位置所能承载的最大的容量为左右两侧最低的高度减去该位置的高度&#xff0c;如果两侧的最低位置小于height【i】&#xff0c…

TSINGSEE青犀边缘计算AI智能分析网关V4客流统计算法的配置步骤及使用

TSINGSEE青犀AI智能分析网关V4内置了近40种AI算法模型&#xff0c;支持对接入的视频图像进行人、车、物、行为、烟火等实时检测分析&#xff0c;上报识别结果&#xff0c;并能进行语音告警播放。硬件支持RTSP、GB28181协议、以及厂家私有协议接入&#xff0c;可兼容市面上常见的…

Python学习从0到1 day21 第二阶段 面向对象 ④ 类型注解

仗剑红尘已是癫&#xff0c;有酒平步上青天 —— 24.4.7 一、变量的类型注解 学习目标 1.理解为什么使用类型注解 2.掌握变量的类型注解语法 为什么使用类型注解 tip&#xff1a;CTRLP&#xff0c;可以提示函数中传入的参数 当我们需要使用pycharm的自动补全功能&#xff0c;又…

Java | Leetcode Java题解之第18题四数之和

题目&#xff1a; 题解&#xff1a; class Solution {public List<List<Integer>> fourSum(int[] nums, int target) {List<List<Integer>> quadruplets new ArrayList<List<Integer>>();if (nums null || nums.length < 4) {return…