文章目录
前言
概念的引入
想象一下,当你有一个团队面临着一个很复杂问题,你是选择一个人硬刚呢?还是发挥团队的作用,将复杂的问题进行拆解成几个甚至几十个中等难度的问题分工解决呢?
相信聪明的你,一定会选择后者,那么解决拆分的问题的团队就是我们所说的函数啦!那么下面来一起了解它吧 !
大纲
首先来了解一下我们要了解的大概内容:
一.函数的定义
看完上面的部分,让我们来了解一下官方的定义吧!
计算机的函数,是一个固定的一个程序段,或称其为一个子程序,它在可以实现固定运算功能的同时,还带有一个入口和一个出口。
官方话语的解释:
1.子程序:一个大型程序中的某部分代码,由一个或多个语句块组成。它负责完成某项特定任务,而且相较于其他代码,具备相对的独立性。
2.入口:指的是函数所带的各个参数。它把函数的参数值代入子程序,供计算机处理;
3.出口:指的是指函数的函数值。它在计算机求得之后,由此口带回给调用它的程序。
二.函数的分类
1.库函数
概念引入
想象一下,我们的日常生活中是不是有很多重复且简单的事情在等着我们?比如:扫地,洗碗等。那如果我们把它交给机器人做,我们的生活岂不是轻松很多?
像这样解决对程序员来说简单重复的工作,就需要像库函数这样的机器人了!而让它帮你干活总得吱一声吧?这就是我们所说的引用头文件了——#include<头文件名>。
计算机术语:
一般是指编译器提供的可在c源程序中调用的函数。可分为两类,一类是c语言标准规定的库函数,一类是编译器特定的库函数。
概念解释:
1.C源程序:指未编译的按照一定的程序设计语言规范书写的文本文件。
推荐的网站:http://www.cplusplus.com/——可以查看库函数的相关信息
说明:网站是外国的(有时可能进不去),阅读需要一定的英文能力,不过别忘了还有翻译软件呢!
库函数的分类
注意:库函数知道怎么用就可以了,简单的可以了解一下源码,过于复杂的初学阶段先不必深究!
了解五步骤
第一步:函数名+介绍+百度专业术语
第二步: 头文件
第三步:所传参数及其类型
第四步:返回类型以及返回值
第五步:实现函数的代码(简单的函数可以了解一下)
说明:IO函数全名为input(输入)output(输出)函数
接下来我们来了解几个比较典型的函数。
例1:
1.介绍printf
标准的输出型函数:printf(程序名)——全名为print(打印)format(格式化)
图解:这是网站的英文介绍
翻译:格式化打印输出到标准输出流。
解释:这里的标准输出流是是流向标准输出设备(显示器)的数据。数据放在存储数据的地方,这个地方我们称之为缓存区。
2.头文件
代码介绍:
int printf( const char *format [, argument]... );
3.所传参数及其类型
参数:字符串(字符常量,转义字符,格式控制字符串)+输出表列(可以是常量也可是变量与格式控制字符串的数量相匹配)
类型:const char类型,const是不可改变的意思,输出列表里的类型可以任意。
4.返回类型及其返回值
类型:
返回值:
代码验证:
#include<stdio.h>
int main()
{
int ret = printf("hello\n");//这里只是举了一个例子来说明一下,可以用不同的值验证!
printf("%d", ret);
return 0;
}
结果:
这里结果与打印的字符个数相等,返回值为打印的字符的个数!
到这里我们已经大概了解了printf函数,至于第五步暂时不用了解。
例2
1.介绍标准输出型函数——scanf
英文介绍:Read formatted data from the standard input stream.
翻译:从标准输入流读取数据的函数
2.头文件
代码:
int scanf( const char *format [,argument]... );
3.所传参数及其类型
参数:
1.字符串(字符常量,转义字符,格式控制字符串)+输出表列(输入变量的地址)
注意:字符串的部分必须与输入的数据严格匹配!
2.:类型:const char类型,const是不可改变的意思,输出列表里的类型是输入变量的地址。
4.返回类型及返回值
返回类型:
返回值:
代码证明:
#include<stdio.h>
int main()
{
int n = 0;
//例一:输入5
//例二:不执行
int ret = scanf("%d", &n);//这里只是举了两个个例子来说明一下,可以用不同的值验证!
printf("%d", ret);
return 0;
}
结果:
例一:
这里结果与打印的字符个数相等,返回值为输入的字符的个数!
例二:
输人失败返回-1****
2.自定义函数
概念引入:当我们面临着库函数解决不了的复杂问题,这时候就该我们自己设置的函数来解决,这就是有程序员巨大发挥空间的自定义函数!
1.例一
假如不知道求字符串长度的函数,让你自己设置一个。
解析:
实现逻辑:
char arr 数组的内容是下图所示:
字符串长度为 6 ——从a开始到f结束,那么在f的后面为\0则可以作为字符串的结束标志。
这时我们就可用指针的方式实现:
将数组名——首元素地址 设为起点读到\0结束,没不为\0就加一,读到\0就跳出循环
这时的函数求的结果即为字符串长度。
代码实现:
#include<stdio.h>
int my_strlen(char* arr)//字符串类型为char因此要用char*的指针进行接收
{
int count = 0;//求字符串长度
while (*arr++ != '\0')//*是将arr的内容拿出来,++的优先级高于*。
{
count++;
}
return count;
}
int main()
{
char arr[10] = "abcdef";
printf("%d", my_strlen(arr));
return 0;
}
2.例二
写一个函数求两个不相等数的较大值
//基本思路:确定参数与返回值以及返回类型,运用if else或者三目操作符进行解决。
#include<stdio.h>
int get_max(int x ,int y)
{
return (x > y ? x : y);//判断两种情况时比if else简便
}
int main()
{
int a = 4;
int b = 3;
printf("%d", get_max(a, b));
return 0;
}
三.函数的使用
1.函数的参数
概念引出:
由于在函数的调用中,传入的参数与在调用函数时所用参数的空间不同,我们把它分成两类,实参和形参,就像你是班长,老师让你完成一项任务,你反手就给班干部去干,这是一个道理。
说明:函数也可无参数,如函数名+(void/啥也不写)就是没有参数。但是如果没有返回类型一定要写,否则会默认为int类型!
实参
概念:实际传入的参数,这里实际上跟内存有关,函数(下)会细说,这里先做了解。
代码举例:
#include<stdio.h>
int get_max(int x ,int y)
{
return (x > y ? x : y);
}
int main()
{
int a = 4;
int b = 3;
printf("%d", get_max(a, b));//这里传入的a,b就为实参
return 0;
}
我们来看一下这里传入的a,b的地址,这是我编译器上调试的结果,兄弟们,可能你们调用的时候会不一样。
形参
概念:函数调用的参数,这里实际上跟内存有关,函数(下)会细说,这里先做了解。
代码还用上面的吧!
#include<stdio.h>
int get_max(int x ,int y)//那么这里函数的参数就是形参。
{
return (x > y ? x : y);
}
int main()
{
int a = 4;
int b = 3;
printf("%d", get_max(a, b));//这里传入的a,b就为实参
return 0;
}
这里我们再来看一下x,y的地址
再跟上面的a,b的地址比较一下就知道什么是形参,什么是实参了。
结论:形参实际上是实参的一份临时拷贝,为啥是临时呢?因为在调用函数结束后,函数所占的空间会随着函数的结束而销毁,因此函数的生命周期就为从调用函数开始到函数执行结束时结束。
注意:形参和实参可以同名!为什么呢?因为函数的级别是相同的,就像两个互不相关的人,各自有着自己独立的生活,但也有可能干相同的事情(吃饭睡觉),因此函数之间是不存在变量重定义的问题!
2.函数的调用
传值调用
注意:这里的值是函数内部所用参数的类型是像整形(int)这样数据类型。
经典代码举例:
交换两个变量的值
#include<stdio.h>
void pretend_swap(int y, int x)
{
int tmp = 0;
tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 2;
int b = 3;
pretend_swap(a,b);
return 0;
}
思考:这里真的交换了吗?
解释:并没有,因为y,x与a,b所占空间根本就不一样,形参是实参的临时拷贝,想一下你把写好的文件复制一份后,对复制的那一份进行修改会影响原本的文件吗?答案很显然是不会。这里是一个道理。
传址调用
注意:这里的址是所存变量的地址是指针
经典代码举例:
交换两变量的值
#include<stdio.h>
void true_swap(int* y, int* x)
{
int tmp = 0;
tmp = *y;
*y = *x;
*x = tmp;
}
int main()
{
int a = 2;
int b = 3;
int* p = &a;
int* p1 = &b;
true_swap(p,p1);
return 0;
}
思考:这里交换了吗?这里的实现逻辑是什么?
解释:交换了,变量的值可以通过变量的地址找到,因此把地址拷贝一份传进去当做形参,通过对地址解引用找到变量的值进行修改,因此完成操作,但是请注意这里的p与y的地址和p1与x的地址并不一样,还是那句话形参是实参的临时拷贝!
3.函数的嵌套调用与链式访问
嵌套调用
概念引入:
每个人都是一个独立的个体,但又彼此依存彼此照顾,这让我们的世界充满了温暖,函数也是独立的个体,彼此调用就是互相帮忙,这让程序充满了神秘,因为函数的实现方式彼此又不知道。
代码举例:
#include<stdio.h>
void print()
{
printf("hehe\n");
}
int main()
{
print();
return 0;
}
场景:这里主函数想打印一个hehe,但是主函数不知道,但是print函数知道,就让print函数帮了个忙,打印了hehe,但是主函数并不知道print函数是咋弄的,这就是函数的神秘,这里的帮忙就是调用,在主函数里面调用别的函数就叫嵌套调用。
链式访问
概念引入:
就像流水线上的汽车一样,是流水线上每一个过程装在汽车上的零件,最终产生了汽车这个整体,那么我们在解决一个问题时,也会分成几个步骤去解,这样分个步骤连起来就是答案了。
代码举例
#include<stdio.h>
int main()
{
printf("%d\n", printf("abcde%d\n", printf("abcdef\n")));
//可拆解为以下几步
//int ret = printf("abcdef\n");
//int ret1= printf("abcde%d\n",ret);
//printf("%d",ret1);
return 0;
}
结果:
解释:从里往外看,最里面是字符串执行打印后返回值是字符串个数:7,再执行第二个printf打印字符串与返回值,此时的字符个数为7,返回值为7,最后以%d的形式进行打印。
4.函数的定义与声明
定义
重点:将函数的实现逻辑体现出来。
代码举例:
int add(int x, int y)
{
return(x + y);
}
#include<stdio.h>
int main()
{
int a = 3;
int b = 4;
printf("%d", add(a, b));
return 0;
}
解释:这里将add函数完整的呈现出来,就叫做函数的定义。
声明
重点:只说明函数的返回类型,以及参数。
代码举例:
int add(int x,int y);//这就是声明
#include<stdio.h>
int main()
{
int a = 3;
int b = 4;
printf("%d", add(a, b));
return 0;
}
int add(int x, int y)
{
return(x + y);
}
解释:这里没有实现的逻辑,这是告诉你函数名,返回类型,以及变量而没有实现逻辑,故为声明!
思考:如果将声明删除程序会正常运行吗?为什么?
解释:会的,在程序的扫描过程中,编译器从上往下扫描,最开始不知道有这么个函数,会报警告,但是往下扫描又有了,因此会正常运行,但会报出一条警告。
拓展:全局变量和static修饰的变量的声明与定义
#include<stdio.h>
int b;
//b = 0;
int main()
{
static int a;
//a = 0;
printf("%d %d",b,a);
return 0;
}
思考:加上注释掉的两行会影响打印结果吗?为什么?
解释:不会,全局变量与static修饰的变量放在静态区,默认为0,因此加不加都一样。
那这里加上注释的为声明(int b;)与定义(b = 0;),如果不加则int b和static int a为声明与定义。
5.递归函数
概念引出
看过盗梦空间吗?当你一层梦境,一层梦境进入时,你会陷入里面,如果不设置一些条件的话,你将永远醒不过来,当条件满足时,你会回到前面的一层梦境,最后醒来!像这样的,满足条件时调用自身,不满足返回某个值或执行某个逻辑的过程就称之为递归。
拓展:迭代:不以递归的形式实现,而是采用某种循环的方式来实现。
重点:
1.调用自己后并且返回上一个函数,总而言之——有来有回。
2.存在限制条件,当满足这个限制条件的时候,递归便不再继续。
3.每次递归调用之后越来越接近这个限制条件。
性质
延时性
解释:在调用结束后返回时,执行的一段代码逻辑。
代码举例:
打印一个数的每一位,用空格隔开。
#include<stdio.h>
void num(int x)
{
if (x > 9)
{
num(x / 10);
}
printf("%d ", x % 10);
}
int main()
{
int n = 0;
scanf("%d", &n);//输入123
num(n);
return 0;
}
执行逻辑图解:
返回才执行代码,而不是一调用就执行代码。
空间占有性
还沿用上面的代码,这是内存的使用情况图解:
假设如果输入的数很大,调用了很多次,会不会将栈区占满呢?答案是会的。并且会报出:stack overflow的错误信息,我们计算机一般称为栈溢出。
简洁性和易读性
递归实现往往实现的比较简洁,只用了几行的代码便完成了,复杂大量的计算,这是因为递归是从结果的角度进行思考的,而且这样比用迭代的方式更容易思考和阅读。
注意事项
当递归无法实现或者栈区不够使用时,我们往往可以使用循环进行代替计算。
经典例题:
1.汉诺塔
故事起源:
汉诺塔(Tower of Hanoi),又称河内塔,是一个源于印度古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。
说明:我们不用64个圆盘我们用四个圆盘和三个圆盘理解一下
规则:
1.每次只能移动柱子最顶端的一个圆盘;
2.每个柱子上,小圆盘永远要位于大圆盘之上;
建议:从第一个盘子开始到第四个盘子都试一遍,试着看能不能发现规律?(画图效果最好)
举例:
一个盘子:
直接从起始柱到目标柱
总共:一步
两个盘子:
第一步:拿一个盘子从起始柱移动到辅助柱
第二步:拿一个盘子从起始柱移动到目标柱
第三步:拿一个盘子从辅助柱移动到目标柱
总共:三步
三个盘子:
总共:7步
四个盘子就交给聪明的你了,这里我就不多说了。
总结规律:
1.走的步数:2n - 1
2.
偶数:第一步为拿一盘子到辅助柱。
奇数:第一步为拿一盘子到目标柱。
3.循环规律拆分成三步
第一步:将n-1个盘子移动到辅助柱
第二步:将剩下的一个盘子移动到目标柱
第三步:将n-1个盘子移动到目标柱
这三句话看起来简单,但是里面蕴含着递归的逻辑,拆分了细说。
这里柱子的关系有点绕口,要认真才能看懂。
n个盘子一次拆分:
第一步:将上面的n-1个盘子移动到辅助柱(这是n-1个盘子移动的“目标柱”)
第二步:将剩下的一个盘子移动到目标柱
第三步:将n-1个盘子移动到目标柱(这时n-1个盘子在辅助柱)
因此转化为:将n-1个盘子(在辅助柱)移动到 目标住
如何将n-1个盘子移动到目标柱呢? 两次拆分(第一次拆分第三步的逻辑):
剩下n-1盘子,从上一次的辅助柱位置开始(将辅助柱看为起始柱)
第一步:将n-2个盘子移动到辅助柱
说明:这个辅助柱是上一次拆分的起始柱,这一次的目标柱,剩下的那个柱子为上一次的目标柱,这一次的辅助柱
第二步:将剩下的一个盘子移动到最初的目标柱
第三步:将n-2个盘子移动到目标柱
如此以往到n-1次时剩下1个盘子
将最后剩下的盘子移动到目标柱时就完成任务了!
这样化繁为简,是不是就是递归的思想呢?
聪明的你明白了吗?
代码实现:
#include<stdio.h>
int hanoi_tower(int x, char a, char b, char c)//a为第一次执行时的起始柱,b为第一次执行的目标柱,c为第一次执行时的辅助柱
{
static int n = 0;
if (1 == x)
{
n++;
printf("第%d次:%c-->%c\n", n,a, b);
}
else
{
hanoi_tower(x - 1, a, c, b);//将x-1个盘子移动到辅助柱,此时辅助柱为你要移动的目标柱
n++;
printf("第%d次:%c-->%c\n", n, a, b);
hanoi_tower(x - 1, c, b, a);//将x-1个盘子从辅助柱移动到目标柱,从c到b
}
}
int main()
{
int plates = 0;//要移动盘子
scanf("%d", & plates);
hanoi_tower(plates,'A','B','C');//A为起始柱,B为目标柱,C为辅助柱。
return 0;
}
将图画出来再与逻辑进行比较可以很好地看清规律。
2.青蛙跳台阶
故事概括:
一只青蛙可以一次跳 1 级台阶或一次跳 2 级台阶,例如: 跳上第一级台阶只有一种跳法:直接跳 1 级即可. 跳上两级台阶,有两种跳法: 每次跳 1 级,跳两次; 或者一次跳 2 级.那么n阶台阶有多少种跳法?
我们可以试着列举一下:
方法:画树状图
当有一个台阶时,有一种跳法。
当有两种台阶时,
当有3个台阶时,
列举到这里,我们可以看出无论多大的数最后都会有一阶台阶和两阶台阶的选择,当n=3时,走法为2阶的跳法加1阶的跳法,那么当n大于等于3时
跳法就为n-1的跳法加上n-2的跳法总终拆分成1和2阶的因子。
我们可以列一个函数来更加具体的表明:
代码实现:
#include<stdio.h>
int fin(int x)
{
if (x == 1)
{
return 1;
}
else if (x == 2)
{
return 2;
}
else
{
return fin(x - 1) + fin(x - 2);
}
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = fin(n);
printf("%d", ret);
return 0;
}
结合数学函数,相信聪明的你能够充分地理解。
尾序
如果能认真看到这里,我坚信你能收获很多很多!也希望这篇文章能帮助到你,如果觉得不错,请点击一下不要钱的赞,如果有误请温柔的指出,在这里感谢大家了