做网站的公司有多少家,公司宣传页设计印刷,企业网站管理的含义,网站信息系统#x1f3af;每日努力一点点#xff0c;技术进步看得见 #x1f3e0;专栏介绍#xff1a;【C语言步行梯】专栏用于介绍C语言相关内容#xff0c;每篇文章将通过图片代码片段网络相关题目的方式编写#xff0c;欢迎订阅~~ 文章目录 什么是函数库函数自定义函数函数执行示例… 每日努力一点点技术进步看得见 专栏介绍【C语言步行梯】专栏用于介绍C语言相关内容每篇文章将通过图片代码片段网络相关题目的方式编写欢迎订阅~~ 文章目录 什么是函数库函数自定义函数函数执行示例函数的参数函数的调用函数的嵌套调用和链式访问函数的定义和声明 函数递归 什么是函数
数学中我们常见到函数的概念。但是你了解C语言中的函数吗C语言中的函数可不是数学上的表达式咱一起来看看它究竟是什么(︶)↗[GO!]
我们知道原材料进入工厂后之后就会有产品产出。函数就像是工厂一样我们将没有处理过的数据放进去它就会返回给我需要的结果。
在维基百科中将函数解释为子程序。因为函数具有独立处理数据或实现一部分功能的能力。下面我们来看一下比较官方的解释
在计算机科学中子程序是一个大型程序中的某部分代码 由一个或多个语句块组成。它负责完成某项特定任务而且相较于其他代码具备相对的独立性。一般会有输入参数并有返回值提供对过程的封装和细节的隐藏。这些代码通常被集成为软件库。
库函数
在C语言中函数分为自定义函数和库函数。自定义函数就是程序员需要自行实现的函数而库函数是C语言库中自带的我们可以在包含对应头文件的情况下使用。
既然程序员可以自行编写函数那为什么会有库函数 在我们刚开始学习C语言时迫不及待地在屏幕上打印Hello World。这时我们调用了printf函数并且是在包含了stdio头文件的前提下。其实printf已经在stdio头文件编写好了我们需要时可以随时调用。
我们先来介绍一些常用的库函数吧。首先看看pow函数它用于计算n的k次方第一个传入的参数是底数第二个传入的参数是指数。例如计算 2 8 2^8 28就可以用pow(2,8)。
#include stdio.h
#include math.h//调用pow函数需要包含math头文件int main()
{printf(%d\n, pow(2, 8));return 0;
}再来介绍一个求字符串长度的函数strlen只要将字符串传入它就会将字符串长度返回。
#include stdio.h
#include string.h//调用strlen函数需要包含string头文件int main()
{printf(%d\n, strlen(Jammingpro));
}最后我们介绍一下字符串拷贝函数strcpy。如果我们要将str1中的字符拷贝到str2中则可以使用strcpy(str2, str1)。
#include stdio.h
#include string.hint main()
{char str1[] Jammingpro;char str2[] xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx;strcpy(str2, str1);printf(%s\n, str2);return 0;
}★ps字符串在存储时最后一个位置会以\0结束表示字符串结束。strcpy在拷贝时会将被拷贝字符串的\0字符一并拷贝下来。
现在我们就能知道为什么要有库函数了。像上面我们描述的基础功能它们不是业务性的代码。我们在开发的过程中每个程序员都可能用的到为了支持可移植性和提高程序的效率所以C语言的基础库中提供了一系列类似的库函数方便程序员进行软件开发。
当然C语言中有那么多的库函数我们不可能记得牢固在需要的时候我们可以上官方文档查询→C语言库函数查询通道
自定义函数
如果库函数能干所有的事情那还要程序员干什么所有更加重要的是自定义函数。
自定义函数和库函数一样有函数名返回值类型和函数参数。 但是不一样的是这些都是我们自己来设计。这给程序员一个很大的发挥空间。下面我们看一看函数的组成↓↓↓
ret_type func_name(para1,...)
{statement;//语句项
}
//ret_type -- 返回值类型
//func_name -- 函数名称
//para1 -- 函数参数 函数执行示例
我们先看一个例子吧下面代码的get_max用于获取两个数中的较大值↓↓↓
#include stdio.hint get_max(int num1, int num2)
{return (num1 num2 ? num1 : num2);
}int main()
{int m 0;int n 0;scanf(%d %d, m, n);printf(较大值是%d\n, get_max(m, n));
}看完这段代码可能还是不大理解它的执行流程。下面我通过图片文字解释的方式模拟程序执行流程。
程序从main函数依次向下执行如图所示执行1、2、3号语句。当执行到printf函数所在行时因为调用了get_max函数此时程序跳转到get_max代码所在位置执行执行完毕后将计算结果返回给调用位置。此时printf再打印出最大值。
函数的参数
在上面的代码中我们看到printf中的get_max()中有两个参数分别是m和n。而main函数上方的get_max函数中也有两个参数即num1和num2。下面我们来聊聊这两个地方的参数有什么不同再聊聊程序调用函数时内存的变化。
★ps程序在执行过程中会将函数信息、变量等保存在栈中。栈保存数据从高地址向低地址处保存。栈有后进先出的特点后放进去的数据要先取出来才能取其他数据。
①函数执行到main函数时会将main函数信息入栈入栈就是保存到栈空间的意思如下图所示。 ②执行m和n的定义语句后会先后在栈中开辟一个空间用于保存m和n的数值。 ③执行scanf语句读取用户输入保存到m和n所在地址。这里假设输入的数值是8和5。
★ps是取地址运算符 ④执行printf时遇到了get_max函数此时调用get_max函数则需要将该函数的信息先入栈。将m的值传递给num1时此时num1入栈并初始化为8将n的值传递给num2时此时num2入栈并初始化为5。 ⑤在get_max函数执行完毕后num2、num1、get_max依次出栈出栈就是从栈空间删除的意思。在整个程序执行完毕n、m、main函数依次出栈。
这里的m和n是实参它们是在函数调用前已经存在的变量。而num1、num2称为形参它们是在函数调用时才开辟空间的变量。下面看一下实参和形参的具体定义
实际参数实参 真实传给函数的参数叫实参。实参可以是常量、变量、表达式、函数等。无论实参是何种类 型的量在进行函数调用时它们都必须有确定的值以便把这些值传送给形参。
形式参数形参 形式参数是指函数名后括号中的变量因为形式参数只有在函数被调用的过程中才实例化分配 内存单元所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在 函数中有效。
现在我有个想法我想实现一个交换两个数数值的函数你来判断一下下面的代码能否实现交换两个数值的效果。
#include stdio.h
void Swap(int num1, int num2)
{int tmp num1;num1 num2;num2 tmp;
}
int main()
{int m 5;int n 6;printf(交换前m %d, n %d\n,m, n);Swap(m, n);printf(交换后m %d, n %d\n, m, n);return 0;
}这段代码无法实现m和n的数值交换。因为m和n传递给num1和num2时num1和num2开辟了新的空间num1和num2的交换并不会影响m和n。两者存放的位置都不一样不会互相影响的。
如果我们真的想实现两个数的数值交换则调用函数时函数中交换的必须是主函数中的m和n而不能再另外开辟空间。咱们来看一下新的代码↓↓↓
#include stdio.h
void Swap(int* pm, int* pn)
{int tmp *pm;*pm *pn;*pn tmp;
}
int main()
{int m 5;int n 6;printf(交换前m %d, n %d\n,m, n);Swap(m, n);printf(交换后m %d, n %d\n, m, n);return 0;
}这段代码在调用函数时将m和n的地址传递给get_max函数get_max函数中使用指针进行接收。在m和n存储位置上的交换操作就能真正交换m和n的数值了。
函数的调用
传值调用 函数的形参和实参分别占有不同内存块对形参的修改不会影响实参。 传址调用 传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。 这种传参方式可以让函数和函数外边的变量建立起正真的联系也就是函数内部可以直接操作函数外部的变量。
学习完上面的内容先让我们应用一下吧。 test1写一个函数判断是不是闰年
#include stdio.hint is_leap_year(int year)
{return ((year % 4 0 year % 100 ! 0) || (year % 400 0));
}int main()
{int year 0;scanf(%d, year);if(is_leap_year(year))printf(是闰年\n);elseprintf(不是闰年\n);return 0;
}★ps闰年满足如下条件中的一个①能被4整除且不能被100整除②能被400整除。
test2写一个函数每调用一次这个函数num的数值就会1
#include stdio.hint add_1(int num)
{return num 1;
}int main()
{int num 0;num add_1(num);printf(%d\n, num);return 0;
}函数的嵌套调用和链式访问
嵌套调用 嵌套调用是指一个函数中调用了另一个函数。我们来看一个例子你就会知道了↓↓↓
#include stdio.hvoid sayHi()
{printf(Hi!\n);
}void introduc()
{printf(starting....\n);sayHi();
}int main()
{introduc();return 0;
}上面代码中introduc函数中被调用后它又调用了sayHi函数。像这种在一个函数中调用另一个函数的情况就称为函数的嵌套调用。
链式调用 关于链式调用我还是需要一个例子来解释。↓↓↓ ★psprintf函数的返回值是它在屏幕上打印的字符个数
#include stdio.h
int main()
{printf(%d , printf(%d , printf(%d , 43)));return 0;
}像上面的代码中调用printf(%d “,43)的返回值作为下一个printf函数的参数的情况称为链式调用。这里用图片解释一下这个代码的执行过程↓↓↓ 先执行printf(”%d ,43)在屏幕上打印43 最后有一个空格一共3个字符所以它的返回值为3。在执行绿色框的printf时它打印3 一共2个字符所以它的返回值为2。在执行最外围的printf时它打印2 “。所以最后打印出43 3 2”。
函数的定义和声明
首先让我们谈一谈函数的定义函数的定义就是给出函数的名称、参数、返回值并且给出函数体给出函数的具体实现。下面给出函数定义的具体描述
函数定义 函数的定义是指函数的具体实现交待函数的功能实现。
例如下面的Add函数就是函数定义因为它给出了Add函数具体是怎么实现的。
int Add(int a, int b)
{return a b;
}上面给出的各个例子中我都会将自定义函数放在main函数之前。因为编译器在处理代码时是从上到下的。假如我们将自定义函数放在main函数的后面若此时main函数中调用了自定义函数编译器此时并不认识因为它还没读到此时编译器将会报错。下图中蓝色部分是编译器已经读到的部分因为get_max函数编译器还没读到当读到调用get_max函数时就会报错。 为了避免编译器报错我们可以在main函数前给出函数的声明即使函数定义在main函数后也不用担心了。
#include stdio.hint get_max(int num1, int num2);//我就是函数的声明
//int get_max(int, int); --写成这样也可以参数名可以省略int main()
{int m 0;int n 0;scanf(%d %d, m, n);printf(较大值是%d\n, get_max(m, n));
}int get_max(int num1, int num2)
{return (num1 num2 ? num1 : num2);
}下面我们来看一下关于函数声明的定义和注意事项
函数声明
告诉编译器有一个函数叫什么参数是什么返回类型是什么。但是具体是不是存在无关 紧要。函数的声明一般出现在函数的使用之前。要满足先声明后使用。函数的声明一般要放在头文件中的。
关于上面的第3条我来解释解释吧。在实际开发过程中一个项目需要很多程序员开发大家不可能同时在一个源文件中编写代码。此时我们可以先在后缀为.h的头文件中给出各个函数的声明在函数的定义的文件中包含这个头文件在调用这些函数的主函数所在文件也包含这个头文件就可以了。
★ps包含C语言库的头文件使用#include头文件名.h调用自定义的头文件使用#include “头文件名.h”
下面就是一个分文件编写的示例。max.h中保存的是函数的声明mac.c保存的是函数的定义。在编译器读取前main.c时#include max.h语句会将max.h中的函数声明文本替换到main.c中当执行到get_max时编译器会自动跳转到max.c中指定对应的函数。 函数递归
什么是递归呢先来看一下递归的定义↓↓↓
程序调用自身的编程技巧称为递归。 递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算大大地减少了程序的代码量。 递归的主要思考方式在于把大事化小
递归的两个必要条件
存在限制条件当满足这个限制条件的时候递归便不再继续。每次递归调用之后越来越接近这个限制条件。
上面我们介绍了函数调用会将函数的相关信息保存到栈中如果无限递归则栈会被装满此时就会报错。
上面这堆关于递归的解释还是让人头大下面给些例子图形解释来理解一下函数递归吧。
下面代码使用函数递归求解n!n的阶乘。
#include stdio.hint cal(int n)
{if(n 1) return 1;else return cal(n - 1) * n;
}int main()
{int n 0;scanf(%d, n);printf(%d! %d,n ,cal(n));return 0;
}以n4为例我们来看一下函数递归的执行过程。main函数指定到printf时调用了cal(4)。下面给出cal(4)的执行步骤
①cal(4)被调用 ②4 1不满足执行else分支即return cal(n-1)*n -- cal(3) * 4 ③cal(3)被调用3 1不满足执行else分支即return cal(n-1)*n -- cal(2)*3 ④cal(2)被调用2 1不满足执行else分支即return cal(n-1)*n -- cal(1)*2 ⑤cal(1)被调用1 1满足返回1 ⑥cal(2)的return语句返回cal(1)2即返回12 ⑦cal(3)的return语句返回cal(2)3即返回23 ⑧cal(4)的return语句返回cal(3)4即返回64给调用处 下面再出一个递归示例接收一个整型数打印它的每一位。例如123打印1 2 3。
#include stdio.h
void printNum(int n)
{if(n 9)printNum(n / 10);printf(%d , n % 10);
}int main()
{int n 0;scanf(%d, n);printNum(n);return 0;
}以123为例借助图解释这段代码的递归过程。
①调用printNum(123)。printNum(123)执行if语句判断条件成立调用printNum(123 / 10)即调用printNum(12) ②printNum(12)执行if语句判断条件成立调用printNum(12 / 10)即调用printNum(1) ③printNum(1)执行if语句判断条件不成立打印1后返回 ④返回printNum(12)向下打印2后返回 ⑤返回printNum(123)向下打印3后返回函数调用处 经过上面的介绍对函数递归有了大致的了解了在介绍函数调用时我们讲到了函数调用时需要保存函数的信息也称为建立栈帧。如果递归的层数过多则时空效率过低。
下面我们通过一个例子来讲解递归存在的问题同时比较一下迭代相比递归的优势。下面是一个以递归形式实现的求解斐波那契数列的代码↓↓↓
#include stdio.hlong long fib(n)
{if(n 2) return 1;else return fib(n - 1) fib(n - 2);
}int main()
{int n 0;scanf(%d, n);printf(第n项为%d\n, fib(n));
}以fib(50)为例它的执行过程如下所示。我们可以发现fib(46)等被多次计算这导致了大量的时间和空间的浪费。 我们可以通过执行下面代码得出在计算fib(50)时重复计算fib(3)多少次这将会是一个很大的数字。
#include stdio.hint count 0;long long fib(n)
{if(n 3) count;if(n 2) return 1;else return fib(n - 1) fib(n - 2);
}int main()
{int n 0;scanf(%d, n);fib(50);printf(%d\n, count);return 0;
}在调试 fib 函数的时候如果你的参数比较大那就会报错 stack overflow栈溢出 这样的信息。 系统分配给程序的栈空间是有限的但是如果出现了死循环或者死递归这样有可能导致一直开辟栈空间最终产生栈空间耗尽的情况这样的现象我们称为栈溢出。
该如何解决上面的问题呢
将递归改写成非递归。使用static对象替代nonstatic局部对象。在递归函数设计中可以使用static对象替代nonstatic局部对象即栈对象这不仅可以减少每次递归调用和返回时产生和释放nonstatic对象的开销而且static对象还可以保存递归调用的中间状态并且可为各个调用层所访问。
咱们再来看看迭代法非递归实现斐波那契数列↓↓↓
#include stdio.hint fib(int n)
{int a 1;int b 1;int c 1;for(int i 2; i n; i){c a b;a b;b c;}return c;
}int main()
{int n 0;scanf(%d, n);printf(%d\n, fib(n));return 0;
}相比于递归的重复计算迭代法并不会出现。将两份代码放Visual Studio执行fib(50)会发现迭代法许久都出不来结果。
★ps
许多问题是以递归的形式进行解释的这只是因为它比非递归的形式更为清晰。但是这些问题的迭代实现往往比递归实现效率更高虽然代码的可读性稍微差些。当一个问题相当复杂难以用迭代实现时此时递归实现的简洁性便可以补偿它所带来的运行时开销。
下面我们用牛刀小试一下
小明每次可以向上爬一阶或者两阶楼梯现输入台阶的阶数请输入爬到楼顶的方法数。例如3阶台阶的方法数为34阶台阶的方法数为5。
#include stdio.hint climb(int n)
{if(n 1) return 1;else return climb(n - 1) climb(n - 2);
}int main()
{int stair 0;scanf(%d, stair);printf(%d\n, climb(stair));return 0;
}爬到第n阶台阶可以是从n-1号台阶爬1阶到达也可以是从n-2号台阶爬2阶到达。由此可以到climb(n) climb(n - 1) climb(n - 2)。这道题是一个递归可实现的问题也可以用动态规划的方法迭代的方式实现哦 这篇文章结束了~~ 如果文章中出现了错误欢迎私信或留言。(๑•̀ㅂ•́)و✧ 有任何疑问请评论或私信哦~~o(▽)ブ