目录
划重点:
1.函数的递归 2.传值调用与传址调用
什么是函数?
维基百科中这样定义:函数是指一段可以直接被另一段程序或代码引用的程序或代码。也叫做子程序、(OOP中)方法。
我们通俗一点理解,函数像一个加工厂,通过一系列的操作,生成一个“产品”。
函数有两类:
库函数:
为什么会有库函数呢?刚开始,有很多常用的函数经常会被用到,像printf,scanf等等。不同的编译器公司设计的函数名,函数类型,函数参数可能都是不同的。别人看代码的时候十分不便,最后呢,统一规定,将那些常用的函数的函数名,函数类型,函数参数设置好,具体的实现方法就交给那些编译器公司。每个编译器公司实现函数的方法可能不一样,但是函数名,函数参数,函数返回类型,和功能都一样的,使用的效果都是一样的,大大提高了程序的效率,支持可移植性。
C语言常用的库函数都有:
IO函数字符串操作函数字符操作函数内存操作函数时间/日期函数数学函数其他库函数需要全部记住吗? No需要学会查询工具的使用:
自定义函数:
自定义函数是编程的灵魂。
我们先了解函数的构造,才能更好的使用。这就是函数的定义:
ret_type fun_name( type para1, .... )
{
//语句;
}
ret_type:函数的返回类型
fun_name:函数名
type para1 :函数的参数,可以有多个参数。在我们写的时候需要注意参数的类型(type)也是要写的我们形参的类型,根据实参来写,实参传过来什么类型,我们函数就设置成什么类型。
例如:传过来的a,b都是 int类型,形参设计的时候也是int。
注意事项:
返回值必须写全,不然会出问题。
例如:
#include<stdio.h>
float max3(float a, float b, float c)
{
if (a > b && a > c)
return a;
if (b > c && b > a)
return b;
if (c > b && c > a)
return c;
}
int main()
{
float a = 0;
float b = 0;
float c = 0;
scanf("%f %f %f", &a, &b, &c);
float m = 0.0;
float a1 = max3(a + b, b, c);
float a2 = max3(a, b + c, c);
float a3 = max3(a, b, b + c);
m = a1 / (a2 + a3);
printf("%.2f", m);
return 0;
}
在牛客网上编译的时候,你可能连你错在哪都不知道。这里我们返回值缺少相等的情况,需要加上。
VS2019
出现这个问题的时候,就是你函数中,你的返回值没有写全。如果,a,b,c相等的时候,函数就不会返回了我们需要注意
修改后:
函数参数
真实传给函数的参数,叫实参。实参可以是:常量、变量、表达式、函数等。无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。
在我们用调用函数时,()里的就是函数的实参,在我们写的自定义函数中()里的就是形参
形式参数只有在函数被调用的过程中才实例化(分配内存单元), 形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有 效。
通过图片我们可以发现,实参和形参地址是不一样的。在内存中的存储是相互独立的。
我们可以这样去理解,形参是实参的一份临时拷贝。在我们使用完Add函数的时候,我们的形参的使命就完成了,它就会销毁,将内存释放。
函数调用
传值调用
顾名思义,在调用的时候,实参是一个数值。什么时候需要用到传值调用呢?我们不对实参进行改变的时候呢,就用我们的传值调用。
函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。
传址调用
顾名思义,在调用的时候,实参是一个地址。
传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量
我们来看例子---交换两个数
#include<stdio.h>
void Swap(int x, int y)
{
int tmp = 0;
tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 2;
int b = 3;
printf("交换前a=%d,b=%d\n", a, b);
Swap(a, b);
printf("交换后a=%d,b=%d\n", a,b);
return 0;
}
运行结果:
a,b的值并没有发生改变,我们调式一下观察。
交换之前:
交换之后:
上文就有讲到,形参和实参的地址是不一样的,我们在使用Swap()函数同样的道理,地址的不同,以至于交换数据的时候,改动的是我们的形参,实参是没有变化的。
这里就要用到我们的传址调用:
#include<stdio.h>
void Swap(int* x, int* y)
{
int tmp = 0;
tmp = *x;
*x = *y;
*y = tmp;
}
int main()
{
int a = 2;
int b = 3;
printf("交换前a=%d,b=%d\n", a, b);
Swap(&a, &b);
printf("交换后a=%d,b=%d\n", a,b);
return 0;
}
我们能发现,指针x,y管理的就是我们a,b的地址。在我们解引用(*)的时候,就可以顺着这个地址,获得到a,b的值,并对它们进行操作(*x--->a,*y--->b)
我们在Swap()函数中,对x,y解引用后,对*x,*y进行交换,在监视中我们可以发现,主函数里的a,b的值也发生了交换。
原因就是,x,y是指针,分别管理,a,b的地址,在解引用后,“它们可以顺藤(地址)摸瓜(改动数值)”
在传址调用的时候,实参为地址,形参需要用指针,我们对指针解引用后,就可以把它当作所对应的实参。例如:int* x (形参) 对应的是 &a(实参), *x(对指针x进行解引用)对应的是 a(的值)。指针解引进行交换数据相当于实参在进行交换数据。
函数的嵌套调用和链式访问
嵌套调用:
嵌套调用是指:调用一个函数后,在这个函数中又调用一个函数。
void print()
{
printf("hehe\n");
}
void test_t()
{
print();
}
int main()
{
test_t();
return 0;
}
主函数中调用 test_t()函数, test_t()函数中调用print()函数 , print()函数中调用printf()函数。
这就一个嵌套调用。
链式访问:
就是将一个函数的返回值,作为函数的参数。
经典题型:打印printf("Hellow World!")的返回值。
int main()
{
printf("\n%d", printf("Hello World!"));
return 0;
}
我们看下printf()的定义
返回值为整形。
返回值是打印的字符个数。
Hello World!-->共有12个字符。第一次打印的时候返回值为12,第二次打印就把这个返回值(12)打印出来了。
这就链式访问。
这个一个记录c/c++函数的一个网站。想了解函数的返回类型,参数,使用方法等等都可以去这里搜搜。
函数定义和函数声明
函数声明
函数声明就是告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数声明决定不了。
函数一般是先要声明在定义。
1.在我们写函数的时候,自定义函数写在了主函数的上面,声明可以省略。如果写在主函数的下面,声明省缺了,就会出问题。
函数声明,就是函数定义去掉{},在加上 ;
修改以后:
在主函数的上面:
2.在写一些函数比较多的项目的时候,我们一般都会将代码分装。例如:
这里是我写的三子棋的一个博客,我们写的时候,会用到很多函数去实现这个游戏。为了增加可读性,看起来更加的舒服,一般我们都是会将实现三子棋游戏的这些函数,放到另一个game.c的文件(自行添加),从我们源文件中隔离,函数声明,是写在头文件中。只要我们在game.c ,源文件中 调用头文件这些函数就可以使用了。非常的方便,看起来也美观。
总结一下第2点:我们函数声明一般写在头文件中,在写一些代码比较多的项目的时候,需要将我们的函数打包分类一下,另外创一个文件,去放这些函数。更详细的可以点那个链接看看。
函数定义
在定义的时候要写清楚,函数的返回类型,函数名,函数的参数,以及交代函数的实现。
函数递归
函数递归,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解。
函数的递归思想主要是: 大事化小。
递归的两个必要条件
1.存在限制条件,当满足这个限制条件的时候,递归便不再继续2.每次递归调用之后越来越接近这个限制条件
函数递归我把它分为两类(我暂且了解的)
一类:存在一定的规律,可以写出出函数公式的, 一般是可以用递归的另一类:是没有函数公式,重复的执行一个或者多个动作的,这个时候,我们可以考虑递归。所谓递归,拆开理解,既存在递推,也存在回归,两个过程。不管是哪类,都要设置好递归条件,保证每一次递推的时候,接近我们限制条件,最后不满足,结束递推,开始回归,回到第一次调用函数的时候,递归结束。我们这里就讲第一类。第二类有型趣的可以看看我写的博客,从有序数组中查找数组
剑指offer中有,有这么一道题,青蛙跳台阶。
简单叙述一下这道题,
一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。
像我们公式类的函数递归,千万不要去想具体的实现方法,那样你会掉头发的。
用数学中的函数来说下,在联系到我们代码。
设F(n)这是一个函数,求的是一只青蛙跳到n阶台阶上的总共的方法(没有表达式,看作一个隐函数,知道它求的是总数就可以了)。
青蛙第一次可能跳一个台阶,剩下有n-1个台阶。F(n-1),青蛙跳n-1个台阶总共的方法。
青蛙第一次可能跳两个台阶,剩下有n-2个台阶。F(n-2),青蛙跳n-2个台阶总共的方法。
将这两种可能算出的结果加起来,不就是青蛙跳到n阶台阶上的所有的方法呢?
我们就能可以得到F(n)=F(n-1)+F(n-2)
如果你还不是能确定的话,我们也可以这样来看
n=1 总共的方法:1 1
n=2 总共的方法:2 2 1+1
n=3 总共的方法:3 1+2 2+1
n=4 总共的方法:5 1*5 1+2+1 1+1+2 2+1+1 2 + 2
n=5 总共的方法:8 .........
n=6 总共的方法:13
.....
我们仔细观察就能够发现F(3)=F(2)+F(1),F(4)=F(3)+F(2)....
验证了我们F(n)=F(n-1)+F(n-2)
那我们具体来实现一下:
#include<stdio.h>
int F(int n)
{
if (n > 2)
return F(n - 1) + F(n - 2);
else
return n;
}
int main()
{
int n = 0;
while ((scanf("%d", &n)) != EOF)//为了方便看结果我们连续输入
{
int ret = F(n);
printf("总共方法为%d\n", ret);
}
return 0;
}
如果你有了解过斐波那契数列的话,我们可以发现青蛙跳台阶其实就是我们斐波那契数列的变形。
这里使用递归的时候,如果n很大,假如我们输入50,计算的结果可能要算很久。因为进行了大量的重复计算。有兴趣的可以自己从F(50)往下面开始写F(50)=F(49)+F(48),多写几组,就会发现问题了。也是可以用非递归的方式实现,和我们实现斐波那契数列数列差不多这里就不在演示了。
我们在回过来看下代码: